@vibecodeqa/cli 0.21.0 → 0.23.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";
@@ -31,17 +31,22 @@ import { computeTrend, formatTrend } from "./trend.js";
31
31
  import { gradeFromScore } from "./types.js";
32
32
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
33
33
  const VERSION = pkg.version;
34
- const args = process.argv.slice(2);
35
- const flags = new Set(args.filter((a) => a.startsWith("--")));
36
- const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
37
- const outputDir = join(cwd, ".vibe-check");
38
- const jsonOnly = flags.has("--json");
39
- const ciMode = flags.has("--ci");
40
- const skipTests = flags.has("--skip-tests");
41
- const watchMode = flags.has("--watch");
42
- const badgeMode = flags.has("--badge");
43
- const sarifMode = flags.has("--sarif");
44
- const uploadMode = flags.has("--upload");
34
+ function parseFlags() {
35
+ const args = process.argv.slice(2);
36
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
37
+ const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
38
+ return {
39
+ cwd,
40
+ outputDir: join(cwd, ".vibe-check"),
41
+ jsonOnly: flags.has("--json"),
42
+ ciMode: flags.has("--ci"),
43
+ skipTests: flags.has("--skip-tests"),
44
+ watchMode: flags.has("--watch"),
45
+ badgeMode: flags.has("--badge"),
46
+ sarifMode: flags.has("--sarif"),
47
+ uploadMode: flags.has("--upload"),
48
+ };
49
+ }
45
50
  function color(grade) {
46
51
  if (grade === "A")
47
52
  return "\x1b[32m";
@@ -49,23 +54,7 @@ function color(grade) {
49
54
  return "\x1b[33m";
50
55
  return "\x1b[31m";
51
56
  }
52
- async function main() {
53
- const start = Date.now();
54
- if (!jsonOnly) {
55
- console.log("");
56
- console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
57
- console.log(` \x1b[2m${cwd}\x1b[0m`);
58
- console.log("");
59
- }
60
- const stack = detectStack(cwd);
61
- if (!jsonOnly) {
62
- const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
63
- console.log(` stack: ${parts.join(" + ")}`);
64
- console.log("");
65
- }
66
- const checks = [];
67
- const isDart = stack.language === "dart";
68
- // All runners grouped by category
57
+ function runChecks(cwd, stack, skipTests, isDart, jsonOnly) {
69
58
  const runners = [
70
59
  // Foundations
71
60
  { name: "structure", fn: () => runStructure(cwd, stack) },
@@ -97,6 +86,7 @@ async function main() {
97
86
  { name: "doc-coherence", fn: () => runDocCoherence(cwd) },
98
87
  { name: "code-coherence", fn: () => runCodeCoherence(cwd) },
99
88
  ];
89
+ const checks = [];
100
90
  for (const runner of runners) {
101
91
  if (!jsonOnly)
102
92
  process.stdout.write(` ${runner.name.padEnd(14)}`);
@@ -113,20 +103,9 @@ async function main() {
113
103
  console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
114
104
  }
115
105
  }
116
- const score = computeScore(checks);
117
- const grade = gradeFromScore(score);
118
- const duration = Date.now() - start;
119
- const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
120
- const report = {
121
- version: VERSION,
122
- timestamp: new Date().toISOString(),
123
- score,
124
- grade,
125
- checks,
126
- meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
127
- };
128
- // Trend comparison (read previous report before overwriting)
129
- const trend = computeTrend(report, outputDir);
106
+ return checks;
107
+ }
108
+ async function writeOutputs(report, outputDir, flags) {
130
109
  if (!existsSync(outputDir))
131
110
  mkdirSync(outputDir, { recursive: true });
132
111
  // Save to history before overwriting current report
@@ -159,58 +138,127 @@ async function main() {
159
138
  writeFileSync(join(reportDir, filename), html);
160
139
  }
161
140
  // Badge SVG
162
- if (badgeMode) {
141
+ if (flags.badgeMode) {
163
142
  const { buildBadge } = await import("./report/svg.js");
164
- const badgeSvg = buildBadge(score, grade);
143
+ const badgeSvg = buildBadge(report.score, report.grade);
165
144
  writeFileSync(join(outputDir, "badge.svg"), badgeSvg);
166
145
  }
167
146
  // SARIF output for GitHub Code Scanning
168
- if (sarifMode) {
147
+ if (flags.sarifMode) {
169
148
  const { generateSARIF } = await import("./report/sarif.js");
170
149
  writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
171
150
  }
172
- // Upload to VibeCode QA dashboard
173
- if (uploadMode) {
174
- const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
175
- const token = process.env.VCQA_TOKEN || "";
176
- try {
177
- const res = await fetch("https://api.vibecodeqa.online/api/reports", {
178
- method: "POST",
179
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
180
- body: JSON.stringify({ repo, report }),
181
- });
182
- if (res.ok) {
183
- const data = await res.json();
184
- if (!jsonOnly)
185
- console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
186
- }
187
- else if (!jsonOnly) {
188
- console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
189
- }
190
- }
191
- catch {
192
- if (!jsonOnly)
193
- console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
194
- }
195
- }
196
- if (jsonOnly) {
151
+ }
152
+ function printResults(report, trend, flags, outputDir) {
153
+ const { score, grade, checks } = report;
154
+ const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
155
+ if (flags.jsonOnly) {
197
156
  console.log(JSON.stringify(report));
198
157
  }
199
158
  else {
200
159
  const gc = color(grade);
201
160
  console.log("");
202
- console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${duration}ms\x1b[0m`);
161
+ console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${report.meta.duration}ms\x1b[0m`);
203
162
  if (trend)
204
163
  console.log(formatTrend(trend));
205
164
  console.log("");
206
165
  console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
207
166
  console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
208
- if (badgeMode)
167
+ if (flags.badgeMode)
209
168
  console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
210
- if (sarifMode)
169
+ if (flags.sarifMode)
211
170
  console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
212
171
  console.log("");
213
172
  }
173
+ }
174
+ async function handleUpload(report, cwd, jsonOnly) {
175
+ const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
176
+ const token = process.env.VCQA_TOKEN || "";
177
+ try {
178
+ const res = await fetch("https://api.vibecodeqa.online/api/reports", {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
181
+ body: JSON.stringify({ repo, report }),
182
+ });
183
+ if (res.ok) {
184
+ const data = (await res.json());
185
+ if (!jsonOnly)
186
+ console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
187
+ }
188
+ else if (!jsonOnly) {
189
+ console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
190
+ }
191
+ }
192
+ catch {
193
+ if (!jsonOnly)
194
+ console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
195
+ }
196
+ }
197
+ async function startWatch(cwd) {
198
+ const { watch } = await import("node:fs");
199
+ const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
200
+ if (srcDirs.length === 0) {
201
+ console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
202
+ process.exit(1);
203
+ }
204
+ console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
205
+ console.log("");
206
+ let debounce = null;
207
+ let running = false;
208
+ for (const dir of srcDirs) {
209
+ watch(dir, { recursive: true }, (_event, filename) => {
210
+ if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
211
+ return;
212
+ if (running)
213
+ return;
214
+ if (debounce)
215
+ clearTimeout(debounce);
216
+ debounce = setTimeout(async () => {
217
+ running = true;
218
+ console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
219
+ await main().catch(() => { });
220
+ running = false;
221
+ }, 500);
222
+ });
223
+ }
224
+ // Keep process alive
225
+ await new Promise(() => { });
226
+ }
227
+ async function main() {
228
+ const flags = parseFlags();
229
+ const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode } = flags;
230
+ const start = Date.now();
231
+ if (!jsonOnly) {
232
+ console.log("");
233
+ console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
234
+ console.log(` \x1b[2m${cwd}\x1b[0m`);
235
+ console.log("");
236
+ }
237
+ const stack = detectStack(cwd);
238
+ if (!jsonOnly) {
239
+ const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
240
+ console.log(` stack: ${parts.join(" + ")}`);
241
+ console.log("");
242
+ }
243
+ const isDart = stack.language === "dart";
244
+ const checks = runChecks(cwd, stack, skipTests, isDart, jsonOnly);
245
+ const score = computeScore(checks);
246
+ const grade = gradeFromScore(score);
247
+ const duration = Date.now() - start;
248
+ const report = {
249
+ version: VERSION,
250
+ timestamp: new Date().toISOString(),
251
+ score,
252
+ grade,
253
+ checks,
254
+ meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
255
+ };
256
+ const trend = computeTrend(report, outputDir);
257
+ await writeOutputs(report, outputDir, flags);
258
+ printResults(report, trend, flags, outputDir);
259
+ if (flags.uploadMode) {
260
+ await handleUpload(report, cwd, jsonOnly);
261
+ }
214
262
  if (ciMode && score < 60) {
215
263
  process.exit(1);
216
264
  }
@@ -224,36 +272,8 @@ async function main() {
224
272
  /* failed to open browser */
225
273
  }
226
274
  }
227
- // Watch mode — re-run on file changes
228
275
  if (watchMode) {
229
- const { watch } = await import("node:fs");
230
- const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
231
- if (srcDirs.length === 0) {
232
- console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
233
- process.exit(1);
234
- }
235
- console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
236
- console.log("");
237
- let debounce = null;
238
- let running = false;
239
- for (const dir of srcDirs) {
240
- watch(dir, { recursive: true }, (_event, filename) => {
241
- if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
242
- return;
243
- if (running)
244
- return; // prevent concurrent re-runs (M5)
245
- if (debounce)
246
- clearTimeout(debounce);
247
- debounce = setTimeout(async () => {
248
- running = true;
249
- console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
250
- await main().catch(() => { });
251
- running = false;
252
- }, 500);
253
- });
254
- }
255
- // Keep process alive
256
- await new Promise(() => { });
276
+ await startWatch(cwd);
257
277
  }
258
278
  }
259
279
  main().catch((err) => {
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
  }