@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.
@@ -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
- // Traces the longest import chains from entry points, showing how a request
644
- // flows through the system. UML-style lifelines with arrows.
643
+ // Shows the RUNTIME FLOW of the application what calls what in order.
644
+ // Detected by analyzing the entry point's exported function calls and
645
+ // which modules they invoke. NOT just import chains.
646
+ //
647
+ // Participants = architectural roles (Entry, Detect, Runners, Score, Report, Output)
648
+ // Messages = actual operations that happen at runtime
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 points (files with 0 importers that aren't utility files)
653
+ // Find the entry point
650
654
  const entries = Object.entries(graph);
651
- const entryPoints = entries
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
- .map(([p]) => p);
657
- if (entryPoints.length === 0)
658
+ });
659
+ if (!entryPoint)
658
660
  return "";
659
- // BFS from first entry point to find the longest chain (max 8 deep)
660
- const entry = entryPoints[0];
661
- const chain = findLongestChain(entry, graph, 8);
662
- if (chain.length < 3)
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 participants = chain.map((p) => basename(p, extname(p)));
666
- const lifelineSpacing = 120;
667
- const W = participants.length * lifelineSpacing + 40;
668
- const messageH = 36;
669
- const headerH = 50;
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 (lifeline headers)
673
- for (let i = 0; i < participants.length; i++) {
725
+ // Participant boxes
726
+ for (let i = 0; i < displayRoles.length; i++) {
674
727
  const x = 20 + i * lifelineSpacing + lifelineSpacing / 2;
675
- const name = participants[i];
676
- const boxW = Math.max(60, name.length * 7 + 16);
677
- svg += `<rect x="${x - boxW / 2}" y="8" width="${boxW}" height="22" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
678
- svg += `<text x="${x}" y="23" text-anchor="middle" fill="#9ca3af" font-size="9" font-weight="600">${name}</text>`;
679
- // Lifeline (dashed vertical)
680
- svg += `<line x1="${x}" y1="30" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
681
- }
682
- // Arrows between lifelines (imports = calls)
683
- for (let i = 0; i < chain.length - 1; i++) {
684
- const fromX = 20 + i * lifelineSpacing + lifelineSpacing / 2;
685
- const toX = 20 + (i + 1) * lifelineSpacing + lifelineSpacing / 2;
728
+ const role = displayRoles[i];
729
+ const label = role.name;
730
+ const subtitle = role.modules > 1 ? `(${role.modules})` : "";
731
+ const boxW = Math.max(70, label.length * 7 + 20);
732
+ svg += `<rect x="${x - boxW / 2}" y="6" width="${boxW}" height="${subtitle ? 30 : 22}" rx="4" fill="#ffffff08" stroke="#ffffff15"/>`;
733
+ svg += `<text x="${x}" y="20" text-anchor="middle" fill="#e5e5e5" font-size="9" font-weight="700">${label}</text>`;
734
+ if (subtitle)
735
+ svg += `<text x="${x}" y="31" text-anchor="middle" fill="#4b5563" font-size="7">${subtitle}</text>`;
736
+ svg += `<line x1="${x}" y1="${subtitle ? 36 : 28}" x2="${x}" y2="${H - 10}" stroke="#ffffff10" stroke-width="1" stroke-dasharray="4,3"/>`;
737
+ }
738
+ // Messages
739
+ for (let i = 0; i < messages.length; i++) {
740
+ const msg = messages[i];
741
+ const fromX = 20 + msg.from * lifelineSpacing + lifelineSpacing / 2;
742
+ const toX = 20 + msg.to * lifelineSpacing + lifelineSpacing / 2;
686
743
  const y = headerH + i * messageH;
687
- // Arrow with target module name as label
688
- svg += `<line x1="${fromX}" y1="${y}" x2="${toX - 6}" y2="${y}" stroke="#6d78d0" stroke-width="1.5" marker-end="url(#seq-arrow)"/>`;
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>`;
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
- function findLongestChain(start, graph, maxDepth) {
697
- let longest = [start];
698
- const visited = new Set([start]);
699
- function dfs(node, path) {
700
- if (path.length > longest.length)
701
- longest = [...path];
702
- if (path.length >= maxDepth)
703
- return;
704
- const info = graph[node];
705
- if (!info)
706
- return;
753
+ // ── Layer Diagram ────────────────────────────────────────────────────
754
+ // Detects application layers (MVC, Clean Architecture, etc.) from module behavior.
755
+ // Layers are determined by fan-in/fan-out patterns + naming conventions.
756
+ export function generateLayerDiagram(details) {
757
+ const graph = details.graph;
758
+ if (!graph || Object.keys(graph).length < 5)
759
+ return "";
760
+ const entries = Object.entries(graph);
761
+ const layerDefs = [
762
+ { id: "entry", label: "Entry / Controller", color: "#6d78d0" },
763
+ { id: "view", label: "View / Output", color: "#06b6d4" },
764
+ { id: "service", label: "Service / Logic", color: "#22c55e" },
765
+ { id: "data", label: "Data / IO", color: "#d97706" },
766
+ { id: "model", label: "Model / Types", color: "#8b5cf6" },
767
+ ];
768
+ const moduleLayer = new Map();
769
+ for (const [path, info] of entries) {
770
+ const name = basename(path, extname(path));
771
+ const fanIn = info.importedBy.length;
772
+ const fanOut = info.imports.length;
773
+ let layer = "service";
774
+ if (fanIn === 0 && fanOut > 5)
775
+ layer = "entry";
776
+ else if (fanIn > 10 && fanOut === 0)
777
+ layer = "model";
778
+ else if (fanIn > 5 && fanOut <= 1)
779
+ layer = "model";
780
+ else if (path.includes("report") || path.includes("html") || path.includes("svg") || path.includes("page") || path.includes("style") || path.includes("component"))
781
+ layer = "view";
782
+ else if (name === "types" || name === "check-meta" || path.includes("types"))
783
+ layer = "model";
784
+ else if (name === "exec" || name === "detect" || name.includes("fs-") || path.includes("history"))
785
+ layer = "data";
786
+ else if (path.includes("runner") || path.includes("check"))
787
+ layer = "service";
788
+ else if (fanOut > fanIn * 2)
789
+ layer = "entry";
790
+ moduleLayer.set(path, layer);
791
+ }
792
+ // Count modules per layer
793
+ const layerCounts = new Map();
794
+ for (const [path, layer] of moduleLayer) {
795
+ const arr = layerCounts.get(layer) || [];
796
+ arr.push(basename(path, extname(path)));
797
+ layerCounts.set(layer, arr);
798
+ }
799
+ // Count violations (imports going UP the stack)
800
+ const layerOrder = ["entry", "view", "service", "data", "model"];
801
+ let violations = 0;
802
+ let totalCrossLayer = 0;
803
+ for (const [path, info] of entries) {
804
+ const myLayer = moduleLayer.get(path);
805
+ const myIdx = layerOrder.indexOf(myLayer);
707
806
  for (const imp of info.imports) {
708
- if (!visited.has(imp) && graph[imp]) {
709
- visited.add(imp);
710
- dfs(imp, [...path, imp]);
711
- visited.delete(imp);
807
+ const impLayer = moduleLayer.get(imp);
808
+ if (impLayer && impLayer !== myLayer) {
809
+ totalCrossLayer++;
810
+ const impIdx = layerOrder.indexOf(impLayer);
811
+ if (impIdx < myIdx)
812
+ violations++; // importing from layer ABOVE = violation
712
813
  }
713
814
  }
714
815
  }
715
- dfs(start, [start]);
716
- return longest;
816
+ // Draw
817
+ const W = 600;
818
+ const layerH = 50;
819
+ const gap = 6;
820
+ const padding = 20;
821
+ const activeLayers = layerDefs.filter((l) => (layerCounts.get(l.id)?.length || 0) > 0);
822
+ const H = padding * 2 + activeLayers.length * (layerH + gap) + 40;
823
+ let svg = "";
824
+ let y = padding;
825
+ // Title
826
+ svg += `<text x="${W / 2}" y="${y}" text-anchor="middle" fill="#9ca3af" font-size="10" font-weight="700">Application Layers</text>`;
827
+ y += 20;
828
+ for (const layer of activeLayers) {
829
+ const modules = layerCounts.get(layer.id) || [];
830
+ const moduleList = modules.slice(0, 8).join(", ") + (modules.length > 8 ? ` +${modules.length - 8}` : "");
831
+ // Layer band
832
+ svg += `<rect x="${padding}" y="${y}" width="${W - padding * 2}" height="${layerH}" rx="6" fill="${layer.color}10" stroke="${layer.color}40"/>`;
833
+ svg += `<text x="${padding + 12}" y="${y + 20}" fill="${layer.color}" font-size="10" font-weight="700">${layer.label}</text>`;
834
+ svg += `<text x="${padding + 12}" y="${y + 36}" fill="#6b7280" font-size="8">${moduleList}</text>`;
835
+ svg += `<text x="${W - padding - 12}" y="${y + 20}" text-anchor="end" fill="#4b5563" font-size="9">${modules.length}</text>`;
836
+ // Arrow down to next layer
837
+ if (activeLayers.indexOf(layer) < activeLayers.length - 1) {
838
+ const arrowY = y + layerH + gap / 2;
839
+ svg += `<line x1="${W / 2}" y1="${y + layerH}" x2="${W / 2}" y2="${arrowY + gap / 2}" stroke="#ffffff15" stroke-width="1" marker-end="url(#layer-arrow)"/>`;
840
+ }
841
+ y += layerH + gap;
842
+ }
843
+ // Violation indicator
844
+ if (violations > 0) {
845
+ svg += `<text x="${W / 2}" y="${y + 10}" text-anchor="middle" fill="var(--warn)" font-size="8">${violations} layer violation${violations > 1 ? "s" : ""} (imports going UP the stack)</text>`;
846
+ }
847
+ else {
848
+ svg += `<text x="${W / 2}" y="${y + 10}" text-anchor="middle" fill="var(--pass)" font-size="8">Clean layering — all dependencies flow downward</text>`;
849
+ }
850
+ const defs = `<defs><marker id="layer-arrow" viewBox="0 0 10 7" refX="5" refY="3.5" markerWidth="6" markerHeight="4" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ffffff30"/></marker></defs>`;
851
+ return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
717
852
  }
718
853
  // ── Container Diagram ────────────────────────────────────────────────
719
854
  // Auto-detects high-level system containers from config files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Code health scanner for the AI coding era. 21 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {