@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.
@@ -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, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
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(" \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v" + VERSION);
47
- console.log(" \x1b[2m" + cwd + "\x1b[0m");
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(" stack: " + parts.join(" + "));
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(" " + runner.name.padEnd(14));
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 + "/100";
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).filter((f) => f.endsWith(".json")).sort();
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 { /* ignore */ }
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(" \x1b[2mReport: " + join(outputDir, "report.html") + "\x1b[0m");
140
- console.log(" \x1b[2mJSON: " + join(outputDir, "report.json") + "\x1b[0m");
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 { /* failed to open browser */ }
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
- ? "react"
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
- ? "biome"
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
- let url = remote
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 { /* dir doesn't exist */ }
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,
@@ -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 ? "#L" + 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.sort((a, b) => a.score - b.score).map((c) => {
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
- }).join("");
89
- const catCards = catScores.map((cs) => {
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.map((c) => {
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
- }).join("");
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
- }).join("");
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).filter(([, v]) => v !== "none" && v !== "unknown").map(([k, v]) => `<span>${k}: <b>${v}</b></span>`).join("")}</div>
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.map((c, i) => {
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
- }).join("");
118
- const subPages = cs.checks.map((c, i) => {
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).filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph").map(([k, v]) => {
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
- }).join("");
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 ? ":" + iss.line : ""}\n${iss.severity}: ${iss.message}${iss.rule ? " (" + iss.rule + ")" : ""}\nCheck: ${c.name}`;
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 + "/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>
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
- }).join("");
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.slice(0, 200).map((i) => {
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
- }).join("");
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.map((f) => {
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
- }).join("");
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.map(([file, d]) => {
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
- }).join("");
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.map((cs) => {
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.map((c) => {
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
- }).join("")}</div>`;
369
- }).join("")}
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.map((_, i) => `${cx + rr * Math.cos(i * step - Math.PI / 2)},${cy + rr * Math.sin(i * step - Math.PI / 2)}`).join(" ");
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.map((c, i) => {
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
- }).join(" ");
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 { name: "architecture", score: 100, grade: "A", details: { skipped: true, reason: "fewer than 2 source files" }, issues: [], duration: Date.now() - start };
20
+ return {
21
+ name: "architecture",
22
+ score: 100,
23
+ grade: "A",
24
+ details: { skipped: true, reason: "fewer than 2 source files" },
25
+ issues: [],
26
+ duration: Date.now() - start,
27
+ };
21
28
  }
22
29
  const graph = buildGraph(files);
23
30
  // ── Circular dependencies ──
@@ -34,7 +41,12 @@ export function runArchitecture(cwd) {
34
41
  for (const [path, node] of graph.nodes) {
35
42
  if (node.importedBy.length >= threshold) {
36
43
  godModules.push(path);
37
- issues.push({ severity: "warning", message: `God module: imported by ${node.importedBy.length}/${files.length} files — consider splitting`, file: path, rule: "god-module" });
44
+ issues.push({
45
+ severity: "warning",
46
+ message: `God module: imported by ${node.importedBy.length}/${files.length} files — consider splitting`,
47
+ file: path,
48
+ rule: "god-module",
49
+ });
38
50
  }
39
51
  }
40
52
  // ── Orphan files (not imported by anyone) ──
@@ -52,7 +64,12 @@ export function runArchitecture(cwd) {
52
64
  for (const [path, node] of graph.nodes) {
53
65
  if (node.imports.length > 10) {
54
66
  highFanOut++;
55
- issues.push({ severity: "warning", message: `High fan-out: imports ${node.imports.length} modules — hard to test in isolation`, file: path, rule: "high-fan-out" });
67
+ issues.push({
68
+ severity: "warning",
69
+ message: `High fan-out: imports ${node.imports.length} modules — hard to test in isolation`,
70
+ file: path,
71
+ rule: "high-fan-out",
72
+ });
56
73
  }
57
74
  }
58
75
  // ── High fan-in + fan-out (connector files) ──
@@ -60,7 +77,12 @@ export function runArchitecture(cwd) {
60
77
  for (const [path, node] of graph.nodes) {
61
78
  if (node.imports.length > 5 && node.importedBy.length > 5) {
62
79
  connectors++;
63
- issues.push({ severity: "warning", message: `Connector: ${node.imports.length} imports, ${node.importedBy.length} importers — high coupling`, file: path, rule: "connector-module" });
80
+ issues.push({
81
+ severity: "warning",
82
+ message: `Connector: ${node.imports.length} imports, ${node.importedBy.length} importers — high coupling`,
83
+ file: path,
84
+ rule: "connector-module",
85
+ });
64
86
  }
65
87
  }
66
88
  // ── Score ──
@@ -152,8 +174,8 @@ function resolveImport(fromPath, importPath, knownFiles) {
152
174
  }
153
175
  // Try index
154
176
  for (const ext of [".ts", ".tsx"]) {
155
- if (knownFiles.has(resolved + "/index" + ext))
156
- return resolved + "/index" + ext;
177
+ if (knownFiles.has(`${resolved}/index${ext}`))
178
+ return `${resolved}/index${ext}`;
157
179
  }
158
180
  return null;
159
181
  }
@@ -28,7 +28,7 @@ export function runComplexity(cwd) {
28
28
  const lines = content.split("\n");
29
29
  totalLines += lines.length;
30
30
  // Simple heuristic: find function boundaries and measure complexity
31
- const funcs = extractFunctions(content, file.replace(cwd + "/", ""));
31
+ const funcs = extractFunctions(content, file.replace(`${cwd}/`, ""));
32
32
  for (const f of funcs) {
33
33
  functions.push(f);
34
34
  if (f.lines > MAX_FUNCTION_LINES) {
@@ -79,9 +79,7 @@ function collectFiles(dir, out) {
79
79
  }
80
80
  else {
81
81
  const ext = extname(entry);
82
- if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") &&
83
- !entry.includes(".test.") &&
84
- !entry.includes(".spec.")) {
82
+ if ((ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") && !entry.includes(".test.") && !entry.includes(".spec.")) {
85
83
  out.push(full);
86
84
  }
87
85
  }