@vibecodeqa/cli 0.16.0 → 0.18.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.
Files changed (47) hide show
  1. package/README.md +73 -63
  2. package/dist/check-meta.d.ts +1 -0
  3. package/dist/check-meta.js +58 -6
  4. package/dist/cli.js +48 -10
  5. package/dist/detect.js +24 -2
  6. package/dist/fs-utils.d.ts +4 -0
  7. package/dist/fs-utils.js +12 -6
  8. package/dist/report/html.d.ts +18 -9
  9. package/dist/report/html.js +108 -68
  10. package/dist/report/pages.d.ts +4 -4
  11. package/dist/report/pages.js +165 -115
  12. package/dist/report/sarif.d.ts +3 -0
  13. package/dist/report/sarif.js +67 -0
  14. package/dist/report/styles.d.ts +1 -1
  15. package/dist/report/styles.js +105 -33
  16. package/dist/report/svg.d.ts +17 -0
  17. package/dist/report/svg.js +99 -0
  18. package/dist/runners/accessibility.d.ts +3 -0
  19. package/dist/runners/accessibility.js +85 -0
  20. package/dist/runners/architecture.d.ts +2 -0
  21. package/dist/runners/architecture.js +232 -20
  22. package/dist/runners/code-coherence.d.ts +17 -0
  23. package/dist/runners/code-coherence.js +39 -0
  24. package/dist/runners/complexity.js +7 -37
  25. package/dist/runners/confusion.js +3 -31
  26. package/dist/runners/context.js +9 -40
  27. package/dist/runners/dependencies.js +28 -0
  28. package/dist/runners/doc-coherence.d.ts +14 -0
  29. package/dist/runners/doc-coherence.js +48 -0
  30. package/dist/runners/docs.js +7 -32
  31. package/dist/runners/duplication.js +9 -37
  32. package/dist/runners/lint.js +17 -0
  33. package/dist/runners/performance.d.ts +10 -0
  34. package/dist/runners/performance.js +174 -0
  35. package/dist/runners/react.d.ts +3 -0
  36. package/dist/runners/react.js +86 -0
  37. package/dist/runners/secrets.js +8 -29
  38. package/dist/runners/security.js +15 -38
  39. package/dist/runners/standards.js +3 -36
  40. package/dist/runners/structure.js +35 -55
  41. package/dist/runners/testing.js +2 -36
  42. package/dist/runners/type-safety.d.ts +1 -1
  43. package/dist/runners/type-safety.js +19 -37
  44. package/dist/runners/types-check.d.ts +1 -1
  45. package/dist/runners/types-check.js +38 -20
  46. package/dist/types.d.ts +5 -5
  47. package/package.json +11 -10
@@ -1,41 +1,44 @@
1
- /** Generate a multi-page navigable HTML report.
1
+ /** Generate a multi-page HTML report as separate files.
2
2
  *
3
- * Architecture:
4
- * Top nav: Overview | Foundations | Quality | Testing | Security | Issues
5
- * Sub-nav: Tabs for each check within a category
6
- * Detail: Per-check stats + full issue list grouped by file
7
- * File map: Cross-check heatmap of issues per file
8
- *
9
- * All in one self-contained HTML file using hash routing + show/hide.
3
+ * Layout:
4
+ * Top nav: Logo | Overview | Foundations | Quality | ... | Issues | Files
5
+ * Page-level navigation. Scrollable on mobile.
6
+ * Sidebar: CONTEXTUAL to current page NOT a duplicate of top nav.
7
+ * Overview: score + category scores
8
+ * Category: individual checks with grades (click to jump)
9
+ * Issues: severity breakdown
10
+ * Files: summary stats
11
+ * Mobile: Hamburger toggles both top nav dropdown and sidebar panel.
10
12
  */
11
13
  import { getCheckMeta } from "../check-meta.js";
12
14
  import { e, fileLink, gc } from "./components.js";
13
- import { categoryPages, filesPage, heatmapPage, issuesPage, overviewPage } from "./pages.js";
15
+ import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
14
16
  import { CSS } from "./styles.js";
15
- const GROUPS = [
16
- { id: "foundations", label: "Foundations", checks: ["structure", "lint", "types", "type-safety", "standards"] },
17
- { id: "quality", label: "Quality", checks: ["complexity", "duplication", "error-handling", "docs"] },
18
- { id: "testing", label: "Testing", checks: ["testing"] },
19
- { id: "arch", label: "Architecture", checks: ["architecture"] },
20
- { id: "security", label: "Security", checks: ["secrets", "security", "dependencies"] },
21
- { id: "llm", label: "LLM Readiness", checks: ["confusion", "context"] },
17
+ export const GROUPS = [
18
+ { 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"] },
20
+ { id: "testing", label: "Testing", file: "testing.html", checks: ["testing"] },
21
+ { id: "arch", label: "Architecture", file: "architecture.html", checks: ["architecture", "performance"] },
22
+ { id: "security", label: "Security", file: "security.html", checks: ["secrets", "security", "dependencies"] },
23
+ { id: "llm", label: "AI Readiness", file: "ai-readiness.html", checks: ["confusion", "context"] },
24
+ { id: "ai", label: "AI Analysis", file: "ai-analysis.html", checks: ["doc-coherence", "code-coherence"] },
22
25
  ];
23
- export function generateHTML(report) {
26
+ export function generatePages(report, historyDir) {
27
+ const pages = new Map();
24
28
  const allChecks = report.checks;
25
29
  const checkMap = new Map(allChecks.map((c) => [c.name, c]));
26
- const active = allChecks.filter((c) => !c.details.skipped);
30
+ const active = allChecks.filter((c) => !c.details.skipped && !c.details.comingSoon);
27
31
  const ru = report.meta.repoUrl;
28
32
  const br = report.meta.branch;
29
33
  const fl = (path, line) => fileLink(path, line, ru, br);
30
34
  const totalIssues = allChecks.reduce((s, c) => s + c.issues.length, 0);
31
35
  const proj = report.meta.cwd.split("/").pop() || "project";
32
- // ── File heatmap: aggregate issues per file across all checks ──
33
36
  const fileIssues = new Map();
34
37
  for (const c of allChecks) {
35
38
  for (const iss of c.issues) {
36
39
  if (!iss.file)
37
40
  continue;
38
- const f = iss.file.split(":")[0]; // strip :line from composite file fields
41
+ const f = iss.file.split(":")[0];
39
42
  const entry = fileIssues.get(f) || { errors: 0, warnings: 0, checks: new Set() };
40
43
  if (iss.severity === "error")
41
44
  entry.errors++;
@@ -48,42 +51,87 @@ export function generateHTML(report) {
48
51
  const topFiles = [...fileIssues.entries()]
49
52
  .map(([file, d]) => ({ file, total: d.errors + d.warnings, errors: d.errors, warnings: d.warnings, checks: [...d.checks] }))
50
53
  .sort((a, b) => b.total - a.total)
51
- .slice(0, 20);
52
- // ── Category averages ──
54
+ .slice(0, 30);
53
55
  const catScores = GROUPS.map((g) => {
54
56
  const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
55
57
  const scored = checks.filter((c) => !c.details.skipped);
56
58
  const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
57
59
  return { ...g, avg, checks };
58
60
  });
59
- // ── Top nav ──
60
- const topNavItems = [
61
- { id: "overview", label: "Overview" },
62
- ...GROUPS.map((g) => ({ id: g.id, label: g.label })),
63
- { id: "issues", label: `Issues (${totalIssues})` },
64
- { id: "files", label: "File Map" },
65
- { id: "heatmap", label: "Heatmap" },
61
+ const w = (id, sidebar, content) => wrap(proj, id, report, totalIssues, sidebar, content);
62
+ // ── 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);
66
+ const clr = isPremium ? "#6366f1" : gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
67
+ const label = isPremium
68
+ ? `<span class="pro-badge" style="font-size:0.5rem;padding:0.08rem 0.35rem">PRO</span>`
69
+ : `<span style="color:${clr}">${cs.avg}</span>`;
70
+ return `<a class="side-cat" href="${cs.file}">${cs.label} ${label}</a>`;
71
+ }).join("")
72
+ + sidebarViews(totalIssues, fileIssues.size);
73
+ pages.set("index.html", w("overview", overviewSidebar, overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
74
+ // ── Category pages: sidebar shows the checks within this category ──
75
+ for (let i = 0; i < GROUPS.length; i++) {
76
+ const g = GROUPS[i];
77
+ 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;
83
+ 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>`;
85
+ 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);
89
+ pages.set(g.file, w(g.id, catSidebar, categoryPage(cs, fl)));
90
+ }
91
+ // ── Issues: sidebar shows severity breakdown ──
92
+ const allIssuesList = allChecks.flatMap((c) => c.issues);
93
+ const errCount = allIssuesList.filter((i) => i.severity === "error").length;
94
+ const warnCount = allIssuesList.filter((i) => i.severity === "warning").length;
95
+ 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);
103
+ pages.set("issues.html", w("issues", issuesSidebar, issuesPage(allChecks, totalIssues, fl)));
104
+ // ── 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);
111
+ pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
112
+ return pages;
113
+ }
114
+ export function generateHTML(report, historyDir) {
115
+ return generatePages(report, historyDir).get("index.html");
116
+ }
117
+ // ── Sidebar fragments ──
118
+ function sidebarScore(report) {
119
+ return `<div class="side-section"><div class="side-label">Score</div><div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>`;
120
+ }
121
+ function sidebarViews(totalIssues, fileCount) {
122
+ return `<div class="side-section side-views"><div class="side-label" style="margin-top:0.3rem">Views</div><a class="side-check" href="issues.html">Issues <span style="color:var(--muted)">${totalIssues}</span></a><a class="side-check" href="files.html">Files <span style="color:var(--muted)">${fileCount}</span></a></div>`;
123
+ }
124
+ // ── Page wrapper ──
125
+ function wrap(proj, currentId, report, totalIssues, sidebar, content) {
126
+ const navItems = [
127
+ { id: "overview", label: "Overview", file: "index.html" },
128
+ ...GROUPS.map((g) => ({ id: g.id, label: g.label, file: g.file })),
129
+ { id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
130
+ { id: "files", label: "Files", file: "files.html" },
66
131
  ];
67
- const topNav = topNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
68
- // ── Sidebar ──
69
- const sidebar = catScores
70
- .map((cs) => {
71
- const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
72
- 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
73
- .map((c) => {
74
- const sk = c.details.skipped;
75
- const meta = getCheckMeta(c.name);
76
- return `<a class="side-check" onclick="go('${cs.id}')" title="${e(meta.label)}"><span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade}</span> ${e(meta.label)}</a>`;
77
- })
78
- .join("")}</div>`;
79
- })
132
+ const nav = navItems
133
+ .map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
80
134
  .join("");
81
- // ── Assemble pages ──
82
- const overview = overviewPage(report, active, totalIssues, catScores);
83
- const catPages = categoryPages(catScores, fl);
84
- const issues = issuesPage(allChecks, totalIssues, fl);
85
- const files = filesPage(topFiles, fl);
86
- const heatmap = heatmapPage(fileIssues, fl);
87
135
  return `<!DOCTYPE html>
88
136
  <html lang="en">
89
137
  <head>
@@ -95,44 +143,36 @@ export function generateHTML(report) {
95
143
  <body>
96
144
 
97
145
  <nav class="top">
98
- <div class="logo"><span>VibeCode</span> QA</div>
99
- ${topNav}
146
+ <a class="logo" href="index.html"><span>VibeCode</span> QA</a>
147
+ <button class="hamburger" onclick="toggleMenu()" aria-label="Menu">&#9776;</button>
148
+ <div class="nav-scroll">${nav}</div>
100
149
  </nav>
101
150
 
102
- <aside class="side">
103
- <div class="side-section">Score<div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>
104
- ${sidebar}
105
- </aside>
151
+ <aside class="side" id="sidebar">${sidebar}</aside>
152
+
106
153
  <div class="content">
107
- ${overview}
108
- ${catPages}
109
- ${issues}
110
- ${files}
111
- ${heatmap}
154
+ ${content}
112
155
  <div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} &mdash; <code>npx @vibecodeqa/cli</code></div>
113
156
  </div>
114
157
 
115
158
  <script>
116
- function go(id){
117
- document.querySelectorAll('.tn').forEach(n=>{n.classList.toggle('active',n.dataset.page===id)});
118
- document.querySelectorAll('.page').forEach(p=>{p.classList.toggle('active',p.id==='p-'+id)});
119
- window.scrollTo(0,0);
159
+ function toggleMenu(){
160
+ document.querySelector('.nav-scroll').classList.toggle('open');
161
+ document.getElementById('sidebar').classList.toggle('open');
120
162
  }
121
163
  function sub(el,cat){
122
164
  const id=el.dataset.sub;
123
165
  el.parentElement.querySelectorAll('.sn').forEach(n=>n.classList.remove('active'));
124
166
  el.classList.add('active');
125
- document.querySelectorAll('#p-'+cat+' .sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
167
+ document.querySelectorAll('.sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
126
168
  }
127
- // Copy-prompt buttons — read from data-attribute (no inline JS with user data)
128
169
  document.addEventListener('click',function(ev){
129
170
  var btn=ev.target.closest('.cp-btn');
130
171
  if(!btn)return;
131
- navigator.clipboard.writeText(btn.dataset.prompt||'');
172
+ var text=btn.dataset.prompt||'';
173
+ try{navigator.clipboard.writeText(text)}catch(e){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta)}
132
174
  btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
133
175
  });
134
- // Init: show overview
135
- document.querySelector('.tn').classList.add('active');
136
176
  </script>
137
177
  </body></html>`;
138
178
  }
@@ -3,6 +3,7 @@ import type { CheckResult, VibeReport } from "../types.js";
3
3
  export interface CatScore {
4
4
  id: string;
5
5
  label: string;
6
+ file?: string;
6
7
  checks: CheckResult[];
7
8
  avg: number;
8
9
  }
@@ -14,11 +15,10 @@ export interface FileEntry {
14
15
  checks: string[];
15
16
  }
16
17
  type FL = (path: string, line?: number) => string;
17
- export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[]): string;
18
- export declare function categoryPages(catScores: CatScore[], fl: FL): string;
18
+ export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[], allChecks: CheckResult[], topFiles: FileEntry[], fl: FL, historyDir?: string): string;
19
+ export declare function categoryPage(cs: CatScore, fl: FL): string;
19
20
  export declare function issuesPage(allChecks: CheckResult[], totalIssues: number, fl: FL): string;
20
- export declare function filesPage(topFiles: FileEntry[], fl: FL): string;
21
- export declare function heatmapPage(fileIssues: Map<string, {
21
+ export declare function filesPage(topFiles: FileEntry[], fileIssues: Map<string, {
22
22
  errors: number;
23
23
  warnings: number;
24
24
  checks: Set<string>;
@@ -1,16 +1,21 @@
1
1
  /** Page renderers for the HTML report. */
2
2
  import { getCheckMeta } from "../check-meta.js";
3
- import { generateArchSVG } from "../runners/architecture.js";
3
+ import { loadHistory } from "../history.js";
4
+ import { generateArchSVG, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
4
5
  import { e, gc, pc } from "./components.js";
5
- import { buildRadar, buildRing } from "./svg.js";
6
- export function overviewPage(report, active, totalIssues, catScores) {
7
- const ringPct = report.score;
8
- const barChart = active
9
- .sort((a, b) => a.score - b.score)
10
- .map((c) => {
11
- 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>`;
12
- })
13
- .join("");
6
+ import { buildPyramid, buildRadar, buildRing, buildTimeline } from "./svg.js";
7
+ // ── Overview ──────────────────────────────────────────────────────────
8
+ export function overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir) {
9
+ const hero = `<div class="hero">
10
+ ${buildRing(report.score, gc(report.grade))}
11
+ <div class="hc">
12
+ <span class="hg" style="color:${gc(report.grade)}">${report.grade}</span>
13
+ <span class="hs" style="color:${gc(report.grade)}">${report.score}/100</span>
14
+ <span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
15
+ </div>
16
+ </div>`;
17
+ const scoredCats = catScores.filter((cs) => cs.checks.some((c) => !c.details.skipped && !c.details.comingSoon));
18
+ const radarSvg = scoredCats.length >= 3 ? buildRadar(scoredCats.map((cs) => ({ label: cs.label, score: cs.avg }))) : "";
14
19
  const catCards = catScores
15
20
  .map((cs) => {
16
21
  const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
@@ -20,147 +25,192 @@ export function overviewPage(report, active, totalIssues, catScores) {
20
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>`;
21
26
  })
22
27
  .join("");
23
- 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>`;
28
+ const href = cs.file || `${cs.id}.html`;
29
+ return `<a class="cc" href="${href}"><div class="cc-s" style="color:${clr}">${cs.avg}</div><div class="cc-l">${cs.label}</div><div class="cc-m">${mini}</div></a>`;
30
+ })
31
+ .join("");
32
+ let timelineSection = "";
33
+ if (historyDir) {
34
+ const history = loadHistory(historyDir);
35
+ if (history.length >= 2) {
36
+ const timelineSvg = buildTimeline(history.map((h) => ({ score: h.score, timestamp: h.timestamp })));
37
+ timelineSection = `<div class="ov-section"><h3>Score Timeline</h3><div class="timeline">${timelineSvg}</div></div>`;
38
+ }
39
+ }
40
+ const barChart = active
41
+ .sort((a, b) => a.score - b.score)
42
+ .map((c) => {
43
+ 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>`;
24
44
  })
25
45
  .join("");
26
- const radarSvg = buildRadar(catScores.map((cs) => ({ label: cs.label, score: cs.avg })));
27
- return `<div id="p-overview" class="page active">
46
+ const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
47
+ const sortedIssues = allIssues
48
+ .sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
49
+ .slice(0, 10);
50
+ let topIssuesHtml = "";
51
+ if (sortedIssues.length > 0) {
52
+ const rows = sortedIssues
53
+ .map((i) => {
54
+ const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
55
+ return `<div class="ov-issue ${i.severity}"><span class="is">${i.severity[0].toUpperCase()}</span><span class="ov-check">${e(i.check)}</span>${loc ? `<span class="ov-loc">${loc}</span>` : ""}<span class="ov-msg">${e(i.message)}</span></div>`;
56
+ })
57
+ .join("");
58
+ const viewAll = allIssues.length > 10 ? `<a class="ov-link" href="issues.html">View all ${allIssues.length} issues \u2192</a>` : "";
59
+ topIssuesHtml = `<div class="ov-section"><h3>Top Issues</h3>${rows}${viewAll}</div>`;
60
+ }
61
+ let fileHotspotsHtml = "";
62
+ if (topFiles.length > 0) {
63
+ const fileRows = topFiles.slice(0, 5).map((f) => {
64
+ const pct = Math.min(100, f.total * 5);
65
+ 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("");
67
+ const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
68
+ fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
69
+ }
70
+ const stackHtml = Object.entries(report.meta.stack)
71
+ .filter(([, v]) => v !== "none" && v !== "unknown")
72
+ .map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
73
+ .join("");
74
+ return `
28
75
  <div class="dash">
29
- <div class="hero">
30
- ${buildRing(ringPct, gc(report.grade))}
31
- <div class="hc"><span class="hg" style="color:${gc(report.grade)}">${report.grade}</span><span class="hs" style="color:${gc(report.grade)}">${report.score}/100</span><span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span></div>
32
- </div>
76
+ ${hero}
33
77
  <div class="radar">${radarSvg}</div>
34
78
  </div>
35
79
  <div class="cats">${catCards}</div>
36
- <h3>All Checks</h3>
37
- <div class="bars">${barChart}</div>
38
- <div class="stack">${Object.entries(report.meta.stack)
39
- .filter(([, v]) => v !== "none" && v !== "unknown")
40
- .map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
41
- .join("")}</div>
42
- </div>`;
80
+ ${timelineSection}
81
+ <div class="ov-section"><h3>All Checks</h3><div class="bars">${barChart}</div></div>
82
+ ${topIssuesHtml}
83
+ ${fileHotspotsHtml}
84
+ <div class="stack">${stackHtml}</div>`;
43
85
  }
44
- export function categoryPages(catScores, fl) {
45
- let catPagesHtml = "";
46
- for (const cs of catScores) {
47
- const subNav = cs.checks
48
- .map((c, i) => {
49
- const sk = c.details.skipped;
50
- 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 ? "\u2014" : c.grade}</span></a>`;
86
+ // ── Single category page ──────────────────────────────────────────
87
+ export function categoryPage(cs, fl) {
88
+ const subNav = cs.checks
89
+ .map((c, i) => {
90
+ const sk = c.details.skipped;
91
+ const premium = c.details.comingSoon;
92
+ const badge = premium ? "PRO" : sk ? "\u2014" : c.grade;
93
+ const clr = premium ? "#6366f1" : sk ? "#555" : gc(c.grade);
94
+ 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>`;
95
+ })
96
+ .join("");
97
+ const subPages = cs.checks
98
+ .map((c, i) => {
99
+ const meta = getCheckMeta(c.name);
100
+ const sk = c.details.skipped;
101
+ const premium = c.details.comingSoon;
102
+ const detailsFiltered = Object.entries(c.details)
103
+ .filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
104
+ .map(([k, v]) => {
105
+ const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
106
+ return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
51
107
  })
52
108
  .join("");
53
- const subPages = cs.checks
54
- .map((c, i) => {
55
- const meta = getCheckMeta(c.name);
56
- const sk = c.details.skipped;
57
- const detailsFiltered = Object.entries(c.details)
58
- .filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
59
- .map(([k, v]) => {
60
- const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
61
- return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
62
- })
63
- .join("");
64
- // Group issues by file
65
- const byFile = new Map();
66
- const noFile = [];
67
- for (const iss of c.issues) {
68
- const f = iss.file?.split(":")[0];
69
- if (f) {
70
- const arr = byFile.get(f) || [];
71
- arr.push(iss);
72
- byFile.set(f, arr);
73
- }
74
- else {
75
- noFile.push(iss);
76
- }
109
+ const byFile = new Map();
110
+ const noFile = [];
111
+ for (const iss of c.issues) {
112
+ const f = iss.file?.split(":")[0];
113
+ if (f) {
114
+ const arr = byFile.get(f) || [];
115
+ arr.push(iss);
116
+ byFile.set(f, arr);
117
+ }
118
+ else {
119
+ noFile.push(iss);
77
120
  }
78
- let issuesHtml = "";
79
- for (const [file, issues] of byFile) {
80
- issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
81
- for (const iss of issues) {
82
- const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
83
- 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">\ud83d\udccb</button></div>`;
84
- }
85
- issuesHtml += `</div>`;
121
+ }
122
+ let issuesHtml = "";
123
+ for (const [file, issues] of byFile) {
124
+ issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
125
+ for (const iss of issues) {
126
+ const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
127
+ 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">\ud83d\udccb</button></div>`;
86
128
  }
87
- if (noFile.length > 0) {
88
- issuesHtml += `<div class="fg"><div class="fn">General</div>`;
89
- for (const iss of noFile) {
90
- issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span><span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}</div>`;
91
- }
92
- issuesHtml += `</div>`;
129
+ issuesHtml += `</div>`;
130
+ }
131
+ if (noFile.length > 0) {
132
+ issuesHtml += `<div class="fg"><div class="fn">General</div>`;
133
+ for (const iss of noFile) {
134
+ issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span><span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}</div>`;
93
135
  }
136
+ issuesHtml += `</div>`;
137
+ }
138
+ if (premium) {
139
+ const det = c.details;
140
+ const desc = det.description || meta.description;
141
+ const detailKvs = Object.entries(det)
142
+ .filter(([k]) => !["premium", "comingSoon", "reason", "description"].includes(k))
143
+ .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
+ .join("");
94
145
  return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
146
+ <div class="pro-card">
147
+ <div class="pro-badge">PRO</div>
148
+ <h3 style="margin-bottom:0.5rem;color:var(--text)">${e(meta.label)}</h3>
149
+ <p class="pro-desc">${e(desc)}</p>
150
+ ${meta.risk ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div></div>` : ""}
151
+ ${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` : ""}
152
+ <p class="pro-cta">Coming soon with VibeCode QA Pro</p>
153
+ </div>
154
+ </div>`;
155
+ }
156
+ return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
95
157
  <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>
96
158
  ${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>` : ""}
97
159
  ${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
98
- ${c.name === "architecture" && !sk ? `<div class="arch-svg">${generateArchSVG(c.details)}</div>` : ""}
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>` : ""}
99
162
  ${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
100
163
  ${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
101
164
  </div>`;
102
- })
103
- .join("");
104
- const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
105
- catPagesHtml += `<div id="p-${cs.id}" class="page">
165
+ })
166
+ .join("");
167
+ const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
168
+ return `
106
169
  <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>
107
170
  <div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
108
171
  <div class="sub-nav">${subNav}</div>
109
- ${subPages}
110
- </div>`;
111
- }
112
- return catPagesHtml;
172
+ ${subPages}`;
113
173
  }
174
+ // ── Issues view ──────────────────────────────────────────
114
175
  export function issuesPage(allChecks, totalIssues, fl) {
115
176
  const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
177
+ const errorCount = allIssues.filter((i) => i.severity === "error").length;
178
+ const warnCount = allIssues.filter((i) => i.severity === "warning").length;
179
+ const infoCount = allIssues.filter((i) => i.severity === "info").length;
116
180
  const issueRows = allIssues
181
+ .sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
117
182
  .slice(0, 200)
118
183
  .map((i) => {
119
184
  const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
120
185
  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>`;
121
186
  })
122
187
  .join("");
123
- return `<div id="p-issues" class="page">
188
+ return `
124
189
  <h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
125
- <div class="isf">${allIssues.filter((i) => i.severity === "error").length} errors \u00b7 ${allIssues.filter((i) => i.severity === "warning").length} warnings</div>
190
+ <div class="isf"><span style="color:var(--fail)">${errorCount} errors</span> \u00b7 <span style="color:var(--warn)">${warnCount} warnings</span>${infoCount > 0 ? ` \u00b7 <span style="color:var(--info)">${infoCount} info</span>` : ""}</div>
126
191
  <table class="it"><thead><tr><th></th><th>Check</th><th>Location</th><th>Message</th><th>Rule</th></tr></thead><tbody>${issueRows}</tbody></table>
127
- ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
128
- </div>`;
192
+ ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}`;
129
193
  }
130
- export function filesPage(topFiles, fl) {
131
- const fileRows = topFiles
194
+ // ── Files view ───────────────────────────────────────────
195
+ export function filesPage(topFiles, fileIssues, fl) {
196
+ if (topFiles.length === 0) {
197
+ return `<h2>File Health</h2><p style="color:var(--muted)">No file-level issues found.</p>`;
198
+ }
199
+ const maxIssues = Math.max(...topFiles.map((f) => f.total));
200
+ const heatmapRows = topFiles
201
+ .slice(0, 30)
132
202
  .map((f) => {
133
- const pct = Math.min(100, f.total * 5);
134
- 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>`;
203
+ const intensity = maxIssues > 0 ? f.total / maxIssues : 0;
204
+ const r = Math.round(239 * intensity);
205
+ const g = Math.round(68 * (1 - intensity) + 197 * (f.errors === 0 ? 0.3 : 0));
206
+ const color = `rgb(${r},${g},30)`;
207
+ const barW = Math.max(4, Math.round(intensity * 200));
208
+ return `<div class="hm-row"><span class="hm-name">${fl(f.file)}</span><div class="hm-bar" style="width:${barW}px;background:${color}" title="${f.total} issues (${f.checks.join(", ")})"></div><span class="hm-count">${f.errors}E ${f.warnings}W</span><span class="hm-checks">${f.checks.join(", ")}</span></div>`;
135
209
  })
136
210
  .join("");
137
- return `<div id="p-files" class="page">
138
- <h2>File Heatmap</h2>
139
- <p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
140
- ${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
141
- </div>`;
142
- }
143
- export function heatmapPage(fileIssues, fl) {
144
- const heatmapFiles = [...fileIssues.entries()].sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings).slice(0, 30);
145
- let heatmapHtml = "";
146
- if (heatmapFiles.length > 0) {
147
- const maxIssues = Math.max(...heatmapFiles.map(([, d]) => d.errors + d.warnings));
148
- heatmapHtml = heatmapFiles
149
- .map(([file, d]) => {
150
- const total = d.errors + d.warnings;
151
- const intensity = maxIssues > 0 ? total / maxIssues : 0;
152
- const r = Math.round(239 * intensity); // red channel
153
- const g = Math.round(68 * (1 - intensity) + 197 * (d.errors === 0 ? 0.3 : 0)); // green
154
- const color = `rgb(${r},${g},30)`;
155
- const barW = Math.max(4, Math.round(intensity * 200));
156
- const checks = [...d.checks].join(", ");
157
- 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>`;
158
- })
159
- .join("");
160
- }
161
- return `<div id="p-heatmap" class="page">
162
- <h2>Code Heatmap</h2>
163
- <p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Visual density of issues per file. Red = errors, orange = warnings. Bar width = relative issue count.</p>
164
- ${heatmapHtml || '<p style="color:var(--muted)">No issues to visualize.</p>'}
165
- </div>`;
211
+ return `
212
+ <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>
215
+ ${heatmapRows}`;
166
216
  }
@@ -0,0 +1,3 @@
1
+ /** SARIF 2.1.0 output for GitHub Code Scanning integration. */
2
+ import type { VibeReport } from "../types.js";
3
+ export declare function generateSARIF(report: VibeReport): string;