@vibecodeqa/cli 0.12.1 → 0.14.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 +2 -2
- package/dist/cli.js +28 -17
- package/dist/detect.js +5 -27
- package/dist/fs-utils.js +11 -3
- package/dist/report/html.js +73 -37
- 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/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
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
/** Code standards check — naming conventions, anti-patterns, config hygiene. */
|
|
2
|
-
import {
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, extname, join } from "node:path";
|
|
4
4
|
import { gradeFromScore } from "../types.js";
|
|
5
5
|
const CODE_SMELLS = [
|
|
6
|
-
{
|
|
6
|
+
{
|
|
7
|
+
name: "console.log",
|
|
8
|
+
pattern: /\bconsole\.(log|debug|info)\s*\(/,
|
|
9
|
+
severity: "warning",
|
|
10
|
+
message: "console.log in production code",
|
|
11
|
+
exclude: /\/\/ ?ok|eslint-disable|biome-ignore/,
|
|
12
|
+
},
|
|
7
13
|
{ name: "var keyword", pattern: /\bvar\s+\w/, severity: "error", message: "Use const/let instead of var" },
|
|
8
14
|
{ name: "loose equality", pattern: /[^!=]==[^=]/, severity: "warning", message: "Use === instead of ==", exclude: /['"]use strict['"]/ },
|
|
9
15
|
{ name: "eval()", pattern: /\beval\s*\(/, severity: "error", message: "eval() is a security risk — never use it" },
|
|
10
16
|
{ name: "new Function()", pattern: /new\s+Function\s*\(/, severity: "error", message: "new Function() is equivalent to eval()" },
|
|
11
|
-
{
|
|
12
|
-
|
|
17
|
+
{
|
|
18
|
+
name: "innerHTML assignment",
|
|
19
|
+
pattern: /\.innerHTML\s*=/,
|
|
20
|
+
severity: "warning",
|
|
21
|
+
message: "innerHTML is an XSS vector — use textContent or DOM APIs",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "dangerouslySetInnerHTML",
|
|
25
|
+
pattern: /dangerouslySetInnerHTML/,
|
|
26
|
+
severity: "error",
|
|
27
|
+
message: "dangerouslySetInnerHTML bypasses React's XSS protection",
|
|
28
|
+
},
|
|
13
29
|
{ name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
|
|
14
30
|
{ name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
|
|
15
31
|
{ name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
|
|
16
|
-
{
|
|
32
|
+
{
|
|
33
|
+
name: "magic number",
|
|
34
|
+
pattern: /(?:timeout|delay|interval|limit|max|min)\s*[:=]\s*\d{4,}(?!\d)/,
|
|
35
|
+
severity: "warning",
|
|
36
|
+
message: "Large magic number — consider a named constant",
|
|
37
|
+
},
|
|
17
38
|
];
|
|
18
39
|
export function runStandards(cwd, stack) {
|
|
19
40
|
const start = Date.now();
|
|
@@ -25,7 +46,9 @@ export function runStandards(cwd, stack) {
|
|
|
25
46
|
try {
|
|
26
47
|
collectFiles(join(cwd, dir), cwd, files);
|
|
27
48
|
}
|
|
28
|
-
catch {
|
|
49
|
+
catch {
|
|
50
|
+
/* dir doesn't exist */
|
|
51
|
+
}
|
|
29
52
|
}
|
|
30
53
|
// ── File naming conventions ──
|
|
31
54
|
let namingViolations = 0;
|
|
@@ -49,7 +72,12 @@ export function runStandards(cwd, stack) {
|
|
|
49
72
|
// PascalCase .ts file (not a component) — unusual
|
|
50
73
|
// Only flag if it's not a class file
|
|
51
74
|
if (!/export (default )?class /.test(f.content)) {
|
|
52
|
-
issues.push({
|
|
75
|
+
issues.push({
|
|
76
|
+
severity: "warning",
|
|
77
|
+
message: `TS file uses PascalCase but doesn't export a class: ${name}`,
|
|
78
|
+
file: f.path,
|
|
79
|
+
rule: "file-naming",
|
|
80
|
+
});
|
|
53
81
|
}
|
|
54
82
|
}
|
|
55
83
|
}
|
|
@@ -71,11 +99,17 @@ export function runStandards(cwd, stack) {
|
|
|
71
99
|
const lines = f.content.split("\n");
|
|
72
100
|
for (let i = 0; i < lines.length; i++) {
|
|
73
101
|
const line = lines[i];
|
|
74
|
-
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
104
|
+
continue;
|
|
105
|
+
if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
|
|
75
106
|
continue;
|
|
76
107
|
for (const check of CODE_SMELLS) {
|
|
108
|
+
// Skip console.log in CLI entry points (intentional output)
|
|
109
|
+
if (check.name === "console.log" && (f.path.includes("cli.") || f.path.includes("bin/")))
|
|
110
|
+
continue;
|
|
77
111
|
if (check.pattern.test(line)) {
|
|
78
|
-
if (check.exclude
|
|
112
|
+
if (check.exclude?.test(line))
|
|
79
113
|
continue;
|
|
80
114
|
smellCount++;
|
|
81
115
|
issues.push({ severity: check.severity, message: check.message, file: f.path, line: i + 1, rule: check.name });
|
|
@@ -94,10 +128,16 @@ export function runStandards(cwd, stack) {
|
|
|
94
128
|
if (tsconfig.compilerOptions?.strict === true)
|
|
95
129
|
strictFound = true;
|
|
96
130
|
}
|
|
97
|
-
catch {
|
|
131
|
+
catch {
|
|
132
|
+
/* no tsconfig */
|
|
133
|
+
}
|
|
98
134
|
}
|
|
99
135
|
if (!strictFound) {
|
|
100
|
-
issues.push({
|
|
136
|
+
issues.push({
|
|
137
|
+
severity: "warning",
|
|
138
|
+
message: 'TypeScript strict mode not enabled — add "strict": true to tsconfig',
|
|
139
|
+
rule: "ts-strict",
|
|
140
|
+
});
|
|
101
141
|
}
|
|
102
142
|
}
|
|
103
143
|
// Tailwind: check for inline styles when TW is available
|
|
@@ -111,7 +151,11 @@ export function runStandards(cwd, stack) {
|
|
|
111
151
|
inlineStyles += matches.length;
|
|
112
152
|
}
|
|
113
153
|
if (inlineStyles > 10) {
|
|
114
|
-
issues.push({
|
|
154
|
+
issues.push({
|
|
155
|
+
severity: "warning",
|
|
156
|
+
message: `${inlineStyles} inline style objects in TSX — prefer Tailwind classes`,
|
|
157
|
+
rule: "prefer-tailwind",
|
|
158
|
+
});
|
|
115
159
|
}
|
|
116
160
|
}
|
|
117
161
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
@@ -137,7 +181,7 @@ function collectFiles(dir, cwd, out) {
|
|
|
137
181
|
else {
|
|
138
182
|
const ext = extname(entry);
|
|
139
183
|
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
140
|
-
out.push({ path: full.replace(cwd
|
|
184
|
+
out.push({ path: full.replace(`${cwd}/`, ""), content: readFileSync(full, "utf-8") });
|
|
141
185
|
}
|
|
142
186
|
}
|
|
143
187
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Project structure check — does the repo have standard files and conventions? */
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
4
|
import { gradeFromScore } from "../types.js";
|
|
5
5
|
const EXPECTED_FILES = [
|
|
6
6
|
{ name: "package.json", path: "package.json", required: true, description: "Package manifest" },
|
|
@@ -54,7 +54,11 @@ export function runStructure(cwd, stack) {
|
|
|
54
54
|
issues.push({ severity: "error", message: `No test files found (${srcCount} source files with zero tests)`, rule: "no-tests" });
|
|
55
55
|
}
|
|
56
56
|
else if (testRatio < 0.3 && srcCount > 3) {
|
|
57
|
-
issues.push({
|
|
57
|
+
issues.push({
|
|
58
|
+
severity: "warning",
|
|
59
|
+
message: `Low test-to-source ratio: ${testCount} tests for ${srcCount} source files (${Math.round(testRatio * 100)}%)`,
|
|
60
|
+
rule: "low-test-ratio",
|
|
61
|
+
});
|
|
58
62
|
}
|
|
59
63
|
// Check package.json has essential scripts
|
|
60
64
|
try {
|
|
@@ -65,7 +69,9 @@ export function runStructure(cwd, stack) {
|
|
|
65
69
|
if (!scripts.build && !scripts.dev)
|
|
66
70
|
issues.push({ severity: "info", message: "No 'build' or 'dev' script in package.json", rule: "no-build-script" });
|
|
67
71
|
}
|
|
68
|
-
catch {
|
|
72
|
+
catch {
|
|
73
|
+
/* no package.json or parse error */
|
|
74
|
+
}
|
|
69
75
|
const errors = issues.filter((i) => i.severity === "error").length;
|
|
70
76
|
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
71
77
|
const score = Math.max(0, Math.min(100, 100 - errors * 15 - warnings * 5));
|
|
@@ -73,7 +79,7 @@ export function runStructure(cwd, stack) {
|
|
|
73
79
|
name: "structure",
|
|
74
80
|
score,
|
|
75
81
|
grade: gradeFromScore(score),
|
|
76
|
-
details: { found, missing, srcFiles: srcCount, testFiles: testCount, testRatio: Math.round(testRatio * 100)
|
|
82
|
+
details: { found, missing, srcFiles: srcCount, testFiles: testCount, testRatio: `${Math.round(testRatio * 100)}%` },
|
|
77
83
|
issues,
|
|
78
84
|
duration: Date.now() - start,
|
|
79
85
|
};
|
|
@@ -84,7 +90,9 @@ function collectAll(cwd, src, test) {
|
|
|
84
90
|
try {
|
|
85
91
|
walk(join(cwd, dir), src, test);
|
|
86
92
|
}
|
|
87
|
-
catch {
|
|
93
|
+
catch {
|
|
94
|
+
/* dir doesn't exist */
|
|
95
|
+
}
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
function walk(dir, src, test) {
|
package/dist/runners/testing.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 5. Test quality — naming, assertions, mocking patterns, snapshot smell
|
|
9
9
|
* 6. Pyramid balance — right ratio of fast-to-slow tests
|
|
10
10
|
*/
|
|
11
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { basename, extname, join } from "node:path";
|
|
13
13
|
import { gradeFromScore } from "../types.js";
|
|
14
14
|
import { run } from "./exec.js";
|
|
@@ -36,7 +36,7 @@ function classifyTestFile(relPath, content) {
|
|
|
36
36
|
return "unit";
|
|
37
37
|
}
|
|
38
38
|
function countPatterns(content) {
|
|
39
|
-
const assertions = (content.match(/\bexpect\s*\(/g) || []).length + (content.match(/\bassert[
|
|
39
|
+
const assertions = (content.match(/\bexpect\s*\(/g) || []).length + (content.match(/\bassert[.(]/g) || []).length;
|
|
40
40
|
const mocks = (content.match(/\b(vi\.fn|jest\.fn|vi\.mock|jest\.mock|vi\.spyOn|jest\.spyOn|sinon\.(stub|spy|mock)|\.mockResolvedValue|\.mockReturnValue|\.mockImplementation)\b/g) || []).length;
|
|
41
41
|
const snapshots = (content.match(/\btoMatchSnapshot|toMatchInlineSnapshot\b/g) || []).length;
|
|
42
42
|
const describes = (content.match(/\bdescribe\s*\(/g) || []).length;
|
|
@@ -67,10 +67,16 @@ function walkTests(dir, cwd, out) {
|
|
|
67
67
|
const ext = extname(entry);
|
|
68
68
|
if (![".ts", ".tsx", ".js", ".jsx"].includes(ext))
|
|
69
69
|
continue;
|
|
70
|
-
if (!entry.includes(".test.") &&
|
|
70
|
+
if (!entry.includes(".test.") &&
|
|
71
|
+
!entry.includes(".spec.") &&
|
|
72
|
+
!entry.includes(".e2e.") &&
|
|
73
|
+
!entry.includes(".int.") &&
|
|
74
|
+
!dir.includes("__tests__") &&
|
|
75
|
+
!dir.includes("/e2e") &&
|
|
76
|
+
!dir.includes("/test"))
|
|
71
77
|
continue;
|
|
72
78
|
const content = readFileSync(full, "utf-8");
|
|
73
|
-
const relPath = full.replace(cwd
|
|
79
|
+
const relPath = full.replace(`${cwd}/`, "");
|
|
74
80
|
const layer = classifyTestFile(relPath, content);
|
|
75
81
|
const patterns = countPatterns(content);
|
|
76
82
|
out.push({
|
|
@@ -88,7 +94,9 @@ function findSourceFiles(cwd) {
|
|
|
88
94
|
try {
|
|
89
95
|
walkSource(join(cwd, dir), cwd, files);
|
|
90
96
|
}
|
|
91
|
-
catch {
|
|
97
|
+
catch {
|
|
98
|
+
/* dir doesn't exist */
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
return files;
|
|
94
102
|
}
|
|
@@ -103,7 +111,7 @@ function walkSource(dir, cwd, out) {
|
|
|
103
111
|
else {
|
|
104
112
|
const ext = extname(entry);
|
|
105
113
|
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
|
|
106
|
-
out.push(full.replace(cwd
|
|
114
|
+
out.push(full.replace(`${cwd}/`, ""));
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
}
|
|
@@ -175,7 +183,9 @@ function collectCoverage(cwd, stack) {
|
|
|
175
183
|
};
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
|
-
catch {
|
|
186
|
+
catch {
|
|
187
|
+
/* parse failed */
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
return null;
|
|
@@ -184,9 +194,7 @@ function collectCoverage(cwd, stack) {
|
|
|
184
194
|
function executeTests(cwd, stack) {
|
|
185
195
|
if (stack.testRunner === "none")
|
|
186
196
|
return null;
|
|
187
|
-
const cmd = stack.testRunner === "vitest"
|
|
188
|
-
? "npx vitest run --reporter=json 2>/dev/null || true"
|
|
189
|
-
: "npx jest --json 2>/dev/null || true";
|
|
197
|
+
const cmd = stack.testRunner === "vitest" ? "npx vitest run --reporter=json 2>/dev/null || true" : "npx jest --json 2>/dev/null || true";
|
|
190
198
|
const { stdout } = run(cmd, cwd, 120_000);
|
|
191
199
|
try {
|
|
192
200
|
const jsonStart = stdout.indexOf("{");
|
|
@@ -199,7 +207,9 @@ function executeTests(cwd, stack) {
|
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
|
-
catch {
|
|
210
|
+
catch {
|
|
211
|
+
/* parse failed */
|
|
212
|
+
}
|
|
203
213
|
return null;
|
|
204
214
|
}
|
|
205
215
|
function analyzeQuality(testFiles) {
|
|
@@ -253,10 +263,30 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
253
263
|
for (const f of testFiles)
|
|
254
264
|
layers[f.layer].push(f);
|
|
255
265
|
const pyramid = {
|
|
256
|
-
unit: {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
unit: {
|
|
267
|
+
present: layers.unit.length > 0,
|
|
268
|
+
files: layers.unit.length,
|
|
269
|
+
assertions: layers.unit.reduce((s, f) => s + f.assertions, 0),
|
|
270
|
+
score: 0,
|
|
271
|
+
},
|
|
272
|
+
integration: {
|
|
273
|
+
present: layers.integration.length > 0,
|
|
274
|
+
files: layers.integration.length,
|
|
275
|
+
assertions: layers.integration.reduce((s, f) => s + f.assertions, 0),
|
|
276
|
+
score: 0,
|
|
277
|
+
},
|
|
278
|
+
component: {
|
|
279
|
+
present: layers.component.length > 0,
|
|
280
|
+
files: layers.component.length,
|
|
281
|
+
assertions: layers.component.reduce((s, f) => s + f.assertions, 0),
|
|
282
|
+
score: 0,
|
|
283
|
+
},
|
|
284
|
+
e2e: {
|
|
285
|
+
present: layers.e2e.length > 0,
|
|
286
|
+
files: layers.e2e.length,
|
|
287
|
+
assertions: layers.e2e.reduce((s, f) => s + f.assertions, 0),
|
|
288
|
+
score: 0,
|
|
289
|
+
},
|
|
260
290
|
};
|
|
261
291
|
// 3. E2E tool detection
|
|
262
292
|
const e2eTool = detectE2E(cwd);
|
|
@@ -267,14 +297,26 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
267
297
|
issues.push({ severity: "error", message: "No unit tests found — every project needs unit tests", rule: "no-unit-tests" });
|
|
268
298
|
}
|
|
269
299
|
if (!pyramid.integration.present && srcFiles.length > 5) {
|
|
270
|
-
issues.push({
|
|
300
|
+
issues.push({
|
|
301
|
+
severity: "warning",
|
|
302
|
+
message: "No integration tests — consider testing service boundaries",
|
|
303
|
+
rule: "no-integration-tests",
|
|
304
|
+
});
|
|
271
305
|
}
|
|
272
306
|
if (needsComponent && !pyramid.component.present) {
|
|
273
|
-
issues.push({
|
|
307
|
+
issues.push({
|
|
308
|
+
severity: "warning",
|
|
309
|
+
message: `${stack.framework} project with no component tests — test your UI components`,
|
|
310
|
+
rule: "no-component-tests",
|
|
311
|
+
});
|
|
274
312
|
}
|
|
275
313
|
if (needsE2E && !pyramid.e2e.present) {
|
|
276
314
|
if (e2eTool.tool === "none") {
|
|
277
|
-
issues.push({
|
|
315
|
+
issues.push({
|
|
316
|
+
severity: "warning",
|
|
317
|
+
message: "No E2E test framework (Playwright/Cypress) — critical user flows untested",
|
|
318
|
+
rule: "no-e2e-framework",
|
|
319
|
+
});
|
|
278
320
|
}
|
|
279
321
|
else if (!e2eTool.configured) {
|
|
280
322
|
issues.push({ severity: "info", message: `${e2eTool.tool} installed but not configured`, rule: "e2e-not-configured" });
|
|
@@ -285,7 +327,7 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
285
327
|
}
|
|
286
328
|
// 5. File pairing analysis
|
|
287
329
|
const pairing = computePairing(srcFiles, testFiles);
|
|
288
|
-
const pairingPct = srcFiles.length > 0 ? Math.round((
|
|
330
|
+
const pairingPct = srcFiles.length > 0 ? Math.round((pairing.paired / Math.max(1, srcFiles.length - countUntestable(srcFiles))) * 100) : 100;
|
|
289
331
|
if (pairingPct < 30) {
|
|
290
332
|
issues.push({ severity: "warning", message: `Only ${pairingPct}% of source files have matching test files`, rule: "low-test-pairing" });
|
|
291
333
|
}
|
|
@@ -299,13 +341,25 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
299
341
|
// 6. Test quality analysis
|
|
300
342
|
const quality = analyzeQuality(testFiles);
|
|
301
343
|
if (quality.avgAssertionsPerTest < 1) {
|
|
302
|
-
issues.push({
|
|
344
|
+
issues.push({
|
|
345
|
+
severity: "warning",
|
|
346
|
+
message: `Low assertion density: ${quality.avgAssertionsPerTest} assertions/test (aim for 2+)`,
|
|
347
|
+
rule: "low-assertions",
|
|
348
|
+
});
|
|
303
349
|
}
|
|
304
350
|
if (quality.mockRatio > 0.5) {
|
|
305
|
-
issues.push({
|
|
351
|
+
issues.push({
|
|
352
|
+
severity: "warning",
|
|
353
|
+
message: `High mock ratio: ${quality.mockRatio} mocks per assertion — tests may not reflect real behavior`,
|
|
354
|
+
rule: "over-mocked",
|
|
355
|
+
});
|
|
306
356
|
}
|
|
307
357
|
if (quality.snapshotRatio > 0.3) {
|
|
308
|
-
issues.push({
|
|
358
|
+
issues.push({
|
|
359
|
+
severity: "warning",
|
|
360
|
+
message: `${Math.round(quality.snapshotRatio * 100)}% of assertions are snapshots — brittle, prefer explicit assertions`,
|
|
361
|
+
rule: "snapshot-heavy",
|
|
362
|
+
});
|
|
309
363
|
}
|
|
310
364
|
// 7. Execute tests + collect coverage (unless --skip-tests)
|
|
311
365
|
let execution = null;
|
|
@@ -321,17 +375,30 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
321
375
|
}
|
|
322
376
|
if (coverage) {
|
|
323
377
|
if (coverage.branches < 50) {
|
|
324
|
-
issues.push({
|
|
378
|
+
issues.push({
|
|
379
|
+
severity: "warning",
|
|
380
|
+
message: `Branch coverage ${coverage.branches}% — below 50% threshold`,
|
|
381
|
+
rule: "low-branch-coverage",
|
|
382
|
+
});
|
|
325
383
|
}
|
|
326
384
|
if (coverage.statements < 60) {
|
|
327
|
-
issues.push({
|
|
385
|
+
issues.push({
|
|
386
|
+
severity: "warning",
|
|
387
|
+
message: `Statement coverage ${coverage.statements}% — below 60% threshold`,
|
|
388
|
+
rule: "low-statement-coverage",
|
|
389
|
+
});
|
|
328
390
|
}
|
|
329
391
|
}
|
|
330
392
|
}
|
|
331
393
|
// 8. Compute composite score
|
|
332
394
|
let score = 0;
|
|
333
395
|
// Pyramid presence (30 points)
|
|
334
|
-
const layerCount = [
|
|
396
|
+
const layerCount = [
|
|
397
|
+
pyramid.unit.present,
|
|
398
|
+
pyramid.integration.present,
|
|
399
|
+
pyramid.component.present && needsComponent,
|
|
400
|
+
pyramid.e2e.present && needsE2E,
|
|
401
|
+
].filter(Boolean).length;
|
|
335
402
|
const expectedLayers = 1 + (srcFiles.length > 5 ? 1 : 0) + (needsComponent ? 1 : 0) + (needsE2E ? 1 : 0);
|
|
336
403
|
const pyramidScore = expectedLayers > 0 ? Math.round((layerCount / expectedLayers) * 30) : 30;
|
|
337
404
|
score += pyramidScore;
|
|
@@ -376,18 +443,20 @@ export function runTesting(cwd, stack, skipExec) {
|
|
|
376
443
|
component: pyramid.component.files,
|
|
377
444
|
e2e: pyramid.e2e.files,
|
|
378
445
|
},
|
|
379
|
-
layersPresent: layerCount
|
|
446
|
+
layersPresent: `${layerCount}/${expectedLayers}`,
|
|
380
447
|
e2eTool: e2eTool.tool,
|
|
381
448
|
testFiles: testFiles.length,
|
|
382
449
|
srcFiles: srcFiles.length,
|
|
383
|
-
pairing: pairingPct
|
|
450
|
+
pairing: `${pairingPct}%`,
|
|
384
451
|
quality: {
|
|
385
452
|
assertionsPerTest: quality.avgAssertionsPerTest,
|
|
386
453
|
mockRatio: quality.mockRatio,
|
|
387
454
|
snapshotRatio: quality.snapshotRatio,
|
|
388
455
|
},
|
|
389
456
|
...(execution ? { passed: execution.passed, failed: execution.failed, total: execution.total } : {}),
|
|
390
|
-
...(coverage
|
|
457
|
+
...(coverage
|
|
458
|
+
? { coverage: { stmts: coverage.statements, branches: coverage.branches, lines: coverage.lines, fns: coverage.functions } }
|
|
459
|
+
: {}),
|
|
391
460
|
},
|
|
392
461
|
issues,
|
|
393
462
|
duration: Date.now() - start,
|
|
@@ -21,18 +21,31 @@ export function runTypeSafety(cwd) {
|
|
|
21
21
|
try {
|
|
22
22
|
collectFiles(join(cwd, dir), files);
|
|
23
23
|
}
|
|
24
|
-
catch {
|
|
24
|
+
catch {
|
|
25
|
+
/* dir doesn't exist */
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
if (files.length === 0) {
|
|
27
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
name: "type-safety",
|
|
31
|
+
score: 100,
|
|
32
|
+
grade: "A",
|
|
33
|
+
details: { skipped: true, reason: "no source files" },
|
|
34
|
+
issues: [],
|
|
35
|
+
duration: Date.now() - start,
|
|
36
|
+
};
|
|
28
37
|
}
|
|
29
38
|
for (const file of files) {
|
|
30
39
|
const content = readFileSync(file, "utf-8");
|
|
31
|
-
const relPath = file.replace(cwd
|
|
40
|
+
const relPath = file.replace(`${cwd}/`, "");
|
|
32
41
|
const lines = content.split("\n");
|
|
33
42
|
for (let i = 0; i < lines.length; i++) {
|
|
34
43
|
const line = lines[i];
|
|
35
|
-
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
46
|
+
continue;
|
|
47
|
+
// Skip pattern definition lines (prevents false positives when scanning own code)
|
|
48
|
+
if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
|
|
36
49
|
continue;
|
|
37
50
|
for (const p of PATTERNS) {
|
|
38
51
|
const matches = line.match(p.pattern);
|
|
@@ -6,8 +6,7 @@ import { run } from "./exec.js";
|
|
|
6
6
|
export function runTypeCheck(cwd) {
|
|
7
7
|
const start = Date.now();
|
|
8
8
|
const issues = [];
|
|
9
|
-
if (!existsSync(join(cwd, "tsconfig.json")) &&
|
|
10
|
-
!existsSync(join(cwd, "tsconfig.app.json"))) {
|
|
9
|
+
if (!existsSync(join(cwd, "tsconfig.json")) && !existsSync(join(cwd, "tsconfig.app.json"))) {
|
|
11
10
|
return {
|
|
12
11
|
name: "types",
|
|
13
12
|
score: 0,
|
|
@@ -25,7 +24,7 @@ export function runTypeCheck(cwd) {
|
|
|
25
24
|
issues.push({
|
|
26
25
|
severity: "error",
|
|
27
26
|
file: match[1],
|
|
28
|
-
line: parseInt(match[2]),
|
|
27
|
+
line: parseInt(match[2], 10),
|
|
29
28
|
rule: match[3],
|
|
30
29
|
message: match[4],
|
|
31
30
|
});
|