@vibecodeqa/cli 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 ──────────────────────────────────────────────────────────
|
|
@@ -261,6 +261,7 @@ function renderArchSection(details) {
|
|
|
261
261
|
if (containerSvg) {
|
|
262
262
|
html += `<h3 style="margin-top:1.5rem">Container Diagram</h3><div class="arch-svg">${containerSvg}</div>`;
|
|
263
263
|
}
|
|
264
|
+
html += `<h3 style="margin-top:1.5rem">Layer Diagram</h3><div class="arch-svg">${generateLayerDiagram(details)}</div>`;
|
|
264
265
|
html += `<h3 style="margin-top:1.5rem">Dependency Graph</h3><div class="arch-svg">${generateArchSVG(details)}</div>`;
|
|
265
266
|
html += `<h3 style="margin-top:1.5rem">Sequence Diagram</h3><div class="arch-svg">${generateSequenceDiagram(details)}</div>`;
|
|
266
267
|
html += `<h3 style="margin-top:1.5rem">Package Diagram</h3><div class="arch-svg">${generatePackageDiagram(details)}</div>`;
|
|
@@ -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:
|