@vibecodeqa/cli 0.26.0 → 0.28.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.
@@ -9,10 +9,10 @@
9
9
  * 6. Layer violations (optional: detect cross-layer imports)
10
10
  * 7. SVG architecture diagram
11
11
  */
12
- import { existsSync } from "node:fs";
13
- import { basename, dirname, extname, join } from "node:path";
12
+ import { basename, dirname, extname } from "node:path";
14
13
  import { getProductionFiles } from "../fs-utils.js";
15
14
  import { gradeFromScore } from "../types.js";
15
+ import { generateContainerDiagram } from "./diagrams.js";
16
16
  export function runArchitecture(cwd) {
17
17
  const start = Date.now();
18
18
  const issues = [];
@@ -356,572 +356,5 @@ function assessArchitecture(nodes, fileCount) {
356
356
  insights.push(`${godCount} god module(s) — consider splitting into focused interfaces.`);
357
357
  return { pattern, patternDescription, layering, stability, crossCoupling, cohesion, rating, insights };
358
358
  }
359
- // ── SVG Architecture Diagram ──
360
- export function generateArchSVG(details) {
361
- const graph = details.graph;
362
- if (!graph || Object.keys(graph).length === 0)
363
- return "";
364
- const nodes = Object.entries(graph);
365
- const nodeCount = nodes.length;
366
- if (nodeCount > 50)
367
- return `<div style="color:#6b7280;font-size:0.75rem">${nodeCount} modules — too many to render. Consider splitting into smaller packages.</div>`;
368
- // Detect cycles for highlighting
369
- const cycleEdges = new Set();
370
- const cycles = details.circularDeps;
371
- if (cycles > 0) {
372
- // Mark edges that participate in cycles (simplified: mutual imports)
373
- for (const [path, info] of nodes) {
374
- for (const imp of info.imports) {
375
- if (graph[imp]?.imports.includes(path)) {
376
- cycleEdges.add(`${path}->${imp}`);
377
- cycleEdges.add(`${imp}->${path}`);
378
- }
379
- }
380
- }
381
- }
382
- // Group by directory
383
- const dirs = new Map();
384
- for (const [path, info] of nodes) {
385
- const dir = info.dir || ".";
386
- const arr = dirs.get(dir) || [];
387
- arr.push(path);
388
- dirs.set(dir, arr);
389
- }
390
- const W = 800, padding = 50;
391
- const dirEntries = [...dirs.entries()];
392
- const dirWidth = (W - padding * 2) / Math.max(dirEntries.length, 1);
393
- const nodeSpacing = 38;
394
- // Position nodes
395
- const positions = new Map();
396
- let dirIdx = 0;
397
- for (const [, paths] of dirEntries) {
398
- const x0 = padding + dirIdx * dirWidth + dirWidth / 2;
399
- for (let i = 0; i < paths.length; i++) {
400
- const y = padding + 55 + i * nodeSpacing;
401
- positions.set(paths[i], { x: x0, y });
402
- }
403
- dirIdx++;
404
- }
405
- const maxGroupLen = Math.max(...[...dirs.values()].map((p) => p.length));
406
- const H = Math.max(320, padding * 2 + 55 + maxGroupLen * nodeSpacing + 50);
407
- // ── Defs: arrowhead marker, glow filter ──
408
- const defs = `<defs>
409
- <marker id="ah" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#818cf850"/></marker>
410
- <marker id="ah-cross" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ef444460"/></marker>
411
- <marker id="ah-cycle" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#f97316"/></marker>
412
- </defs>`;
413
- // ── Background — transparent, inherits page dark bg ──
414
- const bg = `<rect width="${W}" height="${H}" rx="8" fill="none"/>`;
415
- // ── Draw edges (curved bezier paths with arrows) ──
416
- let edgesSvg = "";
417
- for (const [path, info] of nodes) {
418
- const from = positions.get(path);
419
- if (!from)
420
- continue;
421
- for (const imp of info.imports) {
422
- const to = positions.get(imp);
423
- if (!to)
424
- continue;
425
- const isCycle = cycleEdges.has(`${path}->${imp}`);
426
- const isCross = info.dir !== graph[imp]?.dir;
427
- const color = isCycle ? "#f9731680" : isCross ? "#ef444435" : "#818cf820";
428
- const marker = isCycle ? "url(#ah-cycle)" : isCross ? "url(#ah-cross)" : "url(#ah)";
429
- const width = isCycle ? "2" : "1.2";
430
- const dash = isCycle ? ' stroke-dasharray="5,3"' : "";
431
- // Bezier curve: offset control point sideways to avoid straight-line overlap
432
- const dx = to.x - from.x;
433
- const dy = to.y - from.y;
434
- const cx1 = from.x + dx * 0.3 + (dy === 0 ? 0 : Math.sign(dx) * 15);
435
- const cy1 = from.y + dy * 0.3;
436
- const cx2 = to.x - dx * 0.3 + (dy === 0 ? 0 : Math.sign(dx) * 15);
437
- const cy2 = to.y - dy * 0.3;
438
- edgesSvg += `<path d="M${from.x},${from.y} C${cx1},${cy1} ${cx2},${cy2} ${to.x},${to.y}" fill="none" stroke="${color}" stroke-width="${width}" marker-end="${marker}"${dash}/>`;
439
- }
440
- }
441
- // ── Draw directory groups ──
442
- let groupsSvg = "";
443
- dirIdx = 0;
444
- for (const [dName, paths] of dirEntries) {
445
- const x = padding + dirIdx * dirWidth;
446
- const h = paths.length * nodeSpacing + 24;
447
- groupsSvg += `<rect x="${x + 5}" y="${padding + 32}" width="${dirWidth - 10}" height="${h}" rx="8" fill="#ffffff06" stroke="#ffffff10"/>`;
448
- const label = dName === "." ? "root" : dName.split("/").pop();
449
- groupsSvg += `<text x="${x + dirWidth / 2}" y="${padding + 24}" text-anchor="middle" fill="#6b7280" font-size="10" font-weight="700" letter-spacing="0.03em">${label}</text>`;
450
- dirIdx++;
451
- }
452
- // ── Draw nodes ──
453
- let nodesSvg = "";
454
- const godThreshold = Math.max(3, Math.floor(nodeCount * 0.5));
455
- for (const [path] of nodes) {
456
- const pos = positions.get(path);
457
- if (!pos)
458
- continue;
459
- const name = basename(path, extname(path));
460
- const info = graph[path];
461
- const fanIn = info.importedBy.length;
462
- const fanOut = info.imports.length;
463
- // Node color based on health
464
- const isGod = fanIn >= godThreshold;
465
- const isOrphan = fanIn === 0 && !["index", "main", "cli", "App"].includes(name);
466
- const isHighFanOut = fanOut > 10;
467
- const isInCycle = [...cycleEdges].some((e) => e.startsWith(path + "->") || e.endsWith("->" + path));
468
- let nodeColor = "#6d78d0"; // default: softer accent
469
- if (isInCycle)
470
- nodeColor = "#d97706"; // amber for cycle participant
471
- else if (isGod)
472
- nodeColor = "#dc2626"; // red for god module
473
- else if (isOrphan)
474
- nodeColor = "#4b5563"; // dim for orphan
475
- else if (isHighFanOut)
476
- nodeColor = "#ca8a04"; // yellow for high fan-out
477
- const size = Math.min(9, 3 + Math.floor(fanIn * 0.8));
478
- // Node circle with subtle glow for important nodes
479
- if (isGod || isInCycle) {
480
- nodesSvg += `<circle cx="${pos.x}" cy="${pos.y}" r="${size + 4}" fill="${nodeColor}" opacity="0.15"/>`;
481
- }
482
- nodesSvg += `<circle cx="${pos.x}" cy="${pos.y}" r="${size}" fill="${nodeColor}"/>`;
483
- // Label
484
- const labelColor = isOrphan ? "#4b5563" : "#9ca3af";
485
- nodesSvg += `<text x="${pos.x + size + 5}" y="${pos.y + 3}" fill="${labelColor}" font-size="9" font-weight="${isGod ? "700" : "400"}">${name}</text>`;
486
- // Fan-in/fan-out badge (only for notable nodes)
487
- if (fanIn > 2 || fanOut > 5) {
488
- nodesSvg += `<text x="${pos.x + size + 5}" y="${pos.y + 13}" fill="#555" font-size="7">${fanIn}\u2190 ${fanOut}\u2192</text>`;
489
- }
490
- }
491
- // ── Legend ──
492
- const legendY = H - 30;
493
- const legend = `<g transform="translate(${padding}, ${legendY})" font-size="8" fill="#6b7280">
494
- <circle cx="0" cy="0" r="4" fill="#6d78d0"/><text x="8" y="3">module</text>
495
- <circle cx="60" cy="0" r="4" fill="#dc2626"/><text x="68" y="3">god module</text>
496
- <circle cx="140" cy="0" r="4" fill="#d97706"/><text x="148" y="3">in cycle</text>
497
- <circle cx="200" cy="0" r="4" fill="#ca8a04"/><text x="208" y="3">high fan-out</text>
498
- <circle cx="280" cy="0" r="4" fill="#4b5563"/><text x="288" y="3">orphan</text>
499
- <line x1="330" y1="0" x2="350" y2="0" stroke="#ef444440" stroke-width="1.2"/><text x="354" y="3">cross-dir</text>
500
- <line x1="410" y1="0" x2="430" y2="0" stroke="#d97706" stroke-width="1.5" stroke-dasharray="5,3"/><text x="434" y="3">circular</text>
501
- </g>`;
502
- return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${bg}${groupsSvg}${edgesSvg}${nodesSvg}${legend}</svg>`;
503
- }
504
- // ── Dependency Matrix (DSM) ──────────────────────────────────────────
505
- // Standard software architecture visualization. Rows and columns are modules,
506
- // cells show import relationships. Clusters on the diagonal = well-structured packages.
507
- export function generateDSM(details) {
508
- const graph = details.graph;
509
- if (!graph || Object.keys(graph).length === 0)
510
- return "";
511
- const entries = Object.entries(graph);
512
- if (entries.length > 40)
513
- return `<div style="color:#6b7280;font-size:0.75rem">${entries.length} modules — too many for matrix view.</div>`;
514
- if (entries.length < 3)
515
- return "";
516
- // Sort by directory then name for clustering
517
- entries.sort((a, b) => `${a[1].dir}/${a[0]}`.localeCompare(`${b[1].dir}/${b[0]}`));
518
- const paths = entries.map(([p]) => p);
519
- const idx = new Map(paths.map((p, i) => [p, i]));
520
- const n = paths.length;
521
- const cell = 14;
522
- const labelW = 110;
523
- const W = labelW + n * cell + 10;
524
- const H = labelW + n * cell + 10;
525
- // Build adjacency
526
- const matrix = Array.from({ length: n }, () => Array(n).fill(false));
527
- for (const [path, info] of entries) {
528
- const from = idx.get(path);
529
- for (const imp of info.imports) {
530
- const to = idx.get(imp);
531
- if (to !== undefined)
532
- matrix[from][to] = true;
533
- }
534
- }
535
- let svg = "";
536
- const ox = labelW, oy = labelW;
537
- // Grid
538
- for (let i = 0; i <= n; i++) {
539
- svg += `<line x1="${ox}" y1="${oy + i * cell}" x2="${ox + n * cell}" y2="${oy + i * cell}" stroke="#1e1e24" stroke-width="0.5"/>`;
540
- svg += `<line x1="${ox + i * cell}" y1="${oy}" x2="${ox + i * cell}" y2="${oy + n * cell}" stroke="#1e1e24" stroke-width="0.5"/>`;
541
- }
542
- // Cells — row imports col
543
- for (let r = 0; r < n; r++) {
544
- for (let c = 0; c < n; c++) {
545
- if (r === c) {
546
- // Diagonal — highlight
547
- svg += `<rect x="${ox + c * cell}" y="${oy + r * cell}" width="${cell}" height="${cell}" fill="#818cf808"/>`;
548
- }
549
- else if (matrix[r][c]) {
550
- const mutual = matrix[c][r]; // circular?
551
- const color = mutual ? "#d97706" : "#6d78d0";
552
- svg += `<rect x="${ox + c * cell + 2}" y="${oy + r * cell + 2}" width="${cell - 4}" height="${cell - 4}" rx="2" fill="${color}" opacity="0.7"/>`;
553
- }
554
- }
555
- }
556
- // Directory bands (background stripe per dir group)
557
- let prevDir = "";
558
- let bandStart = 0;
559
- const dirColors = ["#ffffff04", "#ffffff08"];
560
- let dirIdx2 = 0;
561
- for (let i = 0; i <= n; i++) {
562
- const dir = i < n ? entries[i][1].dir : "__end__";
563
- if (dir !== prevDir && i > 0) {
564
- const fill = dirColors[dirIdx2 % 2];
565
- svg += `<rect x="${ox}" y="${oy + bandStart * cell}" width="${n * cell}" height="${(i - bandStart) * cell}" fill="${fill}"/>`;
566
- svg += `<rect x="${ox + bandStart * cell}" y="${oy}" width="${(i - bandStart) * cell}" height="${n * cell}" fill="${fill}"/>`;
567
- dirIdx2++;
568
- bandStart = i;
569
- }
570
- prevDir = dir;
571
- }
572
- // Row labels (left) and column labels (top, rotated)
573
- for (let i = 0; i < n; i++) {
574
- const name = basename(paths[i], extname(paths[i]));
575
- svg += `<text x="${ox - 4}" y="${oy + i * cell + cell / 2 + 3}" text-anchor="end" fill="#9ca3af" font-size="7">${name}</text>`;
576
- svg += `<text x="${ox + i * cell + cell / 2}" y="${oy - 4}" text-anchor="start" fill="#9ca3af" font-size="7" transform="rotate(-60 ${ox + i * cell + cell / 2} ${oy - 4})">${name}</text>`;
577
- }
578
- // Legend
579
- svg += `<g transform="translate(${ox}, ${oy + n * cell + 16})" font-size="7" fill="#6b7280">`;
580
- svg += `<rect x="0" y="-4" width="8" height="8" rx="2" fill="#6d78d0" opacity="0.7"/><text x="12" y="3">imports</text>`;
581
- svg += `<rect x="60" y="-4" width="8" height="8" rx="2" fill="#d97706" opacity="0.7"/><text x="72" y="3">mutual (cycle)</text>`;
582
- svg += `</g>`;
583
- return `<svg viewBox="0 0 ${W} ${H + 30}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${svg}</svg>`;
584
- }
585
- // ── Package Nesting Diagram ──────────────────────────────────────────
586
- // UML-style Package diagram: directories as nested boxes, files as items inside.
587
- export function generatePackageDiagram(details) {
588
- const graph = details.graph;
589
- if (!graph || Object.keys(graph).length === 0)
590
- return "";
591
- const entries = Object.entries(graph);
592
- if (entries.length > 50)
593
- return `<div style="color:#6b7280;font-size:0.75rem">${entries.length} modules — too many for package view.</div>`;
594
- // Group by directory
595
- const dirs = new Map();
596
- for (const [path, info] of entries) {
597
- const dir = info.dir || ".";
598
- const arr = dirs.get(dir) || [];
599
- arr.push({ path, fanIn: info.importedBy.length, fanOut: info.imports.length });
600
- dirs.set(dir, arr);
601
- }
602
- const dirEntries = [...dirs.entries()].sort((a, b) => a[0].localeCompare(b[0]));
603
- const boxW = 180;
604
- const fileH = 18;
605
- const headerH = 24;
606
- const gap = 16;
607
- const cols = Math.min(dirEntries.length, 4);
608
- const colW = boxW + gap;
609
- let svg = "";
610
- let maxH = 0;
611
- for (let i = 0; i < dirEntries.length; i++) {
612
- const [dir, files] = dirEntries[i];
613
- const col = i % cols;
614
- const row = Math.floor(i / cols);
615
- const prevRowsH = row * 300; // rough estimate, will adjust
616
- const x = gap + col * colW;
617
- let y = gap + prevRowsH;
618
- const boxH = headerH + files.length * fileH + 8;
619
- // Package box
620
- svg += `<rect x="${x}" y="${y}" width="${boxW}" height="${boxH}" rx="6" fill="#ffffff04" stroke="#ffffff10"/>`;
621
- // Package tab (UML-style)
622
- svg += `<rect x="${x}" y="${y}" width="${Math.min(boxW * 0.6, 100)}" height="${headerH}" rx="4" fill="#ffffff08" stroke="#ffffff10"/>`;
623
- const label = dir === "." ? "root" : dir.replace(/^src\//, "");
624
- svg += `<text x="${x + 8}" y="${y + 16}" fill="#9ca3af" font-size="10" font-weight="700">${label}/</text>`;
625
- svg += `<text x="${x + boxW - 8}" y="${y + 16}" text-anchor="end" fill="#4b5563" font-size="8">${files.length}</text>`;
626
- y += headerH + 4;
627
- // Files inside package
628
- for (const f of files) {
629
- const name = basename(f.path, extname(f.path));
630
- const health = f.fanIn > 5 ? "#d97706" : f.fanOut > 8 ? "#ca8a04" : "#6d78d0";
631
- svg += `<circle cx="${x + 12}" cy="${y + 7}" r="3" fill="${health}"/>`;
632
- svg += `<text x="${x + 20}" y="${y + 10}" fill="#9ca3af" font-size="8">${name}</text>`;
633
- svg += `<text x="${x + boxW - 8}" y="${y + 10}" text-anchor="end" fill="#4b5563" font-size="7">${f.fanIn}\u2190 ${f.fanOut}\u2192</text>`;
634
- y += fileH;
635
- }
636
- maxH = Math.max(maxH, y + 8);
637
- }
638
- const W = gap + cols * colW;
639
- const H = maxH + gap;
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
- }
642
- // ── Sequence Diagram ─────────────────────────────────────────────────
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
649
- export function generateSequenceDiagram(details) {
650
- const graph = details.graph;
651
- if (!graph || Object.keys(graph).length < 3)
652
- return "";
653
- // Find the entry point
654
- const entries = Object.entries(graph);
655
- const entryPoint = entries.find(([path, info]) => {
656
- const name = basename(path, extname(path));
657
- return info.importedBy.length === 0 && ["index", "main", "cli", "App", "app", "server"].includes(name);
658
- });
659
- if (!entryPoint)
660
- return "";
661
- // Determine architectural roles from directory structure
662
- const roles = [];
663
- const dirs = new Map();
664
- for (const [, info] of entries) {
665
- const dir = info.dir || ".";
666
- dirs.set(dir, (dirs.get(dir) || 0) + 1);
667
- }
668
- // Build role list from actual structure
669
- const entryName = basename(entryPoint[0], extname(entryPoint[0]));
670
- roles.push({ name: entryName, dir: "entry", modules: 1 });
671
- // Add directories as participants (sorted by dependency order)
672
- const dirArr = [...dirs.entries()]
673
- .filter(([d]) => d !== (entryPoint[1].dir || "."))
674
- .sort((a, b) => {
675
- // Sort by average fan-in (more depended-upon = earlier in flow)
676
- const aFanIn = entries.filter(([, i]) => i.dir === a[0]).reduce((s, [, i]) => s + i.importedBy.length, 0) / a[1];
677
- const bFanIn = entries.filter(([, i]) => i.dir === b[0]).reduce((s, [, i]) => s + i.importedBy.length, 0) / b[1];
678
- return bFanIn - aFanIn; // most depended-on first
679
- });
680
- for (const [dir, count] of dirArr) {
681
- const label = dir.replace("src/", "").replace("lib/", "") || "core";
682
- roles.push({ name: label, dir, modules: count });
683
- }
684
- if (roles.length < 3)
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)
717
- return "";
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;
724
- let svg = "";
725
- // Participant boxes
726
- for (let i = 0; i < displayRoles.length; i++) {
727
- const x = 20 + i * 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;
743
- const y = headerH + i * messageH;
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>`;
749
- }
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>`;
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>`;
752
- }
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);
806
- for (const imp of info.imports) {
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
813
- }
814
- }
815
- }
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>`;
852
- }
853
- // ── Container Diagram ────────────────────────────────────────────────
854
- // Auto-detects high-level system containers from config files:
855
- // frontend, backend/API, database, worker, static site, etc.
856
- export function generateContainerDiagram(cwd) {
857
- const has = (f) => existsSync(join(cwd, f));
858
- const containers = [];
859
- // Detect containers from config files
860
- if (has("src/App.tsx") || has("src/App.vue") || has("src/App.svelte") || has("web/src/App.tsx")) {
861
- const tech = has("src/App.tsx") ? "React" : has("src/App.vue") ? "Vue" : "Svelte";
862
- containers.push({ name: "Frontend", type: "webapp", tech });
863
- }
864
- if (has("wrangler.toml") || has("wrangler.json")) {
865
- containers.push({ name: "Worker", type: "worker", tech: "Cloudflare Workers" });
866
- }
867
- if (has("Dockerfile") || has("server.ts") || has("src/server.ts") || has("src/index.ts")) {
868
- if (!containers.some((c) => c.name === "Frontend")) {
869
- containers.push({ name: "API Server", type: "api", tech: "Node.js" });
870
- }
871
- }
872
- if (has("prisma/schema.prisma") || has("drizzle.config.ts")) {
873
- const tech = has("prisma/schema.prisma") ? "Prisma" : "Drizzle";
874
- containers.push({ name: "Database", type: "db", tech });
875
- }
876
- if (has("firebase.json") || has(".firebaserc")) {
877
- containers.push({ name: "Firebase", type: "baas", tech: "Firebase" });
878
- }
879
- if (has("supabase/config.toml") || has(".supabase")) {
880
- containers.push({ name: "Supabase", type: "baas", tech: "Supabase" });
881
- }
882
- if (has("pubspec.yaml")) {
883
- containers.push({ name: "Mobile App", type: "mobile", tech: "Flutter" });
884
- }
885
- if (has("package.json") && !containers.length) {
886
- containers.push({ name: "Application", type: "app", tech: "Node.js" });
887
- }
888
- if (containers.length < 2)
889
- return ""; // Only interesting with 2+ containers
890
- // Layout: horizontal boxes with connecting lines
891
- const boxW = 140;
892
- const boxH = 60;
893
- const gap = 30;
894
- const W = containers.length * (boxW + gap) + gap;
895
- const H = 120;
896
- const typeColors = {
897
- webapp: "#6d78d0",
898
- worker: "#d97706",
899
- api: "#22c55e",
900
- db: "#8b5cf6",
901
- baas: "#ec4899",
902
- mobile: "#06b6d4",
903
- app: "#6d78d0",
904
- };
905
- let svg = "";
906
- for (let i = 0; i < containers.length; i++) {
907
- const c = containers[i];
908
- const x = gap + i * (boxW + gap);
909
- const y = (H - boxH) / 2;
910
- const color = typeColors[c.type] || "#6d78d0";
911
- // Box
912
- svg += `<rect x="${x}" y="${y}" width="${boxW}" height="${boxH}" rx="8" fill="${color}15" stroke="${color}50"/>`;
913
- // Name
914
- svg += `<text x="${x + boxW / 2}" y="${y + 24}" text-anchor="middle" fill="#e5e5e5" font-size="10" font-weight="700">${c.name}</text>`;
915
- // Tech
916
- svg += `<text x="${x + boxW / 2}" y="${y + 40}" text-anchor="middle" fill="#6b7280" font-size="8">[${c.tech}]</text>`;
917
- // Connection to next
918
- if (i < containers.length - 1) {
919
- const ax = x + boxW;
920
- const bx = ax + gap;
921
- const ay = H / 2;
922
- svg += `<line x1="${ax}" y1="${ay}" x2="${bx}" y2="${ay}" stroke="#ffffff20" stroke-width="1.5" marker-end="url(#cont-arrow)"/>`;
923
- }
924
- }
925
- const defs = `<defs><marker id="cont-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="#ffffff40"/></marker></defs>`;
926
- return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:${W}px">${defs}${svg}</svg>`;
927
- }
359
+ // Diagram generators extracted to diagrams.ts for file size
360
+ export { generateArchSVG, generateDSM, generateLayerDiagram, generatePackageDiagram, generateSequenceDiagram } from "./diagrams.js";
@@ -0,0 +1,7 @@
1
+ /** Architecture diagram generators — SVG visualizations. */
2
+ export declare function generateArchSVG(details: Record<string, unknown>): string;
3
+ export declare function generateDSM(details: Record<string, unknown>): string;
4
+ export declare function generatePackageDiagram(details: Record<string, unknown>): string;
5
+ export declare function generateSequenceDiagram(details: Record<string, unknown>): string;
6
+ export declare function generateLayerDiagram(details: Record<string, unknown>): string;
7
+ export declare function generateContainerDiagram(cwd: string): string;