@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.
- package/dist/check-meta.js +13 -3
- package/dist/cli.js +22 -14
- package/dist/detect.js +5 -27
- package/dist/fs-utils.js +4 -2
- package/dist/history.d.ts +14 -0
- package/dist/history.js +51 -0
- package/dist/report/html.d.ts +1 -1
- package/dist/report/html.js +113 -34
- package/dist/report/svg.d.ts +9 -0
- package/dist/report/svg.js +23 -0
- package/dist/runners/architecture.js +29 -7
- package/dist/runners/complexity.js +2 -4
- package/dist/runners/confusion.js +107 -21
- package/dist/runners/context.js +31 -7
- package/dist/runners/dependencies.js +4 -12
- package/dist/runners/docs.js +19 -5
- package/dist/runners/duplication.js +13 -4
- package/dist/runners/error-handling.d.ts +3 -0
- package/dist/runners/error-handling.js +48 -0
- package/dist/runners/lint.js +3 -7
- package/dist/runners/secrets.js +3 -20
- package/dist/runners/security.js +121 -19
- package/dist/runners/standards.js +57 -13
- package/dist/runners/structure.js +14 -6
- package/dist/runners/testing.js +97 -28
- package/dist/runners/type-safety.js +17 -4
- package/dist/runners/types-check.js +2 -3
- package/package.json +1 -1
|
@@ -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 {
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
156
|
-
return resolved
|
|
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"],
|
|
21
|
-
["
|
|
22
|
-
["
|
|
23
|
-
["
|
|
24
|
-
["
|
|
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",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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 {
|
|
106
|
+
catch {
|
|
107
|
+
/* dir doesn't exist */
|
|
108
|
+
}
|
|
62
109
|
}
|
|
63
110
|
if (files.length === 0) {
|
|
64
|
-
return {
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
}
|
package/dist/runners/context.js
CHANGED
|
@@ -27,10 +27,19 @@ export function runContext(cwd) {
|
|
|
27
27
|
try {
|
|
28
28
|
collectFiles(join(cwd, dir), cwd, files);
|
|
29
29
|
}
|
|
30
|
-
catch {
|
|
30
|
+
catch {
|
|
31
|
+
/* dir doesn't exist */
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
34
|
if (files.length === 0) {
|
|
33
|
-
return {
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
package/dist/runners/docs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Documentation check — README, JSDoc, code comments. */
|
|
2
|
-
import { existsSync,
|
|
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 {
|
|
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 ") ||
|
|
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({
|
|
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: {
|
|
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 {
|
|
16
|
+
catch {
|
|
17
|
+
/* dir doesn't exist */
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
20
|
if (files.length < 2) {
|
|
19
|
-
return {
|
|
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,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
|
+
}
|
package/dist/runners/lint.js
CHANGED
|
@@ -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
|
}
|
package/dist/runners/secrets.js
CHANGED
|
@@ -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
|
}
|