@vibecodeqa/cli 0.36.2 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/monitor.js
CHANGED
|
@@ -104,6 +104,21 @@ function buildFixPrompt(checkName, issue, cwd) {
|
|
|
104
104
|
prompt += "\n\nAnalyze the code, explain the issue, and provide the fix.";
|
|
105
105
|
return prompt;
|
|
106
106
|
}
|
|
107
|
+
function getGitChanges(cwd) {
|
|
108
|
+
try {
|
|
109
|
+
const out = execSync("git status --porcelain", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
110
|
+
if (!out)
|
|
111
|
+
return [];
|
|
112
|
+
return out.split("\n").map((line) => {
|
|
113
|
+
const status = (line[0] === "?" ? "?" : line.trim()[0]);
|
|
114
|
+
const file = line.slice(3).trim();
|
|
115
|
+
return { status, file };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
107
122
|
// ── Scan via child process — UI never freezes ──
|
|
108
123
|
function runScanProcess(cwd, skipTests) {
|
|
109
124
|
return new Promise((resolve) => {
|
|
@@ -333,6 +348,29 @@ function IssueDetail({ issue, checkName, cwd, height, copied }) {
|
|
|
333
348
|
const promptLines = prompt.split("\n");
|
|
334
349
|
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Issue Detail" }), _jsxs(Text, { children: [_jsxs(Text, { color: sc(issue.severity), bold: true, children: [" ", issue.severity.toUpperCase(), " "] }), _jsx(Text, { dimColor: true, children: checkName }), issue.rule && _jsxs(Text, { dimColor: true, children: [" \u00B7 ", issue.rule] }), copied && _jsx(Text, { color: "green", bold: true, children: " \u2713 Copied!" })] }), _jsxs(Text, { wrap: "wrap", children: [" ", issue.message] }), issue.file && (_jsxs(Text, { color: "cyan", children: [" ", issue.file, issue.line ? `:${issue.line}` : ""] })), ctx && (_jsxs(Box, { flexDirection: "column", height: srcHeight, overflowY: "hidden", children: [_jsxs(Text, { dimColor: true, children: [" \u2500\u2500\u2500 ", ctx.filePath, " \u2500\u2500\u2500"] }), ctx.lines.slice(0, srcHeight - 2).map((l) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: l.highlight ? "yellow" : "gray", children: l.highlight ? "▸" : " " }), _jsxs(Text, { dimColor: true, children: [String(l.num).padStart(4), "\u2502"] }), _jsx(Text, { color: l.highlight ? "white" : undefined, children: l.text })] }, l.num))), _jsx(Text, { dimColor: true, children: " \u2500\u2500\u2500" })] })), _jsxs(Box, { flexDirection: "column", height: Math.max(3, promptHeight), overflowY: "hidden", marginTop: ctx ? 0 : 1, children: [_jsxs(Text, { bold: true, color: "green", children: [" Fix prompt ", _jsx(Text, { dimColor: true, children: "(y to copy)" })] }), promptLines.slice(0, Math.max(1, promptHeight - 1)).map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [" ", line] }, i)))] })] }));
|
|
335
350
|
}
|
|
351
|
+
// ── Git Changes View ──
|
|
352
|
+
function GitChangesView({ cwd, checks, height, cursor }) {
|
|
353
|
+
const changes = useMemo(() => getGitChanges(cwd), [cwd]);
|
|
354
|
+
// Cross-reference with issues
|
|
355
|
+
const issuesByFile = useMemo(() => {
|
|
356
|
+
const map = new Map();
|
|
357
|
+
for (const c of checks) {
|
|
358
|
+
for (const iss of c.issues) {
|
|
359
|
+
if (iss.file && typeof iss.file === "string") {
|
|
360
|
+
map.set(iss.file, (map.get(iss.file) || 0) + 1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return map;
|
|
365
|
+
}, [checks]);
|
|
366
|
+
const statusColor = { M: "yellow", A: "green", D: "red", "?": "gray", R: "cyan" };
|
|
367
|
+
const visibleLines = Math.max(1, height - 5);
|
|
368
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 Git Changes (", changes.length, ")"] }), changes.length === 0 ? (_jsx(Text, { dimColor: true, children: " Working tree clean \u2014 no uncommitted changes." })) : (_jsxs(_Fragment, { children: [changes.slice(0, visibleLines).map((ch, i) => {
|
|
369
|
+
const sel = i === cursor;
|
|
370
|
+
const count = issuesByFile.get(ch.file) || 0;
|
|
371
|
+
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: statusColor[ch.status] || "gray", bold: true, children: [" ", ch.status, " "] }), _jsxs(Text, { color: sel ? "white" : undefined, children: [ch.file, " "] }), count > 0 ? (_jsxs(Text, { color: "yellow", children: [count, " issue", count !== 1 ? "s" : ""] })) : (_jsx(Text, { color: "green", children: "clean" }))] }, ch.file));
|
|
372
|
+
}), changes.length > visibleLines && _jsxs(Text, { dimColor: true, children: [" +", changes.length - visibleLines, " more"] })] }))] }));
|
|
373
|
+
}
|
|
336
374
|
function MonitorApp({ cwd }) {
|
|
337
375
|
const { exit } = useApp();
|
|
338
376
|
const { stdout } = useStdout();
|
|
@@ -465,6 +503,11 @@ function MonitorApp({ cwd }) {
|
|
|
465
503
|
setCursor(mode.issueIdx);
|
|
466
504
|
return;
|
|
467
505
|
}
|
|
506
|
+
if (mode.view === "file-issues") {
|
|
507
|
+
setMode({ view: "git-changes" });
|
|
508
|
+
setCursor(0);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
468
511
|
if (mode.view !== "dashboard") {
|
|
469
512
|
setMode({ view: "dashboard" });
|
|
470
513
|
setCursor(0);
|
|
@@ -529,6 +572,11 @@ function MonitorApp({ cwd }) {
|
|
|
529
572
|
setCursor(0);
|
|
530
573
|
return;
|
|
531
574
|
}
|
|
575
|
+
if (input === "g") {
|
|
576
|
+
setMode({ view: "git-changes" });
|
|
577
|
+
setCursor(0);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
532
580
|
if (input === "c") {
|
|
533
581
|
setMode({ view: "config" });
|
|
534
582
|
setConfigCursor(0);
|
|
@@ -591,6 +639,37 @@ function MonitorApp({ cwd }) {
|
|
|
591
639
|
}
|
|
592
640
|
}
|
|
593
641
|
}
|
|
642
|
+
// ── Git changes: ↑↓ navigate, Enter drill into file issues ──
|
|
643
|
+
if (mode.view === "git-changes") {
|
|
644
|
+
const changes = getGitChanges(cwd);
|
|
645
|
+
if (key.upArrow)
|
|
646
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
647
|
+
if (key.downArrow)
|
|
648
|
+
setCursor((c) => Math.min(changes.length - 1, c + 1));
|
|
649
|
+
if (key.return && changes[cursor]) {
|
|
650
|
+
setMode({ view: "file-issues", file: changes[cursor].file });
|
|
651
|
+
setCursor(0);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ── File issues: ↑↓ navigate, Enter drill into issue detail, y copy ──
|
|
655
|
+
if (mode.view === "file-issues") {
|
|
656
|
+
const fileIssues = state.checks.flatMap((c) => c.issues.filter((i) => i.file === mode.file).map((i) => ({ check: c.name, ...i })));
|
|
657
|
+
if (key.upArrow)
|
|
658
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
659
|
+
if (key.downArrow)
|
|
660
|
+
setCursor((c) => Math.min(fileIssues.length - 1, c + 1));
|
|
661
|
+
if (key.return && fileIssues[cursor]) {
|
|
662
|
+
setMode({ view: "issue-detail", checkName: fileIssues[cursor].check, issueIdx: 0 });
|
|
663
|
+
}
|
|
664
|
+
if (input === "y" && fileIssues[cursor]) {
|
|
665
|
+
const prompt = buildFixPrompt(fileIssues[cursor].check, fileIssues[cursor], cwd);
|
|
666
|
+
if (copyToClipboard(prompt)) {
|
|
667
|
+
setCopied(true);
|
|
668
|
+
addLog(`Copied fix prompt`, "info");
|
|
669
|
+
setTimeout(() => setCopied(false), 2000);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
594
673
|
// ── Trends: ↑↓ scroll ──
|
|
595
674
|
if (mode.view === "trends") {
|
|
596
675
|
if (key.upArrow)
|
|
@@ -602,6 +681,16 @@ function MonitorApp({ cwd }) {
|
|
|
602
681
|
const proj = basename(cwd);
|
|
603
682
|
const p = monCfg.panels;
|
|
604
683
|
// ── Render views ──
|
|
684
|
+
if (mode.view === "git-changes") {
|
|
685
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsx(GitChangesView, { cwd: cwd, checks: state.checks, height: rows - 3, cursor: cursor }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc back \u00B7 \u2191\u2193 select \u00B7 Enter view file issues \u00B7 q quit" }) })] }));
|
|
686
|
+
}
|
|
687
|
+
if (mode.view === "file-issues") {
|
|
688
|
+
const fileIssues = state.checks.flatMap((c) => c.issues.filter((i) => i.file === mode.file).map((i) => ({ check: c.name, ...i })));
|
|
689
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsxs(Box, { flexDirection: "column", height: rows - 3, paddingX: 1, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 ", mode.file] }), _jsxs(Text, { dimColor: true, children: [" ", fileIssues.length, " issue", fileIssues.length !== 1 ? "s" : "", copied && _jsx(Text, { color: "green", bold: true, children: " \u2713 Copied!" })] }), _jsx(Text, { children: " " }), fileIssues.length === 0 ? (_jsx(Text, { color: "green", children: " No issues in this file." })) : (fileIssues.slice(0, rows - 8).map((iss, i) => {
|
|
690
|
+
const sel = i === cursor;
|
|
691
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), iss.line && _jsx(Text, { color: "cyan", children: String(iss.line).padEnd(5) }), _jsx(Text, { dimColor: true, children: iss.check.padEnd(14) }), iss.rule && _jsxs(Text, { dimColor: true, children: ["(", iss.rule, ") "] })] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: sel ? "white" : "gray", children: " " }), _jsx(Text, { color: sel ? "white" : undefined, children: iss.message })] })] }, i));
|
|
692
|
+
}))] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc back \u00B7 \u2191\u2193 select \u00B7 Enter source \u00B7 y copy prompt \u00B7 q quit" }) })] }));
|
|
693
|
+
}
|
|
605
694
|
if (mode.view === "issue-detail") {
|
|
606
695
|
const check = state.checks.find((c) => c.name === mode.checkName);
|
|
607
696
|
const issue = check?.issues[mode.issueIdx];
|
|
@@ -633,7 +722,7 @@ function MonitorApp({ cwd }) {
|
|
|
633
722
|
})] }))] })), mainVisible && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [p.activity && _jsx(ActivityPanel, { log: log, height: activityH }), p.issues && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "issues" ? "magenta" : "gray", paddingX: 1, height: issuesH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", allIssues.length, ") ", panel === "issues" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }), allIssues.slice(0, issuesH - 3).map((iss, i) => {
|
|
634
723
|
const sel = panel === "issues" && i === cursor;
|
|
635
724
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), _jsxs(Text, { dimColor: true, children: [(iss.check || "").slice(0, 11).padEnd(11), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [basename(String(iss.file)).slice(0, 18).padEnd(18), " "] }), _jsx(Text, { children: iss.message.slice(0, 40) })] }, i));
|
|
636
|
-
}), allIssues.length > issuesH - 3 && _jsxs(Text, { dimColor: true, children: [" +", allIssues.length - (issuesH - 3), " more"] })] }))] })), !sidebarVisible && !mainVisible && (_jsx(Box, { height: bodyRows, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "All panels hidden. Press c to configure." }) }))] }), _jsx(Box, { paddingX: 1, justifyContent: "space-between", children: _jsx(Text, { dimColor: true, children: "Tab panel \u00B7 \u2191\u2193
|
|
725
|
+
}), allIssues.length > issuesH - 3 && _jsxs(Text, { dimColor: true, children: [" +", allIssues.length - (issuesH - 3), " more"] })] }))] })), !sidebarVisible && !mainVisible && (_jsx(Box, { height: bodyRows, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "All panels hidden. Press c to configure." }) }))] }), _jsx(Box, { paddingX: 1, justifyContent: "space-between", children: _jsx(Text, { dimColor: true, children: "Tab panel \u00B7 \u2191\u2193 Enter Esc \u00B7 r scan \u00B7 g git \u00B7 t trends \u00B7 c config \u00B7 q quit" }) })] }));
|
|
637
726
|
}
|
|
638
727
|
function Header({ proj, stack, workspace, state }) {
|
|
639
728
|
return (_jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "magenta", bold: true, children: "vcqa monitor" }), _jsxs(Text, { dimColor: true, children: [" v", VERSION] })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: proj }), _jsxs(Text, { dimColor: true, children: [" ", stack.language, "/", stack.framework, workspace.isMonorepo ? ` · ${workspace.tool}` : ""] })] }), _jsxs(Text, { children: [state.score > 0 && _jsxs(Text, { color: gc(state.grade), bold: true, children: [state.grade, " ", state.score, " "] }), _jsx(Text, { dimColor: true, children: state.scanning ? "⟳ scanning" : "● watching" })] })] }));
|
|
@@ -125,8 +125,8 @@ export function runAccessibility(cwd) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
// 7. Check for html lang attribute in index.html
|
|
129
|
-
const htmlPaths = ["index.html", "web/index.html", "public/index.html"];
|
|
128
|
+
// 7. Check for html lang attribute + viewport + mobile meta in index.html
|
|
129
|
+
const htmlPaths = ["index.html", "web/index.html", "public/index.html", "src/index.html"];
|
|
130
130
|
for (const h of htmlPaths) {
|
|
131
131
|
const full = join(cwd, h);
|
|
132
132
|
if (!existsSync(full))
|
|
@@ -136,6 +136,42 @@ export function runAccessibility(cwd) {
|
|
|
136
136
|
missingLang++;
|
|
137
137
|
issues.push({ severity: "warning", message: "<html> missing lang attribute", file: h, rule: "html-lang" });
|
|
138
138
|
}
|
|
139
|
+
// Mobile viewport
|
|
140
|
+
if (!/<meta[^>]*name=["']viewport["']/.test(content)) {
|
|
141
|
+
issues.push({ severity: "error", message: "Missing <meta name=\"viewport\"> — page won't scale on mobile", file: h, rule: "missing-viewport" });
|
|
142
|
+
}
|
|
143
|
+
// charset
|
|
144
|
+
if (!/<meta[^>]*charset=/i.test(content)) {
|
|
145
|
+
issues.push({ severity: "warning", message: "Missing <meta charset> — may cause encoding issues", file: h, rule: "missing-charset" });
|
|
146
|
+
}
|
|
147
|
+
// Touch icon for mobile bookmarks
|
|
148
|
+
if (!/<link[^>]*apple-touch-icon/.test(content) && !/<link[^>]*icon/.test(content)) {
|
|
149
|
+
issues.push({ severity: "info", message: "No favicon or apple-touch-icon — poor mobile bookmark experience", file: h, rule: "missing-icon" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 8. Mobile-unfriendly patterns in components
|
|
153
|
+
for (const f of files) {
|
|
154
|
+
const source = f.rawContent || f.content;
|
|
155
|
+
const lines = source.split("\n");
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
// Fixed pixel widths that break on mobile
|
|
159
|
+
if (/style=.*width:\s*\d{4,}px/.test(line)) {
|
|
160
|
+
issues.push({ severity: "info", message: "Fixed width ≥1000px — likely breaks on mobile", file: f.path, line: i + 1, rule: "fixed-width" });
|
|
161
|
+
}
|
|
162
|
+
// Horizontal scroll containers without overflow handling
|
|
163
|
+
if (/overflow-x:\s*(?:scroll|auto)/.test(line) && !/\btouch\b/.test(line) && !/-webkit-overflow-scrolling/.test(line)) {
|
|
164
|
+
issues.push({ severity: "info", message: "Horizontal scroll without touch-action — poor mobile scroll UX", file: f.path, line: i + 1, rule: "touch-scroll" });
|
|
165
|
+
}
|
|
166
|
+
// Hover-only interactions (no touch fallback)
|
|
167
|
+
if (/onMouseEnter=|@mouseenter|on:mouseenter/.test(line) && !/onClick=|@click|on:click|onTouchStart|@touchstart/.test(line)) {
|
|
168
|
+
issues.push({ severity: "info", message: "Hover-only interaction — unreachable on touch devices", file: f.path, line: i + 1, rule: "hover-only" });
|
|
169
|
+
}
|
|
170
|
+
// Tiny touch targets
|
|
171
|
+
if (/(?:width|height):\s*(?:1[0-9]|[1-9])px/.test(line) && /(?:onClick|@click|on:click|button|<a )/.test(line)) {
|
|
172
|
+
issues.push({ severity: "info", message: "Touch target likely <44px — hard to tap on mobile (WCAG 2.5.8)", file: f.path, line: i + 1, rule: "small-touch-target" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
139
175
|
}
|
|
140
176
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
141
177
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
@@ -82,8 +82,10 @@ export async function runDeadPatterns(cwd) {
|
|
|
82
82
|
let started = false;
|
|
83
83
|
for (let j = i; j < Math.min(i + 20, lines.length); j++) {
|
|
84
84
|
const l = lines[j];
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// For "} catch (e) {" on one line, only count braces after "catch"
|
|
86
|
+
const braceText = j === i && l.includes("catch") ? l.slice(l.indexOf("catch")) : l;
|
|
87
|
+
braceDepth += (braceText.match(/\{/g) || []).length;
|
|
88
|
+
braceDepth -= (braceText.match(/\}/g) || []).length;
|
|
87
89
|
if (braceDepth > 0)
|
|
88
90
|
started = true;
|
|
89
91
|
if (started && j > i) {
|
|
@@ -178,6 +178,36 @@ export function runPerformance(cwd) {
|
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
// ── 7. PWA readiness (web projects only) ──
|
|
182
|
+
const isWebProject = !!(deps.react || deps.vue || deps.svelte || deps["@sveltejs/kit"] || deps.next || deps.nuxt);
|
|
183
|
+
if (isWebProject) {
|
|
184
|
+
const manifestPaths = ["public/manifest.json", "public/manifest.webmanifest", "manifest.json", "web/manifest.json"];
|
|
185
|
+
const hasManifest = manifestPaths.some((p) => existsSync(join(cwd, p)));
|
|
186
|
+
if (!hasManifest) {
|
|
187
|
+
issues.push({ severity: "info", message: "No web app manifest — can't install as PWA or add to home screen", rule: "no-manifest" });
|
|
188
|
+
}
|
|
189
|
+
const swPaths = ["public/sw.js", "public/service-worker.js", "src/service-worker.ts", "src/sw.ts"];
|
|
190
|
+
const hasSW = swPaths.some((p) => existsSync(join(cwd, p))) || !!(deps["workbox-webpack-plugin"] || deps["vite-plugin-pwa"] || deps["next-pwa"]);
|
|
191
|
+
if (!hasSW) {
|
|
192
|
+
issues.push({ severity: "info", message: "No service worker — app won't work offline", rule: "no-service-worker" });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// ── 8. CSS best practices ──
|
|
196
|
+
const cssFiles = getProductionFiles(cwd).filter((f) => f.ext === ".css");
|
|
197
|
+
for (const f of cssFiles) {
|
|
198
|
+
const lines = f.content.split("\n");
|
|
199
|
+
for (let i = 0; i < lines.length; i++) {
|
|
200
|
+
const line = lines[i];
|
|
201
|
+
// !important overuse
|
|
202
|
+
if (/!important/.test(line)) {
|
|
203
|
+
issues.push({ severity: "info", message: "!important — specificity escape hatch, usually a sign of CSS architecture issues", file: f.path, line: i + 1, rule: "css-important" });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// No media queries in CSS with fixed layouts
|
|
207
|
+
if (f.content.length > 500 && !/@media/.test(f.content) && /width:\s*\d{3,}px/.test(f.content)) {
|
|
208
|
+
issues.push({ severity: "info", message: "CSS with fixed widths but no @media queries — likely not responsive", file: f.path, rule: "no-media-queries" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
181
211
|
// Score — proportional to codebase, capped per category
|
|
182
212
|
const totalFiles = sourceFiles.length || 1;
|
|
183
213
|
const barrelPenalty = Math.min(15, (barrelImports / totalFiles) * 200);
|