@vibecodeqa/cli 0.24.0 → 0.26.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.
@@ -64,70 +64,49 @@ export function generatePages(report, historyDir) {
64
64
  const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
65
65
  return { ...g, avg, checks };
66
66
  });
67
- const w = (id, sidebar, content) => wrap(proj, id, report, totalIssues, sidebar, content);
68
- // ── Overview: sidebar shows score + category summary ──
69
- const overviewSidebar = sidebarScore(report) +
70
- catScores
71
- .map((cs) => {
67
+ // ── Build shared sidebar: score + category tree (always visible) ──
68
+ // The sidebar is the MAIN navigation for check categories.
69
+ // Top nav only has: Overview | Checks | Trends | Issues | Files
70
+ function buildSidebar(currentId) {
71
+ let sb = sidebarScore(report);
72
+ // Category tree — always shown, current page highlighted
73
+ sb += `<div class="side-section">`;
74
+ for (const cs of catScores) {
72
75
  const isPremium = cs.checks.every((c) => det(c).comingSoon);
76
+ const isCurrent = cs.id === currentId;
73
77
  const clr = isPremium ? "#6366f1" : gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
74
- const label = isPremium
78
+ const scoreLabel = isPremium
75
79
  ? `<span class="pro-badge" style="font-size:0.5rem;padding:0.08rem 0.35rem">PRO</span>`
76
80
  : `<span style="color:${clr}">${cs.avg}</span>`;
77
- return `<a class="side-cat" href="${cs.file}">${cs.label} ${label}</a>`;
78
- })
79
- .join("") +
80
- sidebarViews(totalIssues, fileIssues.size);
81
- pages.set("index.html", w("overview", overviewSidebar, overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
82
- // ── Category pages: sidebar shows the checks within this category ──
81
+ sb += `<a class="side-cat${isCurrent ? " side-cat-active" : ""}" href="${cs.file}">${cs.label} ${scoreLabel}</a>`;
82
+ // Show individual checks under the CURRENT category
83
+ if (isCurrent) {
84
+ for (const c of cs.checks) {
85
+ const sk = det(c).skipped;
86
+ const premium = det(c).comingSoon;
87
+ const meta = getCheckMeta(c.name);
88
+ const badge = premium
89
+ ? `<span style="color:#6366f1">PRO</span>`
90
+ : `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
91
+ sb += `<a class="side-check" onclick="let t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
92
+ }
93
+ }
94
+ }
95
+ sb += `</div>`;
96
+ sb += sidebarViews(totalIssues, fileIssues.size);
97
+ return sb;
98
+ }
99
+ const w = (id, content) => wrap(proj, id, report, totalIssues, buildSidebar(id), content);
100
+ // ── Generate pages ──
101
+ pages.set("index.html", w("overview", overviewPage(report, active, totalIssues, catScores, allChecks, topFiles, fl, historyDir)));
83
102
  for (let i = 0; i < GROUPS.length; i++) {
84
103
  const g = GROUPS[i];
85
104
  const cs = catScores[i];
86
- const catSidebar = sidebarScore(report) +
87
- `<div class="side-section"><div class="side-cat-title">${cs.label}</div>` +
88
- cs.checks
89
- .map((c) => {
90
- const sk = det(c).skipped;
91
- const premium = det(c).comingSoon;
92
- const meta = getCheckMeta(c.name);
93
- const badge = premium
94
- ? `<span style="color:#6366f1">PRO</span>`
95
- : `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
96
- return `<a class="side-check" onclick="let t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
97
- })
98
- .join("") +
99
- `</div>` +
100
- sidebarViews(totalIssues, fileIssues.size);
101
- pages.set(g.file, w(g.id, catSidebar, categoryPage(cs, fl)));
105
+ pages.set(g.file, w(g.id, categoryPage(cs, fl)));
102
106
  }
103
- // ── Issues: sidebar shows severity breakdown ──
104
- const allIssuesList = allChecks.flatMap((c) => c.issues);
105
- const errCount = allIssuesList.filter((i) => i.severity === "error").length;
106
- const warnCount = allIssuesList.filter((i) => i.severity === "warning").length;
107
- const infoCount = allIssuesList.filter((i) => i.severity === "info").length;
108
- const issuesSidebar = sidebarScore(report) +
109
- `<div class="side-section"><div class="side-cat-title">Breakdown</div>` +
110
- `<div class="side-stat"><span style="color:var(--fail)">${errCount}</span> errors</div>` +
111
- `<div class="side-stat"><span style="color:var(--warn)">${warnCount}</span> warnings</div>` +
112
- `<div class="side-stat"><span style="color:var(--info)">${infoCount}</span> info</div>` +
113
- `</div>` +
114
- sidebarViews(totalIssues, fileIssues.size);
115
- pages.set("issues.html", w("issues", issuesSidebar, issuesPage(allChecks, totalIssues, fl)));
116
- // ── Files: sidebar shows file stats ──
117
- const filesSidebar = sidebarScore(report) +
118
- `<div class="side-section"><div class="side-cat-title">File Health</div>` +
119
- `<div class="side-stat"><span style="color:var(--text)">${fileIssues.size}</span> files with issues</div>` +
120
- `<div class="side-stat"><span style="color:var(--fail)">${topFiles.filter((f) => f.errors > 0).length}</span> with errors</div>` +
121
- `</div>` +
122
- sidebarViews(totalIssues, fileIssues.size);
123
- pages.set("files.html", w("files", filesSidebar, filesPage(topFiles, fileIssues, fl)));
124
- // ── Trends page ──
125
- const trendsSidebar = sidebarScore(report) +
126
- `<div class="side-section"><div class="side-cat-title">History</div>` +
127
- `<div class="side-stat"><span style="color:var(--text)">${historyDir ? "30" : "0"}</span> scans stored</div>` +
128
- `</div>` +
129
- sidebarViews(totalIssues, fileIssues.size);
130
- pages.set("trends.html", w("trends", trendsSidebar, trendsPage(historyDir)));
107
+ pages.set("issues.html", w("issues", issuesPage(allChecks, totalIssues, fl)));
108
+ pages.set("files.html", w("files", filesPage(topFiles, fileIssues, fl)));
109
+ pages.set("trends.html", w("trends", trendsPage(historyDir)));
131
110
  return pages;
132
111
  }
133
112
  export function generateHTML(report, historyDir) {
@@ -142,14 +121,21 @@ function sidebarViews(totalIssues, fileCount) {
142
121
  }
143
122
  // ── Page wrapper ──
144
123
  function wrap(proj, currentId, report, totalIssues, sidebar, content) {
124
+ // Top nav: only high-level sections (not every category page)
125
+ const isCheckPage = GROUPS.some((g) => g.id === currentId);
145
126
  const navItems = [
146
127
  { id: "overview", label: "Overview", file: "index.html" },
147
- ...GROUPS.map((g) => ({ id: g.id, label: g.label, file: g.file })),
128
+ { id: "checks", label: "Checks", file: GROUPS[0].file, active: isCheckPage },
148
129
  { id: "trends", label: "Trends", file: "trends.html" },
149
130
  { id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
150
131
  { id: "files", label: "Files", file: "files.html" },
151
132
  ];
152
- const nav = navItems.map((t) => `<a class="tn${t.id === currentId ? " active" : ""}" href="${t.file}">${t.label}</a>`).join("");
133
+ const nav = navItems
134
+ .map((t) => {
135
+ const active = t.active || t.id === currentId;
136
+ return `<a class="tn${active ? " active" : ""}" href="${t.file}">${t.label}</a>`;
137
+ })
138
+ .join("");
153
139
  return `<!DOCTYPE html>
154
140
  <html lang="en">
155
141
  <head>
@@ -1,7 +1,7 @@
1
1
  /** Page renderers for the HTML report. */
2
2
  import { getCheckMeta } from "../check-meta.js";
3
3
  import { loadHistory } from "../history.js";
4
- import { generateArchSVG, generateDSM, generatePackageDiagram, generateSequenceDiagram } from "../runners/architecture.js";
4
+ import { generateArchSVG, generateDSM, generateLayerDiagram, generatePackageDiagram, generateSequenceDiagram } from "../runners/architecture.js";
5
5
  import { det, e, gc, pc } from "./components.js";
6
6
  import { buildPyramid, buildRadar, buildRing, buildTimeline } from "./svg.js";
7
7
  // ── Overview ──────────────────────────────────────────────────────────
@@ -160,7 +160,7 @@ ${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` :
160
160
  <div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : `${c.score}/100`} \u00b7 weight ${meta.weight}% \u00b7 ${c.duration}ms \u00b7 ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
161
161
  ${meta.description ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">What</span><span>${e(meta.description)}</span></div><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div><div class="ip-row"><span class="ip-label">Fix</span><span>${e(meta.recommendation)}</span></div></div>` : ""}
162
162
  ${sk ? `<p class="skip-r">${e(det(c).reason || "skipped")}</p>` : ""}
163
- ${c.name === "architecture" && !sk ? `${c.details.containerSvg ? `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${c.details.containerSvg}</div>` : ""}<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(c.details)}</div><h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(c.details)}</div><h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(c.details)}</div>` : ""}
163
+ ${c.name === "architecture" && !sk ? renderArchSection(c.details) : ""}
164
164
  ${c.name === "testing" && !sk && det(c).pyramid ? `<div class="arch-svg">${buildPyramid(det(c).pyramid)}</div>` : ""}
165
165
  ${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
166
166
  ${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
@@ -220,6 +220,54 @@ export function filesPage(topFiles, fileIssues, fl) {
220
220
  }, new Set()).size} checks.</p>
221
221
  ${heatmapRows}`;
222
222
  }
223
+ // ── Architecture section renderer ────────────────────────
224
+ function renderArchSection(details) {
225
+ const assessment = details.assessment;
226
+ let html = "";
227
+ // Assessment panel (before diagrams)
228
+ if (assessment) {
229
+ const ratingColors = { excellent: "var(--pass)", good: "#84cc16", fair: "var(--warn)", poor: "var(--fail)" };
230
+ const ratingColor = ratingColors[assessment.rating] || "var(--muted)";
231
+ html += `<div class="info-panel" style="margin-top:1rem">`;
232
+ html += `<div class="ip-row"><span class="ip-label">Pattern</span><span><b>${assessment.pattern}</b> — ${assessment.patternDescription}</span></div>`;
233
+ html += `<div class="ip-row"><span class="ip-label">Rating</span><span style="color:${ratingColor};font-weight:700;text-transform:uppercase">${assessment.rating}</span></div>`;
234
+ html += `<div class="ip-row"><span class="ip-label">Layers</span><span>${assessment.layering} layering</span></div>`;
235
+ html += `<div class="ip-row"><span class="ip-label">Coupling</span><span>${assessment.crossCoupling}% cross-directory imports</span></div>`;
236
+ html += `<div class="ip-row"><span class="ip-label">Cohesion</span><span>${assessment.cohesion}% average internal cohesion</span></div>`;
237
+ html += `</div>`;
238
+ // Insights
239
+ if (assessment.insights.length > 0) {
240
+ html += `<div style="margin:0.8rem 0;font-size:0.75rem">`;
241
+ for (const insight of assessment.insights) {
242
+ html += `<div style="padding:0.2rem 0;color:var(--muted)">\u2022 ${insight}</div>`;
243
+ }
244
+ html += `</div>`;
245
+ }
246
+ // Stability table
247
+ if (assessment.stability.length > 1) {
248
+ html += `<h3 style="margin-top:1rem">Package Stability (Martin)</h3>`;
249
+ html += `<div style="font-size:0.72rem;margin-bottom:1rem">`;
250
+ for (const s of assessment.stability) {
251
+ const barW = Math.round(s.instability * 100);
252
+ const color = s.instability > 0.7 ? "var(--warn)" : s.instability < 0.3 ? "var(--pass)" : "var(--accent)";
253
+ html += `<div class="brow"><span class="bl">${s.package.replace("src/", "")}</span><div class="bb"><div class="bf" style="width:${barW}%;background:${color}"></div></div><span class="bv" style="color:${color}">${s.instability.toFixed(2)}</span></div>`;
254
+ }
255
+ html += `<div style="color:var(--muted);font-size:0.62rem;margin-top:0.3rem">I=0 = maximally stable (many dependents, hard to change). I=1 = maximally unstable (no dependents, easy to change).</div>`;
256
+ html += `</div>`;
257
+ }
258
+ }
259
+ // Diagrams
260
+ const containerSvg = details.containerSvg;
261
+ if (containerSvg) {
262
+ html += `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${containerSvg}</div>`;
263
+ }
264
+ html += `<h3 style="margin-top:1.5rem">Layer Diagram</h3><div class="arch-svg">${generateLayerDiagram(details)}</div>`;
265
+ html += `<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(details)}</div>`;
266
+ html += `<h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(details)}</div>`;
267
+ html += `<h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(details)}</div>`;
268
+ html += `<h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(details)}</div>`;
269
+ return html;
270
+ }
223
271
  // ── Trends page ──────────────────────────────────────────
224
272
  export function trendsPage(historyDir) {
225
273
  if (!historyDir) {
@@ -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;--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/* \u2500\u2500 Trends page \u2500\u2500 */\n.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}\n.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}\n.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}\n.trend-name{font-size:0.78rem;font-weight:700;flex:1}\n.trend-score{font-size:1.1rem;font-weight:900}\n.trend-chart{overflow:hidden}\n.trend-chart svg{width:100%;height:60px}\n.trend-table{margin-bottom:1.5rem}\n.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}\n.trend-row-name{flex:1;font-weight:600}\n.trend-row-val{width:2rem;text-align:center;color:var(--muted)}\n.trend-row-arrow{color:var(--muted);font-size:0.6rem}\n.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}\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";
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(--muted);font-weight:600;cursor:pointer;text-decoration:none;font-size:0.72rem}\n.side-cat:hover{background:#14141a;color:var(--text)}\n.side-cat-active{color:var(--text);font-weight:700;border-left:2px solid var(--accent);padding-left:calc(0.8rem - 2px)}\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/* \u2500\u2500 Trends page \u2500\u2500 */\n.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}\n.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}\n.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}\n.trend-name{font-size:0.78rem;font-weight:700;flex:1}\n.trend-score{font-size:1.1rem;font-weight:900}\n.trend-chart{overflow:hidden}\n.trend-chart svg{width:100%;height:60px}\n.trend-table{margin-bottom:1.5rem}\n.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}\n.trend-row-name{flex:1;font-weight:600}\n.trend-row-val{width:2rem;text-align:center;color:var(--muted)}\n.trend-row-arrow{color:var(--muted);font-size:0.6rem}\n.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}\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";
@@ -22,8 +22,9 @@ code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
22
22
  .side-section:last-child{border-bottom:none}
23
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
24
  .side-score{font-size:1.4rem;font-weight:900;padding:0.2rem 0.8rem}
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}
26
- .side-cat:hover{background:#14141a}
25
+ .side-cat{display:block;padding:0.3rem 0.8rem;color:var(--muted);font-weight:600;cursor:pointer;text-decoration:none;font-size:0.72rem}
26
+ .side-cat:hover{background:#14141a;color:var(--text)}
27
+ .side-cat-active{color:var(--text);font-weight:700;border-left:2px solid var(--accent);padding-left:calc(0.8rem - 2px)}
27
28
  .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
29
  .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}
29
30
  .side-check:hover{color:var(--text);background:#14141a}
@@ -28,5 +28,6 @@ export declare function generateArchSVG(details: Record<string, unknown>): strin
28
28
  export declare function generateDSM(details: Record<string, unknown>): string;
29
29
  export declare function generatePackageDiagram(details: Record<string, unknown>): string;
30
30
  export declare function generateSequenceDiagram(details: Record<string, unknown>): string;
31
+ export declare function generateLayerDiagram(details: Record<string, unknown>): string;
31
32
  export declare function generateContainerDiagram(cwd: string): string;
32
33
  export {};
@@ -94,6 +94,8 @@ export function runArchitecture(cwd) {
94
94
  for (const [path, node] of graph.nodes) {
95
95
  graphData[path] = { imports: node.imports, importedBy: node.importedBy, dir: node.dir };
96
96
  }
97
+ // ── Architecture Assessment ──
98
+ const assessment = assessArchitecture(graph.nodes, files.length);
97
99
  return {
98
100
  name: "architecture",
99
101
  score,
@@ -107,6 +109,7 @@ export function runArchitecture(cwd) {
107
109
  connectors,
108
110
  graph: graphData,
109
111
  containerSvg: generateContainerDiagram(cwd),
112
+ assessment,
110
113
  },
111
114
  issues,
112
115
  duration: Date.now() - start,
@@ -222,6 +225,137 @@ function findCycles(nodes) {
222
225
  function short(path) {
223
226
  return basename(path, extname(path));
224
227
  }
228
+ function assessArchitecture(nodes, fileCount) {
229
+ // Group by directory
230
+ const dirs = new Map();
231
+ for (const [, node] of nodes) {
232
+ const dir = node.dir || ".";
233
+ const arr = dirs.get(dir) || [];
234
+ arr.push(node);
235
+ dirs.set(dir, arr);
236
+ }
237
+ // Detect pattern
238
+ let pattern = "flat";
239
+ let patternDescription = "All files in one directory — no clear separation of concerns.";
240
+ const dirCount = dirs.size;
241
+ const hasRunners = [...dirs.keys()].some((d) => d.includes("runner") || d.includes("plugin") || d.includes("check"));
242
+ const hasReport = [...dirs.keys()].some((d) => d.includes("report") || d.includes("output") || d.includes("view"));
243
+ if (hasRunners && dirCount >= 3) {
244
+ pattern = "plugin";
245
+ patternDescription = "Plugin architecture — core defines interfaces, plugins implement independently.";
246
+ }
247
+ else if (dirCount >= 4 && hasReport) {
248
+ pattern = "layered";
249
+ patternDescription = "Layered architecture — clear separation between input, processing, and output.";
250
+ }
251
+ else if (dirCount >= 3) {
252
+ pattern = "modular";
253
+ patternDescription = "Modular architecture — code organized by feature/responsibility.";
254
+ }
255
+ else if (dirCount === 2) {
256
+ pattern = "two-tier";
257
+ patternDescription = "Two-tier — main code + one sub-package (e.g., src + lib).";
258
+ }
259
+ // Stability per directory (Robert Martin's instability metric)
260
+ const stability = [];
261
+ for (const [dir, dirNodes] of dirs) {
262
+ let ce = 0; // efferent (outgoing deps)
263
+ let ca = 0; // afferent (incoming deps)
264
+ for (const node of dirNodes) {
265
+ ce += node.imports.length;
266
+ ca += node.importedBy.length;
267
+ }
268
+ const instability = ce / Math.max(1, ce + ca);
269
+ const role = instability > 0.7 ? "unstable (easy to change)" : instability < 0.3 ? "stable (hard to change)" : "balanced";
270
+ stability.push({ package: dir, instability: Math.round(instability * 100) / 100, role });
271
+ }
272
+ // Cross-coupling: what % of imports cross directory boundaries?
273
+ // Exclude imports TO stable core (types, utils) — those are expected in plugin arch.
274
+ let totalEdges = 0;
275
+ let crossEdges = 0;
276
+ const stableDirs = stability.filter((s) => s.instability < 0.35).map((s) => s.package);
277
+ for (const [, node] of nodes) {
278
+ for (const imp of node.imports) {
279
+ totalEdges++;
280
+ const impNode = nodes.get(imp);
281
+ if (impNode && impNode.dir !== node.dir) {
282
+ // Importing FROM stable core is expected, don't count as coupling
283
+ if (!stableDirs.includes(impNode.dir || ".")) {
284
+ crossEdges++;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ const crossCoupling = totalEdges > 0 ? Math.round((crossEdges / totalEdges) * 100) : 0;
290
+ // Cohesion: average % of internal edges per directory
291
+ let totalCohesion = 0;
292
+ let dirsCounted = 0;
293
+ for (const [dir, dirNodes] of dirs) {
294
+ if (dirNodes.length < 2)
295
+ continue;
296
+ let internal = 0;
297
+ let total = 0;
298
+ for (const node of dirNodes) {
299
+ for (const imp of node.imports) {
300
+ total++;
301
+ const impNode = nodes.get(imp);
302
+ if (impNode && impNode.dir === dir)
303
+ internal++;
304
+ }
305
+ }
306
+ if (total > 0) {
307
+ totalCohesion += internal / total;
308
+ dirsCounted++;
309
+ }
310
+ }
311
+ const cohesion = dirsCounted > 0 ? Math.round((totalCohesion / dirsCounted) * 100) : 0;
312
+ // Layering: check if dependencies flow in one direction
313
+ // Violations = importing from peer/sibling packages (not from stable core)
314
+ let violations = 0;
315
+ for (const [, node] of nodes) {
316
+ for (const imp of node.imports) {
317
+ const impNode = nodes.get(imp);
318
+ if (impNode && impNode.dir !== node.dir) {
319
+ // Importing from stable core = fine
320
+ if (stableDirs.includes(impNode.dir || "."))
321
+ continue;
322
+ // Two unstable packages importing each other = violation
323
+ const myStability = stability.find((s) => s.package === (node.dir || "."));
324
+ const impStability = stability.find((s) => s.package === (impNode.dir || "."));
325
+ if (myStability && impStability && myStability.instability > 0.5 && impStability.instability > 0.5) {
326
+ violations++;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ const layering = violations === 0 ? "clean" : violations < 5 ? "mixed" : "tangled";
332
+ // Overall rating
333
+ let rating = "excellent";
334
+ if (crossCoupling > 80 || layering === "tangled")
335
+ rating = "poor";
336
+ else if (crossCoupling > 60 || layering === "mixed")
337
+ rating = "fair";
338
+ else if (crossCoupling > 40 || cohesion < 20)
339
+ rating = "good";
340
+ // Generate insights
341
+ const insights = [];
342
+ if (pattern === "plugin")
343
+ insights.push("Plugin pattern detected — runners are independent and interchangeable.");
344
+ if (layering === "clean")
345
+ insights.push("Clean layering — dependencies flow in one direction without violations.");
346
+ if (crossCoupling < 40)
347
+ insights.push(`Low cross-coupling (${crossCoupling}%) — packages are well-isolated.`);
348
+ else
349
+ insights.push(`High cross-coupling (${crossCoupling}%) — packages depend heavily on each other.`);
350
+ if (stability.some((s) => s.instability < 0.3))
351
+ insights.push("Stable foundation detected — core modules rarely change.");
352
+ const godCount = [...nodes.values()].filter((n) => n.importedBy.length > fileCount * 0.5).length;
353
+ if (godCount === 0)
354
+ insights.push("No god modules — healthy dependency distribution.");
355
+ else
356
+ insights.push(`${godCount} god module(s) — consider splitting into focused interfaces.`);
357
+ return { pattern, patternDescription, layering, stability, crossCoupling, cohesion, rating, insights };
358
+ }
225
359
  // ── SVG Architecture Diagram ──
226
360
  export function generateArchSVG(details) {
227
361
  const graph = details.graph;
@@ -506,81 +640,215 @@ export function generatePackageDiagram(details) {
506
640
  return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
507
641
  }
508
642
  // ── Sequence Diagram ─────────────────────────────────────────────────
509
- // Traces the longest import chains from entry points, showing how a request
510
- // flows through the system. UML-style lifelines with arrows.
643
+ // Shows the RUNTIME FLOW of the application what calls what in order.
644
+ // Detected by analyzing the entry point's exported function calls and
645
+ // which modules they invoke. NOT just import chains.
646
+ //
647
+ // Participants = architectural roles (Entry, Detect, Runners, Score, Report, Output)
648
+ // Messages = actual operations that happen at runtime
511
649
  export function generateSequenceDiagram(details) {
512
650
  const graph = details.graph;
513
651
  if (!graph || Object.keys(graph).length < 3)
514
652
  return "";
515
- // Find entry points (files with 0 importers that aren't utility files)
653
+ // Find the entry point
516
654
  const entries = Object.entries(graph);
517
- const entryPoints = entries
518
- .filter(([path, info]) => {
655
+ const entryPoint = entries.find(([path, info]) => {
519
656
  const name = basename(path, extname(path));
520
657
  return info.importedBy.length === 0 && ["index", "main", "cli", "App", "app", "server"].includes(name);
521
- })
522
- .map(([p]) => p);
523
- if (entryPoints.length === 0)
658
+ });
659
+ if (!entryPoint)
660
+ return "";
661
+ // Determine architectural roles from directory structure
662
+ const roles = [];
663
+ const dirs = new Map();
664
+ for (const [, info] of entries) {
665
+ const dir = info.dir || ".";
666
+ dirs.set(dir, (dirs.get(dir) || 0) + 1);
667
+ }
668
+ // Build role list from actual structure
669
+ const entryName = basename(entryPoint[0], extname(entryPoint[0]));
670
+ roles.push({ name: entryName, dir: "entry", modules: 1 });
671
+ // Add directories as participants (sorted by dependency order)
672
+ const dirArr = [...dirs.entries()]
673
+ .filter(([d]) => d !== (entryPoint[1].dir || "."))
674
+ .sort((a, b) => {
675
+ // Sort by average fan-in (more depended-upon = earlier in flow)
676
+ const aFanIn = entries.filter(([, i]) => i.dir === a[0]).reduce((s, [, i]) => s + i.importedBy.length, 0) / a[1];
677
+ const bFanIn = entries.filter(([, i]) => i.dir === b[0]).reduce((s, [, i]) => s + i.importedBy.length, 0) / b[1];
678
+ return bFanIn - aFanIn; // most depended-on first
679
+ });
680
+ for (const [dir, count] of dirArr) {
681
+ const label = dir.replace("src/", "").replace("lib/", "") || "core";
682
+ roles.push({ name: label, dir, modules: count });
683
+ }
684
+ if (roles.length < 3)
524
685
  return "";
525
- // BFS from first entry point to find the longest chain (max 8 deep)
526
- const entry = entryPoints[0];
527
- const chain = findLongestChain(entry, graph, 8);
528
- if (chain.length < 3)
686
+ const maxRoles = Math.min(roles.length, 6);
687
+ const displayRoles = roles.slice(0, maxRoles);
688
+ // Build messages: entry calls each role in order
689
+ // Detect what the entry imports from each directory
690
+ const messages = [];
691
+ const entryImports = entryPoint[1].imports;
692
+ for (let i = 1; i < displayRoles.length; i++) {
693
+ const role = displayRoles[i];
694
+ const importsFromRole = entryImports.filter((imp) => {
695
+ const impInfo = graph[imp];
696
+ return impInfo && (impInfo.dir || ".") === role.dir;
697
+ });
698
+ if (importsFromRole.length > 0) {
699
+ const funcNames = importsFromRole.map((p) => basename(p, extname(p))).slice(0, 2).join(", ");
700
+ messages.push({ from: 0, to: i, label: funcNames });
701
+ }
702
+ }
703
+ // Also show inter-role calls (report imports from runners, etc.)
704
+ for (let i = 1; i < displayRoles.length; i++) {
705
+ for (let j = 1; j < displayRoles.length; j++) {
706
+ if (i === j)
707
+ continue;
708
+ const fromDir = displayRoles[i].dir;
709
+ const toDir = displayRoles[j].dir;
710
+ const crossImports = entries.filter(([, info]) => (info.dir || ".") === fromDir && info.imports.some((imp) => graph[imp] && (graph[imp].dir || ".") === toDir));
711
+ if (crossImports.length > 0 && messages.length < 10) {
712
+ messages.push({ from: i, to: j, label: `${crossImports.length} calls` });
713
+ }
714
+ }
715
+ }
716
+ if (messages.length < 2)
529
717
  return "";
530
- // Draw sequence diagram
531
- const participants = chain.map((p) => basename(p, extname(p)));
532
- const lifelineSpacing = 120;
533
- const W = participants.length * lifelineSpacing + 40;
534
- const messageH = 36;
535
- const headerH = 50;
536
- const H = headerH + (chain.length - 1) * messageH + 40;
718
+ // Draw UML sequence diagram
719
+ const lifelineSpacing = 130;
720
+ const W = displayRoles.length * lifelineSpacing + 40;
721
+ const messageH = 40;
722
+ const headerH = 55;
723
+ const H = headerH + messages.length * messageH + 30;
537
724
  let svg = "";
538
- // Participant boxes (lifeline headers)
539
- for (let i = 0; i < participants.length; i++) {
725
+ // Participant boxes
726
+ for (let i = 0; i < displayRoles.length; i++) {
540
727
  const x = 20 + i * lifelineSpacing + lifelineSpacing / 2;
541
- const name = participants[i];
542
- const boxW = Math.max(60, name.length * 7 + 16);
543
- svg += `<rect x="${x - boxW / 2}" y="8" width="${boxW}" height="22" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
544
- svg += `<text x="${x}" y="23" text-anchor="middle" fill="#9ca3af" font-size="9" font-weight="600">${name}</text>`;
545
- // Lifeline (dashed vertical)
546
- svg += `<line x1="${x}" y1="30" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
547
- }
548
- // Arrows between lifelines (imports = calls)
549
- for (let i = 0; i < chain.length - 1; i++) {
550
- const fromX = 20 + i * lifelineSpacing + lifelineSpacing / 2;
551
- const toX = 20 + (i + 1) * lifelineSpacing + lifelineSpacing / 2;
728
+ const role = displayRoles[i];
729
+ const label = role.name;
730
+ const subtitle = role.modules > 1 ? `(${role.modules})` : "";
731
+ const boxW = Math.max(70, label.length * 7 + 20);
732
+ svg += `<rect x="${x - boxW / 2}" y="6" width="${boxW}" height="${subtitle ? 30 : 22}" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
733
+ svg += `<text x="${x}" y="20" text-anchor="middle" fill="#e5e5e5" font-size="9" font-weight="700">${label}</text>`;
734
+ if (subtitle)
735
+ svg += `<text x="${x}" y="31" text-anchor="middle" fill="#4b5563" font-size="7">${subtitle}</text>`;
736
+ svg += `<line x1="${x}" y1="${subtitle ? 36 : 28}" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
737
+ }
738
+ // Messages
739
+ for (let i = 0; i < messages.length; i++) {
740
+ const msg = messages[i];
741
+ const fromX = 20 + msg.from * lifelineSpacing + lifelineSpacing / 2;
742
+ const toX = 20 + msg.to * lifelineSpacing + lifelineSpacing / 2;
552
743
  const y = headerH + i * messageH;
553
- // Arrow
554
- svg += `<line x1="${fromX}" y1="${y}" x2="${toX - 6}" y2="${y}" stroke="#6d78d0" stroke-width="1.5" marker-end="url(#seq-arrow)"/>`;
555
- // Label (the import)
556
- const label = `import`;
557
- svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">${label}</text>`;
744
+ const isReturn = msg.to < msg.from;
745
+ const color = isReturn ? "#4b5563" : "#6d78d0";
746
+ const dash = isReturn ? ' stroke-dasharray="4,2"' : "";
747
+ svg += `<line x1="${fromX}" y1="${y}" x2="${toX + (toX > fromX ? -6 : 6)}" y2="${y}" stroke="${color}" stroke-width="1.5" marker-end="url(#seq-arrow)"${dash}/>`;
748
+ svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">${msg.label}</text>`;
558
749
  }
559
- // Arrow marker
560
750
  const defs = `<defs><marker id="seq-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="7" markerHeight="5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#6d78d0"/></marker></defs>`;
561
751
  return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
562
752
  }
563
- function findLongestChain(start, graph, maxDepth) {
564
- let longest = [start];
565
- const visited = new Set([start]);
566
- function dfs(node, path) {
567
- if (path.length > longest.length)
568
- longest = [...path];
569
- if (path.length >= maxDepth)
570
- return;
571
- const info = graph[node];
572
- if (!info)
573
- return;
753
+ // ── Layer Diagram ────────────────────────────────────────────────────
754
+ // Detects application layers (MVC, Clean Architecture, etc.) from module behavior.
755
+ // Layers are determined by fan-in/fan-out patterns + naming conventions.
756
+ export function generateLayerDiagram(details) {
757
+ const graph = details.graph;
758
+ if (!graph || Object.keys(graph).length < 5)
759
+ return "";
760
+ const entries = Object.entries(graph);
761
+ const layerDefs = [
762
+ { id: "entry", label: "Entry / Controller", color: "#6d78d0" },
763
+ { id: "view", label: "View / Output", color: "#06b6d4" },
764
+ { id: "service", label: "Service / Logic", color: "#22c55e" },
765
+ { id: "data", label: "Data / IO", color: "#d97706" },
766
+ { id: "model", label: "Model / Types", color: "#8b5cf6" },
767
+ ];
768
+ const moduleLayer = new Map();
769
+ for (const [path, info] of entries) {
770
+ const name = basename(path, extname(path));
771
+ const fanIn = info.importedBy.length;
772
+ const fanOut = info.imports.length;
773
+ let layer = "service";
774
+ if (fanIn === 0 && fanOut > 5)
775
+ layer = "entry";
776
+ else if (fanIn > 10 && fanOut === 0)
777
+ layer = "model";
778
+ else if (fanIn > 5 && fanOut <= 1)
779
+ layer = "model";
780
+ else if (path.includes("report") || path.includes("html") || path.includes("svg") || path.includes("page") || path.includes("style") || path.includes("component"))
781
+ layer = "view";
782
+ else if (name === "types" || name === "check-meta" || path.includes("types"))
783
+ layer = "model";
784
+ else if (name === "exec" || name === "detect" || name.includes("fs-") || path.includes("history"))
785
+ layer = "data";
786
+ else if (path.includes("runner") || path.includes("check"))
787
+ layer = "service";
788
+ else if (fanOut > fanIn * 2)
789
+ layer = "entry";
790
+ moduleLayer.set(path, layer);
791
+ }
792
+ // Count modules per layer
793
+ const layerCounts = new Map();
794
+ for (const [path, layer] of moduleLayer) {
795
+ const arr = layerCounts.get(layer) || [];
796
+ arr.push(basename(path, extname(path)));
797
+ layerCounts.set(layer, arr);
798
+ }
799
+ // Count violations (imports going UP the stack)
800
+ const layerOrder = ["entry", "view", "service", "data", "model"];
801
+ let violations = 0;
802
+ let totalCrossLayer = 0;
803
+ for (const [path, info] of entries) {
804
+ const myLayer = moduleLayer.get(path);
805
+ const myIdx = layerOrder.indexOf(myLayer);
574
806
  for (const imp of info.imports) {
575
- if (!visited.has(imp) && graph[imp]) {
576
- visited.add(imp);
577
- dfs(imp, [...path, imp]);
578
- visited.delete(imp);
807
+ const impLayer = moduleLayer.get(imp);
808
+ if (impLayer && impLayer !== myLayer) {
809
+ totalCrossLayer++;
810
+ const impIdx = layerOrder.indexOf(impLayer);
811
+ if (impIdx < myIdx)
812
+ violations++; // importing from layer ABOVE = violation
579
813
  }
580
814
  }
581
815
  }
582
- dfs(start, [start]);
583
- return longest;
816
+ // Draw
817
+ const W = 600;
818
+ const layerH = 50;
819
+ const gap = 6;
820
+ const padding = 20;
821
+ const activeLayers = layerDefs.filter((l) => (layerCounts.get(l.id)?.length || 0) > 0);
822
+ const H = padding * 2 + activeLayers.length * (layerH + gap) + 40;
823
+ let svg = "";
824
+ let y = padding;
825
+ // Title
826
+ svg += `<text x="${W / 2}" y="${y}" text-anchor="middle" fill="#9ca3af" font-size="10" font-weight="700">Application Layers</text>`;
827
+ y += 20;
828
+ for (const layer of activeLayers) {
829
+ const modules = layerCounts.get(layer.id) || [];
830
+ const moduleList = modules.slice(0, 8).join(", ") + (modules.length > 8 ? ` +${modules.length - 8}` : "");
831
+ // Layer band
832
+ svg += `<rect x="${padding}" y="${y}" width="${W - padding * 2}" height="${layerH}" rx="6" fill="${layer.color}10" stroke="${layer.color}40"/>`;
833
+ svg += `<text x="${padding + 12}" y="${y + 20}" fill="${layer.color}" font-size="10" font-weight="700">${layer.label}</text>`;
834
+ svg += `<text x="${padding + 12}" y="${y + 36}" fill="#6b7280" font-size="8">${moduleList}</text>`;
835
+ svg += `<text x="${W - padding - 12}" y="${y + 20}" text-anchor="end" fill="#4b5563" font-size="9">${modules.length}</text>`;
836
+ // Arrow down to next layer
837
+ if (activeLayers.indexOf(layer) < activeLayers.length - 1) {
838
+ const arrowY = y + layerH + gap / 2;
839
+ svg += `<line x1="${W / 2}" y1="${y + layerH}" x2="${W / 2}" y2="${arrowY + gap / 2}" stroke="#ffffff15" stroke-width="1" marker-end="url(#layer-arrow)"/>`;
840
+ }
841
+ y += layerH + gap;
842
+ }
843
+ // Violation indicator
844
+ if (violations > 0) {
845
+ svg += `<text x="${W / 2}" y="${y + 10}" text-anchor="middle" fill="var(--warn)" font-size="8">${violations} layer violation${violations > 1 ? "s" : ""} (imports going UP the stack)</text>`;
846
+ }
847
+ else {
848
+ svg += `<text x="${W / 2}" y="${y + 10}" text-anchor="middle" fill="var(--pass)" font-size="8">Clean layering — all dependencies flow downward</text>`;
849
+ }
850
+ const defs = `<defs><marker id="layer-arrow" viewBox="0 0 10 7" refX="5" refY="3.5" markerWidth="6" markerHeight="4" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ffffff30"/></marker></defs>`;
851
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
584
852
  }
585
853
  // ── Container Diagram ────────────────────────────────────────────────
586
854
  // Auto-detects high-level system containers from config files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Code health scanner for the AI coding era. 21 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {