@vibecodeqa/cli 0.23.0 → 0.25.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.
- package/dist/report/html.js +49 -55
- package/dist/report/pages.d.ts +1 -0
- package/dist/report/pages.js +115 -1
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +18 -2
- package/dist/runners/architecture.js +138 -6
- package/dist/runners/confusion.js +1 -2
- package/dist/runners/context.js +1 -2
- package/dist/runners/standards.js +6 -1
- package/package.json +1 -1
package/dist/report/html.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { getCheckMeta } from "../check-meta.js";
|
|
14
14
|
import { det, e, fileLink, gc } from "./components.js";
|
|
15
15
|
import { FAVICON_SVG } from "./favicon.js";
|
|
16
|
-
import { categoryPage, filesPage, issuesPage, overviewPage } from "./pages.js";
|
|
16
|
+
import { categoryPage, filesPage, issuesPage, overviewPage, trendsPage } from "./pages.js";
|
|
17
17
|
import { CSS } from "./styles.js";
|
|
18
18
|
export const GROUPS = [
|
|
19
19
|
{ id: "foundations", label: "Foundations", file: "foundations.html", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
@@ -64,63 +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
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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="var t=document.querySelector('[data-sub=\\'${cs.id}-${c.name}\\']');if(t)sub(t,'${cs.id}')" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)));
|
|
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)));
|
|
124
110
|
return pages;
|
|
125
111
|
}
|
|
126
112
|
export function generateHTML(report, historyDir) {
|
|
@@ -135,13 +121,21 @@ function sidebarViews(totalIssues, fileCount) {
|
|
|
135
121
|
}
|
|
136
122
|
// ── Page wrapper ──
|
|
137
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);
|
|
138
126
|
const navItems = [
|
|
139
127
|
{ id: "overview", label: "Overview", file: "index.html" },
|
|
140
|
-
|
|
128
|
+
{ id: "checks", label: "Checks", file: GROUPS[0].file, active: isCheckPage },
|
|
129
|
+
{ id: "trends", label: "Trends", file: "trends.html" },
|
|
141
130
|
{ id: "issues", label: `Issues (${totalIssues})`, file: "issues.html" },
|
|
142
131
|
{ id: "files", label: "Files", file: "files.html" },
|
|
143
132
|
];
|
|
144
|
-
const nav = navItems
|
|
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("");
|
|
145
139
|
return `<!DOCTYPE html>
|
|
146
140
|
<html lang="en">
|
|
147
141
|
<head>
|
|
@@ -178,10 +172,10 @@ function sub(el,cat){
|
|
|
178
172
|
document.querySelectorAll('.sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
179
173
|
}
|
|
180
174
|
document.addEventListener('click',function(ev){
|
|
181
|
-
|
|
175
|
+
const btn=ev.target.closest('.cp-btn');
|
|
182
176
|
if(!btn)return;
|
|
183
|
-
|
|
184
|
-
try{navigator.clipboard.writeText(text)}catch(e){
|
|
177
|
+
const text=btn.dataset.prompt||'';
|
|
178
|
+
try{navigator.clipboard.writeText(text)}catch(e){const ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta)}
|
|
185
179
|
btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
|
|
186
180
|
});
|
|
187
181
|
</script>
|
package/dist/report/pages.d.ts
CHANGED
package/dist/report/pages.js
CHANGED
|
@@ -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 ?
|
|
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,3 +220,117 @@ 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">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(details)}</div>`;
|
|
265
|
+
html += `<h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(details)}</div>`;
|
|
266
|
+
html += `<h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(details)}</div>`;
|
|
267
|
+
html += `<h3 style="margin-top:1.5rem">Dependency Matrix (DSM)</h3><div class="arch-svg">${generateDSM(details)}</div>`;
|
|
268
|
+
return html;
|
|
269
|
+
}
|
|
270
|
+
// ── Trends page ──────────────────────────────────────────
|
|
271
|
+
export function trendsPage(historyDir) {
|
|
272
|
+
if (!historyDir) {
|
|
273
|
+
return `<h2>Trends</h2><p class="muted">No history data yet. Run the scanner multiple times to see trends.</p>`;
|
|
274
|
+
}
|
|
275
|
+
const history = loadHistory(historyDir);
|
|
276
|
+
if (history.length < 2) {
|
|
277
|
+
return `<h2>Trends</h2><p class="muted">Need at least 2 scans to show trends. Run the scanner again.</p>`;
|
|
278
|
+
}
|
|
279
|
+
// Overall score timeline (large)
|
|
280
|
+
const overallChart = buildTimeline(history.map((h) => ({ score: h.score, timestamp: h.timestamp })), { width: 700, height: 150 });
|
|
281
|
+
// Collect all check names from latest entry
|
|
282
|
+
const latest = history[history.length - 1];
|
|
283
|
+
const checkNames = [...latest.checkScores.keys()];
|
|
284
|
+
// Per-check mini charts
|
|
285
|
+
const checkCharts = checkNames
|
|
286
|
+
.map((name) => {
|
|
287
|
+
const data = history
|
|
288
|
+
.map((h) => ({ score: h.checkScores.get(name) ?? 0, timestamp: h.timestamp }))
|
|
289
|
+
.filter((d) => d.score > 0);
|
|
290
|
+
if (data.length < 2)
|
|
291
|
+
return "";
|
|
292
|
+
const current = data[data.length - 1].score;
|
|
293
|
+
const prev = data[data.length - 2].score;
|
|
294
|
+
const delta = current - prev;
|
|
295
|
+
const deltaStr = delta > 0 ? `<span style="color:var(--pass)">+${delta}</span>` : delta < 0 ? `<span style="color:var(--fail)">${delta}</span>` : `<span class="muted">=</span>`;
|
|
296
|
+
const color = current >= 90 ? "var(--pass)" : current >= 75 ? "#84cc16" : current >= 60 ? "var(--warn)" : "var(--fail)";
|
|
297
|
+
const chart = buildTimeline(data, { width: 300, height: 60 });
|
|
298
|
+
return `<div class="trend-card">
|
|
299
|
+
<div class="trend-header"><span class="trend-name">${name}</span><span class="trend-score" style="color:${color}">${current}</span>${deltaStr}</div>
|
|
300
|
+
<div class="trend-chart">${chart}</div>
|
|
301
|
+
</div>`;
|
|
302
|
+
})
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.join("");
|
|
305
|
+
// Score delta table
|
|
306
|
+
const deltaRows = checkNames
|
|
307
|
+
.map((name) => {
|
|
308
|
+
const scores = history.map((h) => h.checkScores.get(name) ?? 0);
|
|
309
|
+
const first = scores.find((s) => s > 0) ?? 0;
|
|
310
|
+
const last = scores[scores.length - 1];
|
|
311
|
+
const delta = last - first;
|
|
312
|
+
if (first === 0 && last === 0)
|
|
313
|
+
return "";
|
|
314
|
+
return { name, first, last, delta };
|
|
315
|
+
})
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.sort((a, b) => b.delta - a.delta);
|
|
318
|
+
const deltaTable = deltaRows
|
|
319
|
+
.map((r) => {
|
|
320
|
+
const clr = r.delta > 0 ? "var(--pass)" : r.delta < 0 ? "var(--fail)" : "var(--muted)";
|
|
321
|
+
return `<div class="trend-row"><span class="trend-row-name">${r.name}</span><span class="trend-row-val">${r.first}</span><span class="trend-row-arrow">\u2192</span><span class="trend-row-val">${r.last}</span><span class="trend-row-delta" style="color:${clr}">${r.delta > 0 ? "+" : ""}${r.delta}</span></div>`;
|
|
322
|
+
})
|
|
323
|
+
.join("");
|
|
324
|
+
return `
|
|
325
|
+
<h2>Trends</h2>
|
|
326
|
+
<p class="muted" style="font-size:0.78rem;margin-bottom:1.5rem">${history.length} scans from ${history[0].timestamp.split("T")[0]} to ${latest.timestamp.split("T")[0]}</p>
|
|
327
|
+
|
|
328
|
+
<h3>Overall Score</h3>
|
|
329
|
+
<div class="timeline">${overallChart}</div>
|
|
330
|
+
|
|
331
|
+
<h3 style="margin-top:2rem">Score Changes (first \u2192 latest)</h3>
|
|
332
|
+
<div class="trend-table">${deltaTable}</div>
|
|
333
|
+
|
|
334
|
+
<h3 style="margin-top:2rem">Per-Check Trends</h3>
|
|
335
|
+
<div class="trend-grid">${checkCharts}</div>`;
|
|
336
|
+
}
|
package/dist/report/styles.d.ts
CHANGED
|
@@ -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.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";
|
package/dist/report/styles.js
CHANGED
|
@@ -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(--
|
|
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}
|
|
@@ -152,6 +153,21 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
152
153
|
.pro-cta{color:#6366f1;font-size:0.72rem;font-weight:600;margin-top:1rem}
|
|
153
154
|
.sn-pro{opacity:0.7}
|
|
154
155
|
|
|
156
|
+
/* ── Trends page ── */
|
|
157
|
+
.trend-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem;margin-top:0.5rem}
|
|
158
|
+
.trend-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:0.8rem}
|
|
159
|
+
.trend-header{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem}
|
|
160
|
+
.trend-name{font-size:0.78rem;font-weight:700;flex:1}
|
|
161
|
+
.trend-score{font-size:1.1rem;font-weight:900}
|
|
162
|
+
.trend-chart{overflow:hidden}
|
|
163
|
+
.trend-chart svg{width:100%;height:60px}
|
|
164
|
+
.trend-table{margin-bottom:1.5rem}
|
|
165
|
+
.trend-row{display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0;border-bottom:1px solid var(--border);font-size:0.75rem}
|
|
166
|
+
.trend-row-name{flex:1;font-weight:600}
|
|
167
|
+
.trend-row-val{width:2rem;text-align:center;color:var(--muted)}
|
|
168
|
+
.trend-row-arrow{color:var(--muted);font-size:0.6rem}
|
|
169
|
+
.trend-row-delta{width:2.5rem;text-align:right;font-weight:700}
|
|
170
|
+
|
|
155
171
|
.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
|
|
156
172
|
.footer a{color:var(--muted)}
|
|
157
173
|
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
@@ -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,
|
|
@@ -145,8 +148,7 @@ function buildGraph(files) {
|
|
|
145
148
|
function parseImports(content) {
|
|
146
149
|
const imports = [];
|
|
147
150
|
const regex = /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
148
|
-
|
|
149
|
-
while ((match = regex.exec(content)) !== null) {
|
|
151
|
+
for (const match of content.matchAll(regex)) {
|
|
150
152
|
if (match[1].startsWith("."))
|
|
151
153
|
imports.push(match[1]);
|
|
152
154
|
}
|
|
@@ -223,6 +225,137 @@ function findCycles(nodes) {
|
|
|
223
225
|
function short(path) {
|
|
224
226
|
return basename(path, extname(path));
|
|
225
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
|
+
}
|
|
226
359
|
// ── SVG Architecture Diagram ──
|
|
227
360
|
export function generateArchSVG(details) {
|
|
228
361
|
const graph = details.graph;
|
|
@@ -551,11 +684,10 @@ export function generateSequenceDiagram(details) {
|
|
|
551
684
|
const fromX = 20 + i * lifelineSpacing + lifelineSpacing / 2;
|
|
552
685
|
const toX = 20 + (i + 1) * lifelineSpacing + lifelineSpacing / 2;
|
|
553
686
|
const y = headerH + i * messageH;
|
|
554
|
-
// Arrow
|
|
687
|
+
// Arrow with target module name as label
|
|
555
688
|
svg += `<line x1="${fromX}" y1="${y}" x2="${toX - 6}" y2="${y}" stroke="#6d78d0" stroke-width="1.5" marker-end="url(#seq-arrow)"/>`;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">${label}</text>`;
|
|
689
|
+
const target = participants[i + 1];
|
|
690
|
+
svg += `<text x="${(fromX + toX) / 2}" y="${y - 6}" text-anchor="middle" fill="#6b7280" font-size="7">import ./${target}</text>`;
|
|
559
691
|
}
|
|
560
692
|
// Arrow marker
|
|
561
693
|
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>`;
|
|
@@ -247,8 +247,7 @@ function extractExports(content) {
|
|
|
247
247
|
/export\s+type\s+(\w+)/g,
|
|
248
248
|
];
|
|
249
249
|
for (const pat of patterns) {
|
|
250
|
-
|
|
251
|
-
while ((match = pat.exec(content)) !== null) {
|
|
250
|
+
for (const match of content.matchAll(pat)) {
|
|
252
251
|
exports.push(match[1]);
|
|
253
252
|
}
|
|
254
253
|
}
|
package/dist/runners/context.js
CHANGED
|
@@ -131,8 +131,7 @@ export function runContext(cwd) {
|
|
|
131
131
|
function parseImports(content) {
|
|
132
132
|
const imports = [];
|
|
133
133
|
const regex = /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
134
|
-
|
|
135
|
-
while ((match = regex.exec(content)) !== null) {
|
|
134
|
+
for (const match of content.matchAll(regex)) {
|
|
136
135
|
const path = match[1];
|
|
137
136
|
// Only count local imports (starting with . or /)
|
|
138
137
|
if (path.startsWith(".") || path.startsWith("/")) {
|
|
@@ -28,7 +28,12 @@ const CODE_SMELLS = [
|
|
|
28
28
|
message: "dangerouslySetInnerHTML bypasses React's XSS protection",
|
|
29
29
|
},
|
|
30
30
|
{ name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
|
|
31
|
-
{
|
|
31
|
+
{
|
|
32
|
+
name: "http:// URL",
|
|
33
|
+
pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1|www\.w3\.org|schemas?\.)/,
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: "Non-HTTPS URL — use https://",
|
|
36
|
+
},
|
|
32
37
|
{ name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
|
|
33
38
|
{
|
|
34
39
|
name: "magic number",
|