depwire-cli 0.9.19 → 0.9.21

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.
@@ -121,13 +121,13 @@ var languages = /* @__PURE__ */ new Map();
121
121
  async function initParser() {
122
122
  if (initialized) return;
123
123
  await Parser.init();
124
- const __dirname3 = path.dirname(fileURLToPath(import.meta.url));
125
- let grammarsDir = path.join(__dirname3, "parser", "grammars");
124
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
125
+ let grammarsDir = path.join(__dirname, "parser", "grammars");
126
126
  if (!existsSync2(grammarsDir)) {
127
- grammarsDir = path.join(path.dirname(__dirname3), "parser", "grammars");
127
+ grammarsDir = path.join(path.dirname(__dirname), "parser", "grammars");
128
128
  }
129
129
  if (!existsSync2(grammarsDir)) {
130
- grammarsDir = path.join(__dirname3, "grammars");
130
+ grammarsDir = path.join(__dirname, "grammars");
131
131
  }
132
132
  const grammarFiles = {
133
133
  "typescript": "tree-sitter-typescript.wasm",
@@ -3142,362 +3142,8 @@ function getArchitectureSummary(graph) {
3142
3142
  };
3143
3143
  }
3144
3144
 
3145
- // src/viz/data.ts
3146
- import { basename } from "path";
3147
- function prepareVizData(graph, projectRoot) {
3148
- const fileSummary = getFileSummary(graph);
3149
- const crossFileEdges = getCrossFileEdges(graph);
3150
- const files = fileSummary.map((f) => ({
3151
- path: f.filePath,
3152
- directory: f.filePath.includes("/") ? f.filePath.substring(0, f.filePath.lastIndexOf("/")) : ".",
3153
- symbolCount: f.symbolCount,
3154
- incomingCount: f.incomingRefs,
3155
- outgoingCount: f.outgoingRefs
3156
- }));
3157
- files.sort((a, b) => {
3158
- if (a.directory !== b.directory) {
3159
- return a.directory.localeCompare(b.directory);
3160
- }
3161
- return a.path.localeCompare(b.path);
3162
- });
3163
- const arcMap = /* @__PURE__ */ new Map();
3164
- for (const edge of crossFileEdges) {
3165
- const key = `${edge.sourceFile}::${edge.targetFile}`;
3166
- if (arcMap.has(key)) {
3167
- const arc = arcMap.get(key);
3168
- arc.edgeCount++;
3169
- if (!arc.edgeKinds.includes(edge.kind)) {
3170
- arc.edgeKinds.push(edge.kind);
3171
- }
3172
- } else {
3173
- arcMap.set(key, {
3174
- sourceFile: edge.sourceFile,
3175
- targetFile: edge.targetFile,
3176
- edgeCount: 1,
3177
- edgeKinds: [edge.kind]
3178
- });
3179
- }
3180
- }
3181
- const arcs = Array.from(arcMap.values());
3182
- const projectName = basename(projectRoot);
3183
- return {
3184
- files,
3185
- arcs,
3186
- stats: {
3187
- totalFiles: files.length,
3188
- totalSymbols: graph.order,
3189
- totalEdges: graph.size,
3190
- totalCrossFileEdges: arcs.reduce((sum, arc) => sum + arc.edgeCount, 0)
3191
- },
3192
- projectName
3193
- };
3194
- }
3195
-
3196
- // src/watcher.ts
3197
- import chokidar from "chokidar";
3198
- function watchProject(projectRoot, callbacks) {
3199
- console.error(`[Watcher] Creating watcher for: ${projectRoot}`);
3200
- const watcherOptions = {
3201
- ignored: [
3202
- "**/node_modules/**",
3203
- "**/vendor/**",
3204
- // Go dependencies
3205
- "**/.git/**",
3206
- "**/dist/**",
3207
- "**/build/**",
3208
- "**/coverage/**",
3209
- "**/.next/**",
3210
- "**/.turbo/**",
3211
- "**/.DS_Store",
3212
- // macOS metadata
3213
- "**/.env",
3214
- // Environment files
3215
- "**/.env.*",
3216
- // Environment variants
3217
- "**/.eslintcache",
3218
- // ESLint cache
3219
- "**/.vscode/**",
3220
- // VS Code settings
3221
- "**/.idea/**"
3222
- // IntelliJ IDEA settings
3223
- ],
3224
- ignoreInitial: true,
3225
- // Don't fire events for existing files
3226
- persistent: true,
3227
- followSymlinks: false,
3228
- usePolling: true,
3229
- // Use polling for macOS reliability
3230
- interval: 1e3,
3231
- // Poll every second
3232
- atomic: true,
3233
- // Handle atomic writes (VS Code, Sublime, etc.)
3234
- awaitWriteFinish: {
3235
- stabilityThreshold: 300,
3236
- // Wait 300ms after last change before firing
3237
- pollInterval: 100
3238
- }
3239
- };
3240
- const watcher = chokidar.watch(projectRoot, watcherOptions);
3241
- console.error("[Watcher] Attaching event listeners...");
3242
- watcher.on("change", (absolutePath) => {
3243
- const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
3244
- if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
3245
- if (absolutePath.endsWith("_test.go")) return;
3246
- const relativePath = absolutePath.replace(projectRoot + "/", "");
3247
- console.error(`[Watcher] Change event: ${relativePath}`);
3248
- callbacks.onFileChanged(relativePath);
3249
- });
3250
- watcher.on("add", (absolutePath) => {
3251
- const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
3252
- if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
3253
- if (absolutePath.endsWith("_test.go")) return;
3254
- const relativePath = absolutePath.replace(projectRoot + "/", "");
3255
- console.error(`[Watcher] Add event: ${relativePath}`);
3256
- callbacks.onFileAdded(relativePath);
3257
- });
3258
- watcher.on("unlink", (absolutePath) => {
3259
- const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
3260
- if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
3261
- if (absolutePath.endsWith("_test.go")) return;
3262
- const relativePath = absolutePath.replace(projectRoot + "/", "");
3263
- console.error(`[Watcher] Unlink event: ${relativePath}`);
3264
- callbacks.onFileDeleted(relativePath);
3265
- });
3266
- watcher.on("error", (error) => {
3267
- console.error("[Watcher] Error:", error);
3268
- });
3269
- watcher.on("ready", () => {
3270
- console.error("[Watcher] Ready \u2014 watching for changes");
3271
- const watched = watcher.getWatched();
3272
- const dirs = Object.keys(watched);
3273
- let fileCount = 0;
3274
- for (const dir of dirs) {
3275
- const files = watched[dir];
3276
- fileCount += files.filter(
3277
- (f) => f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx") || f.endsWith(".mjs") || f.endsWith(".cjs") || f.endsWith(".py") || f.endsWith(".go") && !f.endsWith("_test.go") || f.endsWith(".rs") || f.endsWith(".c") || f.endsWith(".h")
3278
- ).length;
3279
- }
3280
- console.error(`[Watcher] Watching ${fileCount} TypeScript/JavaScript/Python/Go/Rust/C files in ${dirs.length} directories`);
3281
- });
3282
- return watcher;
3283
- }
3284
-
3285
- // src/viz/server.ts
3286
- import express from "express";
3287
- import open from "open";
3288
- import { fileURLToPath as fileURLToPath2 } from "url";
3289
- import { dirname as dirname7, join as join9 } from "path";
3290
- import { WebSocketServer } from "ws";
3291
- var __filename = fileURLToPath2(import.meta.url);
3292
- var __dirname2 = dirname7(__filename);
3293
- var activeServer = null;
3294
- async function findAvailablePort(startPort, maxAttempts = 10) {
3295
- const net = await import("net");
3296
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
3297
- const testPort = startPort + attempt;
3298
- const isAvailable = await new Promise((resolve3) => {
3299
- const server = net.createServer();
3300
- server.once("error", () => {
3301
- resolve3(false);
3302
- });
3303
- server.once("listening", () => {
3304
- server.close();
3305
- resolve3(true);
3306
- });
3307
- server.listen(testPort, "127.0.0.1");
3308
- });
3309
- if (isAvailable) {
3310
- if (attempt > 0) {
3311
- console.error(`Port ${startPort} in use, using port ${testPort} instead`);
3312
- }
3313
- return testPort;
3314
- }
3315
- }
3316
- throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
3317
- }
3318
- async function startVizServer(initialVizData, graph, projectRoot, port = 3333, shouldOpen = true, options) {
3319
- if (activeServer) {
3320
- console.error(`Visualization server already running at ${activeServer.url}`);
3321
- return {
3322
- server: activeServer.server,
3323
- url: activeServer.url,
3324
- alreadyRunning: true
3325
- };
3326
- }
3327
- const availablePort = await findAvailablePort(port);
3328
- const app = express();
3329
- let vizData = initialVizData;
3330
- const publicDir = join9(__dirname2, "viz", "public");
3331
- app.use(express.static(publicDir));
3332
- app.get("/api/graph", (req, res) => {
3333
- res.json(vizData);
3334
- });
3335
- const server = app.listen(availablePort, "127.0.0.1", () => {
3336
- const url2 = `http://127.0.0.1:${availablePort}`;
3337
- console.error(`
3338
- Depwire visualization running at ${url2}`);
3339
- console.error("Press Ctrl+C to stop\n");
3340
- activeServer = { server, port: availablePort, url: url2 };
3341
- if (shouldOpen) {
3342
- open(url2);
3343
- }
3344
- });
3345
- const wss = new WebSocketServer({ server });
3346
- wss.on("connection", (ws) => {
3347
- console.error("Browser connected to WebSocket");
3348
- ws.on("close", () => {
3349
- console.error("Browser disconnected from WebSocket");
3350
- });
3351
- });
3352
- function broadcastRefresh() {
3353
- wss.clients.forEach((client) => {
3354
- if (client.readyState === 1) {
3355
- client.send(JSON.stringify({ type: "refresh" }));
3356
- }
3357
- });
3358
- }
3359
- console.error("Starting file watcher...");
3360
- const watcher = watchProject(projectRoot, {
3361
- onFileChanged: async (filePath) => {
3362
- console.error(`File changed: ${filePath} \u2014 re-parsing project...`);
3363
- try {
3364
- const parsedFiles = await parseProject(projectRoot, options);
3365
- const newGraph = buildGraph(parsedFiles);
3366
- graph.clear();
3367
- newGraph.forEachNode((node, attrs) => {
3368
- graph.addNode(node, attrs);
3369
- });
3370
- newGraph.forEachEdge((edge, attrs, source, target) => {
3371
- graph.addEdge(source, target, attrs);
3372
- });
3373
- vizData = prepareVizData(graph, projectRoot);
3374
- broadcastRefresh();
3375
- console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
3376
- } catch (error) {
3377
- console.error(`Failed to update graph for ${filePath}:`, error);
3378
- }
3379
- },
3380
- onFileAdded: async (filePath) => {
3381
- console.error(`File added: ${filePath} \u2014 re-parsing project...`);
3382
- try {
3383
- const parsedFiles = await parseProject(projectRoot, options);
3384
- const newGraph = buildGraph(parsedFiles);
3385
- graph.clear();
3386
- newGraph.forEachNode((node, attrs) => {
3387
- graph.addNode(node, attrs);
3388
- });
3389
- newGraph.forEachEdge((edge, attrs, source, target) => {
3390
- graph.addEdge(source, target, attrs);
3391
- });
3392
- vizData = prepareVizData(graph, projectRoot);
3393
- broadcastRefresh();
3394
- console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
3395
- } catch (error) {
3396
- console.error(`Failed to update graph for ${filePath}:`, error);
3397
- }
3398
- },
3399
- onFileDeleted: async (filePath) => {
3400
- console.error(`File deleted: ${filePath} \u2014 re-parsing project...`);
3401
- try {
3402
- const parsedFiles = await parseProject(projectRoot, options);
3403
- const newGraph = buildGraph(parsedFiles);
3404
- graph.clear();
3405
- newGraph.forEachNode((node, attrs) => {
3406
- graph.addNode(node, attrs);
3407
- });
3408
- newGraph.forEachEdge((edge, attrs, source, target) => {
3409
- graph.addEdge(source, target, attrs);
3410
- });
3411
- vizData = prepareVizData(graph, projectRoot);
3412
- broadcastRefresh();
3413
- console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
3414
- } catch (error) {
3415
- console.error(`Failed to remove ${filePath} from graph:`, error);
3416
- }
3417
- }
3418
- });
3419
- process.on("SIGINT", () => {
3420
- console.error("\nShutting down visualization server...");
3421
- activeServer = null;
3422
- watcher.close();
3423
- wss.close();
3424
- server.close(() => {
3425
- process.exit(0);
3426
- });
3427
- });
3428
- const url = `http://127.0.0.1:${availablePort}`;
3429
- return { server, url, alreadyRunning: false };
3430
- }
3431
-
3432
- // src/mcp/state.ts
3433
- function createEmptyState() {
3434
- return {
3435
- graph: null,
3436
- projectRoot: null,
3437
- projectName: null,
3438
- watcher: null
3439
- };
3440
- }
3441
- function isProjectLoaded(state) {
3442
- return state.graph !== null && state.projectRoot !== null;
3443
- }
3444
-
3445
- // src/graph/updater.ts
3446
- import { join as join10 } from "path";
3447
- function removeFileFromGraph(graph, filePath) {
3448
- const nodesToRemove = [];
3449
- graph.forEachNode((node, attrs) => {
3450
- if (attrs.filePath === filePath) {
3451
- nodesToRemove.push(node);
3452
- }
3453
- });
3454
- nodesToRemove.forEach((node) => {
3455
- try {
3456
- graph.dropNode(node);
3457
- } catch (error) {
3458
- }
3459
- });
3460
- }
3461
- function addFileToGraph(graph, parsedFile) {
3462
- for (const symbol of parsedFile.symbols) {
3463
- const nodeId = `${parsedFile.filePath}::${symbol.name}`;
3464
- try {
3465
- graph.addNode(nodeId, {
3466
- name: symbol.name,
3467
- kind: symbol.kind,
3468
- filePath: parsedFile.filePath,
3469
- startLine: symbol.location.startLine,
3470
- endLine: symbol.location.endLine,
3471
- exported: symbol.exported,
3472
- scope: symbol.scope
3473
- });
3474
- } catch (error) {
3475
- }
3476
- }
3477
- for (const edge of parsedFile.edges) {
3478
- try {
3479
- graph.mergeEdge(edge.source, edge.target, {
3480
- kind: edge.kind,
3481
- sourceFile: edge.sourceFile,
3482
- targetFile: edge.targetFile
3483
- });
3484
- } catch (error) {
3485
- }
3486
- }
3487
- }
3488
- async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
3489
- removeFileFromGraph(graph, relativeFilePath);
3490
- const absolutePath = join10(projectRoot, relativeFilePath);
3491
- try {
3492
- const parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
3493
- addFileToGraph(graph, parsedFile);
3494
- } catch (error) {
3495
- console.error(`Failed to parse file ${relativeFilePath}:`, error);
3496
- }
3497
- }
3498
-
3499
3145
  // src/health/metrics.ts
3500
- import { dirname as dirname8 } from "path";
3146
+ import { dirname as dirname7 } from "path";
3501
3147
  function scoreToGrade(score) {
3502
3148
  if (score >= 90) return "A";
3503
3149
  if (score >= 80) return "B";
@@ -3530,8 +3176,8 @@ function calculateCouplingScore(graph) {
3530
3176
  totalEdges++;
3531
3177
  fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
3532
3178
  fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
3533
- const sourceDir = dirname8(sourceAttrs.filePath).split("/")[0];
3534
- const targetDir = dirname8(targetAttrs.filePath).split("/")[0];
3179
+ const sourceDir = dirname7(sourceAttrs.filePath).split("/")[0];
3180
+ const targetDir = dirname7(targetAttrs.filePath).split("/")[0];
3535
3181
  if (sourceDir !== targetDir) {
3536
3182
  crossDirEdges++;
3537
3183
  }
@@ -3578,8 +3224,8 @@ function calculateCohesionScore(graph) {
3578
3224
  const sourceAttrs = graph.getNodeAttributes(source);
3579
3225
  const targetAttrs = graph.getNodeAttributes(target);
3580
3226
  if (sourceAttrs.filePath !== targetAttrs.filePath) {
3581
- const sourceDir = dirname8(sourceAttrs.filePath);
3582
- const targetDir = dirname8(targetAttrs.filePath);
3227
+ const sourceDir = dirname7(sourceAttrs.filePath);
3228
+ const targetDir = dirname7(targetAttrs.filePath);
3583
3229
  if (!dirEdges.has(sourceDir)) {
3584
3230
  dirEdges.set(sourceDir, { internal: 0, total: 0 });
3585
3231
  }
@@ -3862,7 +3508,7 @@ function calculateDepthScore(graph) {
3862
3508
 
3863
3509
  // src/health/index.ts
3864
3510
  import { readFileSync as readFileSync6, writeFileSync, existsSync as existsSync8, mkdirSync } from "fs";
3865
- import { join as join11, dirname as dirname9 } from "path";
3511
+ import { join as join9, dirname as dirname8 } from "path";
3866
3512
  function calculateHealthScore(graph, projectRoot) {
3867
3513
  const coupling = calculateCouplingScore(graph);
3868
3514
  const cohesion = calculateCohesionScore(graph);
@@ -3961,7 +3607,7 @@ function getHealthTrend(projectRoot, currentScore) {
3961
3607
  }
3962
3608
  }
3963
3609
  function saveHealthHistory(projectRoot, report) {
3964
- const historyFile = join11(projectRoot, ".depwire", "health-history.json");
3610
+ const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3965
3611
  const entry = {
3966
3612
  timestamp: report.timestamp,
3967
3613
  score: report.overall,
@@ -3984,11 +3630,11 @@ function saveHealthHistory(projectRoot, report) {
3984
3630
  if (history.length > 50) {
3985
3631
  history = history.slice(-50);
3986
3632
  }
3987
- mkdirSync(dirname9(historyFile), { recursive: true });
3633
+ mkdirSync(dirname8(historyFile), { recursive: true });
3988
3634
  writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
3989
3635
  }
3990
3636
  function loadHealthHistory(projectRoot) {
3991
- const historyFile = join11(projectRoot, ".depwire", "health-history.json");
3637
+ const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3992
3638
  if (!existsSync8(historyFile)) {
3993
3639
  return [];
3994
3640
  }
@@ -4277,8 +3923,8 @@ function generateReason(symbol, confidence) {
4277
3923
  return "Potentially unused";
4278
3924
  }
4279
3925
  function isBarrelFile(filePath) {
4280
- const basename6 = path3.basename(filePath);
4281
- return basename6 === "index.ts" || basename6 === "index.js";
3926
+ const basename4 = path3.basename(filePath);
3927
+ return basename4 === "index.ts" || basename4 === "index.js";
4282
3928
  }
4283
3929
  function isTestFile2(filePath) {
4284
3930
  return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
@@ -4458,10 +4104,10 @@ function filterByConfidence(symbols, minConfidence) {
4458
4104
 
4459
4105
  // src/docs/generator.ts
4460
4106
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync12 } from "fs";
4461
- import { join as join14 } from "path";
4107
+ import { join as join12 } from "path";
4462
4108
 
4463
4109
  // src/docs/architecture.ts
4464
- import { dirname as dirname10 } from "path";
4110
+ import { dirname as dirname9 } from "path";
4465
4111
 
4466
4112
  // src/docs/templates.ts
4467
4113
  function header(text, level = 1) {
@@ -4612,7 +4258,7 @@ function generateModuleStructure(graph) {
4612
4258
  function getDirectoryStats(graph) {
4613
4259
  const dirMap = /* @__PURE__ */ new Map();
4614
4260
  graph.forEachNode((node, attrs) => {
4615
- const dir = dirname10(attrs.filePath);
4261
+ const dir = dirname9(attrs.filePath);
4616
4262
  if (dir === ".") return;
4617
4263
  if (!dirMap.has(dir)) {
4618
4264
  dirMap.set(dir, {
@@ -4637,7 +4283,7 @@ function getDirectoryStats(graph) {
4637
4283
  });
4638
4284
  const filesPerDir = /* @__PURE__ */ new Map();
4639
4285
  graph.forEachNode((node, attrs) => {
4640
- const dir = dirname10(attrs.filePath);
4286
+ const dir = dirname9(attrs.filePath);
4641
4287
  if (!filesPerDir.has(dir)) {
4642
4288
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
4643
4289
  }
@@ -4652,8 +4298,8 @@ function getDirectoryStats(graph) {
4652
4298
  graph.forEachEdge((edge, attrs, source, target) => {
4653
4299
  const sourceAttrs = graph.getNodeAttributes(source);
4654
4300
  const targetAttrs = graph.getNodeAttributes(target);
4655
- const sourceDir = dirname10(sourceAttrs.filePath);
4656
- const targetDir = dirname10(targetAttrs.filePath);
4301
+ const sourceDir = dirname9(sourceAttrs.filePath);
4302
+ const targetDir = dirname9(targetAttrs.filePath);
4657
4303
  if (sourceDir !== targetDir) {
4658
4304
  if (!dirEdges.has(sourceDir)) {
4659
4305
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -4860,7 +4506,7 @@ function detectCycles(graph) {
4860
4506
  }
4861
4507
 
4862
4508
  // src/docs/conventions.ts
4863
- import { basename as basename2, extname as extname4 } from "path";
4509
+ import { basename, extname as extname4 } from "path";
4864
4510
  function generateConventions(graph, projectRoot, version) {
4865
4511
  let output = "";
4866
4512
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -4898,7 +4544,7 @@ function generateFileOrganization(graph) {
4898
4544
  graph.forEachNode((node, attrs) => {
4899
4545
  if (!files.has(attrs.filePath)) {
4900
4546
  files.add(attrs.filePath);
4901
- const fileName = basename2(attrs.filePath);
4547
+ const fileName = basename(attrs.filePath);
4902
4548
  if (fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx") {
4903
4549
  barrelFileCount++;
4904
4550
  }
@@ -4957,7 +4603,7 @@ function generateNamingPatterns(graph) {
4957
4603
  graph.forEachNode((node, attrs) => {
4958
4604
  if (!files.has(attrs.filePath)) {
4959
4605
  files.add(attrs.filePath);
4960
- const fileName = basename2(attrs.filePath, extname4(attrs.filePath));
4606
+ const fileName = basename(attrs.filePath, extname4(attrs.filePath));
4961
4607
  if (isCamelCase(fileName)) patterns.files.camelCase++;
4962
4608
  else if (isPascalCase(fileName)) patterns.files.PascalCase++;
4963
4609
  else if (isKebabCase(fileName)) patterns.files.kebabCase++;
@@ -5634,7 +5280,7 @@ function detectCyclesDetailed(graph) {
5634
5280
  }
5635
5281
 
5636
5282
  // src/docs/onboarding.ts
5637
- import { dirname as dirname11 } from "path";
5283
+ import { dirname as dirname10 } from "path";
5638
5284
  function generateOnboarding(graph, projectRoot, version) {
5639
5285
  let output = "";
5640
5286
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -5693,7 +5339,7 @@ function generateQuickOrientation(graph) {
5693
5339
  const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
5694
5340
  const dirs = /* @__PURE__ */ new Set();
5695
5341
  graph.forEachNode((node, attrs) => {
5696
- const dir = dirname11(attrs.filePath);
5342
+ const dir = dirname10(attrs.filePath);
5697
5343
  if (dir !== ".") {
5698
5344
  const topLevel = dir.split("/")[0];
5699
5345
  dirs.add(topLevel);
@@ -5797,7 +5443,7 @@ function generateModuleMap(graph) {
5797
5443
  function getDirectoryStats2(graph) {
5798
5444
  const dirMap = /* @__PURE__ */ new Map();
5799
5445
  graph.forEachNode((node, attrs) => {
5800
- const dir = dirname11(attrs.filePath);
5446
+ const dir = dirname10(attrs.filePath);
5801
5447
  if (dir === ".") return;
5802
5448
  if (!dirMap.has(dir)) {
5803
5449
  dirMap.set(dir, {
@@ -5812,7 +5458,7 @@ function getDirectoryStats2(graph) {
5812
5458
  });
5813
5459
  const filesPerDir = /* @__PURE__ */ new Map();
5814
5460
  graph.forEachNode((node, attrs) => {
5815
- const dir = dirname11(attrs.filePath);
5461
+ const dir = dirname10(attrs.filePath);
5816
5462
  if (!filesPerDir.has(dir)) {
5817
5463
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
5818
5464
  }
@@ -5827,8 +5473,8 @@ function getDirectoryStats2(graph) {
5827
5473
  graph.forEachEdge((edge, attrs, source, target) => {
5828
5474
  const sourceAttrs = graph.getNodeAttributes(source);
5829
5475
  const targetAttrs = graph.getNodeAttributes(target);
5830
- const sourceDir = dirname11(sourceAttrs.filePath);
5831
- const targetDir = dirname11(targetAttrs.filePath);
5476
+ const sourceDir = dirname10(sourceAttrs.filePath);
5477
+ const targetDir = dirname10(targetAttrs.filePath);
5832
5478
  if (sourceDir !== targetDir) {
5833
5479
  if (!dirEdges.has(sourceDir)) {
5834
5480
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -5909,7 +5555,7 @@ function detectClusters(graph) {
5909
5555
  const dirFiles = /* @__PURE__ */ new Map();
5910
5556
  const fileEdges = /* @__PURE__ */ new Map();
5911
5557
  graph.forEachNode((node, attrs) => {
5912
- const dir = dirname11(attrs.filePath);
5558
+ const dir = dirname10(attrs.filePath);
5913
5559
  if (!dirFiles.has(dir)) {
5914
5560
  dirFiles.set(dir, /* @__PURE__ */ new Set());
5915
5561
  }
@@ -5963,8 +5609,8 @@ function inferClusterName(files) {
5963
5609
  if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
5964
5610
  return capitalizeFirst2(sortedWords[0][0]);
5965
5611
  }
5966
- const commonDir = dirname11(files[0]);
5967
- if (files.every((f) => dirname11(f) === commonDir)) {
5612
+ const commonDir = dirname10(files[0]);
5613
+ if (files.every((f) => dirname10(f) === commonDir)) {
5968
5614
  return capitalizeFirst2(commonDir.split("/").pop() || "Core");
5969
5615
  }
5970
5616
  return "Core";
@@ -6022,7 +5668,7 @@ function generateDepwireUsage(projectRoot) {
6022
5668
  }
6023
5669
 
6024
5670
  // src/docs/files.ts
6025
- import { dirname as dirname12, basename as basename3 } from "path";
5671
+ import { dirname as dirname11, basename as basename2 } from "path";
6026
5672
  function generateFiles(graph, projectRoot, version) {
6027
5673
  let output = "";
6028
5674
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -6126,7 +5772,7 @@ function generateDirectoryBreakdown(graph) {
6126
5772
  const fileStats = getFileStats2(graph);
6127
5773
  const dirMap = /* @__PURE__ */ new Map();
6128
5774
  for (const file of fileStats) {
6129
- const dir = dirname12(file.filePath);
5775
+ const dir = dirname11(file.filePath);
6130
5776
  const topDir = dir === "." ? "." : dir.split("/")[0];
6131
5777
  if (!dirMap.has(topDir)) {
6132
5778
  dirMap.set(topDir, {
@@ -6141,7 +5787,7 @@ function generateDirectoryBreakdown(graph) {
6141
5787
  dirStats.symbolCount += file.symbolCount;
6142
5788
  if (file.totalConnections > dirStats.maxConnections) {
6143
5789
  dirStats.maxConnections = file.totalConnections;
6144
- dirStats.mostConnectedFile = basename3(file.filePath);
5790
+ dirStats.mostConnectedFile = basename2(file.filePath);
6145
5791
  }
6146
5792
  }
6147
5793
  if (dirMap.size === 0) {
@@ -6769,7 +6415,7 @@ function generateRecommendations(graph) {
6769
6415
  }
6770
6416
 
6771
6417
  // src/docs/tests.ts
6772
- import { basename as basename4, dirname as dirname13 } from "path";
6418
+ import { basename as basename3, dirname as dirname12 } from "path";
6773
6419
  function generateTests(graph, projectRoot, version) {
6774
6420
  let output = "";
6775
6421
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -6797,8 +6443,8 @@ function getFileCount8(graph) {
6797
6443
  return files.size;
6798
6444
  }
6799
6445
  function isTestFile3(filePath) {
6800
- const fileName = basename4(filePath).toLowerCase();
6801
- const dirPath = dirname13(filePath).toLowerCase();
6446
+ const fileName = basename3(filePath).toLowerCase();
6447
+ const dirPath = dirname12(filePath).toLowerCase();
6802
6448
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
6803
6449
  return true;
6804
6450
  }
@@ -6856,13 +6502,13 @@ function generateTestFileInventory(graph) {
6856
6502
  return output;
6857
6503
  }
6858
6504
  function matchTestToSource(testFile) {
6859
- const testFileName = basename4(testFile);
6860
- const testDir = dirname13(testFile);
6505
+ const testFileName = basename3(testFile);
6506
+ const testDir = dirname12(testFile);
6861
6507
  let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
6862
6508
  const possiblePaths = [];
6863
6509
  possiblePaths.push(testDir + "/" + sourceFileName);
6864
6510
  if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
6865
- const parentDir = dirname13(testDir);
6511
+ const parentDir = dirname12(testDir);
6866
6512
  possiblePaths.push(parentDir + "/" + sourceFileName);
6867
6513
  }
6868
6514
  if (testDir.includes("test")) {
@@ -7018,7 +6664,7 @@ function generateTestCoverageMap(graph) {
7018
6664
  const rows = mappings.slice(0, 30).map((m) => [
7019
6665
  `\`${m.sourceFile}\``,
7020
6666
  m.hasTest ? "\u2705" : "\u274C",
7021
- m.testFile ? `\`${basename4(m.testFile)}\`` : "-",
6667
+ m.testFile ? `\`${basename3(m.testFile)}\`` : "-",
7022
6668
  formatNumber(m.symbolCount)
7023
6669
  ]);
7024
6670
  let output = table(headers, rows);
@@ -7059,7 +6705,7 @@ function generateTestStatistics(graph) {
7059
6705
  `;
7060
6706
  const dirTestCoverage = /* @__PURE__ */ new Map();
7061
6707
  for (const sourceFile of sourceFiles) {
7062
- const dir = dirname13(sourceFile).split("/")[0];
6708
+ const dir = dirname12(sourceFile).split("/")[0];
7063
6709
  if (!dirTestCoverage.has(dir)) {
7064
6710
  dirTestCoverage.set(dir, { total: 0, tested: 0 });
7065
6711
  }
@@ -7082,7 +6728,7 @@ function generateTestStatistics(graph) {
7082
6728
  }
7083
6729
 
7084
6730
  // src/docs/history.ts
7085
- import { dirname as dirname14 } from "path";
6731
+ import { dirname as dirname13 } from "path";
7086
6732
  import { execSync } from "child_process";
7087
6733
  function generateHistory(graph, projectRoot, version) {
7088
6734
  let output = "";
@@ -7363,7 +7009,7 @@ function generateFeatureClusters(graph) {
7363
7009
  const dirFiles = /* @__PURE__ */ new Map();
7364
7010
  const fileEdges = /* @__PURE__ */ new Map();
7365
7011
  graph.forEachNode((node, attrs) => {
7366
- const dir = dirname14(attrs.filePath);
7012
+ const dir = dirname13(attrs.filePath);
7367
7013
  if (!dirFiles.has(dir)) {
7368
7014
  dirFiles.set(dir, /* @__PURE__ */ new Set());
7369
7015
  }
@@ -7445,7 +7091,7 @@ function capitalizeFirst3(str) {
7445
7091
  }
7446
7092
 
7447
7093
  // src/docs/current.ts
7448
- import { dirname as dirname15 } from "path";
7094
+ import { dirname as dirname14 } from "path";
7449
7095
  function generateCurrent(graph, projectRoot, version) {
7450
7096
  let output = "";
7451
7097
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -7583,7 +7229,7 @@ function generateCompleteFileIndex(graph) {
7583
7229
  fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
7584
7230
  const dirGroups = /* @__PURE__ */ new Map();
7585
7231
  for (const info of fileInfos) {
7586
- const dir = dirname15(info.filePath);
7232
+ const dir = dirname14(info.filePath);
7587
7233
  const topDir = dir === "." ? "root" : dir.split("/")[0];
7588
7234
  if (!dirGroups.has(topDir)) {
7589
7235
  dirGroups.set(topDir, []);
@@ -7795,7 +7441,7 @@ function getTopLevelDir2(filePath) {
7795
7441
 
7796
7442
  // src/docs/status.ts
7797
7443
  import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
7798
- import { join as join12 } from "path";
7444
+ import { join as join10 } from "path";
7799
7445
  function generateStatus(graph, projectRoot, version) {
7800
7446
  let output = "";
7801
7447
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -7828,7 +7474,7 @@ function getFileCount11(graph) {
7828
7474
  }
7829
7475
  function extractComments(projectRoot, filePath) {
7830
7476
  const comments = [];
7831
- const fullPath = join12(projectRoot, filePath);
7477
+ const fullPath = join10(projectRoot, filePath);
7832
7478
  if (!existsSync10(fullPath)) {
7833
7479
  return comments;
7834
7480
  }
@@ -8413,9 +8059,9 @@ function generateConfidenceSection(title, description, symbols, projectRoot) {
8413
8059
 
8414
8060
  // src/docs/metadata.ts
8415
8061
  import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
8416
- import { join as join13 } from "path";
8062
+ import { join as join11 } from "path";
8417
8063
  function loadMetadata(outputDir) {
8418
- const metadataPath = join13(outputDir, "metadata.json");
8064
+ const metadataPath = join11(outputDir, "metadata.json");
8419
8065
  if (!existsSync11(metadataPath)) {
8420
8066
  return null;
8421
8067
  }
@@ -8428,7 +8074,7 @@ function loadMetadata(outputDir) {
8428
8074
  }
8429
8075
  }
8430
8076
  function saveMetadata(outputDir, metadata) {
8431
- const metadataPath = join13(outputDir, "metadata.json");
8077
+ const metadataPath = join11(outputDir, "metadata.json");
8432
8078
  writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
8433
8079
  }
8434
8080
  function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
@@ -8510,7 +8156,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8510
8156
  try {
8511
8157
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
8512
8158
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
8513
- const filePath = join14(options.outputDir, "ARCHITECTURE.md");
8159
+ const filePath = join12(options.outputDir, "ARCHITECTURE.md");
8514
8160
  writeFileSync3(filePath, content, "utf-8");
8515
8161
  generated.push("ARCHITECTURE.md");
8516
8162
  } catch (err) {
@@ -8521,7 +8167,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8521
8167
  try {
8522
8168
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
8523
8169
  const content = generateConventions(graph, projectRoot, version);
8524
- const filePath = join14(options.outputDir, "CONVENTIONS.md");
8170
+ const filePath = join12(options.outputDir, "CONVENTIONS.md");
8525
8171
  writeFileSync3(filePath, content, "utf-8");
8526
8172
  generated.push("CONVENTIONS.md");
8527
8173
  } catch (err) {
@@ -8532,7 +8178,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8532
8178
  try {
8533
8179
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
8534
8180
  const content = generateDependencies(graph, projectRoot, version);
8535
- const filePath = join14(options.outputDir, "DEPENDENCIES.md");
8181
+ const filePath = join12(options.outputDir, "DEPENDENCIES.md");
8536
8182
  writeFileSync3(filePath, content, "utf-8");
8537
8183
  generated.push("DEPENDENCIES.md");
8538
8184
  } catch (err) {
@@ -8543,7 +8189,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8543
8189
  try {
8544
8190
  if (options.verbose) console.log("Generating ONBOARDING.md...");
8545
8191
  const content = generateOnboarding(graph, projectRoot, version);
8546
- const filePath = join14(options.outputDir, "ONBOARDING.md");
8192
+ const filePath = join12(options.outputDir, "ONBOARDING.md");
8547
8193
  writeFileSync3(filePath, content, "utf-8");
8548
8194
  generated.push("ONBOARDING.md");
8549
8195
  } catch (err) {
@@ -8554,7 +8200,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8554
8200
  try {
8555
8201
  if (options.verbose) console.log("Generating FILES.md...");
8556
8202
  const content = generateFiles(graph, projectRoot, version);
8557
- const filePath = join14(options.outputDir, "FILES.md");
8203
+ const filePath = join12(options.outputDir, "FILES.md");
8558
8204
  writeFileSync3(filePath, content, "utf-8");
8559
8205
  generated.push("FILES.md");
8560
8206
  } catch (err) {
@@ -8565,7 +8211,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8565
8211
  try {
8566
8212
  if (options.verbose) console.log("Generating API_SURFACE.md...");
8567
8213
  const content = generateApiSurface(graph, projectRoot, version);
8568
- const filePath = join14(options.outputDir, "API_SURFACE.md");
8214
+ const filePath = join12(options.outputDir, "API_SURFACE.md");
8569
8215
  writeFileSync3(filePath, content, "utf-8");
8570
8216
  generated.push("API_SURFACE.md");
8571
8217
  } catch (err) {
@@ -8576,7 +8222,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8576
8222
  try {
8577
8223
  if (options.verbose) console.log("Generating ERRORS.md...");
8578
8224
  const content = generateErrors(graph, projectRoot, version);
8579
- const filePath = join14(options.outputDir, "ERRORS.md");
8225
+ const filePath = join12(options.outputDir, "ERRORS.md");
8580
8226
  writeFileSync3(filePath, content, "utf-8");
8581
8227
  generated.push("ERRORS.md");
8582
8228
  } catch (err) {
@@ -8587,7 +8233,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8587
8233
  try {
8588
8234
  if (options.verbose) console.log("Generating TESTS.md...");
8589
8235
  const content = generateTests(graph, projectRoot, version);
8590
- const filePath = join14(options.outputDir, "TESTS.md");
8236
+ const filePath = join12(options.outputDir, "TESTS.md");
8591
8237
  writeFileSync3(filePath, content, "utf-8");
8592
8238
  generated.push("TESTS.md");
8593
8239
  } catch (err) {
@@ -8598,7 +8244,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8598
8244
  try {
8599
8245
  if (options.verbose) console.log("Generating HISTORY.md...");
8600
8246
  const content = generateHistory(graph, projectRoot, version);
8601
- const filePath = join14(options.outputDir, "HISTORY.md");
8247
+ const filePath = join12(options.outputDir, "HISTORY.md");
8602
8248
  writeFileSync3(filePath, content, "utf-8");
8603
8249
  generated.push("HISTORY.md");
8604
8250
  } catch (err) {
@@ -8609,7 +8255,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8609
8255
  try {
8610
8256
  if (options.verbose) console.log("Generating CURRENT.md...");
8611
8257
  const content = generateCurrent(graph, projectRoot, version);
8612
- const filePath = join14(options.outputDir, "CURRENT.md");
8258
+ const filePath = join12(options.outputDir, "CURRENT.md");
8613
8259
  writeFileSync3(filePath, content, "utf-8");
8614
8260
  generated.push("CURRENT.md");
8615
8261
  } catch (err) {
@@ -8620,7 +8266,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8620
8266
  try {
8621
8267
  if (options.verbose) console.log("Generating STATUS.md...");
8622
8268
  const content = generateStatus(graph, projectRoot, version);
8623
- const filePath = join14(options.outputDir, "STATUS.md");
8269
+ const filePath = join12(options.outputDir, "STATUS.md");
8624
8270
  writeFileSync3(filePath, content, "utf-8");
8625
8271
  generated.push("STATUS.md");
8626
8272
  } catch (err) {
@@ -8631,7 +8277,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8631
8277
  try {
8632
8278
  if (options.verbose) console.log("Generating HEALTH.md...");
8633
8279
  const content = generateHealth(graph, projectRoot, version);
8634
- const filePath = join14(options.outputDir, "HEALTH.md");
8280
+ const filePath = join12(options.outputDir, "HEALTH.md");
8635
8281
  writeFileSync3(filePath, content, "utf-8");
8636
8282
  generated.push("HEALTH.md");
8637
8283
  } catch (err) {
@@ -8642,7 +8288,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
8642
8288
  try {
8643
8289
  if (options.verbose) console.log("Generating DEAD_CODE.md...");
8644
8290
  const content = generateDeadCode(graph, projectRoot, version);
8645
- const filePath = join14(options.outputDir, "DEAD_CODE.md");
8291
+ const filePath = join12(options.outputDir, "DEAD_CODE.md");
8646
8292
  writeFileSync3(filePath, content, "utf-8");
8647
8293
  generated.push("DEAD_CODE.md");
8648
8294
  } catch (err) {
@@ -8685,1492 +8331,354 @@ function getFileCount13(graph) {
8685
8331
  return files.size;
8686
8332
  }
8687
8333
 
8688
- // src/mcp/server.ts
8689
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8690
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8691
-
8692
- // src/mcp/tools.ts
8693
- import { dirname as dirname16, join as join17 } from "path";
8694
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
8695
-
8696
- // src/mcp/connect.ts
8697
- import simpleGit from "simple-git";
8698
- import { existsSync as existsSync13 } from "fs";
8699
- import { join as join15, basename as basename5, resolve as resolve2 } from "path";
8700
- import { tmpdir, homedir } from "os";
8701
- function validateProjectPath(source) {
8702
- const resolved = resolve2(source);
8703
- const blockedPaths = [
8704
- "/etc",
8705
- "/var",
8706
- "/usr",
8707
- "/bin",
8708
- "/sbin",
8709
- "/boot",
8710
- "/proc",
8711
- "/sys",
8712
- join15(homedir(), ".ssh"),
8713
- join15(homedir(), ".gnupg"),
8714
- join15(homedir(), ".aws"),
8715
- join15(homedir(), ".config"),
8716
- join15(homedir(), ".env")
8717
- ];
8718
- for (const blocked of blockedPaths) {
8719
- if (resolved.startsWith(blocked)) {
8720
- return { valid: false, error: `Access denied: ${blocked} is a protected path` };
8721
- }
8722
- }
8723
- return { valid: true };
8334
+ // src/simulation/engine.ts
8335
+ import { dirname as dirname15, join as join13 } from "path";
8336
+ function normalizePath(p) {
8337
+ return p.replace(/^\.\//, "").replace(/\/+$/, "");
8724
8338
  }
8725
- async function connectToRepo(source, subdirectory, state) {
8726
- try {
8727
- let projectRoot;
8728
- let projectName;
8729
- const isGitHub = source.startsWith("https://github.com/") || source.startsWith("git@github.com:");
8730
- if (isGitHub) {
8731
- const match = source.match(/[\/:]([^\/]+?)(?:\.git)?$/);
8732
- if (!match) {
8733
- return {
8734
- error: "Invalid GitHub URL",
8735
- message: "Could not parse repository name from URL"
8736
- };
8737
- }
8738
- projectName = match[1];
8739
- const reposDir = join15(tmpdir(), "depwire-repos");
8740
- const cloneDir = join15(reposDir, projectName);
8741
- console.error(`Connecting to GitHub repo: ${source}`);
8742
- const git = simpleGit();
8743
- if (existsSync13(cloneDir)) {
8744
- console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
8745
- try {
8746
- await git.cwd(cloneDir).pull();
8747
- } catch (error) {
8748
- console.error(`Pull failed, using existing clone: ${error}`);
8749
- }
8750
- } else {
8751
- console.error(`Cloning ${source} to ${cloneDir}...`);
8752
- try {
8753
- await git.clone(source, cloneDir, ["--depth", "1", "--no-recurse-submodules", "--single-branch"]);
8754
- } catch (error) {
8755
- return {
8756
- error: "Failed to clone repository",
8757
- message: `Git clone failed: ${error}. Ensure git is installed and the URL is correct.`
8758
- };
8759
- }
8760
- }
8761
- projectRoot = subdirectory ? join15(cloneDir, subdirectory) : cloneDir;
8762
- } else {
8763
- const validation2 = validateProjectPath(source);
8764
- if (!validation2.valid) {
8765
- return {
8766
- error: "Access denied",
8767
- message: validation2.error
8768
- };
8769
- }
8770
- if (!existsSync13(source)) {
8771
- return {
8772
- error: "Directory not found",
8773
- message: `Directory does not exist: ${source}`
8774
- };
8775
- }
8776
- projectRoot = subdirectory ? join15(source, subdirectory) : source;
8777
- projectName = basename5(projectRoot);
8778
- }
8779
- const validation = validateProjectPath(projectRoot);
8780
- if (!validation.valid) {
8781
- return {
8782
- error: "Access denied",
8783
- message: validation.error
8784
- };
8785
- }
8786
- if (!existsSync13(projectRoot)) {
8787
- return {
8788
- error: "Project root not found",
8789
- message: `Directory does not exist: ${projectRoot}`
8790
- };
8791
- }
8792
- console.error(`Parsing project at ${projectRoot}...`);
8793
- if (state.watcher) {
8794
- console.error("Stopping previous file watcher...");
8795
- await state.watcher.close();
8796
- state.watcher = null;
8339
+ function fileMatch(nodeFilePath, target) {
8340
+ const a = normalizePath(nodeFilePath);
8341
+ const b = normalizePath(target);
8342
+ return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
8343
+ }
8344
+ var SimulationEngine = class {
8345
+ original;
8346
+ constructor(graph) {
8347
+ this.original = graph;
8348
+ }
8349
+ simulate(action) {
8350
+ const clone = this.original.copy();
8351
+ const brokenImports = [];
8352
+ switch (action.type) {
8353
+ case "move":
8354
+ this.applyMove(clone, action.target, action.destination, brokenImports);
8355
+ break;
8356
+ case "delete":
8357
+ this.applyDelete(clone, action.target, brokenImports);
8358
+ break;
8359
+ case "rename":
8360
+ this.applyRename(clone, action.target, action.newName, brokenImports);
8361
+ break;
8362
+ case "split":
8363
+ this.applySplit(clone, action.target, action.newFile, action.symbols, brokenImports);
8364
+ break;
8365
+ case "merge":
8366
+ this.applyMerge(clone, action.target, action.source, brokenImports);
8367
+ break;
8797
8368
  }
8798
- const parsedFiles = await parseProject(projectRoot);
8799
- if (parsedFiles.length === 0) {
8369
+ const diff = this.computeDiff(this.original, clone, brokenImports);
8370
+ const beforeHealth = this.computeHealthScore(this.original);
8371
+ const afterHealth = this.computeHealthScore(clone);
8372
+ const dimensionChanges = beforeHealth.dimensions.map((bd, i) => {
8373
+ const ad = afterHealth.dimensions[i];
8800
8374
  return {
8801
- error: "No source files found",
8802
- message: `No supported source files (.ts, .tsx, .js, .jsx, .py, .go) found in ${projectRoot}`
8375
+ name: bd.name,
8376
+ before: bd.score,
8377
+ after: ad ? ad.score : bd.score,
8378
+ delta: (ad ? ad.score : bd.score) - bd.score
8803
8379
  };
8804
- }
8805
- const graph = buildGraph(parsedFiles);
8806
- state.graph = graph;
8807
- state.projectRoot = projectRoot;
8808
- state.projectName = projectName;
8809
- console.error(`Parsed ${parsedFiles.length} files`);
8810
- console.error("Starting file watcher...");
8811
- state.watcher = watchProject(projectRoot, {
8812
- onFileChanged: async (filePath) => {
8813
- console.error(`File changed: ${filePath}`);
8814
- try {
8815
- await updateFileInGraph(state.graph, projectRoot, filePath);
8816
- console.error(`Graph updated for ${filePath}`);
8817
- } catch (error) {
8818
- console.error(`Failed to update graph for ${filePath}: ${error}`);
8819
- }
8820
- },
8821
- onFileAdded: async (filePath) => {
8822
- console.error(`File added: ${filePath}`);
8823
- try {
8824
- await updateFileInGraph(state.graph, projectRoot, filePath);
8825
- console.error(`Graph updated for ${filePath}`);
8826
- } catch (error) {
8827
- console.error(`Failed to update graph for ${filePath}: ${error}`);
8828
- }
8829
- },
8830
- onFileDeleted: (filePath) => {
8831
- console.error(`File deleted: ${filePath}`);
8832
- try {
8833
- const fileNodes = state.graph.filterNodes(
8834
- (node, attrs) => attrs.filePath === filePath
8835
- );
8836
- fileNodes.forEach((node) => state.graph.dropNode(node));
8837
- console.error(`Removed ${filePath} from graph`);
8838
- } catch (error) {
8839
- console.error(`Failed to remove ${filePath} from graph: ${error}`);
8840
- }
8841
- }
8842
- });
8843
- const summary = getArchitectureSummary(graph);
8844
- const mostConnected = summary.mostConnectedFiles.slice(0, 3);
8845
- const languageBreakdown = {};
8846
- parsedFiles.forEach((file) => {
8847
- const ext = file.filePath.toLowerCase();
8848
- let lang;
8849
- if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
8850
- lang = "typescript";
8851
- } else if (ext.endsWith(".py")) {
8852
- lang = "python";
8853
- } else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
8854
- lang = "javascript";
8855
- } else if (ext.endsWith(".go")) {
8856
- lang = "go";
8857
- } else {
8858
- lang = "other";
8859
- }
8860
- languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
8861
8380
  });
8862
- return {
8863
- connected: true,
8864
- projectRoot,
8865
- projectName,
8866
- stats: {
8867
- files: summary.totalFiles,
8868
- symbols: summary.totalSymbols,
8869
- edges: summary.totalEdges,
8870
- crossFileEdges: summary.crossFileEdges,
8871
- languages: languageBreakdown
8872
- },
8873
- mostConnectedFiles: mostConnected.map((f) => ({
8874
- path: f.filePath,
8875
- connections: f.incomingCount + f.outgoingCount
8876
- })),
8877
- summary: `Connected to ${projectName}. Found ${summary.totalFiles} files with ${summary.totalSymbols} symbols and ${summary.crossFileEdges} cross-file edges.`
8381
+ const healthDelta = {
8382
+ before: beforeHealth.score,
8383
+ after: afterHealth.score,
8384
+ delta: afterHealth.score - beforeHealth.score,
8385
+ improved: afterHealth.score > beforeHealth.score,
8386
+ dimensionChanges
8878
8387
  };
8879
- } catch (error) {
8880
- console.error("Error in connectToRepo:", error);
8881
8388
  return {
8882
- error: "Connection failed",
8883
- message: String(error)
8389
+ action,
8390
+ originalGraph: {
8391
+ nodeCount: this.original.order,
8392
+ edgeCount: this.original.size,
8393
+ healthScore: beforeHealth.score
8394
+ },
8395
+ simulatedGraph: {
8396
+ nodeCount: clone.order,
8397
+ edgeCount: clone.size,
8398
+ healthScore: afterHealth.score
8399
+ },
8400
+ diff,
8401
+ healthDelta
8884
8402
  };
8885
8403
  }
8886
- }
8887
-
8888
- // src/temporal/git.ts
8889
- import { execSync as execSync2 } from "child_process";
8890
- async function getCommitLog(dir, limit) {
8891
- try {
8892
- const limitArg = limit ? `-n ${limit}` : "";
8893
- const output = execSync2(
8894
- `git log ${limitArg} --pretty=format:"%H|%aI|%s|%an"`,
8895
- { cwd: dir, encoding: "utf-8" }
8404
+ // ── Action implementations ─────────────────────────────────────
8405
+ applyMove(clone, target, destination, brokenImports) {
8406
+ const normalizedTarget = normalizePath(target);
8407
+ const normalizedDest = normalizePath(destination);
8408
+ const nodesToMove = clone.filterNodes(
8409
+ (_node, attrs) => fileMatch(attrs.filePath, target)
8896
8410
  );
8897
- if (!output.trim()) {
8898
- return [];
8411
+ if (nodesToMove.length === 0) return;
8412
+ for (const oldId of nodesToMove) {
8413
+ const attrs = clone.getNodeAttributes(oldId);
8414
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
8415
+ const newId = `${normalizedDest}::${symbolName}`;
8416
+ clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
8417
+ const sourceAttrs = clone.getNodeAttributes(source);
8418
+ if (!fileMatch(sourceAttrs.filePath, target)) {
8419
+ brokenImports.push({
8420
+ file: sourceAttrs.filePath,
8421
+ importedSymbol: attrs.name,
8422
+ reason: `imports ${attrs.name} from ${target} (path would break)`
8423
+ });
8424
+ }
8425
+ });
8426
+ if (!clone.hasNode(newId)) {
8427
+ clone.addNode(newId, { ...attrs, filePath: normalizedDest });
8428
+ }
8429
+ clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
8430
+ const newSource = nodesToMove.includes(source) ? `${normalizedDest}::${source.includes("::") ? source.split("::").slice(1).join("::") : clone.getNodeAttributes(source).name}` : source;
8431
+ if (clone.hasNode(newSource) && clone.hasNode(newId)) {
8432
+ clone.mergeEdge(newSource, newId, edgeAttrs);
8433
+ }
8434
+ });
8435
+ clone.forEachOutEdge(oldId, (edge, edgeAttrs, _source, outTarget) => {
8436
+ const newTarget = nodesToMove.includes(outTarget) ? `${normalizedDest}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
8437
+ if (clone.hasNode(newId) && clone.hasNode(newTarget)) {
8438
+ clone.mergeEdge(newId, newTarget, edgeAttrs);
8439
+ }
8440
+ });
8441
+ clone.dropNode(oldId);
8899
8442
  }
8900
- return output.trim().split("\n").map((line) => {
8901
- const [hash, date, message, author] = line.split("|");
8902
- return { hash, date, message, author };
8903
- });
8904
- } catch (error) {
8905
- throw new Error(`Failed to get git log: ${error}`);
8906
8443
  }
8907
- }
8908
- async function getCurrentBranch(dir) {
8909
- try {
8910
- return execSync2("git rev-parse --abbrev-ref HEAD", {
8911
- cwd: dir,
8912
- encoding: "utf-8"
8913
- }).trim();
8914
- } catch (error) {
8915
- throw new Error(`Failed to get current branch: ${error}`);
8444
+ applyDelete(clone, target, brokenImports) {
8445
+ const nodesToDelete = clone.filterNodes(
8446
+ (_node, attrs) => fileMatch(attrs.filePath, target)
8447
+ );
8448
+ for (const nodeId of nodesToDelete) {
8449
+ const attrs = clone.getNodeAttributes(nodeId);
8450
+ clone.forEachInEdge(nodeId, (_edge, _edgeAttrs, source) => {
8451
+ const sourceAttrs = clone.getNodeAttributes(source);
8452
+ if (!fileMatch(sourceAttrs.filePath, target)) {
8453
+ brokenImports.push({
8454
+ file: sourceAttrs.filePath,
8455
+ importedSymbol: attrs.name,
8456
+ reason: `imports ${attrs.name} from ${target} (file deleted)`
8457
+ });
8458
+ }
8459
+ });
8460
+ }
8461
+ for (const nodeId of nodesToDelete) {
8462
+ clone.dropNode(nodeId);
8463
+ }
8916
8464
  }
8917
- }
8918
- async function checkoutCommit(dir, hash) {
8919
- try {
8920
- execSync2(`git checkout -q ${hash}`, { cwd: dir, stdio: "ignore" });
8921
- } catch (error) {
8922
- throw new Error(`Failed to checkout commit ${hash}: ${error}`);
8465
+ applyRename(clone, target, newName, brokenImports) {
8466
+ const destination = join13(dirname15(target), newName);
8467
+ this.applyMove(clone, target, destination, brokenImports);
8923
8468
  }
8924
- }
8925
- async function restoreOriginal(dir, originalBranch) {
8926
- try {
8927
- execSync2(`git checkout -q ${originalBranch}`, {
8928
- cwd: dir,
8929
- stdio: "ignore"
8469
+ applySplit(clone, target, newFile, symbols, brokenImports) {
8470
+ const normalizedNewFile = normalizePath(newFile);
8471
+ const nodesToSplit = clone.filterNodes((_node, attrs) => {
8472
+ return fileMatch(attrs.filePath, target) && symbols.includes(attrs.name);
8930
8473
  });
8931
- } catch (error) {
8932
- throw new Error(`Failed to restore branch ${originalBranch}: ${error}`);
8933
- }
8934
- }
8935
- async function stashChanges(dir) {
8936
- try {
8937
- const status = execSync2("git status --porcelain", {
8938
- cwd: dir,
8939
- encoding: "utf-8"
8940
- }).trim();
8941
- if (status) {
8942
- execSync2('git stash push -q -m "depwire temporal analysis"', {
8943
- cwd: dir,
8944
- stdio: "ignore"
8474
+ if (nodesToSplit.length === 0) return;
8475
+ for (const oldId of nodesToSplit) {
8476
+ const attrs = clone.getNodeAttributes(oldId);
8477
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
8478
+ const newId = `${normalizedNewFile}::${symbolName}`;
8479
+ clone.forEachInEdge(oldId, (_edge, _edgeAttrs, source) => {
8480
+ const sourceAttrs = clone.getNodeAttributes(source);
8481
+ if (!fileMatch(sourceAttrs.filePath, target) && !fileMatch(sourceAttrs.filePath, newFile)) {
8482
+ brokenImports.push({
8483
+ file: sourceAttrs.filePath,
8484
+ importedSymbol: attrs.name,
8485
+ reason: `imports ${attrs.name} from ${target} (symbol moved to ${newFile})`
8486
+ });
8487
+ }
8945
8488
  });
8946
- return true;
8489
+ if (!clone.hasNode(newId)) {
8490
+ clone.addNode(newId, { ...attrs, filePath: normalizedNewFile });
8491
+ }
8492
+ clone.forEachInEdge(oldId, (_edge, edgeAttrs, source) => {
8493
+ if (clone.hasNode(source) && clone.hasNode(newId)) {
8494
+ clone.mergeEdge(source, newId, edgeAttrs);
8495
+ }
8496
+ });
8497
+ clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _source, outTarget) => {
8498
+ if (clone.hasNode(newId) && clone.hasNode(outTarget)) {
8499
+ clone.mergeEdge(newId, outTarget, edgeAttrs);
8500
+ }
8501
+ });
8502
+ clone.dropNode(oldId);
8947
8503
  }
8948
- return false;
8949
- } catch (error) {
8950
- throw new Error(`Failed to stash changes: ${error}`);
8951
8504
  }
8952
- }
8953
- async function popStash(dir) {
8954
- try {
8955
- const stashList = execSync2("git stash list", {
8956
- cwd: dir,
8957
- encoding: "utf-8",
8958
- stdio: ["pipe", "pipe", "ignore"]
8959
- // Suppress stderr
8960
- }).trim();
8961
- if (stashList) {
8962
- execSync2("git stash pop -q", { cwd: dir, stdio: "ignore" });
8505
+ applyMerge(clone, target, source, brokenImports) {
8506
+ const normalizedTarget = normalizePath(target);
8507
+ const sourceNodes = clone.filterNodes(
8508
+ (_node, attrs) => fileMatch(attrs.filePath, source)
8509
+ );
8510
+ const targetNodes = clone.filterNodes(
8511
+ (_node, attrs) => fileMatch(attrs.filePath, target)
8512
+ );
8513
+ const targetSymbols = new Set(
8514
+ targetNodes.map((n) => clone.getNodeAttributes(n).name)
8515
+ );
8516
+ for (const nodeId of sourceNodes) {
8517
+ const name = clone.getNodeAttributes(nodeId).name;
8518
+ if (name !== "__file__" && targetSymbols.has(name)) {
8519
+ throw new Error(
8520
+ `Merge conflict: symbol "${name}" exists in both ${target} and ${source}`
8521
+ );
8522
+ }
8963
8523
  }
8964
- } catch (error) {
8965
- }
8966
- }
8967
- function isGitRepo(dir) {
8968
- try {
8969
- execSync2("git rev-parse --git-dir", { cwd: dir, stdio: "ignore" });
8970
- return true;
8971
- } catch {
8972
- return false;
8973
- }
8974
- }
8975
-
8976
- // src/temporal/sampler.ts
8977
- function sampleCommits(commits, targetCount, strategy) {
8978
- if (commits.length === 0) {
8979
- return [];
8980
- }
8981
- if (commits.length <= targetCount) {
8982
- return commits;
8983
- }
8984
- switch (strategy) {
8985
- case "even":
8986
- return sampleEvenly(commits, targetCount);
8987
- case "weekly":
8988
- return sampleWeekly(commits, targetCount);
8989
- case "monthly":
8990
- return sampleMonthly(commits, targetCount);
8991
- default:
8992
- return sampleEvenly(commits, targetCount);
8524
+ for (const oldId of sourceNodes) {
8525
+ const attrs = clone.getNodeAttributes(oldId);
8526
+ const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
8527
+ const newId = `${normalizedTarget}::${symbolName}`;
8528
+ clone.forEachInEdge(oldId, (_edge, _edgeAttrs, inSource) => {
8529
+ const srcAttrs = clone.getNodeAttributes(inSource);
8530
+ if (!fileMatch(srcAttrs.filePath, source) && !fileMatch(srcAttrs.filePath, target)) {
8531
+ brokenImports.push({
8532
+ file: srcAttrs.filePath,
8533
+ importedSymbol: attrs.name,
8534
+ reason: `imports ${attrs.name} from ${source} (merged into ${target})`
8535
+ });
8536
+ }
8537
+ });
8538
+ if (!clone.hasNode(newId)) {
8539
+ clone.addNode(newId, { ...attrs, filePath: normalizedTarget });
8540
+ }
8541
+ clone.forEachInEdge(oldId, (_edge, edgeAttrs, inSource) => {
8542
+ const resolvedSource = sourceNodes.includes(inSource) ? `${normalizedTarget}::${inSource.includes("::") ? inSource.split("::").slice(1).join("::") : clone.getNodeAttributes(inSource).name}` : inSource;
8543
+ if (clone.hasNode(resolvedSource) && clone.hasNode(newId)) {
8544
+ clone.mergeEdge(resolvedSource, newId, edgeAttrs);
8545
+ }
8546
+ });
8547
+ clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _s, outTarget) => {
8548
+ const resolvedTarget = sourceNodes.includes(outTarget) ? `${normalizedTarget}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
8549
+ if (clone.hasNode(newId) && clone.hasNode(resolvedTarget)) {
8550
+ clone.mergeEdge(newId, resolvedTarget, edgeAttrs);
8551
+ }
8552
+ });
8553
+ clone.dropNode(oldId);
8554
+ }
8555
+ }
8556
+ // ── Diff computation ───────────────────────────────────────────
8557
+ computeDiff(original, simulated, brokenImports) {
8558
+ const originalEdges = this.collectEdges(original);
8559
+ const simulatedEdges = this.collectEdges(simulated);
8560
+ const originalKeys = new Set(originalEdges.map((e) => this.edgeKey(e)));
8561
+ const simulatedKeys = new Set(simulatedEdges.map((e) => this.edgeKey(e)));
8562
+ const addedEdges = simulatedEdges.filter((e) => !originalKeys.has(this.edgeKey(e)));
8563
+ const removedEdges = originalEdges.filter((e) => !simulatedKeys.has(this.edgeKey(e)));
8564
+ const affectedNodeSet = /* @__PURE__ */ new Set();
8565
+ for (const e of [...addedEdges, ...removedEdges]) {
8566
+ affectedNodeSet.add(e.source);
8567
+ affectedNodeSet.add(e.target);
8568
+ }
8569
+ const originalCycles = this.detectCycles(original);
8570
+ const simulatedCycles = this.detectCycles(simulated);
8571
+ const originalCycleKeys = new Set(originalCycles.map((c) => [...c].sort().join(",")));
8572
+ const simulatedCycleKeys = new Set(simulatedCycles.map((c) => [...c].sort().join(",")));
8573
+ const circularDepsIntroduced = simulatedCycles.filter(
8574
+ (c) => !originalCycleKeys.has([...c].sort().join(","))
8575
+ );
8576
+ const circularDepsResolved = originalCycles.filter(
8577
+ (c) => !simulatedCycleKeys.has([...c].sort().join(","))
8578
+ );
8579
+ return {
8580
+ addedEdges,
8581
+ removedEdges,
8582
+ affectedNodes: Array.from(affectedNodeSet),
8583
+ brokenImports,
8584
+ circularDepsIntroduced,
8585
+ circularDepsResolved
8586
+ };
8993
8587
  }
8994
- }
8995
- function sampleEvenly(commits, targetCount) {
8996
- if (targetCount >= commits.length) {
8997
- return commits;
8588
+ collectEdges(graph) {
8589
+ const edges = [];
8590
+ graph.forEachEdge((_edge, attrs, source, target) => {
8591
+ edges.push({ source, target, kind: attrs.kind });
8592
+ });
8593
+ return edges;
8594
+ }
8595
+ edgeKey(e) {
8596
+ return `${e.source}|${e.target}|${e.kind || ""}`;
8597
+ }
8598
+ // ── Cycle detection (adapted from src/health/metrics.ts) ───────
8599
+ detectCycles(graph) {
8600
+ const fileGraph = /* @__PURE__ */ new Map();
8601
+ graph.forEachEdge((_edge, _attrs, source, target) => {
8602
+ const sourceFile = graph.getNodeAttributes(source).filePath;
8603
+ const targetFile = graph.getNodeAttributes(target).filePath;
8604
+ if (sourceFile !== targetFile) {
8605
+ if (!fileGraph.has(sourceFile)) {
8606
+ fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
8607
+ }
8608
+ fileGraph.get(sourceFile).add(targetFile);
8609
+ }
8610
+ });
8611
+ const visited = /* @__PURE__ */ new Set();
8612
+ const recStack = /* @__PURE__ */ new Set();
8613
+ const cycles = [];
8614
+ const dfs = (node, path6) => {
8615
+ if (recStack.has(node)) {
8616
+ const cycleStart = path6.indexOf(node);
8617
+ if (cycleStart >= 0) {
8618
+ cycles.push(path6.slice(cycleStart));
8619
+ }
8620
+ return;
8621
+ }
8622
+ if (visited.has(node)) return;
8623
+ visited.add(node);
8624
+ recStack.add(node);
8625
+ path6.push(node);
8626
+ const neighbors = fileGraph.get(node);
8627
+ if (neighbors) {
8628
+ for (const neighbor of neighbors) {
8629
+ dfs(neighbor, [...path6]);
8630
+ }
8631
+ }
8632
+ recStack.delete(node);
8633
+ };
8634
+ for (const node of fileGraph.keys()) {
8635
+ if (!visited.has(node)) {
8636
+ dfs(node, []);
8637
+ }
8638
+ }
8639
+ const unique = /* @__PURE__ */ new Map();
8640
+ for (const cycle of cycles) {
8641
+ const key = [...cycle].sort().join(",");
8642
+ if (!unique.has(key)) {
8643
+ unique.set(key, cycle);
8644
+ }
8645
+ }
8646
+ return Array.from(unique.values());
8998
8647
  }
8999
- const result = [];
9000
- const step = (commits.length - 1) / (targetCount - 1);
9001
- for (let i = 0; i < targetCount; i++) {
9002
- const index = Math.round(i * step);
9003
- result.push(commits[index]);
8648
+ // ── Health score (side-effect free) ────────────────────────────
8649
+ computeHealthScore(graph) {
8650
+ const dimensions = [
8651
+ calculateCouplingScore(graph),
8652
+ calculateCohesionScore(graph),
8653
+ calculateCircularDepsScore(graph),
8654
+ calculateGodFilesScore(graph),
8655
+ calculateOrphansScore(graph),
8656
+ calculateDepthScore(graph)
8657
+ ];
8658
+ const score = Math.round(
8659
+ dimensions.reduce((sum, dim) => sum + dim.score * dim.weight, 0)
8660
+ );
8661
+ return { score, dimensions };
9004
8662
  }
9005
- return result;
9006
- }
9007
- function sampleWeekly(commits, targetCount) {
9008
- const result = [];
9009
- const first = commits[0];
9010
- const last = commits[commits.length - 1];
9011
- result.push(first);
9012
- const weekMap = /* @__PURE__ */ new Map();
9013
- for (const commit of commits) {
9014
- const date = new Date(commit.date);
9015
- const year = date.getFullYear();
9016
- const week = getWeekNumber(date);
9017
- const key = `${year}-W${week}`;
9018
- weekMap.set(key, commit);
9019
- }
9020
- const weeklyCommits = Array.from(weekMap.values());
9021
- if (weeklyCommits.length <= targetCount) {
9022
- return weeklyCommits;
9023
- }
9024
- const step = Math.floor((weeklyCommits.length - 2) / (targetCount - 2));
9025
- for (let i = 1; i < targetCount - 1; i++) {
9026
- const index = Math.min(i * step, weeklyCommits.length - 2);
9027
- if (weeklyCommits[index] !== first && weeklyCommits[index] !== last) {
9028
- result.push(weeklyCommits[index]);
9029
- }
9030
- }
9031
- if (result[result.length - 1] !== last) {
9032
- result.push(last);
9033
- }
9034
- return result;
9035
- }
9036
- function sampleMonthly(commits, targetCount) {
9037
- const result = [];
9038
- const first = commits[0];
9039
- const last = commits[commits.length - 1];
9040
- result.push(first);
9041
- const monthMap = /* @__PURE__ */ new Map();
9042
- for (const commit of commits) {
9043
- const date = new Date(commit.date);
9044
- const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
9045
- monthMap.set(key, commit);
9046
- }
9047
- const monthlyCommits = Array.from(monthMap.values());
9048
- if (monthlyCommits.length <= targetCount) {
9049
- return monthlyCommits;
9050
- }
9051
- const step = Math.floor((monthlyCommits.length - 2) / (targetCount - 2));
9052
- for (let i = 1; i < targetCount - 1; i++) {
9053
- const index = Math.min(i * step, monthlyCommits.length - 2);
9054
- if (monthlyCommits[index] !== first && monthlyCommits[index] !== last) {
9055
- result.push(monthlyCommits[index]);
9056
- }
9057
- }
9058
- if (result[result.length - 1] !== last) {
9059
- result.push(last);
9060
- }
9061
- return result;
9062
- }
9063
- function getWeekNumber(date) {
9064
- const d = new Date(
9065
- Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
9066
- );
9067
- const dayNum = d.getUTCDay() || 7;
9068
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
9069
- const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
9070
- return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
9071
- }
9072
-
9073
- // src/temporal/snapshots.ts
9074
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync10, mkdirSync as mkdirSync3, existsSync as existsSync14, readdirSync as readdirSync5 } from "fs";
9075
- import { join as join16 } from "path";
9076
- function saveSnapshot(snapshot, outputDir) {
9077
- if (!existsSync14(outputDir)) {
9078
- mkdirSync3(outputDir, { recursive: true });
9079
- }
9080
- const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
9081
- const filepath = join16(outputDir, filename);
9082
- writeFileSync4(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
9083
- }
9084
- function loadSnapshot(commitHash, outputDir) {
9085
- const shortHash = commitHash.substring(0, 8);
9086
- const filepath = join16(outputDir, `${shortHash}.json`);
9087
- if (!existsSync14(filepath)) {
9088
- return null;
9089
- }
9090
- try {
9091
- const content = readFileSync10(filepath, "utf-8");
9092
- return JSON.parse(content);
9093
- } catch {
9094
- return null;
9095
- }
9096
- }
9097
- function createSnapshot(graph, commitHash, commitDate, commitMessage, commitAuthor) {
9098
- const fileMap = /* @__PURE__ */ new Map();
9099
- for (const node of graph.nodes) {
9100
- if (!fileMap.has(node.filePath)) {
9101
- fileMap.set(node.filePath, { symbols: 0, inbound: 0, outbound: 0 });
9102
- }
9103
- fileMap.get(node.filePath).symbols++;
9104
- }
9105
- for (const edge of graph.edges) {
9106
- const sourceNode = graph.nodes.find((n) => n.id === edge.source);
9107
- const targetNode = graph.nodes.find((n) => n.id === edge.target);
9108
- if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
9109
- if (fileMap.has(sourceNode.filePath)) {
9110
- fileMap.get(sourceNode.filePath).outbound++;
9111
- }
9112
- if (fileMap.has(targetNode.filePath)) {
9113
- fileMap.get(targetNode.filePath).inbound++;
9114
- }
9115
- }
9116
- }
9117
- const files = Array.from(fileMap.entries()).map(([path6, data]) => ({
9118
- path: path6,
9119
- symbols: data.symbols,
9120
- connections: data.inbound + data.outbound
9121
- }));
9122
- const edgeMap = /* @__PURE__ */ new Map();
9123
- for (const edge of graph.edges) {
9124
- const sourceNode = graph.nodes.find((n) => n.id === edge.source);
9125
- const targetNode = graph.nodes.find((n) => n.id === edge.target);
9126
- if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
9127
- const key = sourceNode.filePath < targetNode.filePath ? `${sourceNode.filePath}|${targetNode.filePath}` : `${targetNode.filePath}|${sourceNode.filePath}`;
9128
- edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
9129
- }
9130
- }
9131
- const edges = Array.from(edgeMap.entries()).map(([key, weight]) => {
9132
- const [source, target] = key.split("|");
9133
- return { source, target, weight };
9134
- });
9135
- const languages2 = {};
9136
- for (const file of graph.files) {
9137
- const ext = file.split(".").pop() || "unknown";
9138
- const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" || ext === "mjs" || ext === "cjs" ? "javascript" : ext === "py" ? "python" : ext === "go" ? "go" : "other";
9139
- languages2[lang] = (languages2[lang] || 0) + 1;
9140
- }
9141
- return {
9142
- commitHash,
9143
- commitDate,
9144
- commitMessage,
9145
- commitAuthor,
9146
- stats: {
9147
- totalFiles: graph.files.length,
9148
- totalSymbols: graph.nodes.length,
9149
- totalEdges: edges.length,
9150
- languages: languages2
9151
- },
9152
- files,
9153
- edges
9154
- };
9155
- }
9156
-
9157
- // src/mcp/tools.ts
9158
- function getToolsList() {
9159
- return [
9160
- {
9161
- name: "connect_repo",
9162
- description: "Connect Depwire to a codebase for analysis. Accepts a local directory path or a GitHub repository URL. If a GitHub URL is provided, the repo will be cloned automatically. This replaces the currently loaded project.",
9163
- inputSchema: {
9164
- type: "object",
9165
- properties: {
9166
- source: {
9167
- type: "string",
9168
- description: "Local directory path (e.g., '/Users/me/project') or GitHub URL (e.g., 'https://github.com/vercel/next.js')"
9169
- },
9170
- subdirectory: {
9171
- type: "string",
9172
- description: "Subdirectory within the repo to analyze (optional, e.g., 'packages/core/src')"
9173
- }
9174
- },
9175
- required: ["source"]
9176
- }
9177
- },
9178
- {
9179
- name: "get_symbol_info",
9180
- description: "Look up detailed information about a symbol (function, class, variable, type, etc.) by name. Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
9181
- inputSchema: {
9182
- type: "object",
9183
- properties: {
9184
- name: {
9185
- type: "string",
9186
- description: "The symbol name to look up (e.g., 'UserService') or full ID (e.g., 'src/services/UserService.ts::UserService')"
9187
- }
9188
- },
9189
- required: ["name"]
9190
- }
9191
- },
9192
- {
9193
- name: "get_dependencies",
9194
- description: "Get all symbols that a given symbol depends on (what does this symbol use/import/call?). Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
9195
- inputSchema: {
9196
- type: "object",
9197
- properties: {
9198
- symbol: {
9199
- type: "string",
9200
- description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
9201
- }
9202
- },
9203
- required: ["symbol"]
9204
- }
9205
- },
9206
- {
9207
- name: "get_dependents",
9208
- description: "Get all symbols that depend on a given symbol (what uses this symbol?). Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
9209
- inputSchema: {
9210
- type: "object",
9211
- properties: {
9212
- symbol: {
9213
- type: "string",
9214
- description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
9215
- }
9216
- },
9217
- required: ["symbol"]
9218
- }
9219
- },
9220
- {
9221
- name: "impact_analysis",
9222
- description: "Analyze what would break if a symbol is changed, renamed, or removed. Shows direct dependents, transitive dependents (chain reaction), and all affected files. Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation. Use this before making changes to understand the blast radius.",
9223
- inputSchema: {
9224
- type: "object",
9225
- properties: {
9226
- symbol: {
9227
- type: "string",
9228
- description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
9229
- },
9230
- file: {
9231
- type: "string",
9232
- description: "Optional: File path to disambiguate when multiple symbols have the same name (e.g., 'src/router.ts')"
9233
- }
9234
- },
9235
- required: ["symbol"]
9236
- }
9237
- },
9238
- {
9239
- name: "get_file_context",
9240
- description: "Get complete context about a file \u2014 all symbols defined in it, all imports, all exports, and all files that import from it.",
9241
- inputSchema: {
9242
- type: "object",
9243
- properties: {
9244
- filePath: {
9245
- type: "string",
9246
- description: "Relative file path (e.g., 'services/UserService.ts')"
9247
- }
9248
- },
9249
- required: ["filePath"]
9250
- }
9251
- },
9252
- {
9253
- name: "search_symbols",
9254
- description: "Search for symbols by name across the entire codebase. Supports partial matching.",
9255
- inputSchema: {
9256
- type: "object",
9257
- properties: {
9258
- query: {
9259
- type: "string",
9260
- description: "Search query (case-insensitive substring match)"
9261
- },
9262
- limit: {
9263
- type: "number",
9264
- description: "Maximum results to return (default: 20)"
9265
- }
9266
- },
9267
- required: ["query"]
9268
- }
9269
- },
9270
- {
9271
- name: "get_architecture_summary",
9272
- description: "Get a high-level overview of the project's architecture \u2014 file count, symbol count, most connected files, dependency hotspots, and orphan files.",
9273
- inputSchema: {
9274
- type: "object",
9275
- properties: {}
9276
- }
9277
- },
9278
- {
9279
- name: "list_files",
9280
- description: "List all files in the project with basic stats.",
9281
- inputSchema: {
9282
- type: "object",
9283
- properties: {
9284
- directory: {
9285
- type: "string",
9286
- description: "Filter to a specific subdirectory (optional)"
9287
- }
9288
- }
9289
- }
9290
- },
9291
- {
9292
- name: "visualize_graph",
9293
- description: "Render an interactive arc diagram visualization of the current codebase's cross-reference graph. Shows files as bars along the bottom and dependency arcs connecting them, colored by distance. The visualization appears inline in the conversation.",
9294
- inputSchema: {
9295
- type: "object",
9296
- properties: {
9297
- highlight: {
9298
- type: "string",
9299
- description: "File or symbol name to highlight in the visualization (optional)"
9300
- },
9301
- maxFiles: {
9302
- type: "number",
9303
- description: "Limit to top N most connected files (optional, default: all)"
9304
- }
9305
- }
9306
- }
9307
- },
9308
- {
9309
- name: "get_project_docs",
9310
- description: "Retrieve auto-generated codebase documentation. Returns architecture overview, code conventions, dependency maps, and onboarding guides. Documentation must be generated first with `depwire docs` command.",
9311
- inputSchema: {
9312
- type: "object",
9313
- properties: {
9314
- doc_type: {
9315
- type: "string",
9316
- description: "Document type to retrieve: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
9317
- }
9318
- }
9319
- }
9320
- },
9321
- {
9322
- name: "update_project_docs",
9323
- description: "Regenerate codebase documentation with the latest changes. If docs don't exist, generates them for the first time. Use this after significant code changes to keep documentation up-to-date.",
9324
- inputSchema: {
9325
- type: "object",
9326
- properties: {
9327
- doc_type: {
9328
- type: "string",
9329
- description: "Document type to update: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
9330
- }
9331
- }
9332
- }
9333
- },
9334
- {
9335
- name: "get_health_score",
9336
- description: "Get a 0-100 health score for the project's dependency architecture. Scores coupling, cohesion, circular dependencies, god files, orphan files, and dependency depth. Returns overall score, per-dimension breakdown, and actionable recommendations.",
9337
- inputSchema: {
9338
- type: "object",
9339
- properties: {}
9340
- }
9341
- },
9342
- {
9343
- name: "get_temporal_graph",
9344
- description: "Show how the dependency graph evolved over git history. Returns snapshots at sampled commits showing file counts, symbol counts, edge counts, and structural changes over time.",
9345
- inputSchema: {
9346
- type: "object",
9347
- properties: {
9348
- commits: {
9349
- type: "number",
9350
- description: "Number of commits to sample (default: 10)"
9351
- },
9352
- strategy: {
9353
- type: "string",
9354
- enum: ["even", "weekly", "monthly"],
9355
- description: "Sampling strategy (default: even)"
9356
- }
9357
- }
9358
- }
9359
- },
9360
- {
9361
- name: "find_dead_code",
9362
- description: "Find potentially dead code \u2014 symbols that are defined but never referenced anywhere in the codebase. Returns symbols categorized by confidence level (high, medium, low). High confidence means definitely unused. Use this to identify cleanup opportunities.",
9363
- inputSchema: {
9364
- type: "object",
9365
- properties: {
9366
- confidence: {
9367
- type: "string",
9368
- enum: ["high", "medium", "low"],
9369
- description: "Minimum confidence level to return (default: medium)",
9370
- default: "medium"
9371
- }
9372
- }
9373
- }
9374
- }
9375
- ];
9376
- }
9377
- async function handleToolCall(name, args, state) {
9378
- try {
9379
- let result;
9380
- if (name === "connect_repo") {
9381
- result = await connectToRepo(args.source, args.subdirectory, state);
9382
- } else if (name === "get_architecture_summary") {
9383
- if (!isProjectLoaded(state)) {
9384
- result = {
9385
- status: "no_project",
9386
- message: "No project loaded. Use connect_repo to analyze a codebase."
9387
- };
9388
- } else {
9389
- result = handleGetArchitectureSummary(state.graph);
9390
- }
9391
- } else if (name === "visualize_graph") {
9392
- if (!isProjectLoaded(state)) {
9393
- result = {
9394
- error: "No project loaded",
9395
- message: "Use connect_repo to connect to a codebase first"
9396
- };
9397
- } else {
9398
- result = await handleVisualizeGraph(args.highlight, args.maxFiles, state);
9399
- }
9400
- } else if (name === "get_project_docs") {
9401
- if (!isProjectLoaded(state)) {
9402
- result = {
9403
- error: "No project loaded",
9404
- message: "Use connect_repo to connect to a codebase first"
9405
- };
9406
- } else {
9407
- result = await handleGetProjectDocs(args.doc_type || "all", state);
9408
- }
9409
- } else if (name === "update_project_docs") {
9410
- if (!isProjectLoaded(state)) {
9411
- result = {
9412
- error: "No project loaded",
9413
- message: "Use connect_repo to connect to a codebase first"
9414
- };
9415
- } else {
9416
- result = await handleUpdateProjectDocs(args.doc_type || "all", state);
9417
- }
9418
- } else if (name === "get_health_score") {
9419
- if (!isProjectLoaded(state)) {
9420
- result = {
9421
- error: "No project loaded",
9422
- message: "Use connect_repo to connect to a codebase first"
9423
- };
9424
- } else {
9425
- result = handleGetHealthScore(state);
9426
- }
9427
- } else if (name === "get_temporal_graph") {
9428
- if (!isProjectLoaded(state)) {
9429
- result = {
9430
- error: "No project loaded",
9431
- message: "Use connect_repo to connect to a codebase first"
9432
- };
9433
- } else {
9434
- result = await handleGetTemporalGraph(state, args.commits || 10, args.strategy || "even");
9435
- }
9436
- } else if (name === "find_dead_code") {
9437
- if (!isProjectLoaded(state)) {
9438
- result = {
9439
- error: "No project loaded",
9440
- message: "Use connect_repo to connect to a codebase first"
9441
- };
9442
- } else {
9443
- result = handleFindDeadCode(state, args.confidence || "medium");
9444
- }
9445
- } else {
9446
- if (!isProjectLoaded(state)) {
9447
- result = {
9448
- error: "No project loaded",
9449
- message: "Use connect_repo to connect to a codebase first"
9450
- };
9451
- } else {
9452
- const graph = state.graph;
9453
- switch (name) {
9454
- case "get_symbol_info":
9455
- result = handleGetSymbolInfo(args.name, graph);
9456
- break;
9457
- case "get_dependencies":
9458
- result = handleGetDependencies(args.symbol, graph);
9459
- break;
9460
- case "get_dependents":
9461
- result = handleGetDependents(args.symbol, graph);
9462
- break;
9463
- case "impact_analysis":
9464
- result = handleImpactAnalysis(args.symbol, graph, args.file);
9465
- break;
9466
- case "get_file_context":
9467
- result = handleGetFileContext(args.filePath, graph);
9468
- break;
9469
- case "search_symbols":
9470
- result = handleSearchSymbols(args.query, args.limit || 20, graph);
9471
- break;
9472
- case "list_files":
9473
- result = handleListFiles(args.directory, graph);
9474
- break;
9475
- default:
9476
- result = { error: `Unknown tool: ${name}` };
9477
- }
9478
- }
9479
- }
9480
- if (result && typeof result === "object" && "_mcpAppResponse" in result) {
9481
- const appResult = result;
9482
- return {
9483
- content: [
9484
- {
9485
- type: "text",
9486
- text: appResult.text
9487
- },
9488
- {
9489
- type: "resource",
9490
- resource: {
9491
- uri: "ui://depwire/arc-diagram",
9492
- mimeType: "text/html;profile=mcp-app",
9493
- text: appResult.html
9494
- }
9495
- }
9496
- ]
9497
- };
9498
- }
9499
- if (result && typeof result === "object" && "content" in result && Array.isArray(result.content)) {
9500
- return result;
9501
- }
9502
- return {
9503
- content: [
9504
- {
9505
- type: "text",
9506
- text: JSON.stringify(result, null, 2)
9507
- }
9508
- ]
9509
- };
9510
- } catch (error) {
9511
- console.error("Error handling tool call:", error);
9512
- return {
9513
- content: [
9514
- {
9515
- type: "text",
9516
- text: JSON.stringify({ error: String(error) }, null, 2)
9517
- }
9518
- ]
9519
- };
9520
- }
9521
- }
9522
- function createDisambiguationResponse(matches, queryName) {
9523
- const suggestion = matches.length > 0 ? matches[0].id : "";
9524
- const exampleFile = matches.length > 0 ? matches[0].filePath : "";
9525
- return {
9526
- ambiguous: true,
9527
- message: `Found ${matches.length} symbols named '${queryName}'. Disambiguate by:
9528
- 1. Using full ID: '${suggestion}'
9529
- 2. Or adding file parameter: { symbol: '${queryName}', file: '${exampleFile}' }`,
9530
- matches: matches.map((m, index) => ({
9531
- id: m.id,
9532
- kind: m.kind,
9533
- filePath: m.filePath,
9534
- line: m.startLine,
9535
- dependents: m.dependentCount,
9536
- hint: index === 0 && m.dependentCount > 0 ? "Most dependents \u2014 likely the one you want" : ""
9537
- })),
9538
- suggestion
9539
- };
9540
- }
9541
- function handleGetSymbolInfo(name, graph) {
9542
- const matches = findSymbols(graph, name);
9543
- if (matches.length === 0) {
9544
- const fuzzyMatches = searchSymbols(graph, name).slice(0, 10);
9545
- return {
9546
- error: `Symbol '${name}' not found`,
9547
- suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols",
9548
- fuzzyMatches: fuzzyMatches.map((m) => ({
9549
- id: m.id,
9550
- name: m.name,
9551
- kind: m.kind,
9552
- filePath: m.filePath
9553
- }))
9554
- };
9555
- }
9556
- return {
9557
- matches: matches.map((m) => ({
9558
- id: m.id,
9559
- name: m.name,
9560
- kind: m.kind,
9561
- filePath: m.filePath,
9562
- startLine: m.startLine,
9563
- endLine: m.endLine,
9564
- exported: m.exported,
9565
- scope: m.scope,
9566
- dependents: m.dependentCount
9567
- })),
9568
- count: matches.length
9569
- };
9570
- }
9571
- function handleGetDependencies(symbol, graph) {
9572
- const matches = findSymbols(graph, symbol);
9573
- if (matches.length === 0) {
9574
- const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
9575
- return {
9576
- error: `Symbol '${symbol}' not found`,
9577
- suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
9578
- };
9579
- }
9580
- if (matches.length > 1) {
9581
- return createDisambiguationResponse(matches, symbol);
9582
- }
9583
- const target = matches[0];
9584
- const deps = getDependencies(graph, target.id);
9585
- const grouped = {};
9586
- graph.forEachOutEdge(target.id, (edge, attrs, source, targetNode) => {
9587
- const kind = attrs.kind;
9588
- if (!grouped[kind]) {
9589
- grouped[kind] = [];
9590
- }
9591
- const targetAttrs = graph.getNodeAttributes(targetNode);
9592
- grouped[kind].push({
9593
- name: targetAttrs.name,
9594
- filePath: targetAttrs.filePath,
9595
- kind: targetAttrs.kind
9596
- });
9597
- });
9598
- const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
9599
- return {
9600
- symbol: target.id,
9601
- dependencies: grouped,
9602
- totalCount
9603
- };
9604
- }
9605
- function handleGetDependents(symbol, graph) {
9606
- const matches = findSymbols(graph, symbol);
9607
- if (matches.length === 0) {
9608
- const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
9609
- return {
9610
- error: `Symbol '${symbol}' not found`,
9611
- suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
9612
- };
9613
- }
9614
- if (matches.length > 1) {
9615
- return createDisambiguationResponse(matches, symbol);
9616
- }
9617
- const target = matches[0];
9618
- const deps = getDependents(graph, target.id);
9619
- const grouped = {};
9620
- graph.forEachInEdge(target.id, (edge, attrs, source, targetNode) => {
9621
- const kind = attrs.kind;
9622
- if (!grouped[kind]) {
9623
- grouped[kind] = [];
9624
- }
9625
- const sourceAttrs = graph.getNodeAttributes(source);
9626
- grouped[kind].push({
9627
- name: sourceAttrs.name,
9628
- filePath: sourceAttrs.filePath,
9629
- kind: sourceAttrs.kind
9630
- });
9631
- });
9632
- const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
9633
- return {
9634
- symbol: target.id,
9635
- dependents: grouped,
9636
- totalCount
9637
- };
9638
- }
9639
- function handleImpactAnalysis(symbol, graph, file) {
9640
- const matches = findSymbols(graph, symbol);
9641
- if (matches.length === 0) {
9642
- const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
9643
- return {
9644
- error: `Symbol '${symbol}' not found`,
9645
- suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
9646
- };
9647
- }
9648
- let filteredMatches = matches;
9649
- if (file) {
9650
- filteredMatches = matches.filter((m) => m.filePath === file || m.filePath.endsWith(file));
9651
- if (filteredMatches.length === 0) {
9652
- return {
9653
- error: `Symbol '${symbol}' not found in file '${file}'`,
9654
- availableFiles: matches.map((m) => m.filePath),
9655
- suggestion: `The symbol exists in: ${matches.map((m) => m.filePath).join(", ")}`
9656
- };
9657
- }
9658
- }
9659
- if (filteredMatches.length > 1) {
9660
- return createDisambiguationResponse(filteredMatches, symbol);
9661
- }
9662
- const target = filteredMatches[0];
9663
- const impact = getImpact(graph, target.id);
9664
- const directWithKinds = impact.directDependents.map((dep) => {
9665
- let relationship = "unknown";
9666
- graph.forEachEdge(dep.id, target.id, (edge, attrs) => {
9667
- relationship = attrs.kind;
9668
- });
9669
- return {
9670
- name: dep.name,
9671
- filePath: dep.filePath,
9672
- kind: dep.kind,
9673
- relationship
9674
- };
9675
- });
9676
- const transitiveFormatted = impact.transitiveDependents.filter((dep) => !impact.directDependents.some((d) => d.id === dep.id)).map((dep) => ({
9677
- name: dep.name,
9678
- filePath: dep.filePath,
9679
- kind: dep.kind
9680
- }));
9681
- const summary = `Changing ${target.name} would directly affect ${impact.directDependents.length} symbol(s) and transitively affect ${transitiveFormatted.length} more, across ${impact.affectedFiles.length} file(s).`;
9682
- return {
9683
- symbol: {
9684
- id: target.id,
9685
- name: target.name,
9686
- filePath: target.filePath,
9687
- kind: target.kind
9688
- },
9689
- impact: {
9690
- directDependents: directWithKinds,
9691
- transitiveDependents: transitiveFormatted,
9692
- affectedFiles: impact.affectedFiles,
9693
- summary
9694
- }
9695
- };
9696
- }
9697
- function handleGetFileContext(filePath, graph) {
9698
- const fileSymbols = [];
9699
- graph.forEachNode((nodeId, attrs) => {
9700
- if (attrs.filePath === filePath) {
9701
- fileSymbols.push({
9702
- name: attrs.name,
9703
- kind: attrs.kind,
9704
- exported: attrs.exported,
9705
- startLine: attrs.startLine,
9706
- endLine: attrs.endLine,
9707
- scope: attrs.scope
9708
- });
9709
- }
9710
- });
9711
- if (fileSymbols.length === 0) {
9712
- return {
9713
- error: `File '${filePath}' not found`,
9714
- suggestion: "Use list_files to see available files"
9715
- };
9716
- }
9717
- const importsMap = /* @__PURE__ */ new Map();
9718
- graph.forEachNode((nodeId, attrs) => {
9719
- if (attrs.filePath === filePath) {
9720
- graph.forEachOutEdge(nodeId, (edge, edgeAttrs, source, target) => {
9721
- const targetAttrs = graph.getNodeAttributes(target);
9722
- if (targetAttrs.filePath !== filePath) {
9723
- if (!importsMap.has(targetAttrs.filePath)) {
9724
- importsMap.set(targetAttrs.filePath, /* @__PURE__ */ new Set());
9725
- }
9726
- importsMap.get(targetAttrs.filePath).add(targetAttrs.name);
9727
- }
9728
- });
9729
- }
9730
- });
9731
- const imports = Array.from(importsMap.entries()).map(([file, symbols]) => ({
9732
- from: file,
9733
- symbols: Array.from(symbols)
9734
- }));
9735
- const importedByMap = /* @__PURE__ */ new Map();
9736
- graph.forEachNode((nodeId, attrs) => {
9737
- if (attrs.filePath === filePath) {
9738
- graph.forEachInEdge(nodeId, (edge, edgeAttrs, source, target) => {
9739
- const sourceAttrs = graph.getNodeAttributes(source);
9740
- if (sourceAttrs.filePath !== filePath) {
9741
- if (!importedByMap.has(sourceAttrs.filePath)) {
9742
- importedByMap.set(sourceAttrs.filePath, /* @__PURE__ */ new Set());
9743
- }
9744
- importedByMap.get(sourceAttrs.filePath).add(attrs.name);
9745
- }
9746
- });
9747
- }
9748
- });
9749
- const importedBy = Array.from(importedByMap.entries()).map(([file, symbols]) => ({
9750
- file,
9751
- symbols: Array.from(symbols)
9752
- }));
9753
- const summary = `${filePath} defines ${fileSymbols.length} symbol(s), imports from ${imports.length} file(s), and is imported by ${importedBy.length} file(s).`;
9754
- return {
9755
- filePath,
9756
- symbols: fileSymbols,
9757
- imports,
9758
- importedBy,
9759
- summary
9760
- };
9761
- }
9762
- function handleSearchSymbols(query, limit, graph) {
9763
- const results = searchSymbols(graph, query);
9764
- const queryLower = query.toLowerCase();
9765
- results.sort((a, b) => {
9766
- const aName = a.name.toLowerCase();
9767
- const bName = b.name.toLowerCase();
9768
- if (aName === queryLower && bName !== queryLower) return -1;
9769
- if (bName === queryLower && aName !== queryLower) return 1;
9770
- const aStarts = aName.startsWith(queryLower);
9771
- const bStarts = bName.startsWith(queryLower);
9772
- if (aStarts && !bStarts) return -1;
9773
- if (bStarts && !aStarts) return 1;
9774
- return aName.localeCompare(bName);
9775
- });
9776
- const showing = Math.min(limit, results.length);
9777
- return {
9778
- query,
9779
- results: results.slice(0, limit).map((r) => ({
9780
- name: r.name,
9781
- kind: r.kind,
9782
- filePath: r.filePath,
9783
- exported: r.exported,
9784
- scope: r.scope
9785
- })),
9786
- totalMatches: results.length,
9787
- showing
9788
- };
9789
- }
9790
- function handleGetArchitectureSummary(graph) {
9791
- const summary = getArchitectureSummary(graph);
9792
- const fileSummary = getFileSummary(graph);
9793
- const dirMap = /* @__PURE__ */ new Map();
9794
- const languageBreakdown = {};
9795
- fileSummary.forEach((f) => {
9796
- const dir = f.filePath.includes("/") ? dirname16(f.filePath) : ".";
9797
- if (!dirMap.has(dir)) {
9798
- dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
9799
- }
9800
- const entry = dirMap.get(dir);
9801
- entry.fileCount++;
9802
- entry.symbolCount += f.symbolCount;
9803
- const ext = f.filePath.toLowerCase();
9804
- let lang;
9805
- if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
9806
- lang = "typescript";
9807
- } else if (ext.endsWith(".py")) {
9808
- lang = "python";
9809
- } else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
9810
- lang = "javascript";
9811
- } else {
9812
- lang = "other";
9813
- }
9814
- languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
9815
- });
9816
- const directories = Array.from(dirMap.entries()).map(([name, stats]) => ({ name, ...stats })).sort((a, b) => b.symbolCount - a.symbolCount);
9817
- const summaryText = `Project has ${summary.fileCount} files with ${summary.symbolCount} symbols and ${summary.edgeCount} edges. The most connected file is ${summary.mostConnectedFiles[0]?.filePath || "N/A"} with ${summary.mostConnectedFiles[0]?.connections || 0} connections.`;
9818
- return {
9819
- overview: {
9820
- totalFiles: summary.fileCount,
9821
- totalSymbols: summary.symbolCount,
9822
- totalEdges: summary.edgeCount,
9823
- languages: languageBreakdown
9824
- },
9825
- mostConnectedFiles: summary.mostConnectedFiles.slice(0, 10),
9826
- directories: directories.slice(0, 10),
9827
- orphanFiles: summary.orphanFiles,
9828
- summary: summaryText
9829
- };
9830
- }
9831
- function handleListFiles(directory, graph) {
9832
- const fileSummary = getFileSummary(graph);
9833
- let filtered = fileSummary;
9834
- if (directory) {
9835
- filtered = fileSummary.filter((f) => f.filePath.startsWith(directory));
9836
- }
9837
- const files = filtered.map((f) => ({
9838
- path: f.filePath,
9839
- symbolCount: f.symbolCount,
9840
- connections: f.incomingRefs + f.outgoingRefs
9841
- }));
9842
- return {
9843
- files,
9844
- totalFiles: files.length
9845
- };
9846
- }
9847
- async function handleVisualizeGraph(highlight, maxFiles, state) {
9848
- const vizData = prepareVizData(state.graph, state.projectRoot);
9849
- const { url, alreadyRunning } = await startVizServer(
9850
- vizData,
9851
- state.graph,
9852
- state.projectRoot,
9853
- 3456,
9854
- // Use different port from CLI default to avoid conflicts
9855
- false
9856
- // Don't auto-open browser from MCP
9857
- );
9858
- const fileCount = maxFiles && maxFiles < vizData.files.length ? maxFiles : vizData.files.length;
9859
- const arcCount = vizData.arcs.filter((a) => {
9860
- if (!maxFiles || maxFiles >= vizData.files.length) return true;
9861
- const topFiles = vizData.files.sort((a2, b) => b.incomingCount + b.outgoingCount - (a2.incomingCount + a2.outgoingCount)).slice(0, maxFiles).map((f) => f.path);
9862
- return topFiles.includes(a.sourceFile) && topFiles.includes(a.targetFile);
9863
- }).length;
9864
- const statusMessage = alreadyRunning ? "Visualization server is already running." : "Visualization server started.";
9865
- const message = `${statusMessage}
9866
-
9867
- Interactive arc diagram: ${url}
9868
-
9869
- The diagram shows ${fileCount} files and ${arcCount} cross-file dependencies.${highlight ? ` Highlighted: ${highlight}` : ""}
9870
-
9871
- Features:
9872
- \u2022 Hover over arcs to see source \u2192 target details
9873
- \u2022 Click files to filter connections
9874
- \u2022 Search for specific files
9875
- \u2022 Export as SVG or PNG
9876
-
9877
- The server will keep running until you end the MCP session or press Ctrl+C.`;
9878
- return {
9879
- content: [{ type: "text", text: message }]
9880
- };
9881
- }
9882
- async function handleGetProjectDocs(docType, state) {
9883
- const docsDir = join17(state.projectRoot, ".depwire");
9884
- if (!existsSync15(docsDir)) {
9885
- const errorMessage = `Project documentation has not been generated yet.
9886
-
9887
- Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
9888
-
9889
- Once generated, this tool will return the requested documentation.
9890
-
9891
- Available document types:
9892
- - architecture: High-level structural overview
9893
- - conventions: Auto-detected coding patterns
9894
- - dependencies: Complete dependency mapping
9895
- - onboarding: Guide for new developers`;
9896
- return {
9897
- content: [{ type: "text", text: errorMessage }]
9898
- };
9899
- }
9900
- const metadata = loadMetadata(docsDir);
9901
- if (!metadata) {
9902
- return {
9903
- content: [{ type: "text", text: "Documentation directory exists but metadata is missing. Please regenerate with `depwire docs`." }]
9904
- };
9905
- }
9906
- const docsToReturn = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
9907
- let output = "";
9908
- const missing = [];
9909
- for (const doc of docsToReturn) {
9910
- if (!metadata.documents[doc]) {
9911
- missing.push(doc);
9912
- continue;
9913
- }
9914
- const filePath = join17(docsDir, metadata.documents[doc].file);
9915
- if (!existsSync15(filePath)) {
9916
- missing.push(doc);
9917
- continue;
9918
- }
9919
- const content = readFileSync11(filePath, "utf-8");
9920
- if (docsToReturn.length > 1) {
9921
- output += `
9922
-
9923
- ---
9924
-
9925
- # ${doc.toUpperCase()}
9926
-
9927
- `;
9928
- }
9929
- output += content;
9930
- }
9931
- if (missing.length > 0) {
9932
- output += `
9933
-
9934
- ---
9935
-
9936
- **Note:** The following documents are missing: ${missing.join(", ")}. Run \`depwire docs ${state.projectRoot} --update\` to generate them.`;
9937
- }
9938
- return {
9939
- content: [{ type: "text", text: output }]
9940
- };
9941
- }
9942
- async function handleUpdateProjectDocs(docType, state) {
9943
- const startTime = Date.now();
9944
- const docsDir = join17(state.projectRoot, ".depwire");
9945
- console.error("Regenerating project documentation...");
9946
- const parsedFiles = await parseProject(state.projectRoot);
9947
- const graph = buildGraph(parsedFiles);
9948
- const parseTime = (Date.now() - startTime) / 1e3;
9949
- state.graph = graph;
9950
- const packageJsonPath = join17(__dirname, "../../package.json");
9951
- const packageJson = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
9952
- const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
9953
- const docsExist = existsSync15(docsDir);
9954
- const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
9955
- outputDir: docsDir,
9956
- format: "markdown",
9957
- include: docsToGenerate,
9958
- update: docsExist,
9959
- only: docsExist ? docsToGenerate : void 0,
9960
- verbose: false,
9961
- stats: false
9962
- });
9963
- const elapsed = (Date.now() - startTime) / 1e3;
9964
- if (result.success) {
9965
- const fileCount = /* @__PURE__ */ new Set();
9966
- graph.forEachNode((node, attrs) => {
9967
- fileCount.add(attrs.filePath);
9968
- });
9969
- return {
9970
- status: "success",
9971
- message: `Updated ${result.generated.join(", ")} (${fileCount.size} files, ${graph.order} symbols, ${elapsed.toFixed(1)}s)`,
9972
- generated: result.generated,
9973
- stats: {
9974
- files: fileCount.size,
9975
- symbols: graph.order,
9976
- edges: graph.size,
9977
- time: elapsed
9978
- }
9979
- };
9980
- } else {
9981
- return {
9982
- status: "error",
9983
- message: `Failed to update documentation: ${result.errors.join(", ")}`,
9984
- errors: result.errors
9985
- };
9986
- }
9987
- }
9988
- function handleGetHealthScore(state) {
9989
- const graph = state.graph;
9990
- const projectRoot = state.projectRoot;
9991
- const report = calculateHealthScore(graph, projectRoot);
9992
- return report;
9993
- }
9994
- async function handleGetTemporalGraph(state, commits, strategy) {
9995
- const projectRoot = state.projectRoot;
9996
- if (!isGitRepo(projectRoot)) {
9997
- return {
9998
- error: "Not a git repository",
9999
- message: "Temporal analysis requires git history"
10000
- };
10001
- }
10002
- try {
10003
- const allCommits = await getCommitLog(projectRoot);
10004
- if (allCommits.length === 0) {
10005
- return {
10006
- error: "No commits found",
10007
- message: "Repository has no commit history"
10008
- };
10009
- }
10010
- const sampledCommits = sampleCommits(allCommits, commits, strategy);
10011
- const snapshots = [];
10012
- const outputDir = join17(projectRoot, ".depwire", "temporal");
10013
- for (const commit of sampledCommits) {
10014
- const existing = loadSnapshot(commit.hash, outputDir);
10015
- if (existing) {
10016
- snapshots.push(existing);
10017
- }
10018
- }
10019
- if (snapshots.length === 0) {
10020
- return {
10021
- status: "no_snapshots",
10022
- message: "No temporal snapshots found. Run `depwire temporal` to generate them.",
10023
- commits_found: allCommits.length,
10024
- commits_to_sample: sampledCommits.length
10025
- };
10026
- }
10027
- const first = snapshots[0];
10028
- const last = snapshots[snapshots.length - 1];
10029
- const growth = {
10030
- files: last.stats.totalFiles - first.stats.totalFiles,
10031
- symbols: last.stats.totalSymbols - first.stats.totalSymbols,
10032
- edges: last.stats.totalEdges - first.stats.totalEdges
10033
- };
10034
- const trend = growth.files > 0 ? "Growing" : growth.files < 0 ? "Shrinking" : "Stable";
10035
- let biggestGrowth = { index: 0, files: 0, date: "", message: "" };
10036
- for (let i = 1; i < snapshots.length; i++) {
10037
- const delta = snapshots[i].stats.totalFiles - snapshots[i - 1].stats.totalFiles;
10038
- if (delta > biggestGrowth.files) {
10039
- biggestGrowth = {
10040
- index: i,
10041
- files: delta,
10042
- date: snapshots[i].commitDate,
10043
- message: snapshots[i].commitMessage
10044
- };
10045
- }
10046
- }
10047
- return {
10048
- status: "success",
10049
- time_range: {
10050
- from: first.commitDate,
10051
- to: last.commitDate
10052
- },
10053
- snapshots: snapshots.map((s) => ({
10054
- commit: s.commitHash.substring(0, 8),
10055
- date: s.commitDate,
10056
- message: s.commitMessage,
10057
- author: s.commitAuthor,
10058
- files: s.stats.totalFiles,
10059
- symbols: s.stats.totalSymbols,
10060
- edges: s.stats.totalEdges
10061
- })),
10062
- growth,
10063
- trend,
10064
- biggest_growth_period: biggestGrowth.files > 0 ? biggestGrowth : null,
10065
- summary: `Analyzed ${snapshots.length} snapshots from ${new Date(first.commitDate).toLocaleDateString()} to ${new Date(last.commitDate).toLocaleDateString()}. Overall trend: ${trend}.`
10066
- };
10067
- } catch (error) {
10068
- return {
10069
- error: "Failed to analyze temporal graph",
10070
- message: String(error)
10071
- };
10072
- }
10073
- }
10074
- function handleFindDeadCode(state, confidence) {
10075
- if (!state.graph || !state.projectRoot) {
10076
- return {
10077
- error: "No project loaded",
10078
- message: "Use connect_repo to connect to a codebase first"
10079
- };
10080
- }
10081
- try {
10082
- const report = analyzeDeadCode(state.graph, state.projectRoot, {
10083
- confidence,
10084
- includeTests: false,
10085
- verbose: false,
10086
- stats: false,
10087
- json: true
10088
- });
10089
- return {
10090
- status: "success",
10091
- totalSymbols: report.totalSymbols,
10092
- deadSymbols: report.deadSymbols,
10093
- deadPercentage: report.deadPercentage,
10094
- byConfidence: report.byConfidence,
10095
- symbols: report.symbols.map((s) => ({
10096
- name: s.name,
10097
- kind: s.kind,
10098
- file: s.file,
10099
- line: s.line,
10100
- exported: s.exported,
10101
- dependents: s.dependents,
10102
- confidence: s.confidence,
10103
- reason: s.reason
10104
- })),
10105
- summary: `Found ${report.deadSymbols} potentially dead symbols (${report.byConfidence.high} high, ${report.byConfidence.medium} medium, ${report.byConfidence.low} low confidence) out of ${report.totalSymbols} total symbols (${report.deadPercentage.toFixed(1)}% dead code).`
10106
- };
10107
- } catch (error) {
10108
- return {
10109
- error: "Failed to analyze dead code",
10110
- message: String(error)
10111
- };
10112
- }
10113
- }
10114
-
10115
- // src/mcp/server.ts
10116
- async function startMcpServer(state) {
10117
- const server = new Server(
10118
- {
10119
- name: "depwire",
10120
- version: "0.1.0"
10121
- },
10122
- {
10123
- capabilities: {
10124
- tools: {}
10125
- }
10126
- }
10127
- );
10128
- const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
10129
- server.setRequestHandler(ListToolsRequestSchema, async () => {
10130
- return {
10131
- tools: getToolsList()
10132
- };
10133
- });
10134
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
10135
- const { name, arguments: args } = request.params;
10136
- return await handleToolCall(name, args || {}, state);
10137
- });
10138
- const transport = new StdioServerTransport();
10139
- await server.connect(transport);
10140
- console.error("Depwire MCP server started");
10141
- if (state.projectRoot) {
10142
- console.error(`Project: ${state.projectRoot}`);
10143
- } else {
10144
- console.error("No project loaded. Use connect_repo to connect to a codebase.");
10145
- }
10146
- }
8663
+ };
10147
8664
 
10148
8665
  export {
10149
8666
  findProjectRoot,
8667
+ parseTypeScriptFile,
10150
8668
  parseProject,
10151
8669
  buildGraph,
8670
+ findSymbols,
8671
+ getDependencies,
8672
+ getDependents,
10152
8673
  getImpact,
8674
+ getCrossFileEdges,
8675
+ getFileSummary,
10153
8676
  searchSymbols,
10154
8677
  getArchitectureSummary,
10155
- prepareVizData,
10156
- watchProject,
10157
- startVizServer,
10158
- createEmptyState,
10159
- updateFileInGraph,
10160
8678
  calculateHealthScore,
10161
8679
  getHealthTrend,
10162
8680
  analyzeDeadCode,
8681
+ loadMetadata,
10163
8682
  generateDocs,
10164
- getCommitLog,
10165
- getCurrentBranch,
10166
- checkoutCommit,
10167
- restoreOriginal,
10168
- stashChanges,
10169
- popStash,
10170
- isGitRepo,
10171
- sampleCommits,
10172
- saveSnapshot,
10173
- loadSnapshot,
10174
- createSnapshot,
10175
- startMcpServer
8683
+ SimulationEngine
10176
8684
  };