@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
@@ -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
+ }
@@ -1,2 +1,2 @@
1
1
  /** All CSS for the HTML report, extracted for maintainability. */
2
- export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* Top nav */\n.top{position:sticky;top:0;z-index:20;background:#0c0c0fcc;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;display:flex;align-items:center;gap:0}\n.logo{font-weight:800;font-size:1rem;margin-right:1.5rem;padding:0.7rem 0}\n.logo span{color:var(--accent)}\n.tn{padding:0.7rem 0.8rem;font-size:0.78rem;color:var(--muted);text-decoration:none;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n\n/* Sidebar */\n.side{position:fixed;top:42px;left:0;bottom:0;width:200px;background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.8rem 0;font-size:0.7rem;z-index:10}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.3rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\n.side-check{display:block;padding:0.2rem 0.8rem 0.2rem 1.2rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;width:1rem;font-weight:800;text-align:center}\n\n/* Content */\n.content{max-width:900px;margin-left:200px;padding:2rem}\n.page{display:none;animation:fadeIn 0.15s}\n.page.active{display:block}\n@keyframes fadeIn{from{opacity:0}to{opacity:1}}\n\n/* Overview */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;cursor:pointer;transition:border-color 0.15s}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:80px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* Category pages */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\n\n/* Check detail */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:#0d0d12;border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* Issue list grouped by file */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:#555;font-size:0.55rem}\n\n/* All issues table */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:#555;font-size:0.58rem}\n\n/* File heatmap */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.fcs{font-size:0.6rem;color:#555}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n@media(max-width:768px){.side{display:none}.content{margin-left:0;padding:1rem}.cats{grid-template-columns:1fr 1fr}.dash{flex-direction:column}}\n";
2
+ export declare const CSS = "\n:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px}\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:\"Inter\",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}\ncode{font-family:\"SF Mono\",Menlo,monospace;font-size:0.85em}\n\n/* \u2500\u2500 Top nav \u2500\u2500 */\n.top{position:sticky;top:0;z-index:30;background:#0c0c0fdd;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}\n.logo{font-weight:800;font-size:1rem;margin-right:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}\n.logo span{color:var(--accent)}\n.nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}\n.nav-scroll::-webkit-scrollbar{display:none}\n.tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}\n.tn:hover{color:var(--text)}\n.tn.active{color:var(--text);border-bottom-color:var(--accent)}\n.hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}\n\n/* \u2500\u2500 Sidebar \u2500\u2500 */\n.side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}\n.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}\n.side-section:last-child{border-bottom:none}\n.side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:#444;font-weight:600}\n.side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}\n.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a}\n.side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}\n.side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}\n.side-check:hover{color:var(--text);background:#14141a}\n.side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}\n.side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}\n.side-stat span{font-weight:800;font-size:0.8rem}\n.side-views{padding-top:0.3rem}\n.side-views .side-check{padding-left:0.8rem}\n\n/* \u2500\u2500 Content \u2500\u2500 */\n.content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}\n\n/* \u2500\u2500 Overview \u2500\u2500 */\n.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}\n.hero{display:flex;align-items:center;gap:1rem}\n.hero svg{width:100px;height:100px}\n.hc{display:flex;flex-direction:column}\n.hg{font-size:2.5rem;font-weight:900;line-height:1}\n.hs{font-size:1rem;font-weight:600}\n.hd{font-size:0.68rem;color:var(--muted)}\n.radar{flex:1;display:flex;justify-content:center}\n.radar svg{max-width:240px;width:100%}\n.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}\n.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}\n.cc:hover{border-color:var(--accent)}\n.cc-s{font-size:1.8rem;font-weight:900}\n.cc-l{font-size:0.75rem;color:var(--muted)}\n.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}\n.mc{font-size:0.65rem;font-weight:800}\nh3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}\n\n/* \u2500\u2500 Overview sections \u2500\u2500 */\n.ov-section{margin-bottom:1.5rem}\n.ov-issue{font-size:0.68rem;font-family:\"SF Mono\",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}\n.ov-issue .is{flex-shrink:0}\n.ov-issue.error .is{color:var(--fail)}\n.ov-issue.warning .is{color:var(--warn)}\n.ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}\n.ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ov-msg{flex:1;word-break:break-word}\n.ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}\n.ov-link:hover{text-decoration:underline}\n\n/* \u2500\u2500 Timeline \u2500\u2500 */\n.timeline{margin:0.5rem 0;overflow-x:auto}\n.timeline svg{max-width:100%}\n\n/* \u2500\u2500 Bar chart \u2500\u2500 */\n.bars{margin-bottom:1.5rem}\n.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}\n.bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}\n.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.bf{height:100%;border-radius:2px}\n.bv{width:36px;font-weight:700;font-size:0.68rem}\n.stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}\n.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}\n\n/* \u2500\u2500 Category pages \u2500\u2500 */\n.cat-head{margin-bottom:0.3rem}\n.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}\n.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}\n.sn:hover{color:var(--text)}\n.sn.active{color:var(--text);border-bottom-color:var(--accent)}\n.sp{display:none}.sp.active{display:block}\n\n/* \u2500\u2500 Check detail \u2500\u2500 */\n.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}\n.ch-g{font-size:2rem;font-weight:900}\n.ch-s{display:block;font-size:0.7rem;color:var(--muted)}\n.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}\n.info-panel{background:#0d0d12;border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}\n.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}\n.ip-row:last-child{margin-bottom:0}\n.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}\n.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}\n.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}\n.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}\n.k{color:var(--muted);margin-right:0.3rem}\n.v{font-weight:600}\n\n/* \u2500\u2500 Issue list grouped by file \u2500\u2500 */\n.iss-list{margin-top:1rem}\n.fg{margin-bottom:0.8rem}\n.fn{font-size:0.72rem;font-weight:600;font-family:\"SF Mono\",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}\n.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}\n.ir{font-size:0.65rem;font-family:\"SF Mono\",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}\n.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}\n.ir.error .is{color:var(--fail);background:#ef444418}\n.ir.warning .is{color:var(--warn);background:#eab30818}\n.ir.info .is{color:var(--info);background:#6366f118}\n.il{color:var(--accent);min-width:2rem;flex-shrink:0}\n.im{flex:1;word-break:break-word}\n.iru{color:#555;font-size:0.55rem}\n\n/* \u2500\u2500 All issues table \u2500\u2500 */\n.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}\n.it{width:100%;border-collapse:collapse;font-size:0.68rem}\n.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}\n.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:\"SF Mono\",monospace;font-size:0.62rem}\n.it tr.error .is2{color:var(--fail)}\n.it tr.warning .is2{color:var(--warn)}\n.is2{font-weight:800;width:1rem}\n.ic2{color:var(--muted);width:70px}\n.il2{color:var(--muted)}\n.iru2{color:#555;font-size:0.58rem}\n\n/* \u2500\u2500 File health \u2500\u2500 */\n.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}\n.ff{width:200px;font-family:\"SF Mono\",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}\n.fbf{height:100%;border-radius:2px}\n.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}\n.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}\n.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:\"SF Mono\",monospace;font-size:0.65rem}\n.hm-bar{height:14px;border-radius:3px;min-width:4px}\n.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}\n.hm-checks{font-size:0.58rem;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n/* \u2500\u2500 Premium cards \u2500\u2500 */\n.pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}\n.pro-card::before{content:\"\";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}\n.pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}\n.pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}\n.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}\n.sn-pro{opacity:0.7}\n\n.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}\n.footer a{color:var(--muted)}\n.flink{color:var(--accent);text-decoration:none;font-family:\"SF Mono\",monospace}.flink:hover{text-decoration:underline}\n.arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.arch-svg svg{border-radius:8px}\n.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}\n.ir:hover .cp-btn{opacity:0.6}\n\n/* \u2500\u2500 Mobile: hamburger collapses both navs \u2500\u2500 */\n@media(max-width:768px){\n.hamburger{display:block}\n.nav-scroll{display:none}\n.nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}\n.side{display:none}\n.side.open{display:block;z-index:25}\n.top{padding:0 0.8rem}\n.logo{font-size:0.85rem;margin-right:0.5rem}\n.content{margin-left:0;padding:0.8rem}\n.cats{grid-template-columns:1fr 1fr}\n.dash{flex-direction:column;gap:1rem}\n.hero svg{width:80px;height:80px}\n.hg{font-size:2rem}\n.radar svg{max-width:180px}\n.bl{width:60px;font-size:0.62rem}\n.bv{width:30px;font-size:0.6rem}\n.it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}\n.ff{width:120px;font-size:0.58rem}\n.hm-name{width:120px;font-size:0.58rem}\n.hm-checks{display:none}\n.ov-check{width:50px}\n.ov-loc{max-width:120px}\n.ir{font-size:0.6rem}\n.ch-head{flex-wrap:wrap}\n.ch-g{font-size:1.5rem}\n.info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}\n.ip-row{flex-direction:column;gap:0.1rem}\n.kvs{gap:0.4rem}\n.kv{font-size:0.62rem;padding:0.2rem 0.4rem}\n.arch-svg svg{min-width:400px}\n}\n@media(max-width:480px){\n.cats{grid-template-columns:1fr}\n.tn{padding:0 0.4rem;font-size:0.65rem}\n.ff{width:90px}\n.hm-name{width:90px}\n.ov-check{display:none}\n}\n";
@@ -1,36 +1,42 @@
1
1
  /** All CSS for the HTML report, extracted for maintainability. */
2
2
  export const CSS = `
3
- :root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8}
3
+ :root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8;--side-w:200px;--top-h:42px}
4
4
  *{margin:0;padding:0;box-sizing:border-box}
5
5
  body{font-family:"Inter",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
6
6
  code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
7
7
 
8
- /* Top nav */
9
- .top{position:sticky;top:0;z-index:20;background:#0c0c0fcc;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;display:flex;align-items:center;gap:0}
10
- .logo{font-weight:800;font-size:1rem;margin-right:1.5rem;padding:0.7rem 0}
8
+ /* ── Top nav ── */
9
+ .top{position:sticky;top:0;z-index:30;background:#0c0c0fdd;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 1.5rem;display:flex;align-items:center;height:var(--top-h)}
10
+ .logo{font-weight:800;font-size:1rem;margin-right:1rem;flex-shrink:0;text-decoration:none;color:var(--text)}
11
11
  .logo span{color:var(--accent)}
12
- .tn{padding:0.7rem 0.8rem;font-size:0.78rem;color:var(--muted);text-decoration:none;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
12
+ .nav-scroll{display:flex;align-items:center;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1}
13
+ .nav-scroll::-webkit-scrollbar{display:none}
14
+ .tn{padding:0 0.7rem;font-size:0.78rem;color:var(--muted);text-decoration:none;border-bottom:2px solid transparent;transition:all 0.15s;white-space:nowrap;line-height:var(--top-h)}
13
15
  .tn:hover{color:var(--text)}
14
16
  .tn.active{color:var(--text);border-bottom-color:var(--accent)}
17
+ .hamburger{display:none;background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;padding:0 0.4rem;line-height:var(--top-h)}
15
18
 
16
- /* Sidebar */
17
- .side{position:fixed;top:42px;left:0;bottom:0;width:200px;background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.8rem 0;font-size:0.7rem;z-index:10}
19
+ /* ── Sidebar ── */
20
+ .side{position:fixed;top:var(--top-h);left:0;bottom:0;width:var(--side-w);background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.6rem 0;font-size:0.7rem;z-index:20}
18
21
  .side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}
19
22
  .side-section:last-child{border-bottom:none}
20
- .side-score{font-size:1.4rem;font-weight:900;padding:0.3rem 0.8rem}
23
+ .side-label{padding:0.2rem 0.8rem;font-size:0.6rem;text-transform:uppercase;letter-spacing:0.05em;color:#444;font-weight:600}
24
+ .side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}
21
25
  .side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}
22
26
  .side-cat:hover{background:#14141a}
23
- .side-check{display:block;padding:0.2rem 0.8rem 0.2rem 1.2rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}
27
+ .side-cat-title{padding:0.3rem 0.8rem;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--accent);font-weight:700}
28
+ .side-check{display:block;padding:0.15rem 0.8rem 0.15rem 0.8rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}
24
29
  .side-check:hover{color:var(--text);background:#14141a}
25
- .side-check span{display:inline-block;width:1rem;font-weight:800;text-align:center}
30
+ .side-check span{display:inline-block;min-width:2.5rem;font-weight:700;font-size:0.6rem}
31
+ .side-stat{padding:0.15rem 0.8rem;font-size:0.7rem;color:var(--muted)}
32
+ .side-stat span{font-weight:800;font-size:0.8rem}
33
+ .side-views{padding-top:0.3rem}
34
+ .side-views .side-check{padding-left:0.8rem}
26
35
 
27
- /* Content */
28
- .content{max-width:900px;margin-left:200px;padding:2rem}
29
- .page{display:none;animation:fadeIn 0.15s}
30
- .page.active{display:block}
31
- @keyframes fadeIn{from{opacity:0}to{opacity:1}}
36
+ /* ── Content ── */
37
+ .content{margin-left:var(--side-w);padding:1.5rem 2rem;max-width:960px}
32
38
 
33
- /* Overview */
39
+ /* ── Overview ── */
34
40
  .dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}
35
41
  .hero{display:flex;align-items:center;gap:1rem}
36
42
  .hero svg{width:100px;height:100px}
@@ -40,34 +46,52 @@ code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
40
46
  .hd{font-size:0.68rem;color:var(--muted)}
41
47
  .radar{flex:1;display:flex;justify-content:center}
42
48
  .radar svg{max-width:240px;width:100%}
43
- .cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:0.6rem;margin-bottom:2rem}
44
- .cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;cursor:pointer;transition:border-color 0.15s}
49
+ .cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:0.6rem;margin-bottom:2rem}
50
+ .cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;transition:border-color 0.15s;text-decoration:none;color:var(--text);display:block}
45
51
  .cc:hover{border-color:var(--accent)}
46
52
  .cc-s{font-size:1.8rem;font-weight:900}
47
53
  .cc-l{font-size:0.75rem;color:var(--muted)}
48
54
  .cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}
49
55
  .mc{font-size:0.65rem;font-weight:800}
50
56
  h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}
57
+
58
+ /* ── Overview sections ── */
59
+ .ov-section{margin-bottom:1.5rem}
60
+ .ov-issue{font-size:0.68rem;font-family:"SF Mono",monospace;padding:0.2rem 0;display:flex;gap:0.4rem;align-items:baseline;border-bottom:1px solid var(--border)}
61
+ .ov-issue .is{flex-shrink:0}
62
+ .ov-issue.error .is{color:var(--fail)}
63
+ .ov-issue.warning .is{color:var(--warn)}
64
+ .ov-check{color:var(--muted);width:70px;flex-shrink:0;font-size:0.62rem}
65
+ .ov-loc{color:var(--accent);flex-shrink:0;font-size:0.62rem;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
66
+ .ov-msg{flex:1;word-break:break-word}
67
+ .ov-link{display:block;margin-top:0.5rem;font-size:0.72rem;color:var(--accent);text-decoration:none}
68
+ .ov-link:hover{text-decoration:underline}
69
+
70
+ /* ── Timeline ── */
71
+ .timeline{margin:0.5rem 0;overflow-x:auto}
72
+ .timeline svg{max-width:100%}
73
+
74
+ /* ── Bar chart ── */
51
75
  .bars{margin-bottom:1.5rem}
52
76
  .brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}
53
- .bl{width:80px;text-align:right;color:var(--muted);flex-shrink:0}
77
+ .bl{width:90px;text-align:right;color:var(--muted);flex-shrink:0}
54
78
  .bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
55
79
  .bf{height:100%;border-radius:2px}
56
80
  .bv{width:36px;font-weight:700;font-size:0.68rem}
57
- .stack{display:flex;gap:0.35rem;flex-wrap:wrap}
81
+ .stack{display:flex;gap:0.35rem;flex-wrap:wrap;margin-top:1rem}
58
82
  .stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}
59
83
 
60
- /* Category pages */
84
+ /* ── Category pages ── */
61
85
  .cat-head{margin-bottom:0.3rem}
62
86
  .bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}
63
87
  .bf2{height:100%;border-radius:2px}
64
- .sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem}
88
+ .sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap}
65
89
  .sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}
66
90
  .sn:hover{color:var(--text)}
67
91
  .sn.active{color:var(--text);border-bottom-color:var(--accent)}
68
92
  .sp{display:none}.sp.active{display:block}
69
93
 
70
- /* Check detail */
94
+ /* ── Check detail ── */
71
95
  .ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}
72
96
  .ch-g{font-size:2rem;font-weight:900}
73
97
  .ch-s{display:block;font-size:0.7rem;color:var(--muted)}
@@ -82,7 +106,7 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
82
106
  .k{color:var(--muted);margin-right:0.3rem}
83
107
  .v{font-weight:600}
84
108
 
85
- /* Issue list grouped by file */
109
+ /* ── Issue list grouped by file ── */
86
110
  .iss-list{margin-top:1rem}
87
111
  .fg{margin-bottom:0.8rem}
88
112
  .fn{font-size:0.72rem;font-weight:600;font-family:"SF Mono",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}
@@ -91,11 +115,12 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
91
115
  .is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}
92
116
  .ir.error .is{color:var(--fail);background:#ef444418}
93
117
  .ir.warning .is{color:var(--warn);background:#eab30818}
118
+ .ir.info .is{color:var(--info);background:#6366f118}
94
119
  .il{color:var(--accent);min-width:2rem;flex-shrink:0}
95
120
  .im{flex:1;word-break:break-word}
96
121
  .iru{color:#555;font-size:0.55rem}
97
122
 
98
- /* All issues table */
123
+ /* ── All issues table ── */
99
124
  .isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}
100
125
  .it{width:100%;border-collapse:collapse;font-size:0.68rem}
101
126
  .it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
@@ -107,24 +132,71 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
107
132
  .il2{color:var(--muted)}
108
133
  .iru2{color:#555;font-size:0.58rem}
109
134
 
110
- /* File heatmap */
135
+ /* ── File health ── */
111
136
  .fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}
112
137
  .ff{width:200px;font-family:"SF Mono",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
113
138
  .fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
114
139
  .fbf{height:100%;border-radius:2px}
115
140
  .fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}
116
- .fcs{font-size:0.6rem;color:#555}
141
+ .hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}
142
+ .hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"SF Mono",monospace;font-size:0.65rem}
143
+ .hm-bar{height:14px;border-radius:3px;min-width:4px}
144
+ .hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0;min-width:50px}
145
+ .hm-checks{font-size:0.58rem;color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
146
+
147
+ /* ── Premium cards ── */
148
+ .pro-card{background:linear-gradient(135deg,#0f0f1a 0%,#13131f 100%);border:1px solid #2a2a3d;border-radius:0.75rem;padding:1.5rem;position:relative;overflow:hidden}
149
+ .pro-card::before{content:"";position:absolute;top:-50%;right:-50%;width:200%;height:200%;background:radial-gradient(circle,#6366f108 0%,transparent 70%);pointer-events:none}
150
+ .pro-badge{display:inline-block;background:linear-gradient(135deg,#6366f1,#818cf8);color:#fff;font-size:0.6rem;font-weight:800;padding:0.15rem 0.5rem;border-radius:9999px;letter-spacing:0.06em;margin-bottom:0.6rem}
151
+ .pro-desc{color:var(--muted);font-size:0.78rem;line-height:1.6;margin-bottom:0.8rem}
152
+ .pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}
153
+ .sn-pro{opacity:0.7}
117
154
 
118
155
  .footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
119
156
  .footer a{color:var(--muted)}
120
157
  .flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
121
- .arch-svg{margin:1rem 0;overflow-x:auto}
122
- .hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}
123
- .hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"SF Mono",monospace;font-size:0.65rem}
124
- .hm-bar{height:14px;border-radius:3px;min-width:4px}
125
- .hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0}
158
+ .arch-svg{margin:1rem 0;overflow-x:auto;-webkit-overflow-scrolling:touch}
126
159
  .arch-svg svg{border-radius:8px}
127
160
  .cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}
128
161
  .ir:hover .cp-btn{opacity:0.6}
129
- @media(max-width:768px){.side{display:none}.content{margin-left:0;padding:1rem}.cats{grid-template-columns:1fr 1fr}.dash{flex-direction:column}}
162
+
163
+ /* ── Mobile: hamburger collapses both navs ── */
164
+ @media(max-width:768px){
165
+ .hamburger{display:block}
166
+ .nav-scroll{display:none}
167
+ .nav-scroll.open{display:flex;position:absolute;top:var(--top-h);left:0;right:0;background:var(--bg);border-bottom:1px solid var(--border);flex-wrap:wrap;padding:0.3rem 0.5rem;z-index:25}
168
+ .side{display:none}
169
+ .side.open{display:block;z-index:25}
170
+ .top{padding:0 0.8rem}
171
+ .logo{font-size:0.85rem;margin-right:0.5rem}
172
+ .content{margin-left:0;padding:0.8rem}
173
+ .cats{grid-template-columns:1fr 1fr}
174
+ .dash{flex-direction:column;gap:1rem}
175
+ .hero svg{width:80px;height:80px}
176
+ .hg{font-size:2rem}
177
+ .radar svg{max-width:180px}
178
+ .bl{width:60px;font-size:0.62rem}
179
+ .bv{width:30px;font-size:0.6rem}
180
+ .it{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
181
+ .ff{width:120px;font-size:0.58rem}
182
+ .hm-name{width:120px;font-size:0.58rem}
183
+ .hm-checks{display:none}
184
+ .ov-check{width:50px}
185
+ .ov-loc{max-width:120px}
186
+ .ir{font-size:0.6rem}
187
+ .ch-head{flex-wrap:wrap}
188
+ .ch-g{font-size:1.5rem}
189
+ .info-panel{font-size:0.68rem;padding:0.5rem 0.6rem}
190
+ .ip-row{flex-direction:column;gap:0.1rem}
191
+ .kvs{gap:0.4rem}
192
+ .kv{font-size:0.62rem;padding:0.2rem 0.4rem}
193
+ .arch-svg svg{min-width:400px}
194
+ }
195
+ @media(max-width:480px){
196
+ .cats{grid-template-columns:1fr}
197
+ .tn{padding:0 0.4rem;font-size:0.65rem}
198
+ .ff{width:90px}
199
+ .hm-name{width:90px}
200
+ .ov-check{display:none}
201
+ }
130
202
  `;
@@ -4,6 +4,23 @@ export declare function buildRadar(items: {
4
4
  label: string;
5
5
  score: number;
6
6
  }[]): string;
7
+ /** Score timeline — larger chart showing score history over last N runs. */
8
+ export declare function buildTimeline(entries: {
9
+ score: number;
10
+ timestamp: string;
11
+ }[], opts?: {
12
+ width?: number;
13
+ height?: number;
14
+ }): string;
15
+ /** Testing pyramid — proportional triangle showing test layer distribution. */
16
+ export declare function buildPyramid(layers: {
17
+ unit: number;
18
+ integration: number;
19
+ component: number;
20
+ e2e: number;
21
+ }): string;
22
+ /** Badge SVG — shields.io-style badge for README embedding. */
23
+ export declare function buildBadge(score: number, grade: string): string;
7
24
  /** Sparkline — mini line chart for trend display. */
8
25
  export declare function buildSparkline(values: number[], opts?: {
9
26
  width?: number;
@@ -42,6 +42,105 @@ export function buildRadar(items) {
42
42
  }
43
43
  return `<svg viewBox="0 0 240 240">${grid}${axes}<polygon points="${dataPts}" fill="#818cf825" stroke="#818cf8" stroke-width="1.5"/>${dots}</svg>`;
44
44
  }
45
+ /** Score timeline — larger chart showing score history over last N runs. */
46
+ export function buildTimeline(entries, opts) {
47
+ const width = opts?.width ?? 600;
48
+ const height = opts?.height ?? 120;
49
+ const pad = { top: 20, right: 20, bottom: 25, left: 35 };
50
+ const w = width - pad.left - pad.right;
51
+ const h = height - pad.top - pad.bottom;
52
+ if (entries.length === 0)
53
+ return "";
54
+ // Y axis: 0-100 always
55
+ const yScale = (v) => pad.top + h - (v / 100) * h;
56
+ const xScale = (i) => pad.left + (entries.length === 1 ? w / 2 : (i / (entries.length - 1)) * w);
57
+ // Grid lines at 25, 50, 75
58
+ let grid = "";
59
+ for (const v of [25, 50, 75]) {
60
+ const y = yScale(v).toFixed(1);
61
+ grid += `<line x1="${pad.left}" y1="${y}" x2="${pad.left + w}" y2="${y}" stroke="#1e1e24" stroke-width="0.7"/>`;
62
+ grid += `<text x="${pad.left - 6}" y="${y}" text-anchor="end" dominant-baseline="middle" fill="#555" font-size="8">${v}</text>`;
63
+ }
64
+ // Score line + dots
65
+ const points = entries.map((e, i) => `${xScale(i).toFixed(1)},${yScale(e.score).toFixed(1)}`).join(" ");
66
+ // Grade colors per dot
67
+ const dots = entries
68
+ .map((e, i) => {
69
+ const color = e.score >= 90 ? "#22c55e" : e.score >= 75 ? "#84cc16" : e.score >= 60 ? "#eab308" : e.score >= 40 ? "#f97316" : "#ef4444";
70
+ return `<circle cx="${xScale(i).toFixed(1)}" cy="${yScale(e.score).toFixed(1)}" r="3" fill="${color}"><title>${e.timestamp.split("T")[0]} — ${e.score}</title></circle>`;
71
+ })
72
+ .join("");
73
+ // X-axis labels (first, middle, last)
74
+ let xLabels = "";
75
+ const labelIndices = entries.length <= 3 ? entries.map((_, i) => i) : [0, Math.floor(entries.length / 2), entries.length - 1];
76
+ for (const i of labelIndices) {
77
+ const label = entries[i].timestamp.split("T")[0].slice(5); // MM-DD
78
+ xLabels += `<text x="${xScale(i).toFixed(1)}" y="${height - 4}" text-anchor="middle" fill="#555" font-size="7">${label}</text>`;
79
+ }
80
+ // Gradient fill under the line
81
+ const areaPoints = `${xScale(0).toFixed(1)},${yScale(0).toFixed(1)} ${points} ${xScale(entries.length - 1).toFixed(1)},${yScale(0).toFixed(1)}`;
82
+ return `<svg viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
83
+ <defs><linearGradient id="tlg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#818cf8" stop-opacity="0.3"/><stop offset="100%" stop-color="#818cf8" stop-opacity="0.02"/></linearGradient></defs>
84
+ ${grid}
85
+ <polygon points="${areaPoints}" fill="url(#tlg)"/>
86
+ <polyline points="${points}" fill="none" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
87
+ ${dots}${xLabels}
88
+ </svg>`;
89
+ }
90
+ /** Testing pyramid — proportional triangle showing test layer distribution. */
91
+ export function buildPyramid(layers) {
92
+ const total = layers.unit + layers.integration + layers.component + layers.e2e;
93
+ if (total === 0)
94
+ return "";
95
+ const w = 200, h = 160;
96
+ const cx = w / 2;
97
+ // Pyramid: e2e at top (smallest), unit at bottom (largest)
98
+ // Each layer gets proportional height
99
+ const items = [
100
+ { label: "E2E", count: layers.e2e, color: "#ef4444" },
101
+ { label: "Component", count: layers.component, color: "#f97316" },
102
+ { label: "Integration", count: layers.integration, color: "#eab308" },
103
+ { label: "Unit", count: layers.unit, color: "#22c55e" },
104
+ ];
105
+ const layerH = (h - 20) / 4;
106
+ let svg = "";
107
+ for (let i = 0; i < 4; i++) {
108
+ const item = items[i];
109
+ const y = 10 + i * layerH;
110
+ // Trapezoid: wider at bottom
111
+ const topW = ((i + 0.5) / 4) * (w - 40);
112
+ const botW = ((i + 1.5) / 4) * (w - 40);
113
+ const opacity = item.count > 0 ? 1 : 0.2;
114
+ const x1t = cx - topW / 2, x2t = cx + topW / 2;
115
+ const x1b = cx - botW / 2, x2b = cx + botW / 2;
116
+ svg += `<polygon points="${x1t},${y} ${x2t},${y} ${x2b},${y + layerH} ${x1b},${y + layerH}" fill="${item.color}" opacity="${opacity * 0.25}" stroke="${item.color}" stroke-opacity="${opacity * 0.6}" stroke-width="1"/>`;
117
+ svg += `<text x="${cx}" y="${y + layerH / 2 + 3}" text-anchor="middle" fill="${item.count > 0 ? "#e5e5e5" : "#555"}" font-size="9" font-weight="600">${item.label} (${item.count})</text>`;
118
+ }
119
+ return `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">${svg}</svg>`;
120
+ }
121
+ /** Badge SVG — shields.io-style badge for README embedding. */
122
+ export function buildBadge(score, grade) {
123
+ const color = score >= 90 ? "#22c55e" : score >= 75 ? "#84cc16" : score >= 60 ? "#eab308" : score >= 40 ? "#f97316" : "#ef4444";
124
+ const label = "vcqa";
125
+ const value = `${grade} ${score}`;
126
+ const labelW = 36, valueW = 44, totalW = labelW + valueW;
127
+ const h = 20, r = 3;
128
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${h}">
129
+ <linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
130
+ <clipPath id="r"><rect width="${totalW}" height="${h}" rx="${r}" fill="#fff"/></clipPath>
131
+ <g clip-path="url(#r)">
132
+ <rect width="${labelW}" height="${h}" fill="#555"/>
133
+ <rect x="${labelW}" width="${valueW}" height="${h}" fill="${color}"/>
134
+ <rect width="${totalW}" height="${h}" fill="url(#s)"/>
135
+ </g>
136
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
137
+ <text x="${labelW / 2}" y="14" fill="#010101" fill-opacity=".3">${label}</text>
138
+ <text x="${labelW / 2}" y="13">${label}</text>
139
+ <text x="${labelW + valueW / 2}" y="14" fill="#010101" fill-opacity=".3">${value}</text>
140
+ <text x="${labelW + valueW / 2}" y="13">${value}</text>
141
+ </g>
142
+ </svg>`;
143
+ }
45
144
  /** Sparkline — mini line chart for trend display. */
46
145
  export function buildSparkline(values, opts) {
47
146
  const width = opts?.width ?? 120;
@@ -0,0 +1,3 @@
1
+ /** Accessibility check — detects common a11y violations in JSX/TSX code. */
2
+ import type { CheckResult } from "../types.js";
3
+ export declare function runAccessibility(cwd: string): CheckResult;
@@ -0,0 +1,85 @@
1
+ /** Accessibility check — detects common a11y violations in JSX/TSX code. */
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ import { getProductionFiles } from "../fs-utils.js";
6
+ export function runAccessibility(cwd) {
7
+ const start = Date.now();
8
+ const files = getProductionFiles(cwd).filter((f) => f.ext === ".tsx" || f.ext === ".jsx");
9
+ if (files.length === 0) {
10
+ return { name: "accessibility", score: 100, grade: "A", details: { skipped: true, reason: "no JSX/TSX files" }, issues: [], duration: Date.now() - start };
11
+ }
12
+ const issues = [];
13
+ let missingAlt = 0;
14
+ let clickDiv = 0;
15
+ let missingLabel = 0;
16
+ let missingLang = 0;
17
+ let autofocus = 0;
18
+ let positiveTabindex = 0;
19
+ for (const f of files) {
20
+ const lines = f.content.split("\n");
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ const trimmed = line.trim();
24
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
25
+ continue;
26
+ // 1. <img> without alt
27
+ if (/<img\b/.test(trimmed) && !/alt=/.test(trimmed)) {
28
+ const block = lines.slice(i, Math.min(i + 5, lines.length)).join(" ");
29
+ if (/<img\b/.test(block) && !/alt=/.test(block)) {
30
+ missingAlt++;
31
+ issues.push({ severity: "error", message: "<img> missing alt attribute", file: f.path, line: i + 1, rule: "img-alt" });
32
+ }
33
+ }
34
+ // 2. Click handler on non-interactive element without role/keyboard
35
+ if (/onClick=/.test(trimmed) && /<(?:div|span|p|li|section|article|header|footer)\b/.test(trimmed)) {
36
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(" ");
37
+ if (!(/role=/.test(block) && /(?:onKeyDown|onKeyUp|onKeyPress|tabIndex)/.test(block))) {
38
+ clickDiv++;
39
+ issues.push({ severity: "warning", message: "Click handler on non-interactive element without role + keyboard handler", file: f.path, line: i + 1, rule: "click-events" });
40
+ }
41
+ }
42
+ // 3. <input>/<select>/<textarea> without associated label
43
+ if (/<(?:input|select|textarea)\b/.test(trimmed) && !/type=["'](?:hidden|submit|button|reset)["']/.test(trimmed)) {
44
+ const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(" ");
45
+ if (!/aria-label=/.test(block) && !/aria-labelledby=/.test(block) && !/<label/.test(block) && !/id=/.test(trimmed)) {
46
+ missingLabel++;
47
+ issues.push({ severity: "warning", message: "Form control without label, aria-label, or aria-labelledby", file: f.path, line: i + 1, rule: "form-label" });
48
+ }
49
+ }
50
+ // 4. autoFocus
51
+ if (/\bautoFocus\b/.test(trimmed) || /\bautofocus\b/.test(trimmed)) {
52
+ autofocus++;
53
+ issues.push({ severity: "warning", message: "autoFocus can disorient screen reader users", file: f.path, line: i + 1, rule: "no-autofocus" });
54
+ }
55
+ // 5. Positive tabIndex
56
+ if (/tabIndex=\{[1-9]/.test(trimmed) || /tabindex=["'][1-9]/.test(trimmed)) {
57
+ positiveTabindex++;
58
+ issues.push({ severity: "warning", message: "Positive tabIndex disrupts natural tab order — use 0 or -1", file: f.path, line: i + 1, rule: "tabindex" });
59
+ }
60
+ }
61
+ }
62
+ // 6. Check for html lang attribute in index.html
63
+ const htmlPaths = ["index.html", "web/index.html", "public/index.html"];
64
+ for (const h of htmlPaths) {
65
+ const full = join(cwd, h);
66
+ if (!existsSync(full))
67
+ continue;
68
+ const content = readFileSync(full, "utf-8");
69
+ if (/<html\b/.test(content) && !/<html[^>]*lang=/.test(content)) {
70
+ missingLang++;
71
+ issues.push({ severity: "warning", message: "<html> missing lang attribute", file: h, rule: "html-lang" });
72
+ }
73
+ }
74
+ const errors = issues.filter((i) => i.severity === "error").length;
75
+ const warnings = issues.filter((i) => i.severity === "warning").length;
76
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warnings * 4));
77
+ return {
78
+ name: "accessibility",
79
+ score,
80
+ grade: gradeFromScore(score),
81
+ details: { jsxFiles: files.length, missingAlt, clickDiv, missingLabel, missingLang, autofocus, positiveTabindex },
82
+ issues,
83
+ duration: Date.now() - start,
84
+ };
85
+ }
@@ -25,4 +25,6 @@ export interface ArchGraph {
25
25
  }
26
26
  export declare function runArchitecture(cwd: string): CheckResult;
27
27
  export declare function generateArchSVG(details: Record<string, unknown>): string;
28
+ export declare function generateDSM(details: Record<string, unknown>): string;
29
+ export declare function generatePackageDiagram(details: Record<string, unknown>): string;
28
30
  export {};