@vibecodeqa/cli 0.17.0 → 0.18.1

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 (42) hide show
  1. package/README.md +73 -63
  2. package/dist/check-meta.d.ts +1 -0
  3. package/dist/check-meta.js +34 -2
  4. package/dist/cli.js +35 -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 +17 -10
  9. package/dist/report/html.js +106 -73
  10. package/dist/report/pages.d.ts +2 -1
  11. package/dist/report/pages.js +88 -82
  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 +82 -36
  16. package/dist/runners/architecture.d.ts +2 -0
  17. package/dist/runners/architecture.js +232 -20
  18. package/dist/runners/code-coherence.d.ts +17 -0
  19. package/dist/runners/code-coherence.js +39 -0
  20. package/dist/runners/complexity.js +7 -37
  21. package/dist/runners/confusion.js +3 -31
  22. package/dist/runners/context.js +9 -40
  23. package/dist/runners/dependencies.js +28 -0
  24. package/dist/runners/doc-coherence.d.ts +14 -0
  25. package/dist/runners/doc-coherence.js +48 -0
  26. package/dist/runners/docs.js +7 -32
  27. package/dist/runners/duplication.js +9 -37
  28. package/dist/runners/lint.js +17 -0
  29. package/dist/runners/performance.d.ts +10 -0
  30. package/dist/runners/performance.js +174 -0
  31. package/dist/runners/react.js +15 -10
  32. package/dist/runners/secrets.js +8 -29
  33. package/dist/runners/security.js +15 -38
  34. package/dist/runners/standards.js +3 -36
  35. package/dist/runners/structure.js +35 -55
  36. package/dist/runners/testing.js +2 -36
  37. package/dist/runners/type-safety.d.ts +1 -1
  38. package/dist/runners/type-safety.js +19 -37
  39. package/dist/runners/types-check.d.ts +1 -1
  40. package/dist/runners/types-check.js +38 -20
  41. package/dist/types.d.ts +5 -5
  42. package/package.json +11 -10
@@ -1,37 +1,38 @@
1
- /** Generate a multi-page navigable HTML report.
1
+ /** Generate a multi-page HTML report as separate files.
2
2
  *
3
- * Architecture:
4
- * Primary nav: Overview | Foundations | Quality | Testing | Security | Architecture | AI Readiness
5
- * Secondary nav: Issues (N) | Files (right-aligned, visually distinct)
6
- * Sidebar: Score + dimension tree + view links
7
- * Overview: Dashboard with score, radar, timeline, category cards, top issues, file hotspots
8
- * Dimensions: Sub-tabs for each check within a category
9
- * Views: Cross-cutting data slices (issues table, file health map)
10
- *
11
- * All in one self-contained HTML file using show/hide navigation.
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.
12
12
  */
13
13
  import { getCheckMeta } from "../check-meta.js";
14
14
  import { e, fileLink, gc } from "./components.js";
15
- import { categoryPages, filesPage, issuesPage, overviewPage } from "./pages.js";
15
+ import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
16
16
  import { CSS } from "./styles.js";
17
- const GROUPS = [
18
- { id: "foundations", label: "Foundations", checks: ["structure", "lint", "types", "type-safety", "standards"] },
19
- { id: "quality", label: "Quality", checks: ["complexity", "duplication", "error-handling", "react", "accessibility", "docs"] },
20
- { id: "testing", label: "Testing", checks: ["testing"] },
21
- { id: "arch", label: "Architecture", checks: ["architecture"] },
22
- { id: "security", label: "Security", checks: ["secrets", "security", "dependencies"] },
23
- { id: "llm", label: "AI 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"] },
24
25
  ];
25
- export function generateHTML(report, historyDir) {
26
+ export function generatePages(report, historyDir) {
27
+ const pages = new Map();
26
28
  const allChecks = report.checks;
27
29
  const checkMap = new Map(allChecks.map((c) => [c.name, c]));
28
- const active = allChecks.filter((c) => !c.details.skipped);
30
+ const active = allChecks.filter((c) => !c.details.skipped && !c.details.comingSoon);
29
31
  const ru = report.meta.repoUrl;
30
32
  const br = report.meta.branch;
31
33
  const fl = (path, line) => fileLink(path, line, ru, br);
32
34
  const totalIssues = allChecks.reduce((s, c) => s + c.issues.length, 0);
33
35
  const proj = report.meta.cwd.split("/").pop() || "project";
34
- // ── Aggregate file issues across all checks ──
35
36
  const fileIssues = new Map();
36
37
  for (const c of allChecks) {
37
38
  for (const iss of c.issues) {
@@ -51,45 +52,86 @@ export function generateHTML(report, historyDir) {
51
52
  .map(([file, d]) => ({ file, total: d.errors + d.warnings, errors: d.errors, warnings: d.warnings, checks: [...d.checks] }))
52
53
  .sort((a, b) => b.total - a.total)
53
54
  .slice(0, 30);
54
- // ── Category averages ──
55
55
  const catScores = GROUPS.map((g) => {
56
56
  const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
57
57
  const scored = checks.filter((c) => !c.details.skipped);
58
58
  const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
59
59
  return { ...g, avg, checks };
60
60
  });
61
- // ── Primary nav (dimensions) ──
62
- const dimNavItems = [
63
- { id: "overview", label: "Overview" },
64
- ...GROUPS.map((g) => ({ id: g.id, label: g.label })),
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" },
65
131
  ];
66
- const dimNav = dimNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
67
- // ── Secondary nav (data views, right-aligned) ──
68
- const viewNav = [
69
- { id: "issues", label: `Issues (${totalIssues})` },
70
- { id: "files", label: "Files" },
71
- ]
72
- .map((t) => `<a class="tn tn-view" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`)
132
+ const nav = navItems
133
+ .map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`)
73
134
  .join("");
74
- // ── Sidebar ──
75
- const sidebarDims = catScores
76
- .map((cs) => {
77
- const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
78
- 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
79
- .map((c) => {
80
- const sk = c.details.skipped;
81
- const meta = getCheckMeta(c.name);
82
- 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>`;
83
- })
84
- .join("")}</div>`;
85
- })
86
- .join("");
87
- const sidebarViews = `<div class="side-section side-views"><div class="side-views-label">Views</div><a class="side-check" onclick="go('issues')">Issues <span style="color:var(--muted)">${totalIssues}</span></a><a class="side-check" onclick="go('files')">Files <span style="color:var(--muted)">${fileIssues.size}</span></a></div>`;
88
- // ── Assemble pages ──
89
- const overview = overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir);
90
- const catPages = categoryPages(catScores, fl);
91
- const issues = issuesPage(allChecks, totalIssues, fl);
92
- const files = filesPage(topFiles, fileIssues, fl);
93
135
  return `<!DOCTYPE html>
94
136
  <html lang="en">
95
137
  <head>
@@ -101,45 +143,36 @@ export function generateHTML(report, historyDir) {
101
143
  <body>
102
144
 
103
145
  <nav class="top">
104
- <div class="logo"><span>VibeCode</span> QA</div>
105
- <div class="nav-dims">${dimNav}</div>
106
- <div class="nav-views">${viewNav}</div>
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>
107
149
  </nav>
108
150
 
109
- <aside class="side">
110
- <div class="side-section">Score<div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>
111
- ${sidebarDims}
112
- ${sidebarViews}
113
- </aside>
151
+ <aside class="side" id="sidebar">${sidebar}</aside>
152
+
114
153
  <div class="content">
115
- ${overview}
116
- ${catPages}
117
- ${issues}
118
- ${files}
154
+ ${content}
119
155
  <div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} &mdash; <code>npx @vibecodeqa/cli</code></div>
120
156
  </div>
121
157
 
122
158
  <script>
123
- function go(id){
124
- document.querySelectorAll('.tn').forEach(n=>{n.classList.toggle('active',n.dataset.page===id)});
125
- document.querySelectorAll('.page').forEach(p=>{p.classList.toggle('active',p.id==='p-'+id)});
126
- window.scrollTo(0,0);
159
+ function toggleMenu(){
160
+ document.querySelector('.nav-scroll').classList.toggle('open');
161
+ document.getElementById('sidebar').classList.toggle('open');
127
162
  }
128
163
  function sub(el,cat){
129
164
  const id=el.dataset.sub;
130
165
  el.parentElement.querySelectorAll('.sn').forEach(n=>n.classList.remove('active'));
131
166
  el.classList.add('active');
132
- 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)});
133
168
  }
134
- // Copy-prompt buttons
135
169
  document.addEventListener('click',function(ev){
136
170
  var btn=ev.target.closest('.cp-btn');
137
171
  if(!btn)return;
138
- 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)}
139
174
  btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
140
175
  });
141
- // Init: show overview
142
- document.querySelector('.tn').classList.add('active');
143
176
  </script>
144
177
  </body></html>`;
145
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
  }
@@ -15,7 +16,7 @@ export interface FileEntry {
15
16
  }
16
17
  type FL = (path: string, line?: number) => string;
17
18
  export declare function overviewPage(report: VibeReport, active: CheckResult[], totalIssues: number, catScores: CatScore[], allChecks: CheckResult[], topFiles: FileEntry[], fl: FL, historyDir?: string): string;
18
- export declare function categoryPages(catScores: CatScore[], fl: FL): 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
21
  export declare function filesPage(topFiles: FileEntry[], fileIssues: Map<string, {
21
22
  errors: number;
@@ -1,12 +1,11 @@
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 } from "../runners/architecture.js";
4
+ import { generateArchSVG, generateDSM, generatePackageDiagram } from "../runners/architecture.js";
5
5
  import { 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) {
9
- // Hero: score ring + grade
10
9
  const hero = `<div class="hero">
11
10
  ${buildRing(report.score, gc(report.grade))}
12
11
  <div class="hc">
@@ -15,9 +14,8 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
15
14
  <span class="hd">${active.length} checks \u00b7 ${totalIssues} issues \u00b7 ${report.meta.duration}ms</span>
16
15
  </div>
17
16
  </div>`;
18
- // Radar chart
19
- const radarSvg = buildRadar(catScores.map((cs) => ({ label: cs.label, score: cs.avg })));
20
- // Category cards
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 }))) : "";
21
19
  const catCards = catScores
22
20
  .map((cs) => {
23
21
  const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
@@ -27,10 +25,10 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
27
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>`;
28
26
  })
29
27
  .join("");
30
- 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>`;
31
30
  })
32
31
  .join("");
33
- // Score timeline (from history)
34
32
  let timelineSection = "";
35
33
  if (historyDir) {
36
34
  const history = loadHistory(historyDir);
@@ -39,14 +37,12 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
39
37
  timelineSection = `<div class="ov-section"><h3>Score Timeline</h3><div class="timeline">${timelineSvg}</div></div>`;
40
38
  }
41
39
  }
42
- // All checks bar chart
43
40
  const barChart = active
44
41
  .sort((a, b) => a.score - b.score)
45
42
  .map((c) => {
46
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>`;
47
44
  })
48
45
  .join("");
49
- // Top issues preview (10 most severe)
50
46
  const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
51
47
  const sortedIssues = allIssues
52
48
  .sort((a, b) => (a.severity === "error" ? 0 : a.severity === "warning" ? 1 : 2) - (b.severity === "error" ? 0 : b.severity === "warning" ? 1 : 2))
@@ -59,25 +55,23 @@ export function overviewPage(report, active, totalIssues, catScores, allChecks,
59
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>`;
60
56
  })
61
57
  .join("");
62
- const viewAll = allIssues.length > 10 ? `<a class="ov-link" onclick="go('issues')">View all ${allIssues.length} issues \u2192</a>` : "";
58
+ const viewAll = allIssues.length > 10 ? `<a class="ov-link" href="issues.html">View all ${allIssues.length} issues \u2192</a>` : "";
63
59
  topIssuesHtml = `<div class="ov-section"><h3>Top Issues</h3>${rows}${viewAll}</div>`;
64
60
  }
65
- // File hotspots preview (top 5)
66
61
  let fileHotspotsHtml = "";
67
62
  if (topFiles.length > 0) {
68
63
  const fileRows = topFiles.slice(0, 5).map((f) => {
69
64
  const pct = Math.min(100, f.total * 5);
70
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>`;
71
66
  }).join("");
72
- const viewAll = topFiles.length > 5 ? `<a class="ov-link" onclick="go('files')">View all ${topFiles.length} files \u2192</a>` : "";
67
+ const viewAll = topFiles.length > 5 ? `<a class="ov-link" href="files.html">View all ${topFiles.length} files \u2192</a>` : "";
73
68
  fileHotspotsHtml = `<div class="ov-section"><h3>File Hotspots</h3>${fileRows}${viewAll}</div>`;
74
69
  }
75
- // Stack badges
76
70
  const stackHtml = Object.entries(report.meta.stack)
77
71
  .filter(([, v]) => v !== "none" && v !== "unknown")
78
72
  .map(([k, v]) => `<span>${k}: <b>${v}</b></span>`)
79
73
  .join("");
80
- return `<div id="p-overview" class="page active">
74
+ return `
81
75
  <div class="dash">
82
76
  ${hero}
83
77
  <div class="radar">${radarSvg}</div>
@@ -87,82 +81,97 @@ ${timelineSection}
87
81
  <div class="ov-section"><h3>All Checks</h3><div class="bars">${barChart}</div></div>
88
82
  ${topIssuesHtml}
89
83
  ${fileHotspotsHtml}
90
- <div class="stack">${stackHtml}</div>
91
- </div>`;
84
+ <div class="stack">${stackHtml}</div>`;
92
85
  }
93
- // ── Category dimension pages ──────────────────────────────────────────
94
- export function categoryPages(catScores, fl) {
95
- let catPagesHtml = "";
96
- for (const cs of catScores) {
97
- const subNav = cs.checks
98
- .map((c, i) => {
99
- const sk = c.details.skipped;
100
- 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>`;
101
107
  })
102
108
  .join("");
103
- const subPages = cs.checks
104
- .map((c, i) => {
105
- const meta = getCheckMeta(c.name);
106
- const sk = c.details.skipped;
107
- const detailsFiltered = Object.entries(c.details)
108
- .filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
109
- .map(([k, v]) => {
110
- const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
111
- return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
112
- })
113
- .join("");
114
- // Group issues by file
115
- const byFile = new Map();
116
- const noFile = [];
117
- for (const iss of c.issues) {
118
- const f = iss.file?.split(":")[0];
119
- if (f) {
120
- const arr = byFile.get(f) || [];
121
- arr.push(iss);
122
- byFile.set(f, arr);
123
- }
124
- else {
125
- noFile.push(iss);
126
- }
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);
127
117
  }
128
- let issuesHtml = "";
129
- for (const [file, issues] of byFile) {
130
- issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
131
- for (const iss of issues) {
132
- const prompt = `Fix this issue in ${file}${iss.line ? `:${iss.line}` : ""}\n${iss.severity}: ${iss.message}${iss.rule ? ` (${iss.rule})` : ""}\nCheck: ${c.name}`;
133
- 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>`;
134
- }
135
- issuesHtml += `</div>`;
118
+ else {
119
+ noFile.push(iss);
136
120
  }
137
- if (noFile.length > 0) {
138
- issuesHtml += `<div class="fg"><div class="fn">General</div>`;
139
- for (const iss of noFile) {
140
- 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>`;
141
- }
142
- 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>`;
128
+ }
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>`;
143
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("");
144
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}">
145
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>
146
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>` : ""}
147
159
  ${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
148
- ${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>` : ""}
149
161
  ${c.name === "testing" && !sk && c.details.pyramid ? `<div class="arch-svg">${buildPyramid(c.details.pyramid)}</div>` : ""}
150
162
  ${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
151
163
  ${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
152
164
  </div>`;
153
- })
154
- .join("");
155
- const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
156
- 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 `
157
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>
158
170
  <div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
159
171
  <div class="sub-nav">${subNav}</div>
160
- ${subPages}
161
- </div>`;
162
- }
163
- return catPagesHtml;
172
+ ${subPages}`;
164
173
  }
165
- // ── Issues view (cross-cutting) ──────────────────────────────────────
174
+ // ── Issues view ──────────────────────────────────────────
166
175
  export function issuesPage(allChecks, totalIssues, fl) {
167
176
  const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
168
177
  const errorCount = allIssues.filter((i) => i.severity === "error").length;
@@ -176,19 +185,17 @@ export function issuesPage(allChecks, totalIssues, fl) {
176
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>`;
177
186
  })
178
187
  .join("");
179
- return `<div id="p-issues" class="page">
188
+ return `
180
189
  <h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
181
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>
182
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>
183
- ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
184
- </div>`;
192
+ ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}`;
185
193
  }
186
- // ── Files view (merged file map + heatmap) ───────────────────────────
194
+ // ── Files view ───────────────────────────────────────────
187
195
  export function filesPage(topFiles, fileIssues, fl) {
188
196
  if (topFiles.length === 0) {
189
- return `<div id="p-files" class="page"><h2>File Health</h2><p style="color:var(--muted)">No file-level issues found.</p></div>`;
197
+ return `<h2>File Health</h2><p style="color:var(--muted)">No file-level issues found.</p>`;
190
198
  }
191
- // Heatmap (visual density bars)
192
199
  const maxIssues = Math.max(...topFiles.map((f) => f.total));
193
200
  const heatmapRows = topFiles
194
201
  .slice(0, 30)
@@ -201,10 +208,9 @@ export function filesPage(topFiles, fileIssues, fl) {
201
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>`;
202
209
  })
203
210
  .join("");
204
- return `<div id="p-files" class="page">
211
+ return `
205
212
  <h2>File Health</h2>
206
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)
207
- s.add(c); return s; }, new Set()).size} checks. Bar color: red = errors, orange = warnings only. Width = relative issue density.</p>
208
- ${heatmapRows}
209
- </div>`;
214
+ s.add(c); return s; }, new Set()).size} checks.</p>
215
+ ${heatmapRows}`;
210
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;
@@ -0,0 +1,67 @@
1
+ /** SARIF 2.1.0 output for GitHub Code Scanning integration. */
2
+ import { getCheckMeta } from "../check-meta.js";
3
+ export function generateSARIF(report) {
4
+ const rules = [];
5
+ const ruleIndex = new Map();
6
+ const results = [];
7
+ for (const check of report.checks) {
8
+ const meta = getCheckMeta(check.name);
9
+ for (const issue of check.issues) {
10
+ const ruleId = issue.rule || check.name;
11
+ // Register rule if not seen
12
+ if (!ruleIndex.has(ruleId)) {
13
+ ruleIndex.set(ruleId, rules.length);
14
+ rules.push({
15
+ id: ruleId,
16
+ name: ruleId,
17
+ shortDescription: { text: meta.label },
18
+ fullDescription: meta.description ? { text: meta.description } : undefined,
19
+ defaultConfiguration: { level: severityToLevel(issue.severity) },
20
+ helpUri: "https://vibecodeqa.online",
21
+ });
22
+ }
23
+ const result = {
24
+ ruleId,
25
+ level: severityToLevel(issue.severity),
26
+ message: { text: `[${check.name}] ${issue.message}` },
27
+ };
28
+ if (issue.file) {
29
+ const filePath = issue.file.split(":")[0];
30
+ result.locations = [
31
+ {
32
+ physicalLocation: {
33
+ artifactLocation: { uri: filePath },
34
+ ...(issue.line ? { region: { startLine: issue.line } } : {}),
35
+ },
36
+ },
37
+ ];
38
+ }
39
+ results.push(result);
40
+ }
41
+ }
42
+ const sarif = {
43
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
44
+ version: "2.1.0",
45
+ runs: [
46
+ {
47
+ tool: {
48
+ driver: {
49
+ name: "VibeCode QA",
50
+ version: report.version,
51
+ informationUri: "https://vibecodeqa.online",
52
+ rules,
53
+ },
54
+ },
55
+ results,
56
+ },
57
+ ],
58
+ };
59
+ return JSON.stringify(sarif, null, 2);
60
+ }
61
+ function severityToLevel(severity) {
62
+ if (severity === "error")
63
+ return "error";
64
+ if (severity === "warning")
65
+ return "warning";
66
+ return "note";
67
+ }