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.
- package/README.md +105 -33
- package/dist/{chunk-S3NZMIIU.js → chunk-QHVWDUSX.js} +382 -1874
- package/dist/chunk-XBCQPU63.js +2002 -0
- package/dist/index.js +172 -28
- package/dist/mcpb-entry.js +5 -3
- package/dist/sdk.d.ts +237 -0
- package/dist/sdk.js +32 -0
- package/package.json +16 -4
|
@@ -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
|
|
125
|
-
let grammarsDir = path.join(
|
|
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(
|
|
127
|
+
grammarsDir = path.join(path.dirname(__dirname), "parser", "grammars");
|
|
128
128
|
}
|
|
129
129
|
if (!existsSync2(grammarsDir)) {
|
|
130
|
-
grammarsDir = path.join(
|
|
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
|
|
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 =
|
|
3534
|
-
const targetDir =
|
|
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 =
|
|
3582
|
-
const targetDir =
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
4281
|
-
return
|
|
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
|
|
4107
|
+
import { join as join12 } from "path";
|
|
4462
4108
|
|
|
4463
4109
|
// src/docs/architecture.ts
|
|
4464
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4656
|
-
const targetDir =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5831
|
-
const targetDir =
|
|
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 =
|
|
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 =
|
|
5967
|
-
if (files.every((f) =>
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
6801
|
-
const dirPath =
|
|
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 =
|
|
6860
|
-
const testDir =
|
|
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 =
|
|
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 ? `\`${
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
8062
|
+
import { join as join11 } from "path";
|
|
8417
8063
|
function loadMetadata(outputDir) {
|
|
8418
|
-
const metadataPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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/
|
|
8689
|
-
import {
|
|
8690
|
-
|
|
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
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
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
|
|
8799
|
-
|
|
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
|
-
|
|
8802
|
-
|
|
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
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
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
|
-
|
|
8883
|
-
|
|
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
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
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 (
|
|
8898
|
-
|
|
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
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
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
|
-
|
|
8919
|
-
|
|
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
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
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
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
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
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
}
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
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
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
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
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|