depwire-cli 0.9.19 → 0.9.20

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/README.md CHANGED
@@ -9,7 +9,11 @@
9
9
 
10
10
  ![Depwire - Arc diagram visualization of the Hono framework](./assets/depwire-hero.png)
11
11
 
12
- **See how your code connects. Give AI tools full codebase context.**
12
+ **The missing context layer for AI coding assistants.**
13
+
14
+ Deterministic dependency graph. 16 MCP tools. Architecture health. What If simulation.
15
+
16
+ The context layer that turns vibe coding into software engineering.
13
17
 
14
18
  ⭐ **If Depwire helps you, please [star the repo](https://github.com/depwire/depwire)** — it helps this open-source project grow into an enterprise tool.
15
19
 
@@ -24,33 +28,6 @@ Depwire analyzes codebases to build a cross-reference graph showing how every fi
24
28
  - 👀 **Live updates** — Graph stays current as you edit code
25
29
  - 🌍 **Multi-language** — TypeScript, JavaScript, Python, Go, Rust, and C
26
30
 
27
- ## Why Depwire?
28
-
29
- AI coding tools are flying blind. Every time Claude, Cursor, or Copilot touches your code, it's guessing about dependencies, imports, and impact. The result: broken refactors, hallucinated imports, and wasted tokens re-scanning files it already saw.
30
-
31
- **Lost context = lost money + lost time + bad code.**
32
-
33
- **Depwire parsed the entire Hono framework — 352 files, 5,971 symbols, 1,565 dependency edges — in ~3 seconds.** 40% fewer tool calls, 56% fewer file reads vs. no context layer.
34
-
35
- Depwire fixes this by giving AI tools a complete dependency graph of your codebase — not a fuzzy embedding, not a keyword search, but a deterministic, tree-sitter-parsed map of every symbol and connection.
36
-
37
- ### Stop Losing Context
38
- - **No more "start from scratch" chats** — Depwire is the shared knowledge layer that every AI session inherits. New chat? Your AI already knows the architecture.
39
- - **Stop burning tokens** — AI tools query the graph instead of scanning hundreds of files blindly
40
- - **One command, every AI tool** — Claude Desktop, Cursor, VS Code, any MCP-compatible tool gets the same complete picture
41
-
42
- ### Ship Better Code
43
- - **Impact analysis for any change** — renaming a function, moving a file, upgrading a dependency, deleting a module — know the full blast radius before you touch anything
44
- - **Refactor with confidence** — see every downstream consumer, every transitive dependency, 2-3 levels deep
45
- - **Catch dead code** — find symbols nobody references anymore with confidence-based classification (high/medium/low)
46
-
47
- ### Stay in Flow
48
- - **Live graph, always current** — edit a file and the dependency map updates in real-time. No re-indexing, no waiting.
49
- - **Works locally, stays private** — zero cloud accounts, zero data leaving your machine. Just `npm install` and go.
50
-
51
- ### 15 MCP Tools, Not Just Visualization
52
- Depwire isn't just a pretty graph. It's a full context engine with 15 tools that AI assistants call autonomously — architecture summaries, dependency tracing, symbol search, file context, health scores, dead code detection, temporal evolution, and more. The AI decides which tool to use based on your question.
53
-
54
31
  ## Installation
55
32
 
56
33
  ![Installation](./assets/installation.gif)
@@ -91,6 +68,7 @@ depwire docs
91
68
  depwire health
92
69
  depwire dead-code
93
70
  depwire temporal
71
+ depwire whatif
94
72
 
95
73
  # Or specify a directory explicitly
96
74
  npx depwire-cli viz ./my-project
@@ -166,6 +144,43 @@ Settings → Features → Experimental → Enable MCP → Add Server:
166
144
  | `get_health_score` | Get 0-100 dependency health score with recommendations |
167
145
  | `find_dead_code` | Find dead code — symbols defined but never referenced |
168
146
  | `get_temporal_graph` | Show how the graph evolved over git history |
147
+ | `simulate_change` | Simulate architectural changes before touching code |
148
+
149
+ ## What If Simulation
150
+
151
+ Simulate architectural changes before touching a single line of code.
152
+
153
+ ```bash
154
+ # What breaks if I delete this file?
155
+ depwire whatif . --simulate delete --target src/services/auth.ts
156
+
157
+ # What happens if I move this file?
158
+ depwire whatif . --simulate move --target src/utils/helpers.ts --destination src/core/helpers.ts
159
+
160
+ # What happens if I rename this file?
161
+ depwire whatif . --simulate rename --target src/router.ts --new-name routes.ts
162
+ ```
163
+
164
+ Each simulation returns:
165
+ - **Health score delta** — does this change improve or degrade your architecture?
166
+ - **Broken imports** — exactly which files would break and why
167
+ - **Affected nodes** — full blast radius of the change
168
+ - **Circular deps introduced or resolved**
169
+ - **Edge changes** — added and removed dependency connections
170
+
171
+ Supported actions: `move`, `delete`, `rename`, `split`, `merge`
172
+
173
+ ## Why Depwire
174
+
175
+ | Feature | Depwire | Standard RAG (Fuzzy Search) | LLM Native Scanning |
176
+ |---------|---------|----------------------------|---------------------|
177
+ | Logic | Deterministic Graph | Probabilistic Match | Brute Force Reading |
178
+ | Precision | 100% (Tree-sitter AST) | ~70% (Embedding match) | Varies — hallucination prone |
179
+ | Refactor Safety | High — traces full call chains | Low — misses indirect refs | Zero — blind edits |
180
+ | Token Cost | Ultra-low — surgical reads | High — context stuffing | Extreme — scans everything |
181
+ | Circular Detection | Built-in | Not possible | Occasional |
182
+ | What If Simulation | Before touching code | Not possible | Not possible |
183
+ | Architecture Health Score | 0-100 with dimensions | Not possible | Not possible |
169
184
 
170
185
  ## GitHub Action — PR Impact Analysis
171
186
 
@@ -586,6 +601,46 @@ depwire temporal --output ./temp-snapshots
586
601
 
587
602
  Snapshots are cached in `.depwire/temporal/` for fast re-rendering.
588
603
 
604
+ ### `depwire whatif [directory]`
605
+
606
+ Simulate architectural changes before touching code.
607
+
608
+ **Directory argument is optional** — Auto-detects project root.
609
+
610
+ **Options:**
611
+ - `--simulate <action>` — Action to simulate: `move`, `delete`, `rename`, `split`, `merge`
612
+ - `--target <file>` — File to apply the action to
613
+ - `--destination <file>` — Destination path (for move action)
614
+ - `--new-name <name>` — New name (for rename action)
615
+ - `--source <file>` — Source file (for merge action)
616
+ - `--new-file <file>` — New file path (for split action)
617
+ - `--symbols <symbols>` — Comma-separated symbol names (for split action)
618
+
619
+ **Examples:**
620
+ ```bash
621
+ # What breaks if I delete this file?
622
+ depwire whatif --simulate delete --target src/auth/service.ts
623
+
624
+ # What happens if I move this module?
625
+ depwire whatif --simulate move --target src/utils.ts --destination src/core/utils.ts
626
+
627
+ # Rename a file
628
+ depwire whatif --simulate rename --target src/router.ts --new-name routes.ts
629
+
630
+ # Split symbols into a new file
631
+ depwire whatif --simulate split --target src/utils.ts --new-file src/helpers.ts --symbols "formatDate,parseUrl"
632
+
633
+ # Merge two files
634
+ depwire whatif --simulate merge --target src/auth.ts --source src/login.ts
635
+ ```
636
+
637
+ **Output:**
638
+ - Health score delta (before/after with improvement indicator)
639
+ - Broken imports with file and symbol details
640
+ - Affected nodes count
641
+ - Circular dependencies introduced or resolved
642
+ - Added and removed edge counts
643
+
589
644
  ### Error Handling
590
645
 
591
646
  Depwire gracefully handles parse errors:
@@ -649,7 +704,7 @@ See [SECURITY.md](SECURITY.md) for full details.
649
704
 
650
705
  ### ✅ Shipped
651
706
  - [x] Arc diagram visualization
652
- - [x] MCP server (15 tools)
707
+ - [x] MCP server (16 tools)
653
708
  - [x] Multi-language support (TypeScript, JavaScript, Python, Go, Rust, C)
654
709
  - [x] File watching + live refresh
655
710
  - [x] Auto-generated documentation (13 documents)
@@ -660,18 +715,18 @@ See [SECURITY.md](SECURITY.md) for full details.
660
715
  - [x] Auto-detect project root (no path needed)
661
716
  - [x] WASM migration (Windows support)
662
717
  - [x] Cloud dashboard — [app.depwire.dev](https://app.depwire.dev)
718
+ - [x] What If simulation — simulate refactors before touching code
663
719
 
664
- ### 🔜 Coming Next
720
+ ### Coming Next
665
721
  - [ ] New language support (Java, C++, Ruby — community requested)
666
- - [ ] "What If" simulation — simulate refactors before touching code
667
722
  - [ ] Cross-language edge detection (API routes ↔ frontend calls)
668
723
  - [ ] AI-suggested refactors
669
724
  - [ ] Natural language architecture queries
670
725
  - [ ] VSCode extension
671
726
 
672
- ## Depwire Cloud
727
+ ## Cloud Dashboard
673
728
 
674
- Try [Depwire Cloud](https://app.depwire.dev) connect any GitHub repo and get instant visualization, health scoring, and dead code detection in the browser. No CLI needed.
729
+ Prefer a browser interface? [app.depwire.dev](https://app.depwire.dev) gives you the full dependency graph, health score, dead code report, and AI codebase chat without any local setup. Free tier available.
675
730
 
676
731
  - **Free** for public repos
677
732
  - **Pro** ($19/month) — unlimited repos + private repo support
@@ -9371,6 +9371,42 @@ function getToolsList() {
9371
9371
  }
9372
9372
  }
9373
9373
  }
9374
+ },
9375
+ {
9376
+ name: "simulate_change",
9377
+ description: "Simulate an architectural change and see the impact before touching code. Returns health score delta, broken imports, and affected files.",
9378
+ inputSchema: {
9379
+ type: "object",
9380
+ properties: {
9381
+ action: {
9382
+ type: "string",
9383
+ enum: ["move", "delete", "rename", "split", "merge"],
9384
+ description: "Type of change to simulate"
9385
+ },
9386
+ target: {
9387
+ type: "string",
9388
+ description: "File path to apply the action to"
9389
+ },
9390
+ destination: {
9391
+ type: "string",
9392
+ description: "Destination path (for move action)"
9393
+ },
9394
+ newName: {
9395
+ type: "string",
9396
+ description: "New name (for rename action)"
9397
+ },
9398
+ source: {
9399
+ type: "string",
9400
+ description: "Source file (for merge action)"
9401
+ },
9402
+ symbols: {
9403
+ type: "array",
9404
+ items: { type: "string" },
9405
+ description: "Symbols to move (for split action)"
9406
+ }
9407
+ },
9408
+ required: ["action", "target"]
9409
+ }
9374
9410
  }
9375
9411
  ];
9376
9412
  }
@@ -9442,6 +9478,11 @@ async function handleToolCall(name, args, state) {
9442
9478
  } else {
9443
9479
  result = handleFindDeadCode(state, args.confidence || "medium");
9444
9480
  }
9481
+ } else if (name === "simulate_change") {
9482
+ result = {
9483
+ status: "coming_soon",
9484
+ message: "simulate_change will be fully available in v1.0.0. Use the CLI command 'depwire whatif' for simulation in the meantime."
9485
+ };
9445
9486
  } else {
9446
9487
  if (!isProjectLoaded(state)) {
9447
9488
  result = {
@@ -10157,6 +10198,12 @@ export {
10157
10198
  startVizServer,
10158
10199
  createEmptyState,
10159
10200
  updateFileInGraph,
10201
+ calculateCouplingScore,
10202
+ calculateCohesionScore,
10203
+ calculateCircularDepsScore,
10204
+ calculateGodFilesScore,
10205
+ calculateOrphansScore,
10206
+ calculateDepthScore,
10160
10207
  calculateHealthScore,
10161
10208
  getHealthTrend,
10162
10209
  analyzeDeadCode,
package/dist/index.js CHANGED
@@ -2,7 +2,13 @@
2
2
  import {
3
3
  analyzeDeadCode,
4
4
  buildGraph,
5
+ calculateCircularDepsScore,
6
+ calculateCohesionScore,
7
+ calculateCouplingScore,
8
+ calculateDepthScore,
9
+ calculateGodFilesScore,
5
10
  calculateHealthScore,
11
+ calculateOrphansScore,
6
12
  checkoutCommit,
7
13
  createEmptyState,
8
14
  createSnapshot,
@@ -27,11 +33,11 @@ import {
27
33
  stashChanges,
28
34
  updateFileInGraph,
29
35
  watchProject
30
- } from "./chunk-S3NZMIIU.js";
36
+ } from "./chunk-H6Q2OEGP.js";
31
37
 
32
38
  // src/index.ts
33
39
  import { Command } from "commander";
34
- import { resolve, dirname as dirname2, join as join3 } from "path";
40
+ import { resolve as resolve2, dirname as dirname3, join as join4 } from "path";
35
41
  import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
36
42
  import { fileURLToPath as fileURLToPath2 } from "url";
37
43
 
@@ -302,10 +308,10 @@ async function findAvailablePort(startPort) {
302
308
  const net = await import("net");
303
309
  for (let attempt = 0; attempt < 10; attempt++) {
304
310
  const testPort = startPort + attempt;
305
- const isAvailable = await new Promise((resolve2) => {
306
- const server = net.createServer().once("error", () => resolve2(false)).once("listening", () => {
311
+ const isAvailable = await new Promise((resolve3) => {
312
+ const server = net.createServer().once("error", () => resolve3(false)).once("listening", () => {
307
313
  server.close();
308
- resolve2(true);
314
+ resolve3(true);
309
315
  }).listen(testPort, "127.0.0.1");
310
316
  });
311
317
  if (isAvailable) {
@@ -346,13 +352,13 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
346
352
  console.log(" (Could not open browser automatically)");
347
353
  });
348
354
  });
349
- await new Promise((resolve2, reject) => {
355
+ await new Promise((resolve3, reject) => {
350
356
  server.on("error", reject);
351
357
  process.on("SIGINT", () => {
352
358
  console.log("\n\nShutting down temporal server...");
353
359
  server.close(() => {
354
360
  console.log("Server stopped");
355
- resolve2();
361
+ resolve3();
356
362
  process.exit(0);
357
363
  });
358
364
  });
@@ -496,10 +502,475 @@ async function trackCommand(command, version = "unknown") {
496
502
  });
497
503
  }
498
504
 
505
+ // src/commands/whatif.ts
506
+ import { resolve } from "path";
507
+ import chalk from "chalk";
508
+
509
+ // src/simulation/engine.ts
510
+ import { dirname as dirname2, join as join3 } from "path";
511
+ function normalizePath(p) {
512
+ return p.replace(/^\.\//, "").replace(/\/+$/, "");
513
+ }
514
+ function fileMatch(nodeFilePath, target) {
515
+ const a = normalizePath(nodeFilePath);
516
+ const b = normalizePath(target);
517
+ return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
518
+ }
519
+ var SimulationEngine = class {
520
+ original;
521
+ constructor(graph) {
522
+ this.original = graph;
523
+ }
524
+ simulate(action) {
525
+ const clone = this.original.copy();
526
+ const brokenImports = [];
527
+ switch (action.type) {
528
+ case "move":
529
+ this.applyMove(clone, action.target, action.destination, brokenImports);
530
+ break;
531
+ case "delete":
532
+ this.applyDelete(clone, action.target, brokenImports);
533
+ break;
534
+ case "rename":
535
+ this.applyRename(clone, action.target, action.newName, brokenImports);
536
+ break;
537
+ case "split":
538
+ this.applySplit(clone, action.target, action.newFile, action.symbols, brokenImports);
539
+ break;
540
+ case "merge":
541
+ this.applyMerge(clone, action.target, action.source, brokenImports);
542
+ break;
543
+ }
544
+ const diff = this.computeDiff(this.original, clone, brokenImports);
545
+ const beforeHealth = this.computeHealthScore(this.original);
546
+ const afterHealth = this.computeHealthScore(clone);
547
+ const dimensionChanges = beforeHealth.dimensions.map((bd, i) => {
548
+ const ad = afterHealth.dimensions[i];
549
+ return {
550
+ name: bd.name,
551
+ before: bd.score,
552
+ after: ad ? ad.score : bd.score,
553
+ delta: (ad ? ad.score : bd.score) - bd.score
554
+ };
555
+ });
556
+ const healthDelta = {
557
+ before: beforeHealth.score,
558
+ after: afterHealth.score,
559
+ delta: afterHealth.score - beforeHealth.score,
560
+ improved: afterHealth.score > beforeHealth.score,
561
+ dimensionChanges
562
+ };
563
+ return {
564
+ action,
565
+ originalGraph: {
566
+ nodeCount: this.original.order,
567
+ edgeCount: this.original.size,
568
+ healthScore: beforeHealth.score
569
+ },
570
+ simulatedGraph: {
571
+ nodeCount: clone.order,
572
+ edgeCount: clone.size,
573
+ healthScore: afterHealth.score
574
+ },
575
+ diff,
576
+ healthDelta
577
+ };
578
+ }
579
+ // ── Action implementations ─────────────────────────────────────
580
+ applyMove(clone, target, destination, brokenImports) {
581
+ const normalizedTarget = normalizePath(target);
582
+ const normalizedDest = normalizePath(destination);
583
+ const nodesToMove = clone.filterNodes(
584
+ (_node, attrs) => fileMatch(attrs.filePath, target)
585
+ );
586
+ if (nodesToMove.length === 0) return;
587
+ for (const oldId of nodesToMove) {
588
+ const attrs = clone.getNodeAttributes(oldId);
589
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
590
+ const newId = `${normalizedDest}::${symbolName}`;
591
+ clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
592
+ const sourceAttrs = clone.getNodeAttributes(source);
593
+ if (!fileMatch(sourceAttrs.filePath, target)) {
594
+ brokenImports.push({
595
+ file: sourceAttrs.filePath,
596
+ importedSymbol: attrs.name,
597
+ reason: `imports ${attrs.name} from ${target} (path would break)`
598
+ });
599
+ }
600
+ });
601
+ if (!clone.hasNode(newId)) {
602
+ clone.addNode(newId, { ...attrs, filePath: normalizedDest });
603
+ }
604
+ clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
605
+ const newSource = nodesToMove.includes(source) ? `${normalizedDest}::${source.includes("::") ? source.split("::").slice(1).join("::") : clone.getNodeAttributes(source).name}` : source;
606
+ if (clone.hasNode(newSource) && clone.hasNode(newId)) {
607
+ clone.mergeEdge(newSource, newId, edgeAttrs);
608
+ }
609
+ });
610
+ clone.forEachOutEdge(oldId, (edge, edgeAttrs, _source, outTarget) => {
611
+ const newTarget = nodesToMove.includes(outTarget) ? `${normalizedDest}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
612
+ if (clone.hasNode(newId) && clone.hasNode(newTarget)) {
613
+ clone.mergeEdge(newId, newTarget, edgeAttrs);
614
+ }
615
+ });
616
+ clone.dropNode(oldId);
617
+ }
618
+ }
619
+ applyDelete(clone, target, brokenImports) {
620
+ const nodesToDelete = clone.filterNodes(
621
+ (_node, attrs) => fileMatch(attrs.filePath, target)
622
+ );
623
+ for (const nodeId of nodesToDelete) {
624
+ const attrs = clone.getNodeAttributes(nodeId);
625
+ clone.forEachInEdge(nodeId, (_edge, _edgeAttrs, source) => {
626
+ const sourceAttrs = clone.getNodeAttributes(source);
627
+ if (!fileMatch(sourceAttrs.filePath, target)) {
628
+ brokenImports.push({
629
+ file: sourceAttrs.filePath,
630
+ importedSymbol: attrs.name,
631
+ reason: `imports ${attrs.name} from ${target} (file deleted)`
632
+ });
633
+ }
634
+ });
635
+ }
636
+ for (const nodeId of nodesToDelete) {
637
+ clone.dropNode(nodeId);
638
+ }
639
+ }
640
+ applyRename(clone, target, newName, brokenImports) {
641
+ const destination = join3(dirname2(target), newName);
642
+ this.applyMove(clone, target, destination, brokenImports);
643
+ }
644
+ applySplit(clone, target, newFile, symbols, brokenImports) {
645
+ const normalizedNewFile = normalizePath(newFile);
646
+ const nodesToSplit = clone.filterNodes((_node, attrs) => {
647
+ return fileMatch(attrs.filePath, target) && symbols.includes(attrs.name);
648
+ });
649
+ if (nodesToSplit.length === 0) return;
650
+ for (const oldId of nodesToSplit) {
651
+ const attrs = clone.getNodeAttributes(oldId);
652
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
653
+ const newId = `${normalizedNewFile}::${symbolName}`;
654
+ clone.forEachInEdge(oldId, (_edge, _edgeAttrs, source) => {
655
+ const sourceAttrs = clone.getNodeAttributes(source);
656
+ if (!fileMatch(sourceAttrs.filePath, target) && !fileMatch(sourceAttrs.filePath, newFile)) {
657
+ brokenImports.push({
658
+ file: sourceAttrs.filePath,
659
+ importedSymbol: attrs.name,
660
+ reason: `imports ${attrs.name} from ${target} (symbol moved to ${newFile})`
661
+ });
662
+ }
663
+ });
664
+ if (!clone.hasNode(newId)) {
665
+ clone.addNode(newId, { ...attrs, filePath: normalizedNewFile });
666
+ }
667
+ clone.forEachInEdge(oldId, (_edge, edgeAttrs, source) => {
668
+ if (clone.hasNode(source) && clone.hasNode(newId)) {
669
+ clone.mergeEdge(source, newId, edgeAttrs);
670
+ }
671
+ });
672
+ clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _source, outTarget) => {
673
+ if (clone.hasNode(newId) && clone.hasNode(outTarget)) {
674
+ clone.mergeEdge(newId, outTarget, edgeAttrs);
675
+ }
676
+ });
677
+ clone.dropNode(oldId);
678
+ }
679
+ }
680
+ applyMerge(clone, target, source, brokenImports) {
681
+ const normalizedTarget = normalizePath(target);
682
+ const sourceNodes = clone.filterNodes(
683
+ (_node, attrs) => fileMatch(attrs.filePath, source)
684
+ );
685
+ const targetNodes = clone.filterNodes(
686
+ (_node, attrs) => fileMatch(attrs.filePath, target)
687
+ );
688
+ const targetSymbols = new Set(
689
+ targetNodes.map((n) => clone.getNodeAttributes(n).name)
690
+ );
691
+ for (const nodeId of sourceNodes) {
692
+ const name = clone.getNodeAttributes(nodeId).name;
693
+ if (name !== "__file__" && targetSymbols.has(name)) {
694
+ throw new Error(
695
+ `Merge conflict: symbol "${name}" exists in both ${target} and ${source}`
696
+ );
697
+ }
698
+ }
699
+ for (const oldId of sourceNodes) {
700
+ const attrs = clone.getNodeAttributes(oldId);
701
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
702
+ const newId = `${normalizedTarget}::${symbolName}`;
703
+ clone.forEachInEdge(oldId, (_edge, _edgeAttrs, inSource) => {
704
+ const srcAttrs = clone.getNodeAttributes(inSource);
705
+ if (!fileMatch(srcAttrs.filePath, source) && !fileMatch(srcAttrs.filePath, target)) {
706
+ brokenImports.push({
707
+ file: srcAttrs.filePath,
708
+ importedSymbol: attrs.name,
709
+ reason: `imports ${attrs.name} from ${source} (merged into ${target})`
710
+ });
711
+ }
712
+ });
713
+ if (!clone.hasNode(newId)) {
714
+ clone.addNode(newId, { ...attrs, filePath: normalizedTarget });
715
+ }
716
+ clone.forEachInEdge(oldId, (_edge, edgeAttrs, inSource) => {
717
+ const resolvedSource = sourceNodes.includes(inSource) ? `${normalizedTarget}::${inSource.includes("::") ? inSource.split("::").slice(1).join("::") : clone.getNodeAttributes(inSource).name}` : inSource;
718
+ if (clone.hasNode(resolvedSource) && clone.hasNode(newId)) {
719
+ clone.mergeEdge(resolvedSource, newId, edgeAttrs);
720
+ }
721
+ });
722
+ clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _s, outTarget) => {
723
+ const resolvedTarget = sourceNodes.includes(outTarget) ? `${normalizedTarget}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
724
+ if (clone.hasNode(newId) && clone.hasNode(resolvedTarget)) {
725
+ clone.mergeEdge(newId, resolvedTarget, edgeAttrs);
726
+ }
727
+ });
728
+ clone.dropNode(oldId);
729
+ }
730
+ }
731
+ // ── Diff computation ───────────────────────────────────────────
732
+ computeDiff(original, simulated, brokenImports) {
733
+ const originalEdges = this.collectEdges(original);
734
+ const simulatedEdges = this.collectEdges(simulated);
735
+ const originalKeys = new Set(originalEdges.map((e) => this.edgeKey(e)));
736
+ const simulatedKeys = new Set(simulatedEdges.map((e) => this.edgeKey(e)));
737
+ const addedEdges = simulatedEdges.filter((e) => !originalKeys.has(this.edgeKey(e)));
738
+ const removedEdges = originalEdges.filter((e) => !simulatedKeys.has(this.edgeKey(e)));
739
+ const affectedNodeSet = /* @__PURE__ */ new Set();
740
+ for (const e of [...addedEdges, ...removedEdges]) {
741
+ affectedNodeSet.add(e.source);
742
+ affectedNodeSet.add(e.target);
743
+ }
744
+ const originalCycles = this.detectCycles(original);
745
+ const simulatedCycles = this.detectCycles(simulated);
746
+ const originalCycleKeys = new Set(originalCycles.map((c) => [...c].sort().join(",")));
747
+ const simulatedCycleKeys = new Set(simulatedCycles.map((c) => [...c].sort().join(",")));
748
+ const circularDepsIntroduced = simulatedCycles.filter(
749
+ (c) => !originalCycleKeys.has([...c].sort().join(","))
750
+ );
751
+ const circularDepsResolved = originalCycles.filter(
752
+ (c) => !simulatedCycleKeys.has([...c].sort().join(","))
753
+ );
754
+ return {
755
+ addedEdges,
756
+ removedEdges,
757
+ affectedNodes: Array.from(affectedNodeSet),
758
+ brokenImports,
759
+ circularDepsIntroduced,
760
+ circularDepsResolved
761
+ };
762
+ }
763
+ collectEdges(graph) {
764
+ const edges = [];
765
+ graph.forEachEdge((_edge, attrs, source, target) => {
766
+ edges.push({ source, target, kind: attrs.kind });
767
+ });
768
+ return edges;
769
+ }
770
+ edgeKey(e) {
771
+ return `${e.source}|${e.target}|${e.kind || ""}`;
772
+ }
773
+ // ── Cycle detection (adapted from src/health/metrics.ts) ───────
774
+ detectCycles(graph) {
775
+ const fileGraph = /* @__PURE__ */ new Map();
776
+ graph.forEachEdge((_edge, _attrs, source, target) => {
777
+ const sourceFile = graph.getNodeAttributes(source).filePath;
778
+ const targetFile = graph.getNodeAttributes(target).filePath;
779
+ if (sourceFile !== targetFile) {
780
+ if (!fileGraph.has(sourceFile)) {
781
+ fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
782
+ }
783
+ fileGraph.get(sourceFile).add(targetFile);
784
+ }
785
+ });
786
+ const visited = /* @__PURE__ */ new Set();
787
+ const recStack = /* @__PURE__ */ new Set();
788
+ const cycles = [];
789
+ const dfs = (node, path) => {
790
+ if (recStack.has(node)) {
791
+ const cycleStart = path.indexOf(node);
792
+ if (cycleStart >= 0) {
793
+ cycles.push(path.slice(cycleStart));
794
+ }
795
+ return;
796
+ }
797
+ if (visited.has(node)) return;
798
+ visited.add(node);
799
+ recStack.add(node);
800
+ path.push(node);
801
+ const neighbors = fileGraph.get(node);
802
+ if (neighbors) {
803
+ for (const neighbor of neighbors) {
804
+ dfs(neighbor, [...path]);
805
+ }
806
+ }
807
+ recStack.delete(node);
808
+ };
809
+ for (const node of fileGraph.keys()) {
810
+ if (!visited.has(node)) {
811
+ dfs(node, []);
812
+ }
813
+ }
814
+ const unique = /* @__PURE__ */ new Map();
815
+ for (const cycle of cycles) {
816
+ const key = [...cycle].sort().join(",");
817
+ if (!unique.has(key)) {
818
+ unique.set(key, cycle);
819
+ }
820
+ }
821
+ return Array.from(unique.values());
822
+ }
823
+ // ── Health score (side-effect free) ────────────────────────────
824
+ computeHealthScore(graph) {
825
+ const dimensions = [
826
+ calculateCouplingScore(graph),
827
+ calculateCohesionScore(graph),
828
+ calculateCircularDepsScore(graph),
829
+ calculateGodFilesScore(graph),
830
+ calculateOrphansScore(graph),
831
+ calculateDepthScore(graph)
832
+ ];
833
+ const score = Math.round(
834
+ dimensions.reduce((sum, dim) => sum + dim.score * dim.weight, 0)
835
+ );
836
+ return { score, dimensions };
837
+ }
838
+ };
839
+
840
+ // src/commands/whatif.ts
841
+ async function whatif(dir, options) {
842
+ if (!options.simulate) {
843
+ console.log("Usage: depwire whatif [dir] --simulate <action> --target <file> [options]");
844
+ console.log("");
845
+ console.log("Actions: move, delete, rename, split, merge");
846
+ console.log("");
847
+ console.log("Run without --simulate to open interactive browser UI (Phase B)");
848
+ return;
849
+ }
850
+ const validActions = ["move", "delete", "rename", "split", "merge"];
851
+ if (!validActions.includes(options.simulate)) {
852
+ console.error(chalk.red(`Invalid action: ${options.simulate}. Must be one of: ${validActions.join(", ")}`));
853
+ process.exit(1);
854
+ }
855
+ if (!options.target) {
856
+ console.error(chalk.red("--target is required for all simulation actions"));
857
+ process.exit(1);
858
+ }
859
+ const action = buildAction(options);
860
+ const projectRoot = dir === "." ? findProjectRoot() : resolve(dir);
861
+ console.log(`Parsing project: ${projectRoot}`);
862
+ const parsedFiles = await parseProject(projectRoot);
863
+ const graph = buildGraph(parsedFiles);
864
+ console.log(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
865
+ console.log("");
866
+ const engine = new SimulationEngine(graph);
867
+ try {
868
+ const result = engine.simulate(action);
869
+ printResult(result);
870
+ } catch (err) {
871
+ console.error(chalk.red(`Simulation failed: ${err.message}`));
872
+ process.exit(1);
873
+ }
874
+ }
875
+ function buildAction(options) {
876
+ const type = options.simulate;
877
+ const target = options.target;
878
+ switch (type) {
879
+ case "move":
880
+ if (!options.destination) {
881
+ console.error(chalk.red("--destination is required for move action"));
882
+ process.exit(1);
883
+ }
884
+ return { type: "move", target, destination: options.destination };
885
+ case "delete":
886
+ return { type: "delete", target };
887
+ case "rename":
888
+ if (!options.newName) {
889
+ console.error(chalk.red("--new-name is required for rename action"));
890
+ process.exit(1);
891
+ }
892
+ return { type: "rename", target, newName: options.newName };
893
+ case "split":
894
+ if (!options.newFile) {
895
+ console.error(chalk.red("--new-file is required for split action"));
896
+ process.exit(1);
897
+ }
898
+ if (!options.symbols) {
899
+ console.error(chalk.red("--symbols is required for split action (comma-separated)"));
900
+ process.exit(1);
901
+ }
902
+ return {
903
+ type: "split",
904
+ target,
905
+ newFile: options.newFile,
906
+ symbols: options.symbols.split(",").map((s) => s.trim())
907
+ };
908
+ case "merge":
909
+ if (!options.source) {
910
+ console.error(chalk.red("--source is required for merge action"));
911
+ process.exit(1);
912
+ }
913
+ return { type: "merge", target, source: options.source };
914
+ default:
915
+ console.error(chalk.red(`Unknown action: ${type}`));
916
+ process.exit(1);
917
+ }
918
+ }
919
+ function printResult(result) {
920
+ const { action, healthDelta, diff } = result;
921
+ const line = "\u2500".repeat(45);
922
+ console.log(chalk.bold("What If Simulation"));
923
+ console.log(chalk.dim(line));
924
+ const actionStr = formatAction(action);
925
+ console.log(`${chalk.bold("Action:")} ${actionStr}`);
926
+ console.log(chalk.dim(line));
927
+ const deltaSign = healthDelta.delta >= 0 ? "+" : "";
928
+ const deltaColor = healthDelta.improved ? chalk.green : healthDelta.delta === 0 ? chalk.yellow : chalk.red;
929
+ const deltaIcon = healthDelta.improved ? "\u2713 improved" : healthDelta.delta === 0 ? "\u2192 unchanged" : "\u2717 degraded";
930
+ console.log(
931
+ `${chalk.bold("Health Score:")} ${healthDelta.before} \u2192 ${healthDelta.after} ${deltaColor(`(${deltaSign}${healthDelta.delta} ${deltaIcon})`)}`
932
+ );
933
+ const changed = healthDelta.dimensionChanges.filter((d) => d.delta !== 0);
934
+ if (changed.length > 0) {
935
+ for (const d of changed) {
936
+ const dSign = d.delta >= 0 ? "+" : "";
937
+ const dColor = d.delta > 0 ? chalk.green : chalk.red;
938
+ console.log(` ${chalk.dim("\u2022")} ${d.name}: ${d.before} \u2192 ${d.after} ${dColor(`(${dSign}${d.delta})`)}`);
939
+ }
940
+ }
941
+ console.log(`${chalk.bold("Affected Nodes:")} ${diff.affectedNodes.length}`);
942
+ console.log(`${chalk.bold("Broken Imports:")} ${diff.brokenImports.length}`);
943
+ if (diff.brokenImports.length > 0) {
944
+ for (const bi of diff.brokenImports) {
945
+ console.log(` ${chalk.yellow("\u2022")} ${bi.file} ${bi.reason}`);
946
+ }
947
+ }
948
+ console.log(
949
+ `${chalk.bold("Circular Deps:")} ${diff.circularDepsIntroduced.length} introduced, ${diff.circularDepsResolved.length} resolved`
950
+ );
951
+ console.log(`${chalk.bold("Added Edges:")} ${diff.addedEdges.length}`);
952
+ console.log(`${chalk.bold("Removed Edges:")} ${diff.removedEdges.length}`);
953
+ console.log(chalk.dim(line));
954
+ }
955
+ function formatAction(action) {
956
+ switch (action.type) {
957
+ case "move":
958
+ return `MOVE ${action.target} \u2192 ${action.destination}`;
959
+ case "delete":
960
+ return `DELETE ${action.target}`;
961
+ case "rename":
962
+ return `RENAME ${action.target} \u2192 ${action.newName}`;
963
+ case "split":
964
+ return `SPLIT ${action.target} \u2192 ${action.newFile} (${action.symbols.join(", ")})`;
965
+ case "merge":
966
+ return `MERGE ${action.source} \u2192 ${action.target}`;
967
+ }
968
+ }
969
+
499
970
  // src/index.ts
500
971
  var __filename2 = fileURLToPath2(import.meta.url);
501
- var __dirname2 = dirname2(__filename2);
502
- var packageJsonPath = join3(__dirname2, "../package.json");
972
+ var __dirname2 = dirname3(__filename2);
973
+ var packageJsonPath = join4(__dirname2, "../package.json");
503
974
  var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
504
975
  var program = new Command();
505
976
  program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
@@ -507,7 +978,7 @@ program.command("parse").description("Parse a TypeScript project and build depen
507
978
  trackCommand("parse", packageJson.version);
508
979
  const startTime = Date.now();
509
980
  try {
510
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
981
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
511
982
  console.log(`Parsing project: ${projectRoot}`);
512
983
  const parsedFiles = await parseProject(projectRoot, {
513
984
  exclude: options.exclude,
@@ -546,7 +1017,7 @@ Orphan Files (no cross-references): ${summary.orphanFiles.length}`);
546
1017
  program.command("query").description("Query impact analysis for a symbol").argument("<directory>", "Project directory").argument("<symbol-name>", "Symbol name to query").action(async (directory, symbolName) => {
547
1018
  trackCommand("query", packageJson.version);
548
1019
  try {
549
- const projectRoot = resolve(directory);
1020
+ const projectRoot = resolve2(directory);
550
1021
  const cacheFile = "depwire-output.json";
551
1022
  let graph;
552
1023
  if (existsSync(cacheFile)) {
@@ -595,7 +1066,7 @@ Total Transitive Dependents: ${impact.transitiveDependents.length}`);
595
1066
  program.command("viz").description("Launch interactive arc diagram visualization").argument("[directory]", "Project directory to visualize (defaults to current directory or auto-detected project root)").option("-p, --port <number>", "Server port", "3333").option("--no-open", "Don't auto-open browser").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
596
1067
  trackCommand("viz", packageJson.version);
597
1068
  try {
598
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
1069
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
599
1070
  console.log(`Parsing project: ${projectRoot}`);
600
1071
  const parsedFiles = await parseProject(projectRoot, {
601
1072
  exclude: options.exclude,
@@ -618,7 +1089,7 @@ program.command("viz").description("Launch interactive arc diagram visualization
618
1089
  program.command("temporal").description("Visualize how the dependency graph evolved over git history").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--commits <number>", "Number of commits to sample", "20").option("--strategy <type>", "Sampling strategy: even, weekly, monthly", "even").option("-p, --port <number>", "Server port", "3334").option("--output <path>", "Save snapshots to custom path (default: .depwire/temporal/)").option("--verbose", "Show progress for each commit being parsed").option("--stats", "Show summary statistics at end").action(async (directory, options) => {
619
1090
  trackCommand("temporal", packageJson.version);
620
1091
  try {
621
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
1092
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
622
1093
  await runTemporalAnalysis(projectRoot, {
623
1094
  commits: parseInt(options.commits, 10),
624
1095
  strategy: options.strategy,
@@ -638,11 +1109,11 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
638
1109
  const state = createEmptyState();
639
1110
  let projectRootToConnect = null;
640
1111
  if (directory) {
641
- projectRootToConnect = resolve(directory);
1112
+ projectRootToConnect = resolve2(directory);
642
1113
  } else {
643
1114
  const detectedRoot = findProjectRoot();
644
1115
  const cwd = process.cwd();
645
- if (detectedRoot !== cwd || existsSync(join3(cwd, "package.json")) || existsSync(join3(cwd, "tsconfig.json")) || existsSync(join3(cwd, "go.mod")) || existsSync(join3(cwd, "pyproject.toml")) || existsSync(join3(cwd, "setup.py")) || existsSync(join3(cwd, ".git"))) {
1116
+ 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"))) {
646
1117
  projectRootToConnect = detectedRoot;
647
1118
  }
648
1119
  }
@@ -699,8 +1170,8 @@ program.command("docs").description("Generate comprehensive codebase documentati
699
1170
  trackCommand("docs", packageJson.version);
700
1171
  const startTime = Date.now();
701
1172
  try {
702
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
703
- const outputDir = options.output ? resolve(options.output) : join3(projectRoot, ".depwire");
1173
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1174
+ const outputDir = options.output ? resolve2(options.output) : join4(projectRoot, ".depwire");
704
1175
  const includeList = options.include.split(",").map((s) => s.trim());
705
1176
  const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
706
1177
  if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
@@ -762,16 +1233,16 @@ async function promptGitignore() {
762
1233
  input: process.stdin,
763
1234
  output: process.stdout
764
1235
  });
765
- return new Promise((resolve2) => {
1236
+ return new Promise((resolve3) => {
766
1237
  rl.question("Add .depwire/ to .gitignore? [Y/n] ", (answer) => {
767
1238
  rl.close();
768
1239
  const normalized = answer.trim().toLowerCase();
769
- resolve2(normalized === "" || normalized === "y" || normalized === "yes");
1240
+ resolve3(normalized === "" || normalized === "y" || normalized === "yes");
770
1241
  });
771
1242
  });
772
1243
  }
773
1244
  function addToGitignore(projectRoot, pattern) {
774
- const gitignorePath = join3(projectRoot, ".gitignore");
1245
+ const gitignorePath = join4(projectRoot, ".gitignore");
775
1246
  try {
776
1247
  let content = "";
777
1248
  if (existsSyncNode(gitignorePath)) {
@@ -796,7 +1267,7 @@ ${pattern}
796
1267
  program.command("health").description("Analyze dependency architecture health (0-100 score)").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--json", "Output as JSON").option("--verbose", "Show detailed breakdown").action(async (directory, options) => {
797
1268
  trackCommand("health", packageJson.version);
798
1269
  try {
799
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
1270
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
800
1271
  const startTime = Date.now();
801
1272
  const parsedFiles = await parseProject(projectRoot);
802
1273
  const graph = buildGraph(parsedFiles);
@@ -820,7 +1291,7 @@ program.command("health").description("Analyze dependency architecture health (0
820
1291
  program.command("dead-code").description("Identify dead code - symbols defined but never referenced").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--confidence <level>", "Minimum confidence level to show: high, medium, low (default: medium)", "medium").option("--json", "Output as JSON (for CI/automation)").option("--verbose", "Show detailed info for each dead symbol").option("--stats", "Show summary statistics").option("--include-tests", "Include test files in analysis").option("--include-low", "Shortcut for --confidence low").option("--debug", "Show debug information (exclusion stats)").action(async (directory, options) => {
821
1292
  trackCommand("dead-code", packageJson.version);
822
1293
  try {
823
- const projectRoot = directory ? resolve(directory) : findProjectRoot();
1294
+ const projectRoot = directory ? resolve2(directory) : findProjectRoot();
824
1295
  const startTime = Date.now();
825
1296
  const parsedFiles = await parseProject(projectRoot);
826
1297
  const graph = buildGraph(parsedFiles);
@@ -847,4 +1318,13 @@ Analysis completed in ${(totalTime / 1e3).toFixed(2)}s
847
1318
  process.exit(1);
848
1319
  }
849
1320
  });
1321
+ program.command("whatif").description("Simulate architectural changes before touching code").argument("[directory]", "Project directory (defaults to auto-detected project root)").option("--simulate <action>", "Action to simulate: move, delete, rename, split, merge").option("--target <file>", "File to apply the action to").option("--destination <file>", "Destination path (for move action)").option("--new-name <name>", "New name (for rename action)").option("--source <file>", "Source file (for merge action)").option("--new-file <file>", "New file path (for split action)").option("--symbols <symbols>", "Comma-separated symbol names (for split action)").action(async (directory, options) => {
1322
+ trackCommand("whatif", packageJson.version);
1323
+ try {
1324
+ await whatif(directory || ".", options);
1325
+ } catch (err) {
1326
+ console.error("Error running simulation:", err);
1327
+ process.exit(1);
1328
+ }
1329
+ });
850
1330
  program.parse();
@@ -6,7 +6,7 @@ import {
6
6
  startMcpServer,
7
7
  updateFileInGraph,
8
8
  watchProject
9
- } from "./chunk-S3NZMIIU.js";
9
+ } from "./chunk-H6Q2OEGP.js";
10
10
 
11
11
  // src/mcpb-entry.ts
12
12
  import { resolve } from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depwire-cli",
3
- "version": "0.9.19",
3
+ "version": "0.9.20",
4
4
  "description": "The missing context layer for AI coding assistants. Dependency graph, MCP server, architecture health, dead code detection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,14 @@
28
28
  "impact-analysis",
29
29
  "refactoring",
30
30
  "arc-diagram",
31
- "cross-reference"
31
+ "cross-reference",
32
+ "whatif",
33
+ "simulation",
34
+ "architectural-simulation",
35
+ "what-if-analysis",
36
+ "refactor-simulation",
37
+ "blast-radius",
38
+ "deterministic-graph"
32
39
  ],
33
40
  "author": "Atef Ataya (https://www.youtube.com/@atefataya)",
34
41
  "license": "BUSL-1.1",