@vibecodeqa/cli 0.13.0 → 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 +19 -13
- package/dist/detect.js +5 -27
- package/dist/fs-utils.js +4 -2
- package/dist/report/html.js +62 -32
- 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) {
|
|
@@ -149,7 +153,9 @@ async function main() {
|
|
|
149
153
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
150
154
|
execFileSync(openCmd, [join(outputDir, "report.html")], { stdio: "ignore" });
|
|
151
155
|
}
|
|
152
|
-
catch {
|
|
156
|
+
catch {
|
|
157
|
+
/* failed to open browser */
|
|
158
|
+
}
|
|
153
159
|
}
|
|
154
160
|
// Watch mode — re-run on file changes
|
|
155
161
|
if (watchMode) {
|
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
|
@@ -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;
|
|
@@ -67,7 +69,7 @@ function walk(dir, cwd, out, exts) {
|
|
|
67
69
|
if (statSync(full).size > 1_000_000)
|
|
68
70
|
continue;
|
|
69
71
|
const content = readFileSync(full, "utf-8");
|
|
70
|
-
const relPath = full.replace(cwd
|
|
72
|
+
const relPath = full.replace(`${cwd}/`, "");
|
|
71
73
|
const isTest = entry.includes(".test.") || entry.includes(".spec.") || relPath.includes("__tests__");
|
|
72
74
|
out.push({
|
|
73
75
|
path: relPath,
|
package/dist/report/html.js
CHANGED
|
@@ -18,7 +18,7 @@ function fileLink(path, line, repoUrl, branch) {
|
|
|
18
18
|
const clean = path.split(":")[0];
|
|
19
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,7 +156,7 @@ 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 prompt = `Fix this issue in ${file}${iss.line ?
|
|
159
|
+
const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
|
|
144
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>`;
|
|
145
161
|
}
|
|
146
162
|
issuesHtml += `</div>`;
|
|
@@ -153,14 +169,15 @@ export function generateHTML(report) {
|
|
|
153
169
|
issuesHtml += `</div>`;
|
|
154
170
|
}
|
|
155
171
|
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
156
|
-
<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>
|
|
157
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>` : ""}
|
|
158
174
|
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
159
175
|
${c.name === "architecture" && !sk ? `<div class="arch-svg">${generateArchSVG(c.details)}</div>` : ""}
|
|
160
176
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
161
177
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
162
178
|
</div>`;
|
|
163
|
-
})
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
164
181
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
165
182
|
catPages += `<div id="p-${cs.id}" class="page">
|
|
166
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>
|
|
@@ -171,10 +188,13 @@ ${subPages}
|
|
|
171
188
|
}
|
|
172
189
|
// All Issues page
|
|
173
190
|
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
174
|
-
const issueRows = allIssues
|
|
191
|
+
const issueRows = allIssues
|
|
192
|
+
.slice(0, 200)
|
|
193
|
+
.map((i) => {
|
|
175
194
|
const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
|
|
176
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>`;
|
|
177
|
-
})
|
|
196
|
+
})
|
|
197
|
+
.join("");
|
|
178
198
|
const issuesPage = `<div id="p-issues" class="page">
|
|
179
199
|
<h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
|
|
180
200
|
<div class="isf">${allIssues.filter((i) => i.severity === "error").length} errors · ${allIssues.filter((i) => i.severity === "warning").length} warnings</div>
|
|
@@ -182,23 +202,24 @@ ${subPages}
|
|
|
182
202
|
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
|
|
183
203
|
</div>`;
|
|
184
204
|
// File heatmap page
|
|
185
|
-
const fileRows = topFiles
|
|
205
|
+
const fileRows = topFiles
|
|
206
|
+
.map((f) => {
|
|
186
207
|
const pct = Math.min(100, f.total * 5);
|
|
187
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>`;
|
|
188
|
-
})
|
|
209
|
+
})
|
|
210
|
+
.join("");
|
|
189
211
|
const filesPage = `<div id="p-files" class="page">
|
|
190
212
|
<h2>File Heatmap</h2>
|
|
191
213
|
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
|
|
192
214
|
${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
193
215
|
</div>`;
|
|
194
216
|
// Codebase heatmap — each file = row of pixels, color = issue density
|
|
195
|
-
const heatmapFiles = [...fileIssues.entries()]
|
|
196
|
-
.sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings)
|
|
197
|
-
.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);
|
|
198
218
|
let heatmapHtml = "";
|
|
199
219
|
if (heatmapFiles.length > 0) {
|
|
200
220
|
const maxIssues = Math.max(...heatmapFiles.map(([, d]) => d.errors + d.warnings));
|
|
201
|
-
heatmapHtml = heatmapFiles
|
|
221
|
+
heatmapHtml = heatmapFiles
|
|
222
|
+
.map(([file, d]) => {
|
|
202
223
|
const total = d.errors + d.warnings;
|
|
203
224
|
const intensity = maxIssues > 0 ? total / maxIssues : 0;
|
|
204
225
|
const r = Math.round(239 * intensity); // red channel
|
|
@@ -207,7 +228,8 @@ ${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
|
207
228
|
const barW = Math.max(4, Math.round(intensity * 200));
|
|
208
229
|
const checks = [...d.checks].join(", ");
|
|
209
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>`;
|
|
210
|
-
})
|
|
231
|
+
})
|
|
232
|
+
.join("");
|
|
211
233
|
}
|
|
212
234
|
const heatmapPage = `<div id="p-heatmap" class="page">
|
|
213
235
|
<h2>Code Heatmap</h2>
|
|
@@ -359,14 +381,18 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
359
381
|
|
|
360
382
|
<aside class="side">
|
|
361
383
|
<div class="side-section">Score<div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>
|
|
362
|
-
${catScores
|
|
384
|
+
${catScores
|
|
385
|
+
.map((cs) => {
|
|
363
386
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
364
|
-
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) => {
|
|
365
389
|
const sk = c.details.skipped;
|
|
366
390
|
const meta = getCheckMeta(c.name);
|
|
367
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>`;
|
|
368
|
-
})
|
|
369
|
-
|
|
392
|
+
})
|
|
393
|
+
.join("")}</div>`;
|
|
394
|
+
})
|
|
395
|
+
.join("")}
|
|
370
396
|
</aside>
|
|
371
397
|
<div class="content">
|
|
372
398
|
${overviewPage}
|
|
@@ -417,7 +443,9 @@ function buildRadar(items) {
|
|
|
417
443
|
let grid = "";
|
|
418
444
|
for (const pct of [25, 50, 75, 100]) {
|
|
419
445
|
const rr = (pct / 100) * r;
|
|
420
|
-
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(" ");
|
|
421
449
|
grid += `<polygon points="${pts}" fill="none" stroke="#1e1e24" stroke-width="0.7"/>`;
|
|
422
450
|
}
|
|
423
451
|
let axes = "";
|
|
@@ -428,11 +456,13 @@ function buildRadar(items) {
|
|
|
428
456
|
const ly = cy + (r + 16) * Math.sin(a);
|
|
429
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>`;
|
|
430
458
|
}
|
|
431
|
-
const dataPts = items
|
|
459
|
+
const dataPts = items
|
|
460
|
+
.map((c, i) => {
|
|
432
461
|
const a = i * step - Math.PI / 2;
|
|
433
462
|
const rr = (c.score / 100) * r;
|
|
434
463
|
return `${cx + rr * Math.cos(a)},${cy + rr * Math.sin(a)}`;
|
|
435
|
-
})
|
|
464
|
+
})
|
|
465
|
+
.join(" ");
|
|
436
466
|
let dots = "";
|
|
437
467
|
for (let i = 0; i < n; i++) {
|
|
438
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
|
}
|