@vibecodeqa/cli 0.13.0 → 0.15.0

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.
@@ -10,14 +10,21 @@
10
10
  * 7. SVG architecture diagram
11
11
  */
12
12
  import { basename, dirname, extname } from "node:path";
13
- import { gradeFromScore } from "../types.js";
14
13
  import { getProductionFiles } from "../fs-utils.js";
14
+ import { gradeFromScore } from "../types.js";
15
15
  export function runArchitecture(cwd) {
16
16
  const start = Date.now();
17
17
  const issues = [];
18
18
  const files = getProductionFiles(cwd);
19
19
  if (files.length < 2) {
20
- return { name: "architecture", score: 100, grade: "A", details: { skipped: true, reason: "fewer than 2 source files" }, issues: [], duration: Date.now() - start };
20
+ return {
21
+ name: "architecture",
22
+ score: 100,
23
+ grade: "A",
24
+ details: { skipped: true, reason: "fewer than 2 source files" },
25
+ issues: [],
26
+ duration: Date.now() - start,
27
+ };
21
28
  }
22
29
  const graph = buildGraph(files);
23
30
  // ── Circular dependencies ──
@@ -34,7 +41,12 @@ export function runArchitecture(cwd) {
34
41
  for (const [path, node] of graph.nodes) {
35
42
  if (node.importedBy.length >= threshold) {
36
43
  godModules.push(path);
37
- issues.push({ severity: "warning", message: `God module: imported by ${node.importedBy.length}/${files.length} files — consider splitting`, file: path, rule: "god-module" });
44
+ issues.push({
45
+ severity: "warning",
46
+ message: `God module: imported by ${node.importedBy.length}/${files.length} files — consider splitting`,
47
+ file: path,
48
+ rule: "god-module",
49
+ });
38
50
  }
39
51
  }
40
52
  // ── Orphan files (not imported by anyone) ──
@@ -52,7 +64,12 @@ export function runArchitecture(cwd) {
52
64
  for (const [path, node] of graph.nodes) {
53
65
  if (node.imports.length > 10) {
54
66
  highFanOut++;
55
- issues.push({ severity: "warning", message: `High fan-out: imports ${node.imports.length} modules — hard to test in isolation`, file: path, rule: "high-fan-out" });
67
+ issues.push({
68
+ severity: "warning",
69
+ message: `High fan-out: imports ${node.imports.length} modules — hard to test in isolation`,
70
+ file: path,
71
+ rule: "high-fan-out",
72
+ });
56
73
  }
57
74
  }
58
75
  // ── High fan-in + fan-out (connector files) ──
@@ -60,7 +77,12 @@ export function runArchitecture(cwd) {
60
77
  for (const [path, node] of graph.nodes) {
61
78
  if (node.imports.length > 5 && node.importedBy.length > 5) {
62
79
  connectors++;
63
- issues.push({ severity: "warning", message: `Connector: ${node.imports.length} imports, ${node.importedBy.length} importers — high coupling`, file: path, rule: "connector-module" });
80
+ issues.push({
81
+ severity: "warning",
82
+ message: `Connector: ${node.imports.length} imports, ${node.importedBy.length} importers — high coupling`,
83
+ file: path,
84
+ rule: "connector-module",
85
+ });
64
86
  }
65
87
  }
66
88
  // ── Score ──
@@ -152,8 +174,8 @@ function resolveImport(fromPath, importPath, knownFiles) {
152
174
  }
153
175
  // Try index
154
176
  for (const ext of [".ts", ".tsx"]) {
155
- if (knownFiles.has(resolved + "/index" + ext))
156
- return resolved + "/index" + ext;
177
+ if (knownFiles.has(`${resolved}/index${ext}`))
178
+ return `${resolved}/index${ext}`;
157
179
  }
158
180
  return null;
159
181
  }
@@ -28,7 +28,7 @@ export function runComplexity(cwd) {
28
28
  const lines = content.split("\n");
29
29
  totalLines += lines.length;
30
30
  // Simple heuristic: find function boundaries and measure complexity
31
- const funcs = extractFunctions(content, file.replace(cwd + "/", ""));
31
+ const funcs = extractFunctions(content, file.replace(`${cwd}/`, ""));
32
32
  for (const f of funcs) {
33
33
  functions.push(f);
34
34
  if (f.lines > MAX_FUNCTION_LINES) {
@@ -79,9 +79,7 @@ function collectFiles(dir, out) {
79
79
  }
80
80
  else {
81
81
  const ext = extname(entry);
82
- if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") &&
83
- !entry.includes(".test.") &&
84
- !entry.includes(".spec.")) {
82
+ if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") && !entry.includes(".test.") && !entry.includes(".spec.")) {
85
83
  out.push(full);
86
84
  }
87
85
  }
@@ -17,20 +17,65 @@ import { basename, extname, join } from "node:path";
17
17
  import { gradeFromScore } from "../types.js";
18
18
  // ── Pattern dictionaries ──
19
19
  const SYNONYM_PAIRS = [
20
- ["utils", "helpers"], ["util", "helper"],
21
- ["types", "interfaces"], ["types", "models"], ["interfaces", "models"],
22
- ["constants", "config"], ["constants", "settings"], ["config", "settings"],
23
- ["service", "controller"], ["service", "handler"], ["controller", "handler"],
24
- ["store", "state"], ["context", "provider"],
20
+ ["utils", "helpers"],
21
+ ["util", "helper"],
22
+ ["types", "interfaces"],
23
+ ["types", "models"],
24
+ ["interfaces", "models"],
25
+ ["constants", "config"],
26
+ ["constants", "settings"],
27
+ ["config", "settings"],
28
+ ["service", "controller"],
29
+ ["service", "handler"],
30
+ ["controller", "handler"],
31
+ ["store", "state"],
32
+ ["context", "provider"],
25
33
  ];
26
34
  const GENERIC_NAMES = new Set([
27
- "process", "handle", "run", "execute", "do", "perform", "make",
28
- "get", "set", "update", "create", "delete", "remove", "add",
29
- "data", "result", "item", "value", "info", "temp", "obj",
30
- "stuff", "thing", "ret", "val", "res", "output", "input",
31
- "response", "request", "payload", "body", "args", "params",
32
- "list", "arr", "map", "dict", "collection",
33
- "callback", "cb", "fn", "func", "handler",
35
+ "process",
36
+ "handle",
37
+ "run",
38
+ "execute",
39
+ "do",
40
+ "perform",
41
+ "make",
42
+ "get",
43
+ "set",
44
+ "update",
45
+ "create",
46
+ "delete",
47
+ "remove",
48
+ "add",
49
+ "data",
50
+ "result",
51
+ "item",
52
+ "value",
53
+ "info",
54
+ "temp",
55
+ "obj",
56
+ "stuff",
57
+ "thing",
58
+ "ret",
59
+ "val",
60
+ "res",
61
+ "output",
62
+ "input",
63
+ "response",
64
+ "request",
65
+ "payload",
66
+ "body",
67
+ "args",
68
+ "params",
69
+ "list",
70
+ "arr",
71
+ "map",
72
+ "dict",
73
+ "collection",
74
+ "callback",
75
+ "cb",
76
+ "fn",
77
+ "func",
78
+ "handler",
34
79
  ]);
35
80
  const AMBIGUOUS_ABBREVS = {
36
81
  auth: "authentication OR authorization",
@@ -58,10 +103,19 @@ export function runConfusion(cwd) {
58
103
  try {
59
104
  collectFiles(join(cwd, dir), cwd, files);
60
105
  }
61
- catch { /* dir doesn't exist */ }
106
+ catch {
107
+ /* dir doesn't exist */
108
+ }
62
109
  }
63
110
  if (files.length === 0) {
64
- return { name: "confusion", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
111
+ return {
112
+ name: "confusion",
113
+ score: 100,
114
+ grade: "A",
115
+ details: { skipped: true, reason: "no source files" },
116
+ issues: [],
117
+ duration: Date.now() - start,
118
+ };
65
119
  }
66
120
  let fileConfusability = 0;
67
121
  let genericNames = 0;
@@ -75,13 +129,23 @@ export function runConfusion(cwd) {
75
129
  // Near-identical (Levenshtein ≤ 2)
76
130
  if (a !== b && levenshtein(a, b) <= 2) {
77
131
  fileConfusability++;
78
- issues.push({ severity: "warning", message: `Similar filenames: ${a} ↔ ${b} (edit distance ${levenshtein(a, b)})`, file: files[i].path, rule: "similar-filename" });
132
+ issues.push({
133
+ severity: "warning",
134
+ message: `Similar filenames: ${a} ↔ ${b} (edit distance ${levenshtein(a, b)})`,
135
+ file: files[i].path,
136
+ rule: "similar-filename",
137
+ });
79
138
  }
80
139
  // Synonym pairs
81
140
  for (const [s1, s2] of SYNONYM_PAIRS) {
82
141
  if ((a.includes(s1) && b.includes(s2)) || (a.includes(s2) && b.includes(s1))) {
83
142
  fileConfusability++;
84
- issues.push({ severity: "warning", message: `Synonym filenames: ${a} ↔ ${b} (${s1}/${s2} are interchangeable — pick one convention)`, file: files[i].path, rule: "synonym-filename" });
143
+ issues.push({
144
+ severity: "warning",
145
+ message: `Synonym filenames: ${a} ↔ ${b} (${s1}/${s2} are interchangeable — pick one convention)`,
146
+ file: files[i].path,
147
+ rule: "synonym-filename",
148
+ });
85
149
  break;
86
150
  }
87
151
  }
@@ -98,13 +162,25 @@ export function runConfusion(cwd) {
98
162
  const funcMatch = line.match(/^export\s+(?:async\s+)?function\s+(\w+)/);
99
163
  if (funcMatch && GENERIC_NAMES.has(funcMatch[1].toLowerCase())) {
100
164
  genericNames++;
101
- issues.push({ severity: "warning", message: `Generic export name: ${funcMatch[1]}() — not descriptive enough for LLM comprehension`, file: f.path, line: i + 1, rule: "generic-name" });
165
+ issues.push({
166
+ severity: "warning",
167
+ message: `Generic export name: ${funcMatch[1]}() — not descriptive enough for LLM comprehension`,
168
+ file: f.path,
169
+ line: i + 1,
170
+ rule: "generic-name",
171
+ });
102
172
  }
103
173
  // Match standalone variable assignments with generic names
104
174
  const varMatch = line.match(/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=/);
105
175
  if (varMatch && GENERIC_NAMES.has(varMatch[1].toLowerCase()) && varMatch[1].length <= 6) {
106
176
  genericNames++;
107
- issues.push({ severity: "warning", message: `Generic variable: ${varMatch[1]} — use a descriptive name`, file: f.path, line: i + 1, rule: "generic-name" });
177
+ issues.push({
178
+ severity: "warning",
179
+ message: `Generic variable: ${varMatch[1]} — use a descriptive name`,
180
+ file: f.path,
181
+ line: i + 1,
182
+ rule: "generic-name",
183
+ });
108
184
  }
109
185
  }
110
186
  }
@@ -120,7 +196,12 @@ export function runConfusion(cwd) {
120
196
  for (const [name, paths] of exportMap) {
121
197
  if (paths.length > 1) {
122
198
  exportCollisions++;
123
- issues.push({ severity: "error", message: `Export collision: "${name}" exported from ${paths.length} files — LLMs may reference the wrong one`, file: paths.join(", "), rule: "export-collision" });
199
+ issues.push({
200
+ severity: "error",
201
+ message: `Export collision: "${name}" exported from ${paths.length} files — LLMs may reference the wrong one`,
202
+ file: paths.join(", "),
203
+ rule: "export-collision",
204
+ });
124
205
  }
125
206
  }
126
207
  // ── 4. Ambiguous abbreviations in filenames and exports ──
@@ -129,7 +210,12 @@ export function runConfusion(cwd) {
129
210
  for (const part of nameParts) {
130
211
  if (AMBIGUOUS_ABBREVS[part]) {
131
212
  ambiguousAbbrevs++;
132
- issues.push({ severity: "warning", message: `Ambiguous abbreviation "${part}" in filename — could mean: ${AMBIGUOUS_ABBREVS[part]}`, file: f.path, rule: "ambiguous-abbreviation" });
213
+ issues.push({
214
+ severity: "warning",
215
+ message: `Ambiguous abbreviation "${part}" in filename — could mean: ${AMBIGUOUS_ABBREVS[part]}`,
216
+ file: f.path,
217
+ rule: "ambiguous-abbreviation",
218
+ });
133
219
  }
134
220
  }
135
221
  }
@@ -189,7 +275,7 @@ function collectFiles(dir, cwd, out) {
189
275
  const ext = extname(entry);
190
276
  if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
191
277
  const content = readFileSync(full, "utf-8");
192
- const relPath = full.replace(cwd + "/", "");
278
+ const relPath = full.replace(`${cwd}/`, "");
193
279
  const base = basename(entry, ext);
194
280
  out.push({ path: relPath, base, content, exports: extractExports(content) });
195
281
  }
@@ -27,10 +27,19 @@ export function runContext(cwd) {
27
27
  try {
28
28
  collectFiles(join(cwd, dir), cwd, files);
29
29
  }
30
- catch { /* dir doesn't exist */ }
30
+ catch {
31
+ /* dir doesn't exist */
32
+ }
31
33
  }
32
34
  if (files.length === 0) {
33
- return { name: "context", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
35
+ return {
36
+ name: "context",
37
+ score: 100,
38
+ grade: "A",
39
+ details: { skipped: true, reason: "no source files" },
40
+ issues: [],
41
+ duration: Date.now() - start,
42
+ };
34
43
  }
35
44
  let highTokenFiles = 0;
36
45
  let heavyImportFiles = 0;
@@ -42,7 +51,12 @@ export function runContext(cwd) {
42
51
  totalTokens += f.tokens;
43
52
  if (f.tokens > MAX_FILE_TOKENS) {
44
53
  highTokenFiles++;
45
- issues.push({ severity: "warning", message: `~${f.tokens} tokens (>${MAX_FILE_TOKENS}) — large context cost for LLMs`, file: f.path, rule: "high-token-count" });
54
+ issues.push({
55
+ severity: "warning",
56
+ message: `~${f.tokens} tokens (>${MAX_FILE_TOKENS}) — large context cost for LLMs`,
57
+ file: f.path,
58
+ rule: "high-token-count",
59
+ });
46
60
  }
47
61
  }
48
62
  // ── 2. Import count per file ──
@@ -50,7 +64,12 @@ export function runContext(cwd) {
50
64
  totalImports += f.imports.length;
51
65
  if (f.imports.length > MAX_IMPORTS) {
52
66
  heavyImportFiles++;
53
- issues.push({ severity: "warning", message: `${f.imports.length} imports (>${MAX_IMPORTS}) — consider splitting or co-locating`, file: f.path, rule: "heavy-imports" });
67
+ issues.push({
68
+ severity: "warning",
69
+ message: `${f.imports.length} imports (>${MAX_IMPORTS}) — consider splitting or co-locating`,
70
+ file: f.path,
71
+ rule: "heavy-imports",
72
+ });
54
73
  }
55
74
  }
56
75
  // ── 3. Circular dependency detection ──
@@ -81,7 +100,12 @@ export function runContext(cwd) {
81
100
  const exportCount = (f.content.match(/\bexport\s+/g) || []).length;
82
101
  if (f.imports.length > 8 && exportCount <= 1) {
83
102
  contextSinks++;
84
- issues.push({ severity: "warning", message: `${f.imports.length} imports but only ${exportCount} export — hard to understand in isolation`, file: f.path, rule: "context-sink" });
103
+ issues.push({
104
+ severity: "warning",
105
+ message: `${f.imports.length} imports but only ${exportCount} export — hard to understand in isolation`,
106
+ file: f.path,
107
+ rule: "context-sink",
108
+ });
85
109
  }
86
110
  }
87
111
  // ── Score ──
@@ -141,7 +165,7 @@ function resolveImport(fromPath, importPath) {
141
165
  return resolved;
142
166
  }
143
167
  // Return with .ts as default assumption
144
- return resolved + ".ts";
168
+ return `${resolved}.ts`;
145
169
  }
146
170
  // ── Cycle detection (DFS) ──
147
171
  function findCycles(graph) {
@@ -190,7 +214,7 @@ function collectFiles(dir, cwd, out) {
190
214
  const ext = extname(entry);
191
215
  if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
192
216
  const content = readFileSync(full, "utf-8");
193
- const relPath = full.replace(cwd + "/", "");
217
+ const relPath = full.replace(`${cwd}/`, "");
194
218
  const imports = parseImports(content);
195
219
  const tokens = Math.round(content.length / CHARS_PER_TOKEN);
196
220
  out.push({ path: relPath, content, imports, tokens });
@@ -6,12 +6,8 @@ export function runDependencies(cwd, stack) {
6
6
  const issues = [];
7
7
  const pm = stack.packageManager;
8
8
  // Vulnerability audit
9
- const auditCmd = pm === "pnpm"
10
- ? "pnpm audit --json"
11
- : pm === "yarn"
12
- ? "yarn audit --json"
13
- : "npm audit --json";
14
- const auditResult = run(auditCmd + " 2>/dev/null || true", cwd);
9
+ const auditCmd = pm === "pnpm" ? "pnpm audit --json" : pm === "yarn" ? "yarn audit --json" : "npm audit --json";
10
+ const auditResult = run(`${auditCmd} 2>/dev/null || true`, cwd);
15
11
  let vulnCritical = 0, vulnHigh = 0, vulnModerate = 0, vulnLow = 0;
16
12
  try {
17
13
  const audit = JSON.parse(auditResult.stdout);
@@ -57,7 +53,7 @@ export function runDependencies(cwd, stack) {
57
53
  });
58
54
  // Outdated check
59
55
  const outdatedCmd = pm === "pnpm" ? "pnpm outdated --json" : "npm outdated --json";
60
- const outdatedResult = run(outdatedCmd + " 2>/dev/null || true", cwd);
56
+ const outdatedResult = run(`${outdatedCmd} 2>/dev/null || true`, cwd);
61
57
  let outdatedCount = 0;
62
58
  let majorOutdated = 0;
63
59
  try {
@@ -81,11 +77,7 @@ export function runDependencies(cwd, stack) {
81
77
  message: `${majorOutdated} packages behind by a major version`,
82
78
  });
83
79
  // Score: -25 per critical, -15 per high, -5 per moderate, -1 per major outdated
84
- const score = Math.max(0, Math.min(100, 100 -
85
- vulnCritical * 25 -
86
- vulnHigh * 15 -
87
- vulnModerate * 5 -
88
- majorOutdated * 1));
80
+ const score = Math.max(0, Math.min(100, 100 - vulnCritical * 25 - vulnHigh * 15 - vulnModerate * 5 - majorOutdated * 1));
89
81
  return {
90
82
  name: "dependencies",
91
83
  score,
@@ -1,5 +1,5 @@
1
1
  /** Documentation check — README, JSDoc, code comments. */
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { extname, join } from "node:path";
4
4
  import { gradeFromScore } from "../types.js";
5
5
  export function runDocs(cwd) {
@@ -40,7 +40,9 @@ export function runDocs(cwd) {
40
40
  try {
41
41
  collectFiles(join(cwd, dir), files);
42
42
  }
43
- catch { /* dir doesn't exist */ }
43
+ catch {
44
+ /* dir doesn't exist */
45
+ }
44
46
  }
45
47
  let totalExports = 0;
46
48
  let documentedExports = 0;
@@ -49,7 +51,10 @@ export function runDocs(cwd) {
49
51
  const lines = content.split("\n");
50
52
  for (let i = 0; i < lines.length; i++) {
51
53
  const line = lines[i].trim();
52
- if (line.startsWith("export function ") || line.startsWith("export async function ") || line.startsWith("export class ") || line.startsWith("export interface ")) {
54
+ if (line.startsWith("export function ") ||
55
+ line.startsWith("export async function ") ||
56
+ line.startsWith("export class ") ||
57
+ line.startsWith("export interface ")) {
53
58
  totalExports++;
54
59
  // Check if preceded by a JSDoc or // comment
55
60
  const prevLine = i > 0 ? lines[i - 1].trim() : "";
@@ -63,7 +68,11 @@ export function runDocs(cwd) {
63
68
  const pct = Math.round((documentedExports / totalExports) * 100);
64
69
  exportDocScore = pct;
65
70
  if (pct < 30) {
66
- issues.push({ severity: "warning", message: `Only ${pct}% of exports have documentation (${documentedExports}/${totalExports})`, rule: "undocumented-exports" });
71
+ issues.push({
72
+ severity: "warning",
73
+ message: `Only ${pct}% of exports have documentation (${documentedExports}/${totalExports})`,
74
+ rule: "undocumented-exports",
75
+ });
67
76
  }
68
77
  }
69
78
  else {
@@ -74,7 +83,12 @@ export function runDocs(cwd) {
74
83
  name: "docs",
75
84
  score,
76
85
  grade: gradeFromScore(score),
77
- details: { readmeLines: existsSync(readmePath) ? readFileSync(readmePath, "utf-8").split("\n").length : 0, totalExports, documentedExports, documentedPct: totalExports > 0 ? Math.round((documentedExports / totalExports) * 100) + "%" : "n/a" },
86
+ details: {
87
+ readmeLines: existsSync(readmePath) ? readFileSync(readmePath, "utf-8").split("\n").length : 0,
88
+ totalExports,
89
+ documentedExports,
90
+ documentedPct: totalExports > 0 ? `${Math.round((documentedExports / totalExports) * 100)}%` : "n/a",
91
+ },
78
92
  issues,
79
93
  duration: Date.now() - start,
80
94
  };
@@ -13,10 +13,19 @@ export function runDuplication(cwd) {
13
13
  try {
14
14
  collectFiles(join(cwd, dir), files);
15
15
  }
16
- catch { /* dir doesn't exist */ }
16
+ catch {
17
+ /* dir doesn't exist */
18
+ }
17
19
  }
18
20
  if (files.length < 2) {
19
- return { name: "duplication", score: 100, grade: "A", details: { filesScanned: files.length, duplicates: 0 }, issues: [], duration: Date.now() - start };
21
+ return {
22
+ name: "duplication",
23
+ score: 100,
24
+ grade: "A",
25
+ details: { filesScanned: files.length, duplicates: 0 },
26
+ issues: [],
27
+ duration: Date.now() - start,
28
+ };
20
29
  }
21
30
  // Simple line-based duplicate detection
22
31
  // Build a map of normalized line hashes → locations
@@ -24,7 +33,7 @@ export function runDuplication(cwd) {
24
33
  let totalSourceLines = 0;
25
34
  for (const file of files) {
26
35
  const content = readFileSync(file, "utf-8");
27
- const relPath = file.replace(cwd + "/", "");
36
+ const relPath = file.replace(`${cwd}/`, "");
28
37
  const lines = content.split("\n");
29
38
  totalSourceLines += lines.length;
30
39
  for (let i = 0; i <= lines.length - MIN_LINES; i++) {
@@ -77,7 +86,7 @@ export function runDuplication(cwd) {
77
86
  name: "duplication",
78
87
  score,
79
88
  grade: gradeFromScore(score),
80
- details: { filesScanned: files.length, totalSourceLines, duplicateBlocks: duplicates.length, duplicationPct: dupPct + "%" },
89
+ details: { filesScanned: files.length, totalSourceLines, duplicateBlocks: duplicates.length, duplicationPct: `${dupPct}%` },
81
90
  issues,
82
91
  duration: Date.now() - start,
83
92
  };
@@ -0,0 +1,3 @@
1
+ /** Error handling check — detects poor error handling patterns. */
2
+ import type { CheckResult, StackInfo } from "../types.js";
3
+ export declare function runErrorHandling(cwd: string, stack: StackInfo): CheckResult;
@@ -0,0 +1,48 @@
1
+ /** Error handling check — detects poor error handling patterns. */
2
+ import { gradeFromScore } from "../types.js";
3
+ import { getProductionFiles } from "../fs-utils.js";
4
+ export function runErrorHandling(cwd, stack) {
5
+ const start = Date.now();
6
+ const issues = [];
7
+ const files = getProductionFiles(cwd);
8
+ if (files.length === 0) {
9
+ return { name: "error-handling", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
10
+ }
11
+ let emptyCatch = 0;
12
+ let throwString = 0;
13
+ for (const f of files) {
14
+ const lines = f.content.split("\n");
15
+ for (let i = 0; i < lines.length; i++) {
16
+ const line = lines[i].trim();
17
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line) || /catch\s*\{\s*\}/.test(line)) {
18
+ emptyCatch++;
19
+ issues.push({ severity: "error", message: "Empty catch block", file: f.path, line: i + 1, rule: "empty-catch" });
20
+ }
21
+ if (/\bthrow\s+["'`]/.test(line)) {
22
+ throwString++;
23
+ issues.push({ severity: "warning", message: "throw string literal — use throw new Error()", file: f.path, line: i + 1, rule: "throw-string" });
24
+ }
25
+ }
26
+ }
27
+ let hasErrorBoundary = false;
28
+ if (stack.framework === "react") {
29
+ for (const f of files) {
30
+ if (f.content.includes("componentDidCatch") || f.content.includes("ErrorBoundary")) {
31
+ hasErrorBoundary = true;
32
+ break;
33
+ }
34
+ }
35
+ if (!hasErrorBoundary && files.some((f) => f.ext === ".tsx")) {
36
+ issues.push({ severity: "warning", message: "React project with no Error Boundary", rule: "no-error-boundary" });
37
+ }
38
+ }
39
+ const score = Math.max(0, Math.min(100, 100 - emptyCatch * 5 - throwString * 2 - (stack.framework === "react" && !hasErrorBoundary ? 3 : 0)));
40
+ return {
41
+ name: "error-handling",
42
+ score,
43
+ grade: gradeFromScore(score),
44
+ details: { emptyCatch, throwString, hasErrorBoundary: stack.framework === "react" ? hasErrorBoundary : "n/a" },
45
+ issues,
46
+ duration: Date.now() - start,
47
+ };
48
+ }
@@ -11,11 +11,7 @@ export function runLint(cwd, stack) {
11
11
  const diagnostics = data.diagnostics || [];
12
12
  for (const d of diagnostics) {
13
13
  issues.push({
14
- severity: d.severity === "error"
15
- ? "error"
16
- : d.severity === "warning"
17
- ? "warning"
18
- : "info",
14
+ severity: d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info",
19
15
  message: d.description || d.message || "lint issue",
20
16
  file: d.location?.path,
21
17
  line: d.location?.span?.start?.line,
@@ -27,9 +23,9 @@ export function runLint(cwd, stack) {
27
23
  // biome may not output valid JSON on some errors — count from summary
28
24
  const errors = stdout.match(/Found (\d+) error/)?.[1] || "0";
29
25
  const warnings = stdout.match(/Found (\d+) warning/)?.[1] || "0";
30
- for (let i = 0; i < parseInt(errors); i++)
26
+ for (let i = 0; i < parseInt(errors, 10); i++)
31
27
  issues.push({ severity: "error", message: "lint error" });
32
- for (let i = 0; i < parseInt(warnings); i++)
28
+ for (let i = 0; i < parseInt(warnings, 10); i++)
33
29
  issues.push({ severity: "warning", message: "lint warning" });
34
30
  }
35
31
  }
@@ -39,7 +39,7 @@ export function runSecrets(cwd) {
39
39
  collectFiles(cwd, files);
40
40
  for (const file of files) {
41
41
  const content = readFileSync(file, "utf-8");
42
- const relPath = file.replace(cwd + "/", "");
42
+ const relPath = file.replace(`${cwd}/`, "");
43
43
  const lines = content.split("\n");
44
44
  for (let i = 0; i < lines.length; i++) {
45
45
  const line = lines[i];
@@ -74,14 +74,7 @@ export function runSecrets(cwd) {
74
74
  }
75
75
  function collectFiles(dir, out) {
76
76
  for (const entry of readdirSync(dir)) {
77
- if ([
78
- "node_modules",
79
- "dist",
80
- ".git",
81
- ".vibe-check",
82
- "coverage",
83
- "test-results",
84
- ].includes(entry))
77
+ if (["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results"].includes(entry))
85
78
  continue;
86
79
  const full = join(dir, entry);
87
80
  const stat = statSync(full);
@@ -90,17 +83,7 @@ function collectFiles(dir, out) {
90
83
  }
91
84
  else {
92
85
  const ext = extname(entry);
93
- if ([
94
- ".ts",
95
- ".tsx",
96
- ".js",
97
- ".jsx",
98
- ".json",
99
- ".env",
100
- ".yaml",
101
- ".yml",
102
- ".toml",
103
- ].includes(ext)) {
86
+ if ([".ts", ".tsx", ".js", ".jsx", ".json", ".env", ".yaml", ".yml", ".toml"].includes(ext)) {
104
87
  out.push(full);
105
88
  }
106
89
  }