@vibecodeqa/cli 0.25.0 → 0.27.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 +1 -1
- package/dist/report/pages.js +10 -24
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +3 -6
- package/dist/runners/architecture.d.ts +1 -0
- package/dist/runners/architecture.js +189 -54
- package/package.json +1 -1
package/dist/report/html.js
CHANGED
|
@@ -88,7 +88,7 @@ export function generatePages(report, historyDir) {
|
|
|
88
88
|
const badge = premium
|
|
89
89
|
? `<span style="color:#6366f1">PRO</span>`
|
|
90
90
|
: `<span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "\u2014" : c.grade} ${sk ? "" : c.score}</span>`;
|
|
91
|
-
sb += `<a class="side-check"
|
|
91
|
+
sb += `<a class="side-check" href="${cs.file}#${c.name}" title="${e(meta.label)}">${badge} ${e(meta.label)}</a>`;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
}
|
package/dist/report/pages.js
CHANGED
|
@@ -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 ──────────────────────────────────────────────────────────
|
|
@@ -88,22 +88,13 @@ ${fileHotspotsHtml}
|
|
|
88
88
|
}
|
|
89
89
|
// ── Single category page ──────────────────────────────────────────
|
|
90
90
|
export function categoryPage(cs, fl) {
|
|
91
|
-
const
|
|
92
|
-
.map((c
|
|
93
|
-
const sk = det(c).skipped;
|
|
94
|
-
const premium = det(c).comingSoon;
|
|
95
|
-
const badge = premium ? "PRO" : sk ? "\u2014" : c.grade;
|
|
96
|
-
const clr = premium ? "#6366f1" : sk ? "#555" : gc(c.grade);
|
|
97
|
-
return `<a class="sn${i === 0 ? " active" : ""}${premium ? " sn-pro" : ""}" data-sub="${cs.id}-${c.name}" onclick="sub(this,'${cs.id}')">${e(c.name)} <span style="color:${clr}">${badge}</span></a>`;
|
|
98
|
-
})
|
|
99
|
-
.join("");
|
|
100
|
-
const subPages = cs.checks
|
|
101
|
-
.map((c, i) => {
|
|
91
|
+
const checkSections = cs.checks
|
|
92
|
+
.map((c) => {
|
|
102
93
|
const meta = getCheckMeta(c.name);
|
|
103
94
|
const sk = det(c).skipped;
|
|
104
95
|
const premium = det(c).comingSoon;
|
|
105
96
|
const detailsFiltered = Object.entries(c.details)
|
|
106
|
-
.filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph")
|
|
97
|
+
.filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph" && k !== "containerSvg" && k !== "assessment")
|
|
107
98
|
.map(([k, v]) => {
|
|
108
99
|
const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
109
100
|
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
@@ -141,22 +132,17 @@ export function categoryPage(cs, fl) {
|
|
|
141
132
|
if (premium) {
|
|
142
133
|
const d = c.details;
|
|
143
134
|
const desc = d.description || meta.description;
|
|
144
|
-
|
|
145
|
-
.filter(([k]) => !["premium", "comingSoon", "reason", "description"].includes(k))
|
|
146
|
-
.map(([k, v]) => `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(Array.isArray(v) ? v.join(", ") : String(v))}</span></div>`)
|
|
147
|
-
.join("");
|
|
148
|
-
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
135
|
+
return `<section class="check-section" id="${c.name}">
|
|
149
136
|
<div class="pro-card">
|
|
150
137
|
<div class="pro-badge">PRO</div>
|
|
151
138
|
<h3 style="margin-bottom:0.5rem;color:var(--text)">${e(meta.label)}</h3>
|
|
152
139
|
<p class="pro-desc">${e(desc)}</p>
|
|
153
140
|
${meta.risk ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div></div>` : ""}
|
|
154
|
-
${detailKvs ? `<div class="kvs" style="margin-top:0.8rem">${detailKvs}</div>` : ""}
|
|
155
141
|
<p class="pro-cta">Coming soon with VibeCode QA Pro</p>
|
|
156
142
|
</div>
|
|
157
|
-
</
|
|
143
|
+
</section>`;
|
|
158
144
|
}
|
|
159
|
-
return `<
|
|
145
|
+
return `<section class="check-section" id="${c.name}">
|
|
160
146
|
<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
147
|
${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
148
|
${sk ? `<p class="skip-r">${e(det(c).reason || "skipped")}</p>` : ""}
|
|
@@ -164,15 +150,14 @@ ${c.name === "architecture" && !sk ? renderArchSection(c.details) : ""}
|
|
|
164
150
|
${c.name === "testing" && !sk && det(c).pyramid ? `<div class="arch-svg">${buildPyramid(det(c).pyramid)}</div>` : ""}
|
|
165
151
|
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
166
152
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
167
|
-
</
|
|
153
|
+
</section>`;
|
|
168
154
|
})
|
|
169
155
|
.join("");
|
|
170
156
|
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
171
157
|
return `
|
|
172
158
|
<div class="cat-head"><span style="color:${clr};font-size:1.8rem;font-weight:900">${cs.avg}</span><span style="color:${clr}">/100</span><span style="color:var(--muted);margin-left:0.5rem">${cs.label}</span></div>
|
|
173
159
|
<div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
|
|
174
|
-
|
|
175
|
-
${subPages}`;
|
|
160
|
+
${checkSections}`;
|
|
176
161
|
}
|
|
177
162
|
// ── Issues view ──────────────────────────────────────────
|
|
178
163
|
export function issuesPage(allChecks, totalIssues, fl) {
|
|
@@ -261,6 +246,7 @@ function renderArchSection(details) {
|
|
|
261
246
|
if (containerSvg) {
|
|
262
247
|
html += `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${containerSvg}</div>`;
|
|
263
248
|
}
|
|
249
|
+
html += `<h3 style="margin-top:1.5rem">Layer Diagram</h3><div class="arch-svg">${generateLayerDiagram(details)}</div>`;
|
|
264
250
|
html += `<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(details)}</div>`;
|
|
265
251
|
html += `<h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(details)}</div>`;
|
|
266
252
|
html += `<h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(details)}</div>`;
|
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(--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";
|
|
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:1.5rem;overflow:hidden}\n.bf2{height:100%;border-radius:2px}\n.check-section{margin-bottom:2.5rem;padding-top:0.5rem;border-top:1px solid var(--border)}\n.check-section:first-of-type{border-top:none}\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
|
@@ -84,13 +84,10 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
84
84
|
|
|
85
85
|
/* ── Category pages ── */
|
|
86
86
|
.cat-head{margin-bottom:0.3rem}
|
|
87
|
-
.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:
|
|
87
|
+
.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1.5rem;overflow:hidden}
|
|
88
88
|
.bf2{height:100%;border-radius:2px}
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
.sn:hover{color:var(--text)}
|
|
92
|
-
.sn.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
93
|
-
.sp{display:none}.sp.active{display:block}
|
|
89
|
+
.check-section{margin-bottom:2.5rem;padding-top:0.5rem;border-top:1px solid var(--border)}
|
|
90
|
+
.check-section:first-of-type{border-top:none}
|
|
94
91
|
|
|
95
92
|
/* ── Check detail ── */
|
|
96
93
|
.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}
|
|
@@ -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 {};
|
|
@@ -640,80 +640,215 @@ export function generatePackageDiagram(details) {
|
|
|
640
640
|
return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
|
|
641
641
|
}
|
|
642
642
|
// ── Sequence Diagram ─────────────────────────────────────────────────
|
|
643
|
-
//
|
|
644
|
-
//
|
|
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
|
|
645
649
|
export function generateSequenceDiagram(details) {
|
|
646
650
|
const graph = details.graph;
|
|
647
651
|
if (!graph || Object.keys(graph).length < 3)
|
|
648
652
|
return "";
|
|
649
|
-
// Find entry
|
|
653
|
+
// Find the entry point
|
|
650
654
|
const entries = Object.entries(graph);
|
|
651
|
-
const
|
|
652
|
-
.filter(([path, info]) => {
|
|
655
|
+
const entryPoint = entries.find(([path, info]) => {
|
|
653
656
|
const name = basename(path, extname(path));
|
|
654
657
|
return info.importedBy.length === 0 && ["index", "main", "cli", "App", "app", "server"].includes(name);
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
if (entryPoints.length === 0)
|
|
658
|
+
});
|
|
659
|
+
if (!entryPoint)
|
|
658
660
|
return "";
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
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)
|
|
685
|
+
return "";
|
|
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)
|
|
663
717
|
return "";
|
|
664
|
-
// Draw sequence diagram
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
const
|
|
670
|
-
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;
|
|
671
724
|
let svg = "";
|
|
672
|
-
// Participant boxes
|
|
673
|
-
for (let i = 0; i <
|
|
725
|
+
// Participant boxes
|
|
726
|
+
for (let i = 0; i < displayRoles.length; i++) {
|
|
674
727
|
const x = 20 + i * lifelineSpacing + lifelineSpacing / 2;
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
svg += `<
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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;
|
|
686
743
|
const y = headerH + i * messageH;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
svg += `<
|
|
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>`;
|
|
691
749
|
}
|
|
692
|
-
// Arrow marker
|
|
693
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>`;
|
|
694
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>`;
|
|
695
752
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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);
|
|
707
806
|
for (const imp of info.imports) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
|
712
813
|
}
|
|
713
814
|
}
|
|
714
815
|
}
|
|
715
|
-
|
|
716
|
-
|
|
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>`;
|
|
717
852
|
}
|
|
718
853
|
// ── Container Diagram ────────────────────────────────────────────────
|
|
719
854
|
// Auto-detects high-level system containers from config files:
|