@vibecodeqa/cli 0.21.0 → 0.22.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.
@@ -179,7 +179,7 @@ export const CHECK_META = {
179
179
  weight: 4,
180
180
  description: "Checks common accessibility violations: images without alt text, click handlers on non-interactive elements without keyboard support, form controls without labels, autoFocus usage, positive tabIndex, and missing html lang attribute.",
181
181
  risk: "1 in 4 adults has a disability (CDC). Missing alt text makes images invisible to screen readers. Click-only divs exclude keyboard users. Unlabeled inputs are unusable with assistive technology. Missing lang attribute breaks screen reader pronunciation.",
182
- recommendation: "Add alt text to all images (use alt=\"\" for decorative). Use <button> for clickable elements, not <div onClick>. Label all form controls with <label>, aria-label, or aria-labelledby. Set lang on <html>.",
182
+ recommendation: 'Add alt text to all images (use alt="" for decorative). Use <button> for clickable elements, not <div onClick>. Label all form controls with <label>, aria-label, or aria-labelledby. Set lang on <html>.',
183
183
  },
184
184
  performance: {
185
185
  name: "performance",
package/dist/cli.js CHANGED
@@ -4,21 +4,21 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFile
4
4
  import { join, resolve } from "node:path";
5
5
  import { detectRepoUrl, detectStack } from "./detect.js";
6
6
  import { generatePages } from "./report/html.js";
7
+ import { runAccessibility } from "./runners/accessibility.js";
7
8
  import { runArchitecture } from "./runners/architecture.js";
8
9
  import { runBestPractices } from "./runners/best-practices.js";
10
+ import { runCodeCoherence } from "./runners/code-coherence.js";
9
11
  import { runComplexity } from "./runners/complexity.js";
10
12
  import { runConfusion } from "./runners/confusion.js";
11
13
  import { runContext } from "./runners/context.js";
12
14
  import { runDependencies } from "./runners/dependencies.js";
15
+ import { runDocCoherence } from "./runners/doc-coherence.js";
13
16
  import { runDocs } from "./runners/docs.js";
14
17
  import { runDuplication } from "./runners/duplication.js";
15
- import { runPerformance } from "./runners/performance.js";
16
18
  import { runErrorHandling } from "./runners/error-handling.js";
17
19
  import { runLint } from "./runners/lint.js";
20
+ import { runPerformance } from "./runners/performance.js";
18
21
  import { runReact } from "./runners/react.js";
19
- import { runAccessibility } from "./runners/accessibility.js";
20
- import { runDocCoherence } from "./runners/doc-coherence.js";
21
- import { runCodeCoherence } from "./runners/code-coherence.js";
22
22
  import { runSecrets } from "./runners/secrets.js";
23
23
  import { runSecurity } from "./runners/security.js";
24
24
  import { runStandards } from "./runners/standards.js";
@@ -180,7 +180,7 @@ async function main() {
180
180
  body: JSON.stringify({ repo, report }),
181
181
  });
182
182
  if (res.ok) {
183
- const data = await res.json();
183
+ const data = (await res.json());
184
184
  if (!jsonOnly)
185
185
  console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
186
186
  }
package/dist/fs-utils.js CHANGED
@@ -1,7 +1,18 @@
1
1
  /** Shared filesystem utilities — eliminates duplicate file-walking across runners. */
2
2
  import { lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { basename, extname, join } from "node:path";
4
- const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results", "__pycache__", ".dart_tool", "build", ".flutter-plugins"]);
4
+ const SKIP_DIRS = new Set([
5
+ "node_modules",
6
+ "dist",
7
+ ".git",
8
+ ".vibe-check",
9
+ "coverage",
10
+ "test-results",
11
+ "__pycache__",
12
+ ".dart_tool",
13
+ "build",
14
+ ".flutter-plugins",
15
+ ]);
5
16
  const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart"]);
6
17
  const ALL_EXTS = new Set([...CODE_EXTS, ".json", ".env", ".yaml", ".yml", ".toml"]);
7
18
  /** Walk source directories and return all code files. */
@@ -76,7 +87,11 @@ function walk(dir, cwd, out, exts) {
76
87
  continue;
77
88
  const content = readFileSync(full, "utf-8");
78
89
  const relPath = full.replace(`${cwd}/`, "");
79
- const isTest = entry.includes(".test.") || entry.includes(".spec.") || entry.endsWith("_test.dart") || relPath.includes("__tests__") || relPath.includes("test/");
90
+ const isTest = entry.includes(".test.") ||
91
+ entry.includes(".spec.") ||
92
+ entry.endsWith("_test.dart") ||
93
+ relPath.includes("__tests__") ||
94
+ relPath.includes("test/");
80
95
  out.push({
81
96
  path: relPath,
82
97
  fullPath: full,
@@ -1,5 +1,13 @@
1
1
  /** Reusable HTML components and helpers for the report. */
2
2
  import type { Priority } from "../check-meta.js";
3
+ import type { CheckResult } from "../types.js";
4
+ /** Type-safe accessor for check detail flags. */
5
+ export declare function det(c: CheckResult): {
6
+ skipped?: boolean;
7
+ comingSoon?: boolean;
8
+ reason?: string;
9
+ [k: string]: unknown;
10
+ };
3
11
  /** HTML-escape a string. */
4
12
  export declare function e(s: string): string;
5
13
  /** Make a file path a clickable GitHub link if repoUrl is available. */
@@ -1,4 +1,8 @@
1
1
  /** Reusable HTML components and helpers for the report. */
2
+ /** Type-safe accessor for check detail flags. */
3
+ export function det(c) {
4
+ return c.details;
5
+ }
2
6
  /** HTML-escape a string. */
3
7
  export function e(s) {
4
8
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -0,0 +1,2 @@
1
+ /** Simple SVG favicon — "VQ" monogram in accent purple. */
2
+ export declare const FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><rect width=\"32\" height=\"32\" rx=\"6\" fill=\"#818cf8\"/><text x=\"16\" y=\"22\" text-anchor=\"middle\" font-family=\"system-ui,sans-serif\" font-size=\"16\" font-weight=\"900\" fill=\"#fff\">VQ</text></svg>";
@@ -0,0 +1,2 @@
1
+ /** Simple SVG favicon — "VQ" monogram in accent purple. */
2
+ export const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#818cf8"/><text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" font-weight="900" fill="#fff">VQ</text></svg>`;
@@ -11,12 +11,18 @@
11
11
  * Mobile: Hamburger toggles both top nav dropdown and sidebar panel.
12
12
  */
13
13
  import { getCheckMeta } from "../check-meta.js";
14
- import { e, fileLink, gc } from "./components.js";
14
+ import { det, e, fileLink, gc } from "./components.js";
15
+ import { FAVICON_SVG } from "./favicon.js";
15
16
  import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
16
17
  import { CSS } from "./styles.js";
17
18
  export const GROUPS = [
18
19
  { id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
19
- { id: "quality", label: "Quality", file: "quality.html", checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs", "best-practices"] },
20
+ {
21
+ id: "quality",
22
+ label: "Quality",
23
+ file: "quality.html",
24
+ checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs", "best-practices"],
25
+ },
20
26
  { id: "testing", label: "Testing", file: "testing.html", checks: ["testing"] },
21
27
  { id: "arch", label: "Architecture", file: "architecture.html", checks: ["architecture", "performance"] },
22
28
  { id: "security", label: "Security", file: "security.html", checks: ["secrets", "security", "dependencies"] },
@@ -27,7 +33,7 @@ export function generatePages(report, historyDir) {
27
33
  const pages = new Map();
28
34
  const allChecks = report.checks;
29
35
  const checkMap = new Map(allChecks.map((c) => [c.name, c]));
30
- const active = allChecks.filter((c) => !c.details.skipped && !c.details.comingSoon);
36
+ const active = allChecks.filter((c) => !det(c).skipped && !det(c).comingSoon);
31
37
  const ru = report.meta.repoUrl;
32
38
  const br = report.meta.branch;
33
39
  const fl = (path, line) => fileLink(path, line, ru, br);
@@ -54,38 +60,44 @@ export function generatePages(report, historyDir) {
54
60
  .slice(0, 30);
55
61
  const catScores = GROUPS.map((g) => {
56
62
  const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
57
- const scored = checks.filter((c) => !c.details.skipped);
63
+ const scored = checks.filter((c) => !det(c).skipped);
58
64
  const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
59
65
  return { ...g, avg, checks };
60
66
  });
61
67
  const w = (id, sidebar, content) => wrap(proj, id, report, totalIssues, sidebar, content);
62
68
  // ── Overview: sidebar shows score + category summary ──
63
- const overviewSidebar = sidebarScore(report)
64
- + catScores.map((cs) => {
65
- const isPremium = cs.checks.every((c) => c.details.comingSoon);
69
+ const overviewSidebar = sidebarScore(report) +
70
+ catScores
71
+ .map((cs) => {
72
+ const isPremium = cs.checks.every((c) => det(c).comingSoon);
66
73
  const clr = isPremium ? "#6366f1" : gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
67
74
  const label = isPremium
68
75
  ? `<span class="pro-badge" style="font-size:0.5rem;padding:0.08rem 0.35rem">PRO</span>`
69
76
  : `<span style="color:${clr}">${cs.avg}</span>`;
70
77
  return `<a class="side-cat" href="${cs.file}">${cs.label} ${label}</a>`;
71
- }).join("")
72
- + sidebarViews(totalIssues, fileIssues.size);
78
+ })
79
+ .join("") +
80
+ sidebarViews(totalIssues, fileIssues.size);
73
81
  pages.set("index.html", w("overview", overviewSidebar, overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
74
82
  // ── Category pages: sidebar shows the checks within this category ──
75
83
  for (let i = 0; i < GROUPS.length; i++) {
76
84
  const g = GROUPS[i];
77
85
  const cs = catScores[i];
78
- const catSidebar = sidebarScore(report)
79
- + `<div class="side-section"><div class="side-cat-title">${cs.label}</div>`
80
- + cs.checks.map((c) => {
81
- const sk = c.details.skipped;
82
- const premium = c.details.comingSoon;
86
+ const catSidebar = sidebarScore(report) +
87
+ `<div class="side-section"><div class="side-cat-title">${cs.label}</div>` +
88
+ cs.checks
89
+ .map((c) => {
90
+ const sk = det(c).skipped;
91
+ const premium = det(c).comingSoon;
83
92
  const meta = getCheckMeta(c.name);
84
- const badge = premium ? `<span style="color:#6366f1">PRO</span>` : `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
93
+ const badge = premium
94
+ ? `<span style="color:#6366f1">PRO</span>`
95
+ : `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
85
96
  return `<a class="side-check" onclick="var t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
86
- }).join("")
87
- + `</div>`
88
- + sidebarViews(totalIssues, fileIssues.size);
97
+ })
98
+ .join("") +
99
+ `</div>` +
100
+ sidebarViews(totalIssues, fileIssues.size);
89
101
  pages.set(g.file, w(g.id, catSidebar, categoryPage(cs, fl)));
90
102
  }
91
103
  // ── Issues: sidebar shows severity breakdown ──
@@ -93,21 +105,21 @@ export function generatePages(report, historyDir) {
93
105
  const errCount = allIssuesList.filter((i) => i.severity === "error").length;
94
106
  const warnCount = allIssuesList.filter((i) => i.severity === "warning").length;
95
107
  const infoCount = allIssuesList.filter((i) => i.severity === "info").length;
96
- const issuesSidebar = sidebarScore(report)
97
- + `<div class="side-section"><div class="side-cat-title">Breakdown</div>`
98
- + `<div class="side-stat"><span style="color:var(--fail)">${errCount}</span> errors</div>`
99
- + `<div class="side-stat"><span style="color:var(--warn)">${warnCount}</span> warnings</div>`
100
- + `<div class="side-stat"><span style="color:var(--info)">${infoCount}</span> info</div>`
101
- + `</div>`
102
- + sidebarViews(totalIssues, fileIssues.size);
108
+ const issuesSidebar = sidebarScore(report) +
109
+ `<div class="side-section"><div class="side-cat-title">Breakdown</div>` +
110
+ `<div class="side-stat"><span style="color:var(--fail)">${errCount}</span> errors</div>` +
111
+ `<div class="side-stat"><span style="color:var(--warn)">${warnCount}</span> warnings</div>` +
112
+ `<div class="side-stat"><span style="color:var(--info)">${infoCount}</span> info</div>` +
113
+ `</div>` +
114
+ sidebarViews(totalIssues, fileIssues.size);
103
115
  pages.set("issues.html", w("issues", issuesSidebar, issuesPage(allChecks, totalIssues, fl)));
104
116
  // ── Files: sidebar shows file stats ──
105
- const filesSidebar = sidebarScore(report)
106
- + `<div class="side-section"><div class="side-cat-title">File Health</div>`
107
- + `<div class="side-stat"><span style="color:var(--text)">${fileIssues.size}</span> files with issues</div>`
108
- + `<div class="side-stat"><span style="color:var(--fail)">${topFiles.filter(f => f.errors > 0).length}</span> with errors</div>`
109
- + `</div>`
110
- + sidebarViews(totalIssues, fileIssues.size);
117
+ const filesSidebar = sidebarScore(report) +
118
+ `<div class="side-section"><div class="side-cat-title">File Health</div>` +
119
+ `<div class="side-stat"><span style="color:var(--text)">${fileIssues.size}</span> files with issues</div>` +
120
+ `<div class="side-stat"><span style="color:var(--fail)">${topFiles.filter((f) => f.errors > 0).length}</span> with errors</div>` +
121
+ `</div>` +
122
+ sidebarViews(totalIssues, fileIssues.size);
111
123
  pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
112
124
  return pages;
113
125
  }
@@ -129,14 +141,13 @@ function wrap(proj, currentId, report, totalIssues, sidebar, content) {
129
141
  { id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
130
142
  { id: "files", label: "Files", file: "files.html" },
131
143
  ];
132
- const nav = navItems
133
- .map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
134
- .join("");
144
+ const nav = navItems.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`).join("");
135
145
  return `<!DOCTYPE html>
136
146
  <html lang="en">
137
147
  <head>
138
148
  <meta charset="utf-8">
139
149
  <meta name="viewport" content="width=device-width,initial-scale=1">
150
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}">
140
151
  <title>VibeCode QA \u2014 ${e(proj)}</title>
141
152
  <style>${CSS}</style>
142
153
  </head>
@@ -1,8 +1,8 @@
1
1
  /** Page renderers for the HTML report. */
2
2
  import { getCheckMeta } from "../check-meta.js";
3
3
  import { loadHistory } from "../history.js";
4
- import { generateArchSVG, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
5
- import { e, gc, pc } from "./components.js";
4
+ import { generateArchSVG, generateDSM, generatePackageDiagram, generateSequenceDiagram } from "../runners/architecture.js";
5
+ import { det, e, gc, pc } from "./components.js";
6
6
  import { buildPyramid, buildRadar, buildRing, buildTimeline } from "./svg.js";
7
7
  // ── Overview ──────────────────────────────────────────────────────────
8
8
  export function overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir) {
@@ -14,14 +14,14 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
14
14
  <span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
15
15
  </div>
16
16
  </div>`;
17
- const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !c.details.skipped && !c.details.comingSoon));
17
+ const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !det(c).skipped && !det(c).comingSoon));
18
18
  const radarSvg = scoredCats.length >= 3 ? buildRadar(scoredCats.map((cs) => ({ label: cs.label, score: cs.avg }))) : "";
19
19
  const catCards = catScores
20
20
  .map((cs) => {
21
21
  const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
22
22
  const mini = cs.checks
23
23
  .map((c) => {
24
- const sk = c.details.skipped;
24
+ const sk = det(c).skipped;
25
25
  return `<span class="mc" style="color:${sk ? "#555" : gc(c.grade)}" title="${e(c.name)}: ${sk ? "skip" : c.score}">${sk ? "\u2014" : c.grade}</span>`;
26
26
  })
27
27
  .join("");
@@ -60,10 +60,13 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
60
60
  }
61
61
  let fileHotspotsHtml = "";
62
62
  if (topFiles.length > 0) {
63
- const fileRows = topFiles.slice(0, 5).map((f) => {
63
+ const fileRows = topFiles
64
+ .slice(0, 5)
65
+ .map((f) => {
64
66
  const pct = Math.min(100, f.total * 5);
65
67
  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></div>`;
66
- }).join("");
68
+ })
69
+ .join("");
67
70
  const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
68
71
  fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
69
72
  }
@@ -87,8 +90,8 @@ ${fileHotspotsHtml}
87
90
  export function categoryPage(cs, fl) {
88
91
  const subNav = cs.checks
89
92
  .map((c, i) => {
90
- const sk = c.details.skipped;
91
- const premium = c.details.comingSoon;
93
+ const sk = det(c).skipped;
94
+ const premium = det(c).comingSoon;
92
95
  const badge = premium ? "PRO" : sk ? "\u2014" : c.grade;
93
96
  const clr = premium ? "#6366f1" : sk ? "#555" : gc(c.grade);
94
97
  return `<a class="sn${i === 0 ? " active" : ""}${premium ? " sn-pro" : ""}" data-sub="${cs.id}-${c.name}" onclick="sub(this,'${cs.id}')">${e(c.name)} <span style="color:${clr}">${badge}</span></a>`;
@@ -97,8 +100,8 @@ export function categoryPage(cs, fl) {
97
100
  const subPages = cs.checks
98
101
  .map((c, i) => {
99
102
  const meta = getCheckMeta(c.name);
100
- const sk = c.details.skipped;
101
- const premium = c.details.comingSoon;
103
+ const sk = det(c).skipped;
104
+ const premium = det(c).comingSoon;
102
105
  const detailsFiltered = Object.entries(c.details)
103
106
  .filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
104
107
  .map(([k, v]) => {
@@ -136,9 +139,9 @@ export function categoryPage(cs, fl) {
136
139
  issuesHtml += `</div>`;
137
140
  }
138
141
  if (premium) {
139
- const det = c.details;
140
- const desc = det.description || meta.description;
141
- const detailKvs = Object.entries(det)
142
+ const d = c.details;
143
+ const desc = d.description || meta.description;
144
+ const detailKvs = Object.entries(d)
142
145
  .filter(([k]) => !["premium", "comingSoon", "reason", "description"].includes(k))
143
146
  .map(([k, v]) => `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(Array.isArray(v) ? v.join(", ") : String(v))}</span></div>`)
144
147
  .join("");
@@ -156,9 +159,9 @@ ${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` :
156
159
  return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
157
160
  <div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : `${c.score}/100`} \u00b7 weight ${meta.weight}% \u00b7 ${c.duration}ms \u00b7 ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
158
161
  ${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
- ${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
160
- ${c.name === "architecture" && !sk ? `<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(c.details)}</div><h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(c.details)}</div>` : ""}
161
- ${c.name === "testing" && !sk && c.details.pyramid ? `<div class="arch-svg">${buildPyramid(c.details.pyramid)}</div>` : ""}
162
+ ${sk ? `<p class="skip-r">${e(det(c).reason || "skipped")}</p>` : ""}
163
+ ${c.name === "architecture" && !sk ? `${c.details.containerSvg ? `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${c.details.containerSvg}</div>` : ""}<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(c.details)}</div><h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(c.details)}</div>` : ""}
164
+ ${c.name === "testing" && !sk && det(c).pyramid ? `<div class="arch-svg">${buildPyramid(det(c).pyramid)}</div>` : ""}
162
165
  ${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
163
166
  ${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
164
167
  </div>`;
@@ -210,7 +213,10 @@ export function filesPage(topFiles, fileIssues, fl) {
210
213
  .join("");
211
214
  return `
212
215
  <h2>File Health</h2>
213
- <p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">${fileIssues.size} files with issues across ${topFiles.reduce((s, f) => { for (const c of f.checks)
214
- s.add(c); return s; }, new Set()).size} checks.</p>
216
+ <p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">${fileIssues.size} files with issues across ${topFiles.reduce((s, f) => {
217
+ for (const c of f.checks)
218
+ s.add(c);
219
+ return s;
220
+ }, new Set()).size} checks.</p>
215
221
  ${heatmapRows}`;
216
222
  }
@@ -157,10 +157,12 @@ export function buildSparkline(values, opts) {
157
157
  const range = max - min || 1;
158
158
  const step = width / (values.length - 1);
159
159
  const points = values.map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / range) * (height - 4) - 2).toFixed(1)}`).join(" ");
160
- const dots = values.map((v, i) => {
160
+ const dots = values
161
+ .map((v, i) => {
161
162
  const x = (i * step).toFixed(1);
162
163
  const y = (height - ((v - min) / range) * (height - 4) - 2).toFixed(1);
163
164
  return `<circle cx="${x}" cy="${y}" r="1.5" fill="${color}"/>`;
164
- }).join("");
165
+ })
166
+ .join("");
165
167
  return `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>${dots}</svg>`;
166
168
  }
@@ -1,13 +1,20 @@
1
1
  /** Accessibility check — detects common a11y violations in JSX/TSX code. */
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { gradeFromScore } from "../types.js";
5
4
  import { getProductionFiles } from "../fs-utils.js";
5
+ import { gradeFromScore } from "../types.js";
6
6
  export function runAccessibility(cwd) {
7
7
  const start = Date.now();
8
8
  const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
9
9
  if (files.length === 0) {
10
- return { name: "accessibility", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
10
+ return {
11
+ name: "accessibility",
12
+ score: 100,
13
+ grade: "A",
14
+ details: { skipped: true, reason: "no JSX/TSX files" },
15
+ issues: [],
16
+ duration: Date.now() - start,
17
+ };
11
18
  }
12
19
  const issues = [];
13
20
  let missingAlt = 0;
@@ -36,7 +43,13 @@ export function runAccessibility(cwd) {
36
43
  const block = lines.slice(i, Math.min(i + 3, lines.length)).join(" ");
37
44
  if (!(/role=/.test(block) && /(?:onKeyDown|onKeyUp|onKeyPress|tabIndex)/.test(block))) {
38
45
  clickDiv++;
39
- issues.push({ severity: "warning", message: "Click handler on non-interactive element without role + keyboard handler", file: f.path, line: i + 1, rule: "click-events" });
46
+ issues.push({
47
+ severity: "warning",
48
+ message: "Click handler on non-interactive element without role + keyboard handler",
49
+ file: f.path,
50
+ line: i + 1,
51
+ rule: "click-events",
52
+ });
40
53
  }
41
54
  }
42
55
  // 3. <input>/<select>/<textarea> without associated label
@@ -44,18 +57,36 @@ export function runAccessibility(cwd) {
44
57
  const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(" ");
45
58
  if (!/aria-label=/.test(block) && !/aria-labelledby=/.test(block) && !/<label/.test(block) && !/id=/.test(trimmed)) {
46
59
  missingLabel++;
47
- issues.push({ severity: "warning", message: "Form control without label, aria-label, or aria-labelledby", file: f.path, line: i + 1, rule: "form-label" });
60
+ issues.push({
61
+ severity: "warning",
62
+ message: "Form control without label, aria-label, or aria-labelledby",
63
+ file: f.path,
64
+ line: i + 1,
65
+ rule: "form-label",
66
+ });
48
67
  }
49
68
  }
50
69
  // 4. autoFocus
51
70
  if (/\bautoFocus\b/.test(trimmed) || /\bautofocus\b/.test(trimmed)) {
52
71
  autofocus++;
53
- issues.push({ severity: "warning", message: "autoFocus can disorient screen reader users", file: f.path, line: i + 1, rule: "no-autofocus" });
72
+ issues.push({
73
+ severity: "warning",
74
+ message: "autoFocus can disorient screen reader users",
75
+ file: f.path,
76
+ line: i + 1,
77
+ rule: "no-autofocus",
78
+ });
54
79
  }
55
80
  // 5. Positive tabIndex
56
81
  if (/tabIndex=\{[1-9]/.test(trimmed) || /tabindex=["'][1-9]/.test(trimmed)) {
57
82
  positiveTabindex++;
58
- issues.push({ severity: "warning", message: "Positive tabIndex disrupts natural tab order — use 0 or -1", file: f.path, line: i + 1, rule: "tabindex" });
83
+ issues.push({
84
+ severity: "warning",
85
+ message: "Positive tabIndex disrupts natural tab order — use 0 or -1",
86
+ file: f.path,
87
+ line: i + 1,
88
+ rule: "tabindex",
89
+ });
59
90
  }
60
91
  }
61
92
  }
@@ -27,4 +27,6 @@ export declare function runArchitecture(cwd: string): CheckResult;
27
27
  export declare function generateArchSVG(details: Record<string, unknown>): string;
28
28
  export declare function generateDSM(details: Record<string, unknown>): string;
29
29
  export declare function generatePackageDiagram(details: Record<string, unknown>): string;
30
+ export declare function generateSequenceDiagram(details: Record<string, unknown>): string;
31
+ export declare function generateContainerDiagram(cwd: string): string;
30
32
  export {};
@@ -9,7 +9,8 @@
9
9
  * 6. Layer violations (optional: detect cross-layer imports)
10
10
  * 7. SVG architecture diagram
11
11
  */
12
- import { basename, dirname, extname } from "node:path";
12
+ import { existsSync } from "node:fs";
13
+ import { basename, dirname, extname, join } from "node:path";
13
14
  import { getProductionFiles } from "../fs-utils.js";
14
15
  import { gradeFromScore } from "../types.js";
15
16
  export function runArchitecture(cwd) {
@@ -105,6 +106,7 @@ export function runArchitecture(cwd) {
105
106
  highFanOut,
106
107
  connectors,
107
108
  graph: graphData,
109
+ containerSvg: generateContainerDiagram(cwd),
108
110
  },
109
111
  issues,
110
112
  duration: Date.now() - start,
@@ -504,3 +506,155 @@ export function generatePackageDiagram(details) {
504
506
  const H = maxH + gap;
505
507
  return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
506
508
  }
509
+ // ── Sequence Diagram ─────────────────────────────────────────────────
510
+ // Traces the longest import chains from entry points, showing how a request
511
+ // flows through the system. UML-style lifelines with arrows.
512
+ export function generateSequenceDiagram(details) {
513
+ const graph = details.graph;
514
+ if (!graph || Object.keys(graph).length < 3)
515
+ return "";
516
+ // Find entry points (files with 0 importers that aren't utility files)
517
+ const entries = Object.entries(graph);
518
+ const entryPoints = entries
519
+ .filter(([path, info]) => {
520
+ const name = basename(path, extname(path));
521
+ return info.importedBy.length === 0 && ["index", "main", "cli", "App", "app", "server"].includes(name);
522
+ })
523
+ .map(([p]) => p);
524
+ if (entryPoints.length === 0)
525
+ return "";
526
+ // BFS from first entry point to find the longest chain (max 8 deep)
527
+ const entry = entryPoints[0];
528
+ const chain = findLongestChain(entry, graph, 8);
529
+ if (chain.length < 3)
530
+ return "";
531
+ // Draw sequence diagram
532
+ const participants = chain.map((p) => basename(p, extname(p)));
533
+ const lifelineSpacing = 120;
534
+ const W = participants.length * lifelineSpacing + 40;
535
+ const messageH = 36;
536
+ const headerH = 50;
537
+ const H = headerH + (chain.length - 1) * messageH + 40;
538
+ let svg = "";
539
+ // Participant boxes (lifeline headers)
540
+ for (let i = 0; i < participants.length; i++) {
541
+ const x = 20 + i * lifelineSpacing + lifelineSpacing / 2;
542
+ const name = participants[i];
543
+ const boxW = Math.max(60, name.length * 7 + 16);
544
+ svg += `<rect x="${x - boxW / 2}" y="8" width="${boxW}" height="22" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
545
+ svg += `<text x="${x}" y="23" text-anchor="middle" fill="#9ca3af" font-size="9" font-weight="600">${name}</text>`;
546
+ // Lifeline (dashed vertical)
547
+ svg += `<line x1="${x}" y1="30" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
548
+ }
549
+ // Arrows between lifelines (imports = calls)
550
+ for (let i = 0; i < chain.length - 1; i++) {
551
+ const fromX = 20 + i * lifelineSpacing + lifelineSpacing / 2;
552
+ const toX = 20 + (i + 1) * lifelineSpacing + lifelineSpacing / 2;
553
+ const y = headerH + i * messageH;
554
+ // Arrow
555
+ svg += `<line x1="${fromX}" y1="${y}" x2="${toX - 6}" y2="${y}" stroke="#6d78d0" stroke-width="1.5" marker-end="url(#seq-arrow)"/>`;
556
+ // Label (the import)
557
+ const label = `import`;
558
+ svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">${label}</text>`;
559
+ }
560
+ // Arrow marker
561
+ const defs = `<defs><marker id="seq-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="7" markerHeight="5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6d78d0"/></marker></defs>`;
562
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
563
+ }
564
+ function findLongestChain(start, graph, maxDepth) {
565
+ let longest = [start];
566
+ const visited = new Set([start]);
567
+ function dfs(node, path) {
568
+ if (path.length > longest.length)
569
+ longest = [...path];
570
+ if (path.length >= maxDepth)
571
+ return;
572
+ const info = graph[node];
573
+ if (!info)
574
+ return;
575
+ for (const imp of info.imports) {
576
+ if (!visited.has(imp) && graph[imp]) {
577
+ visited.add(imp);
578
+ dfs(imp, [...path, imp]);
579
+ visited.delete(imp);
580
+ }
581
+ }
582
+ }
583
+ dfs(start, [start]);
584
+ return longest;
585
+ }
586
+ // ── Container Diagram ────────────────────────────────────────────────
587
+ // Auto-detects high-level system containers from config files:
588
+ // frontend, backend/API, database, worker, static site, etc.
589
+ export function generateContainerDiagram(cwd) {
590
+ const has = (f) => existsSync(join(cwd, f));
591
+ const containers = [];
592
+ // Detect containers from config files
593
+ if (has("src/App.tsx") || has("src/App.vue") || has("src/App.svelte") || has("web/src/App.tsx")) {
594
+ const tech = has("src/App.tsx") ? "React" : has("src/App.vue") ? "Vue" : "Svelte";
595
+ containers.push({ name: "Frontend", type: "webapp", tech });
596
+ }
597
+ if (has("wrangler.toml") || has("wrangler.json")) {
598
+ containers.push({ name: "Worker", type: "worker", tech: "Cloudflare Workers" });
599
+ }
600
+ if (has("Dockerfile") || has("server.ts") || has("src/server.ts") || has("src/index.ts")) {
601
+ if (!containers.some((c) => c.name === "Frontend")) {
602
+ containers.push({ name: "API Server", type: "api", tech: "Node.js" });
603
+ }
604
+ }
605
+ if (has("prisma/schema.prisma") || has("drizzle.config.ts")) {
606
+ const tech = has("prisma/schema.prisma") ? "Prisma" : "Drizzle";
607
+ containers.push({ name: "Database", type: "db", tech });
608
+ }
609
+ if (has("firebase.json") || has(".firebaserc")) {
610
+ containers.push({ name: "Firebase", type: "baas", tech: "Firebase" });
611
+ }
612
+ if (has("supabase/config.toml") || has(".supabase")) {
613
+ containers.push({ name: "Supabase", type: "baas", tech: "Supabase" });
614
+ }
615
+ if (has("pubspec.yaml")) {
616
+ containers.push({ name: "Mobile App", type: "mobile", tech: "Flutter" });
617
+ }
618
+ if (has("package.json") && !containers.length) {
619
+ containers.push({ name: "Application", type: "app", tech: "Node.js" });
620
+ }
621
+ if (containers.length < 2)
622
+ return ""; // Only interesting with 2+ containers
623
+ // Layout: horizontal boxes with connecting lines
624
+ const boxW = 140;
625
+ const boxH = 60;
626
+ const gap = 30;
627
+ const W = containers.length * (boxW + gap) + gap;
628
+ const H = 120;
629
+ const typeColors = {
630
+ webapp: "#6d78d0",
631
+ worker: "#d97706",
632
+ api: "#22c55e",
633
+ db: "#8b5cf6",
634
+ baas: "#ec4899",
635
+ mobile: "#06b6d4",
636
+ app: "#6d78d0",
637
+ };
638
+ let svg = "";
639
+ for (let i = 0; i < containers.length; i++) {
640
+ const c = containers[i];
641
+ const x = gap + i * (boxW + gap);
642
+ const y = (H - boxH) / 2;
643
+ const color = typeColors[c.type] || "#6d78d0";
644
+ // Box
645
+ svg += `<rect x="${x}" y="${y}" width="${boxW}" height="${boxH}" rx="8" fill="${color}15" stroke="${color}50"/>`;
646
+ // Name
647
+ svg += `<text x="${x + boxW / 2}" y="${y + 24}" text-anchor="middle" fill="#e5e5e5" font-size="10" font-weight="700">${c.name}</text>`;
648
+ // Tech
649
+ svg += `<text x="${x + boxW / 2}" y="${y + 40}" text-anchor="middle" fill="#6b7280" font-size="8">[${c.tech}]</text>`;
650
+ // Connection to next
651
+ if (i < containers.length - 1) {
652
+ const ax = x + boxW;
653
+ const bx = ax + gap;
654
+ const ay = H / 2;
655
+ svg += `<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${ay}" stroke="#ffffff20" stroke-width="1.5" marker-end="url(#cont-arrow)"/>`;
656
+ }
657
+ }
658
+ const defs = `<defs><marker id="cont-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="7" markerHeight="5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ffffff40"/></marker></defs>`;
659
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
660
+ }
@@ -19,26 +19,28 @@ export function runBestPractices(cwd) {
19
19
  let practices = 0;
20
20
  let followed = 0;
21
21
  const has = (f) => existsSync(join(cwd, f));
22
- const read = (f) => { try {
23
- return readFileSync(join(cwd, f), "utf-8");
24
- }
25
- catch {
26
- return "";
27
- } };
22
+ const read = (f) => {
23
+ try {
24
+ return readFileSync(join(cwd, f), "utf-8");
25
+ }
26
+ catch {
27
+ return "";
28
+ }
29
+ };
28
30
  // ── 1. CI/CD Best Practices ──
29
31
  // Check for GitHub Actions workflows
30
32
  const hasWorkflows = has(".github/workflows");
31
33
  practices++;
32
34
  if (hasWorkflows) {
33
35
  followed++;
34
- const workflows = readdirSync(join(cwd, ".github/workflows")).filter(f => f.endsWith(".yml") || f.endsWith(".yaml"));
36
+ const workflows = readdirSync(join(cwd, ".github/workflows")).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
35
37
  for (const wf of workflows) {
36
38
  const content = read(`.github/workflows/${wf}`);
37
39
  // Check: actions pinned to SHA (not @v4, @main)
38
40
  const actionUses = content.match(/uses:\s*([^\n]+)/g) || [];
39
- const unpinned = actionUses.filter(u => !u.includes("@") || (!u.match(/@[a-f0-9]{40}/) && !u.includes("@sha")));
41
+ const unpinned = actionUses.filter((u) => !u.includes("@") || (!u.match(/@[a-f0-9]{40}/) && !u.includes("@sha")));
40
42
  // Only flag third-party actions (not actions/*)
41
- const unpinnedThirdParty = unpinned.filter(u => !u.includes("actions/") && !u.includes("pnpm/"));
43
+ const unpinnedThirdParty = unpinned.filter((u) => !u.includes("actions/") && !u.includes("pnpm/"));
42
44
  if (unpinnedThirdParty.length > 0) {
43
45
  issues.push({
44
46
  severity: "info",
@@ -114,7 +116,11 @@ export function runBestPractices(cwd) {
114
116
  followed++;
115
117
  }
116
118
  else {
117
- issues.push({ severity: "info", message: "No engine constraints (engines in package.json or .nvmrc) — Node version not pinned", rule: "pin-node-version" });
119
+ issues.push({
120
+ severity: "info",
121
+ message: "No engine constraints (engines in package.json or .nvmrc) — Node version not pinned",
122
+ rule: "pin-node-version",
123
+ });
118
124
  }
119
125
  // npm provenance / package.json has repository field
120
126
  if (pkg) {
@@ -123,7 +129,11 @@ export function runBestPractices(cwd) {
123
129
  followed++;
124
130
  }
125
131
  else {
126
- issues.push({ severity: "info", message: "package.json missing repository field — provenance attestation won't link to source", rule: "repository-field" });
132
+ issues.push({
133
+ severity: "info",
134
+ message: "package.json missing repository field — provenance attestation won't link to source",
135
+ rule: "repository-field",
136
+ });
127
137
  }
128
138
  }
129
139
  // ── 3. Repo Hygiene ──
@@ -149,14 +159,22 @@ export function runBestPractices(cwd) {
149
159
  followed++;
150
160
  }
151
161
  else {
152
- issues.push({ severity: "info", message: "No CONTRIBUTING.md — onboarding is harder for new contributors", rule: "contributing-guide" });
162
+ issues.push({
163
+ severity: "info",
164
+ message: "No CONTRIBUTING.md — onboarding is harder for new contributors",
165
+ rule: "contributing-guide",
166
+ });
153
167
  }
154
168
  // ── 4. Developer Experience ──
155
169
  // .env.example
156
170
  practices++;
157
171
  const hasEnvFiles = has(".env") || has(".env.local") || has(".env.development");
158
172
  if (hasEnvFiles && !has(".env.example")) {
159
- issues.push({ severity: "info", message: "Has .env files but no .env.example — new developers won't know what vars are needed", rule: "env-example" });
173
+ issues.push({
174
+ severity: "info",
175
+ message: "Has .env files but no .env.example — new developers won't know what vars are needed",
176
+ rule: "env-example",
177
+ });
160
178
  }
161
179
  else {
162
180
  followed++;
@@ -168,7 +186,11 @@ export function runBestPractices(cwd) {
168
186
  followed++;
169
187
  }
170
188
  else {
171
- issues.push({ severity: "info", message: "No pre-commit hooks (husky/lefthook) — lint/format not enforced before commit", rule: "pre-commit-hooks" });
189
+ issues.push({
190
+ severity: "info",
191
+ message: "No pre-commit hooks (husky/lefthook) — lint/format not enforced before commit",
192
+ rule: "pre-commit-hooks",
193
+ });
172
194
  }
173
195
  // Renovate/Dependabot for automated dependency updates
174
196
  practices++;
@@ -176,16 +198,29 @@ export function runBestPractices(cwd) {
176
198
  followed++;
177
199
  }
178
200
  else {
179
- issues.push({ severity: "info", message: "No Dependabot/Renovate — dependency updates are manual and often forgotten", rule: "automated-deps" });
201
+ issues.push({
202
+ severity: "info",
203
+ message: "No Dependabot/Renovate — dependency updates are manual and often forgotten",
204
+ rule: "automated-deps",
205
+ });
180
206
  }
181
207
  // ── 5. Code Quality Tooling ──
182
208
  // Linter configured
183
209
  practices++;
184
- if (has("biome.json") || has(".eslintrc.json") || has(".eslintrc.js") || has("eslint.config.js") || has("eslint.config.ts") || has("analysis_options.yaml")) {
210
+ if (has("biome.json") ||
211
+ has(".eslintrc.json") ||
212
+ has(".eslintrc.js") ||
213
+ has("eslint.config.js") ||
214
+ has("eslint.config.ts") ||
215
+ has("analysis_options.yaml")) {
185
216
  followed++;
186
217
  }
187
218
  else {
188
- issues.push({ severity: "warning", message: "No linter config (ESLint/Biome/dart analyze) — code style not enforced", rule: "linter-config" });
219
+ issues.push({
220
+ severity: "warning",
221
+ message: "No linter config (ESLint/Biome/dart analyze) — code style not enforced",
222
+ rule: "linter-config",
223
+ });
189
224
  }
190
225
  // Formatter configured
191
226
  practices++;
@@ -193,7 +228,11 @@ export function runBestPractices(cwd) {
193
228
  followed++;
194
229
  }
195
230
  else {
196
- issues.push({ severity: "info", message: "No formatter config (Prettier/Biome/.editorconfig) — inconsistent code formatting", rule: "formatter-config" });
231
+ issues.push({
232
+ severity: "info",
233
+ message: "No formatter config (Prettier/Biome/.editorconfig) — inconsistent code formatting",
234
+ rule: "formatter-config",
235
+ });
197
236
  }
198
237
  // TypeScript strict mode
199
238
  practices++;
@@ -202,7 +241,11 @@ export function runBestPractices(cwd) {
202
241
  followed++;
203
242
  }
204
243
  else {
205
- issues.push({ severity: "info", message: "TypeScript strict mode not enabled — allows implicit any and null errors", rule: "ts-strict-mode" });
244
+ issues.push({
245
+ severity: "info",
246
+ message: "TypeScript strict mode not enabled — allows implicit any and null errors",
247
+ rule: "ts-strict-mode",
248
+ });
206
249
  }
207
250
  // ── 6. Testing Best Practices ──
208
251
  // Test script exists
@@ -219,7 +262,11 @@ export function runBestPractices(cwd) {
219
262
  followed++;
220
263
  }
221
264
  else {
222
- issues.push({ severity: "info", message: "No test coverage configuration — coverage thresholds not enforced", rule: "coverage-config" });
265
+ issues.push({
266
+ severity: "info",
267
+ message: "No test coverage configuration — coverage thresholds not enforced",
268
+ rule: "coverage-config",
269
+ });
223
270
  }
224
271
  // ── 7. Docker / Deployment ──
225
272
  // Dockerfile best practices (if Docker is used)
@@ -230,7 +277,11 @@ export function runBestPractices(cwd) {
230
277
  followed++;
231
278
  }
232
279
  else if (dockerfile.includes(":latest")) {
233
- issues.push({ severity: "warning", message: "Dockerfile uses :latest tag — pin to a specific version for reproducible builds", rule: "docker-pin-version" });
280
+ issues.push({
281
+ severity: "warning",
282
+ message: "Dockerfile uses :latest tag — pin to a specific version for reproducible builds",
283
+ rule: "docker-pin-version",
284
+ });
234
285
  }
235
286
  else {
236
287
  followed++;
@@ -242,7 +293,11 @@ export function runBestPractices(cwd) {
242
293
  followed++;
243
294
  }
244
295
  else if (dockerfile.length > 100) {
245
- issues.push({ severity: "info", message: "Dockerfile is single-stage — consider multi-stage to reduce image size", rule: "docker-multi-stage" });
296
+ issues.push({
297
+ severity: "info",
298
+ message: "Dockerfile is single-stage — consider multi-stage to reduce image size",
299
+ rule: "docker-multi-stage",
300
+ });
246
301
  }
247
302
  else {
248
303
  followed++;
@@ -253,7 +308,11 @@ export function runBestPractices(cwd) {
253
308
  followed++;
254
309
  }
255
310
  else {
256
- issues.push({ severity: "info", message: "No .dockerignore — node_modules and build artifacts will bloat Docker image", rule: "dockerignore" });
311
+ issues.push({
312
+ severity: "info",
313
+ message: "No .dockerignore — node_modules and build artifacts will bloat Docker image",
314
+ rule: "dockerignore",
315
+ });
257
316
  }
258
317
  }
259
318
  // ── 8. Git Practices ──
@@ -264,7 +323,11 @@ export function runBestPractices(cwd) {
264
323
  followed++;
265
324
  }
266
325
  else if (gitignore) {
267
- issues.push({ severity: "info", message: ".gitignore exists but may be incomplete — ensure build artifacts are excluded", rule: "gitignore-complete" });
326
+ issues.push({
327
+ severity: "info",
328
+ message: ".gitignore exists but may be incomplete — ensure build artifacts are excluded",
329
+ rule: "gitignore-complete",
330
+ });
268
331
  }
269
332
  else {
270
333
  followed++; // no gitignore = handled by structure check
@@ -275,7 +338,11 @@ export function runBestPractices(cwd) {
275
338
  followed++;
276
339
  }
277
340
  else {
278
- issues.push({ severity: "info", message: "No commit convention enforcement (commitlint/changesets) — changelog generation is manual", rule: "conventional-commits" });
341
+ issues.push({
342
+ severity: "info",
343
+ message: "No commit convention enforcement (commitlint/changesets) — changelog generation is manual",
344
+ rule: "conventional-commits",
345
+ });
279
346
  }
280
347
  // ── 9. Monitoring & Observability ──
281
348
  // Error tracking (Sentry, Bugsnag, etc.) — only for apps/servers, not CLI tools
@@ -286,7 +353,11 @@ export function runBestPractices(cwd) {
286
353
  followed++;
287
354
  }
288
355
  else {
289
- issues.push({ severity: "info", message: "No error tracking (Sentry/Bugsnag) — production errors may go unnoticed", rule: "error-tracking" });
356
+ issues.push({
357
+ severity: "info",
358
+ message: "No error tracking (Sentry/Bugsnag) — production errors may go unnoticed",
359
+ rule: "error-tracking",
360
+ });
290
361
  }
291
362
  }
292
363
  // ── 10. API & Configuration ──
@@ -298,7 +369,11 @@ export function runBestPractices(cwd) {
298
369
  else {
299
370
  const hasEnvUsage = pkg.includes("process.env") || read("src/index.ts").includes("process.env") || read("src/main.ts").includes("process.env");
300
371
  if (hasEnvUsage) {
301
- issues.push({ severity: "info", message: "Uses env vars but no validation library (zod/envalid) — missing vars crash at runtime", rule: "env-validation" });
372
+ issues.push({
373
+ severity: "info",
374
+ message: "Uses env vars but no validation library (zod/envalid) — missing vars crash at runtime",
375
+ rule: "env-validation",
376
+ });
302
377
  }
303
378
  else {
304
379
  followed++;
@@ -20,7 +20,9 @@ export function runDependencies(cwd, stack) {
20
20
  }
21
21
  }
22
22
  }
23
- catch { /* parse failed */ }
23
+ catch {
24
+ /* parse failed */
25
+ }
24
26
  if (majorOutdated > 0)
25
27
  issues.push({ severity: "warning", message: `${majorOutdated} packages behind by a major version` });
26
28
  const score = Math.max(0, Math.min(100, 100 - majorOutdated));
@@ -28,7 +28,14 @@ export function runDuplication(cwd) {
28
28
  const block = lines
29
29
  .slice(i, i + MIN_LINES)
30
30
  .map((l) => l.trim())
31
- .filter((l) => l.length > 0 && !l.startsWith("//") && !l.startsWith("*") && !l.startsWith("import ") && !l.startsWith("export {") && l !== "{" && l !== "}" && l !== "");
31
+ .filter((l) => l.length > 0 &&
32
+ !l.startsWith("//") &&
33
+ !l.startsWith("*") &&
34
+ !l.startsWith("import ") &&
35
+ !l.startsWith("export {") &&
36
+ l !== "{" &&
37
+ l !== "}" &&
38
+ l !== "");
32
39
  if (block.length < MIN_LINES - 2)
33
40
  continue; // too many empty/trivial lines
34
41
  const key = block.join("\n");
@@ -1,12 +1,19 @@
1
1
  /** Error handling check — detects poor error handling patterns. */
2
- import { gradeFromScore } from "../types.js";
3
2
  import { getProductionFiles } from "../fs-utils.js";
3
+ import { gradeFromScore } from "../types.js";
4
4
  export function runErrorHandling(cwd, stack) {
5
5
  const start = Date.now();
6
6
  const issues = [];
7
7
  const files = getProductionFiles(cwd);
8
8
  if (files.length === 0) {
9
- return { name: "error-handling", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
9
+ return {
10
+ name: "error-handling",
11
+ score: 100,
12
+ grade: "A",
13
+ details: { skipped: true, reason: "no source files" },
14
+ issues: [],
15
+ duration: Date.now() - start,
16
+ };
10
17
  }
11
18
  let emptyCatch = 0;
12
19
  let throwString = 0;
@@ -20,7 +27,13 @@ export function runErrorHandling(cwd, stack) {
20
27
  }
21
28
  if (/\bthrow\s+["'`]/.test(line)) {
22
29
  throwString++;
23
- issues.push({ severity: "warning", message: "throw string literal — use throw new Error()", file: f.path, line: i + 1, rule: "throw-string" });
30
+ issues.push({
31
+ severity: "warning",
32
+ message: "throw string literal — use throw new Error()",
33
+ file: f.path,
34
+ line: i + 1,
35
+ rule: "throw-string",
36
+ });
24
37
  }
25
38
  }
26
39
  }
@@ -135,7 +135,9 @@ export function runPerformance(cwd) {
135
135
  try {
136
136
  bundleSizeKB = Math.round(dirSizeKB(distPath));
137
137
  }
138
- catch { /* can't read dist */ }
138
+ catch {
139
+ /* can't read dist */
140
+ }
139
141
  break;
140
142
  }
141
143
  }
@@ -1,14 +1,28 @@
1
1
  /** React-specific checks — hooks rules, conditional hooks, missing keys, prop spreading. */
2
- import { gradeFromScore } from "../types.js";
3
2
  import { getProductionFiles } from "../fs-utils.js";
3
+ import { gradeFromScore } from "../types.js";
4
4
  export function runReact(cwd, stack) {
5
5
  const start = Date.now();
6
6
  if (stack.framework !== "react") {
7
- return { name: "react", score: 100, grade: "A", details: { skipped: true, reason: "not a React project" }, issues: [], duration: Date.now() - start };
7
+ return {
8
+ name: "react",
9
+ score: 100,
10
+ grade: "A",
11
+ details: { skipped: true, reason: "not a React project" },
12
+ issues: [],
13
+ duration: Date.now() - start,
14
+ };
8
15
  }
9
16
  const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
10
17
  if (files.length === 0) {
11
- return { name: "react", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
18
+ return {
19
+ name: "react",
20
+ score: 100,
21
+ grade: "A",
22
+ details: { skipped: true, reason: "no JSX/TSX files" },
23
+ issues: [],
24
+ duration: Date.now() - start,
25
+ };
12
26
  }
13
27
  const issues = [];
14
28
  let conditionalHooks = 0;
@@ -41,7 +55,13 @@ export function runReact(cwd, stack) {
41
55
  // 1. Hooks called inside conditionals
42
56
  if (condBraceDepth > 0 && /\buse[A-Z]\w*\s*\(/.test(trimmed) && !/\/\//.test(trimmed.split("use")[0])) {
43
57
  conditionalHooks++;
44
- issues.push({ severity: "error", message: "Hook called inside conditional — violates Rules of Hooks", file: f.path, line: i + 1, rule: "conditional-hook" });
58
+ issues.push({
59
+ severity: "error",
60
+ message: "Hook called inside conditional — violates Rules of Hooks",
61
+ file: f.path,
62
+ line: i + 1,
63
+ rule: "conditional-hook",
64
+ });
45
65
  }
46
66
  // 2. Missing key in .map() returning JSX
47
67
  if (/\.map\s*\(/.test(trimmed)) {
@@ -55,12 +75,24 @@ export function runReact(cwd, stack) {
55
75
  // 3. index as key
56
76
  if (/key=\{(?:i|idx|index)\}/.test(trimmed) || /key=\{.*(?:, *(?:i|idx|index)\))/.test(trimmed)) {
57
77
  indexKeys++;
58
- issues.push({ severity: "warning", message: "Using index as key — can cause rendering bugs with reorderable lists", file: f.path, line: i + 1, rule: "index-key" });
78
+ issues.push({
79
+ severity: "warning",
80
+ message: "Using index as key — can cause rendering bugs with reorderable lists",
81
+ file: f.path,
82
+ line: i + 1,
83
+ rule: "index-key",
84
+ });
59
85
  }
60
86
  // 4. Prop spreading ({...props} on DOM elements)
61
87
  if (/\{\.\.\.(?!children)\w+\}/.test(trimmed) && /<[a-z]/.test(trimmed)) {
62
88
  propSpreading++;
63
- issues.push({ severity: "warning", message: "Spreading props onto DOM element — can pass unexpected attributes", file: f.path, line: i + 1, rule: "prop-spreading" });
89
+ issues.push({
90
+ severity: "warning",
91
+ message: "Spreading props onto DOM element — can pass unexpected attributes",
92
+ file: f.path,
93
+ line: i + 1,
94
+ rule: "prop-spreading",
95
+ });
64
96
  }
65
97
  // 5. Inline arrow functions in JSX event handlers (performance)
66
98
  if (/on[A-Z]\w*=\{(?:\(\) =>|function)/.test(trimmed)) {
@@ -70,7 +102,11 @@ export function runReact(cwd, stack) {
70
102
  }
71
103
  // Only warn about inline handlers if there are many
72
104
  if (inlineHandlers > 15) {
73
- issues.push({ severity: "warning", message: `${inlineHandlers} inline arrow functions in JSX handlers — extract to named functions for readability`, rule: "inline-handlers" });
105
+ issues.push({
106
+ severity: "warning",
107
+ message: `${inlineHandlers} inline arrow functions in JSX handlers — extract to named functions for readability`,
108
+ rule: "inline-handlers",
109
+ });
74
110
  }
75
111
  const errors = issues.filter((i) => i.severity === "error").length;
76
112
  const warnings = issues.filter((i) => i.severity === "warning").length;
@@ -42,9 +42,11 @@ export function runTypeSafety(cwd, isDart = false) {
42
42
  const trimmed = line.trim();
43
43
  if (trimmed.startsWith("//") || trimmed.startsWith("*"))
44
44
  continue;
45
- // Skip pattern definition lines (prevents false positives when scanning own code)
45
+ // Skip pattern definition lines and string-heavy lines (prevents false positives)
46
46
  if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
47
47
  continue;
48
+ if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
49
+ continue;
48
50
  for (const p of PATTERNS) {
49
51
  const matches = line.match(p.pattern);
50
52
  if (matches) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Code health scanner for the AI coding era. 21 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {