depwire-cli 0.9.23 → 0.9.25
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/{chunk-OBWFFD3M.js → chunk-ORGAO3HT.js} +20 -0
- package/dist/index.js +273 -17
- package/dist/mcpb-entry.js +1 -1
- package/dist/viz/public/arc.js +185 -7
- package/package.json +1 -1
|
@@ -445,6 +445,16 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
projectRoot = subdirectory ? join3(cloneDir, subdirectory) : cloneDir;
|
|
448
|
+
if (subdirectory) {
|
|
449
|
+
const resolvedRoot = resolve(cloneDir);
|
|
450
|
+
const resolvedProject = resolve(projectRoot);
|
|
451
|
+
if (!resolvedProject.startsWith(resolvedRoot + "/") && resolvedProject !== resolvedRoot) {
|
|
452
|
+
return {
|
|
453
|
+
error: "Access denied",
|
|
454
|
+
message: "Subdirectory must be within the project root"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
448
458
|
} else {
|
|
449
459
|
const validation2 = validateProjectPath(source);
|
|
450
460
|
if (!validation2.valid) {
|
|
@@ -460,6 +470,16 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
460
470
|
};
|
|
461
471
|
}
|
|
462
472
|
projectRoot = subdirectory ? join3(source, subdirectory) : source;
|
|
473
|
+
if (subdirectory) {
|
|
474
|
+
const resolvedRoot = resolve(source);
|
|
475
|
+
const resolvedProject = resolve(projectRoot);
|
|
476
|
+
if (!resolvedProject.startsWith(resolvedRoot + "/") && resolvedProject !== resolvedRoot) {
|
|
477
|
+
return {
|
|
478
|
+
error: "Access denied",
|
|
479
|
+
message: "Subdirectory must be within the project root"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
463
483
|
projectName = basename2(projectRoot);
|
|
464
484
|
}
|
|
465
485
|
const validation = validateProjectPath(projectRoot);
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
stashChanges,
|
|
18
18
|
updateFileInGraph,
|
|
19
19
|
watchProject
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-ORGAO3HT.js";
|
|
21
21
|
import {
|
|
22
22
|
SimulationEngine,
|
|
23
23
|
analyzeDeadCode,
|
|
@@ -34,9 +34,9 @@ import {
|
|
|
34
34
|
|
|
35
35
|
// src/index.ts
|
|
36
36
|
import { Command } from "commander";
|
|
37
|
-
import { resolve as resolve2, dirname as
|
|
37
|
+
import { resolve as resolve2, dirname as dirname3, join as join4 } from "path";
|
|
38
38
|
import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
|
|
39
|
-
import { fileURLToPath as
|
|
39
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
40
40
|
|
|
41
41
|
// src/graph/serializer.ts
|
|
42
42
|
import { DirectedGraph } from "graphology";
|
|
@@ -502,13 +502,269 @@ async function trackCommand(command, version = "unknown") {
|
|
|
502
502
|
// src/commands/whatif.ts
|
|
503
503
|
import { resolve } from "path";
|
|
504
504
|
import chalk from "chalk";
|
|
505
|
+
|
|
506
|
+
// src/viz/whatif-server.ts
|
|
507
|
+
import express2 from "express";
|
|
508
|
+
import open2 from "open";
|
|
509
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
510
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
511
|
+
|
|
512
|
+
// src/viz/generate-whatif-html.ts
|
|
513
|
+
function generateWhatIfHtml(currentVizData, simulatedVizData, simulationResult, operation, target) {
|
|
514
|
+
const { healthDelta, diff } = simulationResult;
|
|
515
|
+
const deltaSign = healthDelta.delta >= 0 ? "+" : "";
|
|
516
|
+
const deltaLabel = healthDelta.delta === 0 ? "unchanged" : healthDelta.improved ? `${deltaSign}${healthDelta.delta} \u2713 improved` : `${healthDelta.delta} \u2717 degraded`;
|
|
517
|
+
const deltaColor = healthDelta.delta === 0 ? "#fbbf24" : healthDelta.improved ? "#4ade80" : "#f87171";
|
|
518
|
+
const opBadge = operation !== "none" ? `<span style="background:${deltaColor};color:#000;padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;margin-left:12px;">${operation} ${target}</span>` : "";
|
|
519
|
+
const brokenImportsHtml = diff.brokenImports.length > 0 ? `<details style="margin-top:16px;background:#16213e;border:1px solid #2a2a4a;border-radius:8px;padding:12px 16px;">
|
|
520
|
+
<summary style="cursor:pointer;color:#f87171;font-weight:600;font-size:14px;">Broken Imports (${diff.brokenImports.length})</summary>
|
|
521
|
+
<ul style="margin:8px 0 0 16px;padding:0;list-style:none;">
|
|
522
|
+
${diff.brokenImports.map((bi) => `<li style="color:#e0e0e0;font-size:13px;padding:4px 0;font-family:monospace;">${bi.file} \u2192 <span style="color:#f87171;">${bi.importedSymbol}</span></li>`).join("")}
|
|
523
|
+
</ul>
|
|
524
|
+
</details>` : "";
|
|
525
|
+
const currentDataJson = JSON.stringify(currentVizData);
|
|
526
|
+
const simulatedDataJson = JSON.stringify(simulatedVizData);
|
|
527
|
+
return `<!DOCTYPE html>
|
|
528
|
+
<html lang="en">
|
|
529
|
+
<head>
|
|
530
|
+
<meta charset="UTF-8">
|
|
531
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
532
|
+
<title>Depwire \u2014 What If Simulation</title>
|
|
533
|
+
<link rel="stylesheet" href="/style.css">
|
|
534
|
+
<style>
|
|
535
|
+
body { overflow: auto; height: auto; }
|
|
536
|
+
.whatif-header {
|
|
537
|
+
background: #16213e;
|
|
538
|
+
border-bottom: 1px solid #2a2a4a;
|
|
539
|
+
padding: 16px 24px;
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
gap: 16px;
|
|
543
|
+
}
|
|
544
|
+
.whatif-header h1 {
|
|
545
|
+
margin: 0;
|
|
546
|
+
font-size: 20px;
|
|
547
|
+
font-weight: 600;
|
|
548
|
+
background: linear-gradient(135deg, #4a9eff, #7c3aed);
|
|
549
|
+
-webkit-background-clip: text;
|
|
550
|
+
-webkit-text-fill-color: transparent;
|
|
551
|
+
background-clip: text;
|
|
552
|
+
}
|
|
553
|
+
.health-banner {
|
|
554
|
+
background: #0f1729;
|
|
555
|
+
border: 1px solid #2a2a4a;
|
|
556
|
+
border-radius: 8px;
|
|
557
|
+
padding: 16px 24px;
|
|
558
|
+
margin: 16px 24px;
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
gap: 32px;
|
|
562
|
+
flex-wrap: wrap;
|
|
563
|
+
}
|
|
564
|
+
.health-score {
|
|
565
|
+
font-size: 22px;
|
|
566
|
+
font-weight: 700;
|
|
567
|
+
}
|
|
568
|
+
.health-stat {
|
|
569
|
+
font-size: 14px;
|
|
570
|
+
color: #a0a0a0;
|
|
571
|
+
}
|
|
572
|
+
.health-stat strong {
|
|
573
|
+
color: #e0e0e0;
|
|
574
|
+
font-size: 18px;
|
|
575
|
+
}
|
|
576
|
+
.panels {
|
|
577
|
+
display: flex;
|
|
578
|
+
flex-direction: row;
|
|
579
|
+
gap: 0;
|
|
580
|
+
width: 100%;
|
|
581
|
+
height: calc(100vh - 180px);
|
|
582
|
+
min-height: 400px;
|
|
583
|
+
}
|
|
584
|
+
.panel {
|
|
585
|
+
flex: 1;
|
|
586
|
+
min-width: 0;
|
|
587
|
+
display: flex;
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
border-right: 1px solid #2a2a4a;
|
|
590
|
+
overflow: hidden;
|
|
591
|
+
position: relative;
|
|
592
|
+
}
|
|
593
|
+
.panel:last-child { border-right: none; }
|
|
594
|
+
.panel-label {
|
|
595
|
+
background: #16213e;
|
|
596
|
+
padding: 8px 16px;
|
|
597
|
+
font-size: 13px;
|
|
598
|
+
font-weight: 600;
|
|
599
|
+
color: #a0a0a0;
|
|
600
|
+
border-bottom: 1px solid #2a2a4a;
|
|
601
|
+
display: flex;
|
|
602
|
+
justify-content: space-between;
|
|
603
|
+
flex-shrink: 0;
|
|
604
|
+
}
|
|
605
|
+
.panel-diagram {
|
|
606
|
+
flex: 1;
|
|
607
|
+
overflow: hidden;
|
|
608
|
+
position: relative;
|
|
609
|
+
}
|
|
610
|
+
.panel-diagram svg {
|
|
611
|
+
display: block;
|
|
612
|
+
width: 100%;
|
|
613
|
+
height: 100%;
|
|
614
|
+
}
|
|
615
|
+
.broken-section {
|
|
616
|
+
padding: 0 24px 24px;
|
|
617
|
+
}
|
|
618
|
+
</style>
|
|
619
|
+
</head>
|
|
620
|
+
<body>
|
|
621
|
+
<div class="whatif-header">
|
|
622
|
+
<h1>depwire \u2014 What If Simulation</h1>
|
|
623
|
+
${opBadge}
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<div class="health-banner">
|
|
627
|
+
<div class="health-score" style="color:${deltaColor}">
|
|
628
|
+
Health Score: ${healthDelta.before} \u2192 ${healthDelta.after}
|
|
629
|
+
<span style="font-size:16px;margin-left:8px;">(${deltaLabel})</span>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="health-stat"><strong>${diff.affectedNodes.length}</strong> Affected Nodes</div>
|
|
632
|
+
<div class="health-stat"><strong>${diff.brokenImports.length}</strong> Broken Imports</div>
|
|
633
|
+
<div class="health-stat"><strong>${diff.removedEdges.length}</strong> Removed Edges</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div class="panels">
|
|
637
|
+
<div class="panel">
|
|
638
|
+
<div class="panel-label">
|
|
639
|
+
<span>Current</span>
|
|
640
|
+
<span>${currentVizData.stats.totalFiles} files</span>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="panel-diagram" id="arc-diagram-current">
|
|
643
|
+
<svg id="svg-current"></svg>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="tooltip" id="tooltip-current"></div>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="panel">
|
|
648
|
+
<div class="panel-label">
|
|
649
|
+
<span>After ${operation !== "none" ? operation.toUpperCase() : "\u2014"}</span>
|
|
650
|
+
<span>${simulatedVizData.stats.totalFiles} files</span>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="panel-diagram" id="arc-diagram-simulated">
|
|
653
|
+
<svg id="svg-simulated"></svg>
|
|
654
|
+
</div>
|
|
655
|
+
<div class="tooltip" id="tooltip-simulated"></div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div class="broken-section">
|
|
660
|
+
${brokenImportsHtml}
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<script>window.__depwireWhatIf = true;</script>
|
|
664
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
|
665
|
+
<script src="/arc.js"></script>
|
|
666
|
+
<script>
|
|
667
|
+
const currentData = ${currentDataJson};
|
|
668
|
+
const simulatedData = ${simulatedDataJson};
|
|
669
|
+
|
|
670
|
+
const left = window.createArcDiagram('arc-diagram-current', 'svg-current', 'tooltip-current', currentData);
|
|
671
|
+
const right = window.createArcDiagram('arc-diagram-simulated', 'svg-simulated', 'tooltip-simulated', simulatedData);
|
|
672
|
+
|
|
673
|
+
left.render();
|
|
674
|
+
right.render();
|
|
675
|
+
|
|
676
|
+
window.addEventListener('resize', () => {
|
|
677
|
+
left.render();
|
|
678
|
+
right.render();
|
|
679
|
+
});
|
|
680
|
+
</script>
|
|
681
|
+
</body>
|
|
682
|
+
</html>`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/viz/whatif-server.ts
|
|
686
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
687
|
+
var __dirname2 = dirname2(__filename2);
|
|
688
|
+
async function findAvailablePort2(startPort, maxAttempts = 10) {
|
|
689
|
+
const net = await import("net");
|
|
690
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
691
|
+
const testPort = startPort + attempt;
|
|
692
|
+
const isAvailable = await new Promise((resolve3) => {
|
|
693
|
+
const server = net.createServer();
|
|
694
|
+
server.once("error", () => {
|
|
695
|
+
resolve3(false);
|
|
696
|
+
});
|
|
697
|
+
server.once("listening", () => {
|
|
698
|
+
server.close();
|
|
699
|
+
resolve3(true);
|
|
700
|
+
});
|
|
701
|
+
server.listen(testPort, "127.0.0.1");
|
|
702
|
+
});
|
|
703
|
+
if (isAvailable) {
|
|
704
|
+
if (attempt > 0) {
|
|
705
|
+
console.error(`Port ${startPort} in use, using port ${testPort} instead`);
|
|
706
|
+
}
|
|
707
|
+
return testPort;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
711
|
+
}
|
|
712
|
+
async function serveWhatIfViz(currentVizData, simulatedVizData, simulationResult, operation, target) {
|
|
713
|
+
const availablePort = await findAvailablePort2(3335);
|
|
714
|
+
const app = express2();
|
|
715
|
+
app.get("/", (_req, res) => {
|
|
716
|
+
const html = generateWhatIfHtml(currentVizData, simulatedVizData, simulationResult, operation, target);
|
|
717
|
+
res.type("html").send(html);
|
|
718
|
+
});
|
|
719
|
+
app.get("/favicon.ico", (_req, res) => {
|
|
720
|
+
res.sendFile(join3(__dirname2, "..", "..", "icon.png"));
|
|
721
|
+
});
|
|
722
|
+
const publicDir = join3(__dirname2, "viz", "public");
|
|
723
|
+
app.use(express2.static(publicDir));
|
|
724
|
+
app.get("/api/graph", (_req, res) => {
|
|
725
|
+
res.json(currentVizData);
|
|
726
|
+
});
|
|
727
|
+
app.get("/api/current", (_req, res) => {
|
|
728
|
+
res.json(currentVizData);
|
|
729
|
+
});
|
|
730
|
+
app.get("/api/simulated", (_req, res) => {
|
|
731
|
+
res.json(simulatedVizData);
|
|
732
|
+
});
|
|
733
|
+
app.get("/api/result", (_req, res) => {
|
|
734
|
+
res.json(simulationResult);
|
|
735
|
+
});
|
|
736
|
+
const server = app.listen(availablePort, "127.0.0.1", () => {
|
|
737
|
+
const url = `http://127.0.0.1:${availablePort}`;
|
|
738
|
+
console.error(`
|
|
739
|
+
Opening What If UI at ${url}`);
|
|
740
|
+
console.error("Press Ctrl+C to stop\n");
|
|
741
|
+
open2(url);
|
|
742
|
+
});
|
|
743
|
+
process.on("SIGINT", () => {
|
|
744
|
+
console.error("\nShutting down What If server...");
|
|
745
|
+
server.close(() => {
|
|
746
|
+
process.exit(0);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/commands/whatif.ts
|
|
505
752
|
async function whatif(dir, options) {
|
|
506
753
|
if (!options.simulate) {
|
|
507
|
-
|
|
508
|
-
console.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
console.
|
|
754
|
+
const projectRoot2 = dir === "." ? findProjectRoot() : resolve(dir);
|
|
755
|
+
console.error(`Parsing project: ${projectRoot2}`);
|
|
756
|
+
const parsedFiles2 = await parseProject(projectRoot2);
|
|
757
|
+
const graph2 = buildGraph(parsedFiles2);
|
|
758
|
+
console.error(`Built graph: ${graph2.order} symbols, ${graph2.size} edges`);
|
|
759
|
+
const vizData = prepareVizData(graph2, projectRoot2);
|
|
760
|
+
const emptyResult = {
|
|
761
|
+
action: { type: "delete", target: "" },
|
|
762
|
+
originalGraph: { nodeCount: graph2.order, edgeCount: graph2.size, healthScore: 0 },
|
|
763
|
+
simulatedGraph: { nodeCount: graph2.order, edgeCount: graph2.size, healthScore: 0 },
|
|
764
|
+
diff: { addedEdges: [], removedEdges: [], affectedNodes: [], brokenImports: [], circularDepsIntroduced: [], circularDepsResolved: [] },
|
|
765
|
+
healthDelta: { before: 0, after: 0, delta: 0, improved: false, dimensionChanges: [] }
|
|
766
|
+
};
|
|
767
|
+
await serveWhatIfViz(vizData, vizData, emptyResult, "none", "");
|
|
512
768
|
return;
|
|
513
769
|
}
|
|
514
770
|
const validActions = ["move", "delete", "rename", "split", "merge"];
|
|
@@ -522,11 +778,11 @@ async function whatif(dir, options) {
|
|
|
522
778
|
}
|
|
523
779
|
const action = buildAction(options);
|
|
524
780
|
const projectRoot = dir === "." ? findProjectRoot() : resolve(dir);
|
|
525
|
-
console.
|
|
781
|
+
console.error(`Parsing project: ${projectRoot}`);
|
|
526
782
|
const parsedFiles = await parseProject(projectRoot);
|
|
527
783
|
const graph = buildGraph(parsedFiles);
|
|
528
|
-
console.
|
|
529
|
-
console.
|
|
784
|
+
console.error(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
|
|
785
|
+
console.error("");
|
|
530
786
|
const engine = new SimulationEngine(graph);
|
|
531
787
|
try {
|
|
532
788
|
const result = engine.simulate(action);
|
|
@@ -632,9 +888,9 @@ function formatAction(action) {
|
|
|
632
888
|
}
|
|
633
889
|
|
|
634
890
|
// src/index.ts
|
|
635
|
-
var
|
|
636
|
-
var
|
|
637
|
-
var packageJsonPath =
|
|
891
|
+
var __filename3 = fileURLToPath3(import.meta.url);
|
|
892
|
+
var __dirname3 = dirname3(__filename3);
|
|
893
|
+
var packageJsonPath = join4(__dirname3, "../package.json");
|
|
638
894
|
var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
639
895
|
var program = new Command();
|
|
640
896
|
program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
|
|
@@ -777,7 +1033,7 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
|
|
|
777
1033
|
} else {
|
|
778
1034
|
const detectedRoot = findProjectRoot();
|
|
779
1035
|
const cwd = process.cwd();
|
|
780
|
-
if (detectedRoot !== cwd || existsSync(
|
|
1036
|
+
if (detectedRoot !== cwd || existsSync(join4(cwd, "package.json")) || existsSync(join4(cwd, "tsconfig.json")) || existsSync(join4(cwd, "go.mod")) || existsSync(join4(cwd, "pyproject.toml")) || existsSync(join4(cwd, "setup.py")) || existsSync(join4(cwd, ".git"))) {
|
|
781
1037
|
projectRootToConnect = detectedRoot;
|
|
782
1038
|
}
|
|
783
1039
|
}
|
|
@@ -835,7 +1091,7 @@ program.command("docs").description("Generate comprehensive codebase documentati
|
|
|
835
1091
|
const startTime = Date.now();
|
|
836
1092
|
try {
|
|
837
1093
|
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
838
|
-
const outputDir = options.output ? resolve2(options.output) :
|
|
1094
|
+
const outputDir = options.output ? resolve2(options.output) : join4(projectRoot, ".depwire");
|
|
839
1095
|
const includeList = options.include.split(",").map((s) => s.trim());
|
|
840
1096
|
const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
|
|
841
1097
|
if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
|
|
@@ -906,7 +1162,7 @@ async function promptGitignore() {
|
|
|
906
1162
|
});
|
|
907
1163
|
}
|
|
908
1164
|
function addToGitignore(projectRoot, pattern) {
|
|
909
|
-
const gitignorePath =
|
|
1165
|
+
const gitignorePath = join4(projectRoot, ".gitignore");
|
|
910
1166
|
try {
|
|
911
1167
|
let content = "";
|
|
912
1168
|
if (existsSyncNode(gitignorePath)) {
|
package/dist/mcpb-entry.js
CHANGED
package/dist/viz/public/arc.js
CHANGED
|
@@ -1,4 +1,176 @@
|
|
|
1
1
|
// Arc Diagram Renderer using D3.js
|
|
2
|
+
|
|
3
|
+
// ── Factory function for creating isolated arc diagram instances ──
|
|
4
|
+
window.createArcDiagram = function(containerId, svgId, tooltipId, data) {
|
|
5
|
+
let _data = data;
|
|
6
|
+
let _svg = null;
|
|
7
|
+
let _g = null;
|
|
8
|
+
let _filePositions = new Map();
|
|
9
|
+
let _selectedFile = null;
|
|
10
|
+
let _selectedArc = null;
|
|
11
|
+
|
|
12
|
+
function _showTooltip(event, content) {
|
|
13
|
+
const el = document.getElementById(tooltipId);
|
|
14
|
+
if (!el) return;
|
|
15
|
+
el.innerHTML = content;
|
|
16
|
+
el.style.left = (event.pageX + 10) + 'px';
|
|
17
|
+
el.style.top = (event.pageY + 10) + 'px';
|
|
18
|
+
el.classList.add('show');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _hideTooltip() {
|
|
22
|
+
const el = document.getElementById(tooltipId);
|
|
23
|
+
if (!el) return;
|
|
24
|
+
el.classList.remove('show');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function _clearSelection() {
|
|
28
|
+
_selectedFile = null;
|
|
29
|
+
_selectedArc = null;
|
|
30
|
+
const ctr = d3.select('#' + containerId);
|
|
31
|
+
ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
|
|
32
|
+
ctr.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function render() {
|
|
36
|
+
const container = document.getElementById(containerId);
|
|
37
|
+
if (!container || !_data) return;
|
|
38
|
+
|
|
39
|
+
const width = container.clientWidth;
|
|
40
|
+
const height = container.clientHeight;
|
|
41
|
+
_filePositions.clear();
|
|
42
|
+
_selectedFile = null;
|
|
43
|
+
_selectedArc = null;
|
|
44
|
+
|
|
45
|
+
const svgEl = d3.select('#' + svgId);
|
|
46
|
+
svgEl.selectAll('*').remove();
|
|
47
|
+
_svg = svgEl.attr('width', width).attr('height', height);
|
|
48
|
+
_g = _svg.append('g');
|
|
49
|
+
|
|
50
|
+
const zoom = d3.zoom().scaleExtent([0.5, 4]).on('zoom', (event) => {
|
|
51
|
+
_g.attr('transform', event.transform);
|
|
52
|
+
});
|
|
53
|
+
_svg.call(zoom);
|
|
54
|
+
|
|
55
|
+
const margin = { top: 60, right: 40, bottom: 120, left: 40 };
|
|
56
|
+
const plotWidth = width - margin.left - margin.right;
|
|
57
|
+
const plotHeight = height - margin.top - margin.bottom;
|
|
58
|
+
const baseline = margin.top + plotHeight;
|
|
59
|
+
|
|
60
|
+
const totalSymbols = d3.sum(_data.files, d => d.symbolCount);
|
|
61
|
+
const minBarWidth = 4;
|
|
62
|
+
const gap = 2;
|
|
63
|
+
let x = margin.left;
|
|
64
|
+
|
|
65
|
+
_data.files.forEach(file => {
|
|
66
|
+
const barWidth = Math.max(minBarWidth, (file.symbolCount / totalSymbols) * plotWidth * 0.8);
|
|
67
|
+
_filePositions.set(file.path, { x: x + barWidth / 2, width: barWidth, file: file });
|
|
68
|
+
x += barWidth + gap;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const directories = [...new Set(_data.files.map(f => f.directory))];
|
|
72
|
+
const colorScale = d3.scaleOrdinal().domain(directories)
|
|
73
|
+
.range(['#4a9eff', '#7c3aed', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']);
|
|
74
|
+
|
|
75
|
+
const maxDistance = d3.max(_data.arcs, arc => {
|
|
76
|
+
const s = _filePositions.get(arc.sourceFile);
|
|
77
|
+
const t = _filePositions.get(arc.targetFile);
|
|
78
|
+
return (s && t) ? Math.abs(t.x - s.x) : 0;
|
|
79
|
+
}) || 1;
|
|
80
|
+
|
|
81
|
+
const ctr = d3.select('#' + containerId);
|
|
82
|
+
|
|
83
|
+
_g.selectAll('.arc').data(_data.arcs).enter().append('path')
|
|
84
|
+
.attr('class', 'arc')
|
|
85
|
+
.attr('d', d => {
|
|
86
|
+
const s = _filePositions.get(d.sourceFile);
|
|
87
|
+
const t = _filePositions.get(d.targetFile);
|
|
88
|
+
if (!s || !t) return null;
|
|
89
|
+
const x1 = s.x, x2 = t.x, dist = Math.abs(x2 - x1), midX = (x1 + x2) / 2;
|
|
90
|
+
return `M ${x1} ${baseline} Q ${midX} ${baseline - dist * 0.4} ${x2} ${baseline}`;
|
|
91
|
+
})
|
|
92
|
+
.attr('stroke', d => {
|
|
93
|
+
const s = _filePositions.get(d.sourceFile);
|
|
94
|
+
const t = _filePositions.get(d.targetFile);
|
|
95
|
+
if (!s || !t) return '#4a9eff';
|
|
96
|
+
return d3.interpolateRainbow(Math.abs(t.x - s.x) / maxDistance);
|
|
97
|
+
})
|
|
98
|
+
.attr('stroke-width', d => Math.min(4, 1 + Math.log(d.edgeCount)))
|
|
99
|
+
.on('mouseover', function(event, d) {
|
|
100
|
+
if (_selectedArc) return;
|
|
101
|
+
d3.select(this).classed('highlighted', true);
|
|
102
|
+
ctr.selectAll('.arc').filter(a => a !== d).classed('dimmed', true);
|
|
103
|
+
ctr.selectAll('.file-bar').each(function(f) {
|
|
104
|
+
const match = f.path === d.sourceFile || f.path === d.targetFile;
|
|
105
|
+
d3.select(this).classed('highlighted', match).classed('dimmed', !match);
|
|
106
|
+
});
|
|
107
|
+
_showTooltip(event, `<div class="tooltip-line"><strong>${d.sourceFile}</strong> → <strong>${d.targetFile}</strong></div><div class="tooltip-line"><span class="tooltip-label">Edges:</span> ${d.edgeCount}</div>`);
|
|
108
|
+
})
|
|
109
|
+
.on('mouseout', function() {
|
|
110
|
+
if (_selectedArc) return;
|
|
111
|
+
d3.select(this).classed('highlighted', false);
|
|
112
|
+
ctr.selectAll('.arc').classed('dimmed', false);
|
|
113
|
+
ctr.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
|
|
114
|
+
_hideTooltip();
|
|
115
|
+
})
|
|
116
|
+
.on('click', function(event, d) {
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
if (_selectedArc === d) { _selectedArc = null; ctr.selectAll('.arc,.file-bar').classed('highlighted', false).classed('dimmed', false); _hideTooltip(); return; }
|
|
119
|
+
_selectedArc = d; _selectedFile = null;
|
|
120
|
+
ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', true);
|
|
121
|
+
d3.select(this).classed('highlighted', true).classed('dimmed', false);
|
|
122
|
+
ctr.selectAll('.file-bar').each(function(f) {
|
|
123
|
+
const match = f.path === d.sourceFile || f.path === d.targetFile;
|
|
124
|
+
d3.select(this).classed('highlighted', match).classed('dimmed', !match);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
_g.selectAll('.file-bar').data(_data.files).enter().append('rect')
|
|
129
|
+
.attr('class', 'file-bar')
|
|
130
|
+
.attr('x', d => { const p = _filePositions.get(d.path); return p.x - p.width / 2; })
|
|
131
|
+
.attr('y', baseline).attr('width', d => _filePositions.get(d.path).width).attr('height', 8)
|
|
132
|
+
.attr('fill', d => colorScale(d.directory))
|
|
133
|
+
.on('mouseover', function(event, d) {
|
|
134
|
+
if (_selectedFile) return;
|
|
135
|
+
d3.select(this).classed('highlighted', true);
|
|
136
|
+
const connected = _data.arcs.filter(a => a.sourceFile === d.path || a.targetFile === d.path);
|
|
137
|
+
ctr.selectAll('.arc').classed('highlighted', a => connected.includes(a)).classed('dimmed', a => !connected.includes(a));
|
|
138
|
+
ctr.selectAll('.file-bar').filter(f => f !== d).classed('dimmed', true);
|
|
139
|
+
_showTooltip(event, `<div class="tooltip-line"><strong>${d.path}</strong></div><div class="tooltip-line"><span class="tooltip-label">Symbols:</span> ${d.symbolCount} | In: ${d.incomingCount} | Out: ${d.outgoingCount}</div>`);
|
|
140
|
+
})
|
|
141
|
+
.on('mouseout', function() {
|
|
142
|
+
if (_selectedFile) return;
|
|
143
|
+
d3.select(this).classed('highlighted', false);
|
|
144
|
+
ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
|
|
145
|
+
ctr.selectAll('.file-bar').classed('dimmed', false);
|
|
146
|
+
_hideTooltip();
|
|
147
|
+
})
|
|
148
|
+
.on('click', function(event, d) {
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
if (_selectedFile === d) { _selectedFile = null; ctr.selectAll('.arc,.file-bar').classed('highlighted', false).classed('dimmed', false); return; }
|
|
151
|
+
_selectedFile = d; _selectedArc = null;
|
|
152
|
+
const connected = _data.arcs.filter(a => a.sourceFile === d.path || a.targetFile === d.path);
|
|
153
|
+
ctr.selectAll('.arc').classed('highlighted', a => connected.includes(a)).classed('dimmed', a => !connected.includes(a));
|
|
154
|
+
ctr.selectAll('.file-bar').classed('highlighted', f => f === d).classed('dimmed', f => f !== d);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
_g.selectAll('.file-label').data(_data.files).enter().append('text')
|
|
158
|
+
.attr('class', 'file-label')
|
|
159
|
+
.attr('x', d => _filePositions.get(d.path).x)
|
|
160
|
+
.attr('y', baseline + 20)
|
|
161
|
+
.attr('transform', d => `rotate(-45, ${_filePositions.get(d.path).x}, ${baseline + 20})`)
|
|
162
|
+
.attr('text-anchor', 'end')
|
|
163
|
+
.text(d => d.path.split('/').pop());
|
|
164
|
+
|
|
165
|
+
_svg.append('text').attr('x', 10).attr('y', 20).attr('fill', '#4a9eff')
|
|
166
|
+
.attr('font-size', '12px').attr('cursor', 'pointer').text('↺ Reset View')
|
|
167
|
+
.on('click', () => { _svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity); _clearSelection(); });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { render };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ── Legacy global state and functions (used by depwire viz) ──
|
|
2
174
|
let graphData = null;
|
|
3
175
|
let svg = null;
|
|
4
176
|
let g = null;
|
|
@@ -23,12 +195,16 @@ async function init() {
|
|
|
23
195
|
// Render diagram
|
|
24
196
|
renderArcDiagram();
|
|
25
197
|
|
|
26
|
-
// Setup interactions
|
|
27
|
-
|
|
28
|
-
|
|
198
|
+
// Setup interactions (not in whatif mode)
|
|
199
|
+
if (!window.__depwireWhatIf) {
|
|
200
|
+
setupSearch();
|
|
201
|
+
setupExport();
|
|
202
|
+
}
|
|
29
203
|
|
|
30
|
-
// Setup WebSocket for live updates
|
|
31
|
-
|
|
204
|
+
// Setup WebSocket for live updates (not in whatif mode)
|
|
205
|
+
if (!window.__depwireWhatIf) {
|
|
206
|
+
setupWebSocket();
|
|
207
|
+
}
|
|
32
208
|
|
|
33
209
|
// Handle window resize
|
|
34
210
|
window.addEventListener('resize', () => {
|
|
@@ -575,5 +751,7 @@ d3.select('body').on('click', () => {
|
|
|
575
751
|
}
|
|
576
752
|
});
|
|
577
753
|
|
|
578
|
-
// Initialize on page load
|
|
579
|
-
|
|
754
|
+
// Initialize on page load — only if NOT in What If context
|
|
755
|
+
if (!window.__depwireWhatIf) {
|
|
756
|
+
init();
|
|
757
|
+
}
|