@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
package/dist/check-meta.js
CHANGED
|
@@ -153,7 +153,7 @@ export const CHECK_META = {
|
|
|
153
153
|
},
|
|
154
154
|
};
|
|
155
155
|
export function getCheckMeta(name) {
|
|
156
|
-
return CHECK_META[name] || {
|
|
156
|
+
return (CHECK_META[name] || {
|
|
157
157
|
name,
|
|
158
158
|
label: name,
|
|
159
159
|
category: "Other",
|
|
@@ -162,5 +162,5 @@ export function getCheckMeta(name) {
|
|
|
162
162
|
description: "",
|
|
163
163
|
risk: "",
|
|
164
164
|
recommendation: "",
|
|
165
|
-
};
|
|
165
|
+
});
|
|
166
166
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** vibe-check — code health scanner for the AI coding era. */
|
|
3
|
-
import { existsSync, mkdirSync,
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { detectRepoUrl, detectStack } from "./detect.js";
|
|
6
6
|
import { generateHTML } from "./report/html.js";
|
|
7
|
-
import { runComplexity } from "./runners/complexity.js";
|
|
8
7
|
import { runArchitecture } from "./runners/architecture.js";
|
|
8
|
+
import { runComplexity } from "./runners/complexity.js";
|
|
9
9
|
import { runConfusion } from "./runners/confusion.js";
|
|
10
10
|
import { runContext } from "./runners/context.js";
|
|
11
11
|
import { runDependencies } from "./runners/dependencies.js";
|
|
@@ -17,8 +17,8 @@ import { runSecurity } from "./runners/security.js";
|
|
|
17
17
|
import { runStandards } from "./runners/standards.js";
|
|
18
18
|
import { runStructure } from "./runners/structure.js";
|
|
19
19
|
import { runTesting } from "./runners/testing.js";
|
|
20
|
-
import { runTypeCheck } from "./runners/types-check.js";
|
|
21
20
|
import { runTypeSafety } from "./runners/type-safety.js";
|
|
21
|
+
import { runTypeCheck } from "./runners/types-check.js";
|
|
22
22
|
import { computeScore } from "./score.js";
|
|
23
23
|
import { computeTrend, formatTrend } from "./trend.js";
|
|
24
24
|
import { gradeFromScore } from "./types.js";
|
|
@@ -43,14 +43,14 @@ async function main() {
|
|
|
43
43
|
const start = Date.now();
|
|
44
44
|
if (!jsonOnly) {
|
|
45
45
|
console.log("");
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(
|
|
46
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
47
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
48
48
|
console.log("");
|
|
49
49
|
}
|
|
50
50
|
const stack = detectStack(cwd);
|
|
51
51
|
if (!jsonOnly) {
|
|
52
52
|
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
53
|
-
console.log(
|
|
53
|
+
console.log(` stack: ${parts.join(" + ")}`);
|
|
54
54
|
console.log("");
|
|
55
55
|
}
|
|
56
56
|
const checks = [];
|
|
@@ -80,14 +80,14 @@ async function main() {
|
|
|
80
80
|
];
|
|
81
81
|
for (const runner of runners) {
|
|
82
82
|
if (!jsonOnly)
|
|
83
|
-
process.stdout.write(
|
|
83
|
+
process.stdout.write(` ${runner.name.padEnd(14)}`);
|
|
84
84
|
const result = runner.fn();
|
|
85
85
|
checks.push(result);
|
|
86
86
|
if (!jsonOnly) {
|
|
87
87
|
const skipped = result.details.skipped;
|
|
88
88
|
const c = skipped ? "\x1b[2m" : color(result.grade);
|
|
89
89
|
const label = skipped ? "skip" : result.grade;
|
|
90
|
-
const scoreStr = skipped ? "—" : result.score
|
|
90
|
+
const scoreStr = skipped ? "—" : `${result.score}/100`;
|
|
91
91
|
const issueStr = result.issues.length > 0 ? ` \x1b[2m${result.issues.length} issues\x1b[0m` : "";
|
|
92
92
|
console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
93
93
|
}
|
|
@@ -115,13 +115,17 @@ async function main() {
|
|
|
115
115
|
const historyFile = join(historyDir, `${report.timestamp.replace(/[:.]/g, "-")}.json`);
|
|
116
116
|
writeFileSync(historyFile, JSON.stringify(report, null, 2));
|
|
117
117
|
// Keep only last 30 history entries
|
|
118
|
-
const historyFiles = readdirSync(historyDir)
|
|
118
|
+
const historyFiles = readdirSync(historyDir)
|
|
119
|
+
.filter((f) => f.endsWith(".json"))
|
|
120
|
+
.sort();
|
|
119
121
|
if (historyFiles.length > 30) {
|
|
120
122
|
for (const old of historyFiles.slice(0, historyFiles.length - 30)) {
|
|
121
123
|
try {
|
|
122
124
|
unlinkSync(join(historyDir, old));
|
|
123
125
|
}
|
|
124
|
-
catch {
|
|
126
|
+
catch {
|
|
127
|
+
/* ignore */
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
writeFileSync(join(outputDir, "report.json"), JSON.stringify(report, null, 2));
|
|
@@ -136,8 +140,8 @@ async function main() {
|
|
|
136
140
|
if (trend)
|
|
137
141
|
console.log(formatTrend(trend));
|
|
138
142
|
console.log("");
|
|
139
|
-
console.log(
|
|
140
|
-
console.log(
|
|
143
|
+
console.log(` \x1b[2mReport: ${join(outputDir, "report.html")}\x1b[0m`);
|
|
144
|
+
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
141
145
|
console.log("");
|
|
142
146
|
}
|
|
143
147
|
if (ciMode && score < 60) {
|
|
@@ -145,11 +149,13 @@ async function main() {
|
|
|
145
149
|
}
|
|
146
150
|
if (!jsonOnly && !ciMode && !watchMode) {
|
|
147
151
|
try {
|
|
148
|
-
const {
|
|
152
|
+
const { execFileSync } = await import("node:child_process");
|
|
149
153
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
150
|
-
|
|
154
|
+
execFileSync(openCmd, [join(outputDir, "report.html")], { stdio: "ignore" });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* failed to open browser */
|
|
151
158
|
}
|
|
152
|
-
catch { /* failed to open browser */ }
|
|
153
159
|
}
|
|
154
160
|
// Watch mode — re-run on file changes
|
|
155
161
|
if (watchMode) {
|
|
@@ -162,15 +168,20 @@ async function main() {
|
|
|
162
168
|
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
163
169
|
console.log("");
|
|
164
170
|
let debounce = null;
|
|
171
|
+
let running = false;
|
|
165
172
|
for (const dir of srcDirs) {
|
|
166
173
|
watch(dir, { recursive: true }, (_event, filename) => {
|
|
167
174
|
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
168
175
|
return;
|
|
176
|
+
if (running)
|
|
177
|
+
return; // prevent concurrent re-runs (M5)
|
|
169
178
|
if (debounce)
|
|
170
179
|
clearTimeout(debounce);
|
|
171
|
-
debounce = setTimeout(() => {
|
|
180
|
+
debounce = setTimeout(async () => {
|
|
181
|
+
running = true;
|
|
172
182
|
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
173
|
-
main().catch(() => { });
|
|
183
|
+
await main().catch(() => { });
|
|
184
|
+
running = false;
|
|
174
185
|
}, 500);
|
|
175
186
|
});
|
|
176
187
|
}
|
package/dist/detect.js
CHANGED
|
@@ -20,33 +20,11 @@ export function detectStack(cwd) {
|
|
|
20
20
|
: allDeps.react || allDeps.vue
|
|
21
21
|
? "javascript"
|
|
22
22
|
: "unknown";
|
|
23
|
-
const framework = allDeps.react
|
|
24
|
-
|
|
25
|
-
: allDeps.vue
|
|
26
|
-
? "vue"
|
|
27
|
-
: allDeps.svelte
|
|
28
|
-
? "svelte"
|
|
29
|
-
: "none";
|
|
30
|
-
const bundler = allDeps.vite
|
|
31
|
-
? "vite"
|
|
32
|
-
: allDeps.webpack
|
|
33
|
-
? "webpack"
|
|
34
|
-
: allDeps.esbuild
|
|
35
|
-
? "esbuild"
|
|
36
|
-
: "none";
|
|
23
|
+
const framework = allDeps.react ? "react" : allDeps.vue ? "vue" : allDeps.svelte ? "svelte" : "none";
|
|
24
|
+
const bundler = allDeps.vite ? "vite" : allDeps.webpack ? "webpack" : allDeps.esbuild ? "esbuild" : "none";
|
|
37
25
|
const testRunner = allDeps.vitest ? "vitest" : allDeps.jest ? "jest" : "none";
|
|
38
|
-
const linter = allDeps["@biomejs/biome"]
|
|
39
|
-
|
|
40
|
-
: allDeps.eslint
|
|
41
|
-
? "eslint"
|
|
42
|
-
: "none";
|
|
43
|
-
const packageManager = has("pnpm-lock.yaml")
|
|
44
|
-
? "pnpm"
|
|
45
|
-
: has("bun.lockb")
|
|
46
|
-
? "bun"
|
|
47
|
-
: has("yarn.lock")
|
|
48
|
-
? "yarn"
|
|
49
|
-
: "npm";
|
|
26
|
+
const linter = allDeps["@biomejs/biome"] ? "biome" : allDeps.eslint ? "eslint" : "none";
|
|
27
|
+
const packageManager = has("pnpm-lock.yaml") ? "pnpm" : has("bun.lockb") ? "bun" : has("yarn.lock") ? "yarn" : "npm";
|
|
50
28
|
return { language, framework, bundler, testRunner, linter, packageManager };
|
|
51
29
|
}
|
|
52
30
|
/** Detect GitHub/GitLab repo URL from git remote. */
|
|
@@ -55,7 +33,7 @@ export function detectRepoUrl(cwd) {
|
|
|
55
33
|
const remote = execSync("git remote get-url origin", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
56
34
|
const branch = execSync("git branch --show-current", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim() || "main";
|
|
57
35
|
// Convert SSH to HTTPS
|
|
58
|
-
|
|
36
|
+
const url = remote
|
|
59
37
|
.replace(/^git@github\.com:/, "https://github.com/")
|
|
60
38
|
.replace(/^git@gitlab\.com:/, "https://gitlab.com/")
|
|
61
39
|
.replace(/\.git$/, "");
|
package/dist/fs-utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared filesystem utilities — eliminates duplicate file-walking across runners. */
|
|
2
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, extname, join } from "node:path";
|
|
4
4
|
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results", "__pycache__"]);
|
|
5
5
|
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
@@ -12,7 +12,9 @@ export function collectSourceFiles(cwd, opts) {
|
|
|
12
12
|
try {
|
|
13
13
|
walk(join(cwd, dir), cwd, files, opts?.extraExts ? ALL_EXTS : CODE_EXTS);
|
|
14
14
|
}
|
|
15
|
-
catch {
|
|
15
|
+
catch {
|
|
16
|
+
/* dir doesn't exist */
|
|
17
|
+
}
|
|
16
18
|
}
|
|
17
19
|
if (opts?.includeTests)
|
|
18
20
|
return files;
|
|
@@ -53,6 +55,9 @@ function walk(dir, cwd, out, exts) {
|
|
|
53
55
|
if (SKIP_DIRS.has(entry))
|
|
54
56
|
continue;
|
|
55
57
|
const full = join(dir, entry);
|
|
58
|
+
// Skip symlinks to prevent traversal attacks (H3)
|
|
59
|
+
if (lstatSync(full).isSymbolicLink())
|
|
60
|
+
continue;
|
|
56
61
|
if (statSync(full).isDirectory()) {
|
|
57
62
|
walk(full, cwd, out, exts);
|
|
58
63
|
}
|
|
@@ -60,8 +65,11 @@ function walk(dir, cwd, out, exts) {
|
|
|
60
65
|
const ext = extname(entry);
|
|
61
66
|
if (!exts.has(ext))
|
|
62
67
|
continue;
|
|
68
|
+
// Skip files over 1MB to prevent memory issues (M1)
|
|
69
|
+
if (statSync(full).size > 1_000_000)
|
|
70
|
+
continue;
|
|
63
71
|
const content = readFileSync(full, "utf-8");
|
|
64
|
-
const relPath = full.replace(cwd
|
|
72
|
+
const relPath = full.replace(`${cwd}/`, "");
|
|
65
73
|
const isTest = entry.includes(".test.") || entry.includes(".spec.") || relPath.includes("__tests__");
|
|
66
74
|
out.push({
|
|
67
75
|
path: relPath,
|
package/dist/report/html.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { getCheckMeta } from "../check-meta.js";
|
|
12
12
|
import { generateArchSVG } from "../runners/architecture.js";
|
|
13
13
|
function e(s) {
|
|
14
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
14
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15
15
|
}
|
|
16
16
|
/** Make a file path a clickable GitHub link if repoUrl is available. */
|
|
17
17
|
function fileLink(path, line, repoUrl, branch) {
|
|
18
|
-
const clean = path.split(":")[0];
|
|
19
|
-
if (!repoUrl)
|
|
18
|
+
const clean = path.split(":")[0];
|
|
19
|
+
if (!repoUrl || !/^https?:\/\//.test(repoUrl))
|
|
20
20
|
return e(path);
|
|
21
|
-
const href = `${repoUrl}/blob/${branch}/${clean}${line ?
|
|
21
|
+
const href = `${repoUrl}/blob/${branch}/${clean}${line ? `#L${line}` : ""}`;
|
|
22
22
|
return `<a href="${e(href)}" target="_blank" rel="noopener" class="flink">${e(path)}</a>`;
|
|
23
23
|
}
|
|
24
24
|
function gc(grade) {
|
|
@@ -83,17 +83,24 @@ export function generateHTML(report) {
|
|
|
83
83
|
const topNav = topNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
|
|
84
84
|
// Overview page
|
|
85
85
|
const ringPct = report.score;
|
|
86
|
-
const barChart = active
|
|
86
|
+
const barChart = active
|
|
87
|
+
.sort((a, b) => a.score - b.score)
|
|
88
|
+
.map((c) => {
|
|
87
89
|
return `<div class="brow"><span class="bl">${e(c.name)}</span><div class="bb"><div class="bf" style="width:${c.score}%;background:${gc(c.grade)}"></div></div><span class="bv" style="color:${gc(c.grade)}">${c.grade} ${c.score}</span></div>`;
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
+
})
|
|
91
|
+
.join("");
|
|
92
|
+
const catCards = catScores
|
|
93
|
+
.map((cs) => {
|
|
90
94
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
91
|
-
const mini = cs.checks
|
|
95
|
+
const mini = cs.checks
|
|
96
|
+
.map((c) => {
|
|
92
97
|
const sk = c.details.skipped;
|
|
93
98
|
return `<span class="mc" style="color:${sk ? "#555" : gc(c.grade)}" title="${e(c.name)}: ${sk ? "skip" : c.score}">${sk ? "—" : c.grade}</span>`;
|
|
94
|
-
})
|
|
99
|
+
})
|
|
100
|
+
.join("");
|
|
95
101
|
return `<div class="cc" onclick="go('${cs.id}')"><div class="cc-s" style="color:${clr}">${cs.avg}</div><div class="cc-l">${cs.label}</div><div class="cc-m">${mini}</div></div>`;
|
|
96
|
-
})
|
|
102
|
+
})
|
|
103
|
+
.join("");
|
|
97
104
|
const radarSvg = buildRadar(catScores.map((cs) => ({ label: cs.label, score: cs.avg })));
|
|
98
105
|
const overviewPage = `<div id="p-overview" class="page active">
|
|
99
106
|
<div class="dash">
|
|
@@ -106,22 +113,31 @@ export function generateHTML(report) {
|
|
|
106
113
|
<div class="cats">${catCards}</div>
|
|
107
114
|
<h3>All Checks</h3>
|
|
108
115
|
<div class="bars">${barChart}</div>
|
|
109
|
-
<div class="stack">${Object.entries(report.meta.stack)
|
|
116
|
+
<div class="stack">${Object.entries(report.meta.stack)
|
|
117
|
+
.filter(([, v]) => v !== "none" && v !== "unknown")
|
|
118
|
+
.map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
|
|
119
|
+
.join("")}</div>
|
|
110
120
|
</div>`;
|
|
111
121
|
// Category pages (with sub-nav tabs for each check)
|
|
112
122
|
let catPages = "";
|
|
113
123
|
for (const cs of catScores) {
|
|
114
|
-
const subNav = cs.checks
|
|
124
|
+
const subNav = cs.checks
|
|
125
|
+
.map((c, i) => {
|
|
115
126
|
const sk = c.details.skipped;
|
|
116
127
|
return `<a class="sn${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}" onclick="sub(this,'${cs.id}')">${e(c.name)} <span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span></a>`;
|
|
117
|
-
})
|
|
118
|
-
|
|
128
|
+
})
|
|
129
|
+
.join("");
|
|
130
|
+
const subPages = cs.checks
|
|
131
|
+
.map((c, i) => {
|
|
119
132
|
const meta = getCheckMeta(c.name);
|
|
120
133
|
const sk = c.details.skipped;
|
|
121
|
-
const detailsFiltered = Object.entries(c.details)
|
|
134
|
+
const detailsFiltered = Object.entries(c.details)
|
|
135
|
+
.filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
|
|
136
|
+
.map(([k, v]) => {
|
|
122
137
|
const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
123
138
|
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
124
|
-
})
|
|
139
|
+
})
|
|
140
|
+
.join("");
|
|
125
141
|
// Group issues by file
|
|
126
142
|
const byFile = new Map();
|
|
127
143
|
const noFile = [];
|
|
@@ -140,9 +156,8 @@ export function generateHTML(report) {
|
|
|
140
156
|
for (const [file, issues] of byFile) {
|
|
141
157
|
issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
|
|
142
158
|
for (const iss of issues) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span>${iss.line ? `<span class="il">${iss.line}</span>` : ""}<span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}<button class="cp-btn" onclick="navigator.clipboard.writeText('${safePrompt}');this.textContent='✓';setTimeout(()=>this.textContent='📋',1000)" title="Copy fix prompt">📋</button></div>`;
|
|
159
|
+
const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
|
|
160
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span>${iss.line ? `<span class="il">${iss.line}</span>` : ""}<span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}<button class="cp-btn" data-prompt="${e(prompt)}" title="Copy fix prompt">📋</button></div>`;
|
|
146
161
|
}
|
|
147
162
|
issuesHtml += `</div>`;
|
|
148
163
|
}
|
|
@@ -154,14 +169,15 @@ export function generateHTML(report) {
|
|
|
154
169
|
issuesHtml += `</div>`;
|
|
155
170
|
}
|
|
156
171
|
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
157
|
-
<div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : c.score
|
|
172
|
+
<div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : `${c.score}/100`} · weight ${meta.weight}% · ${c.duration}ms · ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
|
|
158
173
|
${meta.description ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">What</span><span>${e(meta.description)}</span></div><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div><div class="ip-row"><span class="ip-label">Fix</span><span>${e(meta.recommendation)}</span></div></div>` : ""}
|
|
159
174
|
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
160
175
|
${c.name === "architecture" && !sk ? `<div class="arch-svg">${generateArchSVG(c.details)}</div>` : ""}
|
|
161
176
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
162
177
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
163
178
|
</div>`;
|
|
164
|
-
})
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
165
181
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
166
182
|
catPages += `<div id="p-${cs.id}" class="page">
|
|
167
183
|
<div class="cat-head"><span style="color:${clr};font-size:1.8rem;font-weight:900">${cs.avg}</span><span style="color:${clr}">/100</span><span style="color:var(--muted);margin-left:0.5rem">${cs.label}</span></div>
|
|
@@ -172,10 +188,13 @@ ${subPages}
|
|
|
172
188
|
}
|
|
173
189
|
// All Issues page
|
|
174
190
|
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
175
|
-
const issueRows = allIssues
|
|
191
|
+
const issueRows = allIssues
|
|
192
|
+
.slice(0, 200)
|
|
193
|
+
.map((i) => {
|
|
176
194
|
const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
|
|
177
195
|
return `<tr class="${i.severity}"><td class="is2">${i.severity[0].toUpperCase()}</td><td class="ic2">${e(i.check)}</td><td class="il2">${loc}</td><td>${e(i.message)}</td><td class="iru2">${e(i.rule || "")}</td></tr>`;
|
|
178
|
-
})
|
|
196
|
+
})
|
|
197
|
+
.join("");
|
|
179
198
|
const issuesPage = `<div id="p-issues" class="page">
|
|
180
199
|
<h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
|
|
181
200
|
<div class="isf">${allIssues.filter((i) => i.severity === "error").length} errors · ${allIssues.filter((i) => i.severity === "warning").length} warnings</div>
|
|
@@ -183,23 +202,24 @@ ${subPages}
|
|
|
183
202
|
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
|
|
184
203
|
</div>`;
|
|
185
204
|
// File heatmap page
|
|
186
|
-
const fileRows = topFiles
|
|
205
|
+
const fileRows = topFiles
|
|
206
|
+
.map((f) => {
|
|
187
207
|
const pct = Math.min(100, f.total * 5);
|
|
188
208
|
return `<div class="fr"><span class="ff">${fl(f.file)}</span><div class="fb"><div class="fbf" style="width:${pct}%;background:${f.errors > 0 ? "var(--fail)" : "var(--warn)"}"></div></div><span class="fv">${f.errors}E ${f.warnings}W</span><span class="fcs">${f.checks.join(", ")}</span></div>`;
|
|
189
|
-
})
|
|
209
|
+
})
|
|
210
|
+
.join("");
|
|
190
211
|
const filesPage = `<div id="p-files" class="page">
|
|
191
212
|
<h2>File Heatmap</h2>
|
|
192
213
|
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
|
|
193
214
|
${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
194
215
|
</div>`;
|
|
195
216
|
// Codebase heatmap — each file = row of pixels, color = issue density
|
|
196
|
-
const heatmapFiles = [...fileIssues.entries()]
|
|
197
|
-
.sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings)
|
|
198
|
-
.slice(0, 30);
|
|
217
|
+
const heatmapFiles = [...fileIssues.entries()].sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings).slice(0, 30);
|
|
199
218
|
let heatmapHtml = "";
|
|
200
219
|
if (heatmapFiles.length > 0) {
|
|
201
220
|
const maxIssues = Math.max(...heatmapFiles.map(([, d]) => d.errors + d.warnings));
|
|
202
|
-
heatmapHtml = heatmapFiles
|
|
221
|
+
heatmapHtml = heatmapFiles
|
|
222
|
+
.map(([file, d]) => {
|
|
203
223
|
const total = d.errors + d.warnings;
|
|
204
224
|
const intensity = maxIssues > 0 ? total / maxIssues : 0;
|
|
205
225
|
const r = Math.round(239 * intensity); // red channel
|
|
@@ -208,7 +228,8 @@ ${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
|
208
228
|
const barW = Math.max(4, Math.round(intensity * 200));
|
|
209
229
|
const checks = [...d.checks].join(", ");
|
|
210
230
|
return `<div class="hm-row"><span class="hm-name">${fl(file)}</span><div class="hm-bar" style="width:${barW}px;background:${color}" title="${total} issues (${checks})"></div><span class="hm-count">${d.errors}E ${d.warnings}W</span></div>`;
|
|
211
|
-
})
|
|
231
|
+
})
|
|
232
|
+
.join("");
|
|
212
233
|
}
|
|
213
234
|
const heatmapPage = `<div id="p-heatmap" class="page">
|
|
214
235
|
<h2>Code Heatmap</h2>
|
|
@@ -360,14 +381,18 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
360
381
|
|
|
361
382
|
<aside class="side">
|
|
362
383
|
<div class="side-section">Score<div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>
|
|
363
|
-
${catScores
|
|
384
|
+
${catScores
|
|
385
|
+
.map((cs) => {
|
|
364
386
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
365
|
-
return `<div class="side-section"><a class="side-cat" onclick="go('${cs.id}')">${cs.label} <span style="color:${clr}">${cs.avg}</span></a>${cs.checks
|
|
387
|
+
return `<div class="side-section"><a class="side-cat" onclick="go('${cs.id}')">${cs.label} <span style="color:${clr}">${cs.avg}</span></a>${cs.checks
|
|
388
|
+
.map((c) => {
|
|
366
389
|
const sk = c.details.skipped;
|
|
367
390
|
const meta = getCheckMeta(c.name);
|
|
368
391
|
return `<a class="side-check" onclick="go('${cs.id}')" title="${e(meta.label)}"><span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span> ${e(meta.label)}</a>`;
|
|
369
|
-
})
|
|
370
|
-
|
|
392
|
+
})
|
|
393
|
+
.join("")}</div>`;
|
|
394
|
+
})
|
|
395
|
+
.join("")}
|
|
371
396
|
</aside>
|
|
372
397
|
<div class="content">
|
|
373
398
|
${overviewPage}
|
|
@@ -390,6 +415,13 @@ function sub(el,cat){
|
|
|
390
415
|
el.classList.add('active');
|
|
391
416
|
document.querySelectorAll('#p-'+cat+' .sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
392
417
|
}
|
|
418
|
+
// Copy-prompt buttons — read from data-attribute (no inline JS with user data)
|
|
419
|
+
document.addEventListener('click',function(ev){
|
|
420
|
+
var btn=ev.target.closest('.cp-btn');
|
|
421
|
+
if(!btn)return;
|
|
422
|
+
navigator.clipboard.writeText(btn.dataset.prompt||'');
|
|
423
|
+
btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
|
|
424
|
+
});
|
|
393
425
|
// Init: show overview
|
|
394
426
|
document.querySelector('.tn').classList.add('active');
|
|
395
427
|
</script>
|
|
@@ -411,7 +443,9 @@ function buildRadar(items) {
|
|
|
411
443
|
let grid = "";
|
|
412
444
|
for (const pct of [25, 50, 75, 100]) {
|
|
413
445
|
const rr = (pct / 100) * r;
|
|
414
|
-
const pts = items
|
|
446
|
+
const pts = items
|
|
447
|
+
.map((_, i) => `${cx + rr * Math.cos(i * step - Math.PI / 2)},${cy + rr * Math.sin(i * step - Math.PI / 2)}`)
|
|
448
|
+
.join(" ");
|
|
415
449
|
grid += `<polygon points="${pts}" fill="none" stroke="#1e1e24" stroke-width="0.7"/>`;
|
|
416
450
|
}
|
|
417
451
|
let axes = "";
|
|
@@ -422,11 +456,13 @@ function buildRadar(items) {
|
|
|
422
456
|
const ly = cy + (r + 16) * Math.sin(a);
|
|
423
457
|
axes += `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="middle" fill="#6b7280" font-size="9" font-weight="600">${items[i].label}</text>`;
|
|
424
458
|
}
|
|
425
|
-
const dataPts = items
|
|
459
|
+
const dataPts = items
|
|
460
|
+
.map((c, i) => {
|
|
426
461
|
const a = i * step - Math.PI / 2;
|
|
427
462
|
const rr = (c.score / 100) * r;
|
|
428
463
|
return `${cx + rr * Math.cos(a)},${cy + rr * Math.sin(a)}`;
|
|
429
|
-
})
|
|
464
|
+
})
|
|
465
|
+
.join(" ");
|
|
430
466
|
let dots = "";
|
|
431
467
|
for (let i = 0; i < n; i++) {
|
|
432
468
|
const a = i * step - Math.PI / 2;
|
|
@@ -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
|
}
|