depwire-cli 0.2.6 → 0.3.1
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 +56 -2
- package/dist/{chunk-V3SB37U3.js → chunk-LOX5NEND.js} +2057 -101
- package/dist/index.js +101 -1
- package/dist/mcpb-entry.js +1 -1
- package/package.json +1 -1
|
@@ -2044,6 +2044,43 @@ function buildGraph(parsedFiles) {
|
|
|
2044
2044
|
}
|
|
2045
2045
|
|
|
2046
2046
|
// src/graph/queries.ts
|
|
2047
|
+
function findSymbols(graph, query) {
|
|
2048
|
+
if (query.includes("::")) {
|
|
2049
|
+
if (graph.hasNode(query)) {
|
|
2050
|
+
const attrs = graph.getNodeAttributes(query);
|
|
2051
|
+
return [{
|
|
2052
|
+
id: query,
|
|
2053
|
+
name: attrs.name,
|
|
2054
|
+
kind: attrs.kind,
|
|
2055
|
+
filePath: attrs.filePath,
|
|
2056
|
+
startLine: attrs.startLine,
|
|
2057
|
+
endLine: attrs.endLine,
|
|
2058
|
+
exported: attrs.exported,
|
|
2059
|
+
scope: attrs.scope,
|
|
2060
|
+
dependentCount: graph.inDegree(query)
|
|
2061
|
+
}];
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const queryLower = query.toLowerCase();
|
|
2065
|
+
const results = [];
|
|
2066
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
2067
|
+
if (attrs.name.toLowerCase() === queryLower) {
|
|
2068
|
+
results.push({
|
|
2069
|
+
id: nodeId,
|
|
2070
|
+
name: attrs.name,
|
|
2071
|
+
kind: attrs.kind,
|
|
2072
|
+
filePath: attrs.filePath,
|
|
2073
|
+
startLine: attrs.startLine,
|
|
2074
|
+
endLine: attrs.endLine,
|
|
2075
|
+
exported: attrs.exported,
|
|
2076
|
+
scope: attrs.scope,
|
|
2077
|
+
dependentCount: graph.inDegree(nodeId)
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
});
|
|
2081
|
+
results.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
2082
|
+
return results;
|
|
2083
|
+
}
|
|
2047
2084
|
function getDependencies(graph, symbolId) {
|
|
2048
2085
|
if (!graph.hasNode(symbolId)) return [];
|
|
2049
2086
|
const dependencies = [];
|
|
@@ -2364,7 +2401,7 @@ import { fileURLToPath } from "url";
|
|
|
2364
2401
|
import { dirname as dirname5, join as join7 } from "path";
|
|
2365
2402
|
import { WebSocketServer } from "ws";
|
|
2366
2403
|
var __filename = fileURLToPath(import.meta.url);
|
|
2367
|
-
var
|
|
2404
|
+
var __dirname2 = dirname5(__filename);
|
|
2368
2405
|
var activeServer = null;
|
|
2369
2406
|
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
2370
2407
|
const net = await import("net");
|
|
@@ -2402,7 +2439,7 @@ async function startVizServer(initialVizData, graph, projectRoot, port = 3333, s
|
|
|
2402
2439
|
const availablePort = await findAvailablePort(port);
|
|
2403
2440
|
const app = express();
|
|
2404
2441
|
let vizData = initialVizData;
|
|
2405
|
-
const publicDir = join7(
|
|
2442
|
+
const publicDir = join7(__dirname2, "viz", "public");
|
|
2406
2443
|
app.use(express.static(publicDir));
|
|
2407
2444
|
app.get("/api/graph", (req, res) => {
|
|
2408
2445
|
res.json(vizData);
|
|
@@ -2491,97 +2528,1824 @@ Depwire visualization running at ${url2}`);
|
|
|
2491
2528
|
}
|
|
2492
2529
|
}
|
|
2493
2530
|
});
|
|
2494
|
-
process.on("SIGINT", () => {
|
|
2495
|
-
console.error("\nShutting down visualization server...");
|
|
2496
|
-
activeServer = null;
|
|
2497
|
-
watcher.close();
|
|
2498
|
-
wss.close();
|
|
2499
|
-
server.close(() => {
|
|
2500
|
-
process.exit(0);
|
|
2531
|
+
process.on("SIGINT", () => {
|
|
2532
|
+
console.error("\nShutting down visualization server...");
|
|
2533
|
+
activeServer = null;
|
|
2534
|
+
watcher.close();
|
|
2535
|
+
wss.close();
|
|
2536
|
+
server.close(() => {
|
|
2537
|
+
process.exit(0);
|
|
2538
|
+
});
|
|
2539
|
+
});
|
|
2540
|
+
const url = `http://127.0.0.1:${availablePort}`;
|
|
2541
|
+
return { server, url, alreadyRunning: false };
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/mcp/state.ts
|
|
2545
|
+
function createEmptyState() {
|
|
2546
|
+
return {
|
|
2547
|
+
graph: null,
|
|
2548
|
+
projectRoot: null,
|
|
2549
|
+
projectName: null,
|
|
2550
|
+
watcher: null
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function isProjectLoaded(state) {
|
|
2554
|
+
return state.graph !== null && state.projectRoot !== null;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// src/graph/updater.ts
|
|
2558
|
+
import { join as join8 } from "path";
|
|
2559
|
+
function removeFileFromGraph(graph, filePath) {
|
|
2560
|
+
const nodesToRemove = [];
|
|
2561
|
+
graph.forEachNode((node, attrs) => {
|
|
2562
|
+
if (attrs.filePath === filePath) {
|
|
2563
|
+
nodesToRemove.push(node);
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
nodesToRemove.forEach((node) => {
|
|
2567
|
+
try {
|
|
2568
|
+
graph.dropNode(node);
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
function addFileToGraph(graph, parsedFile) {
|
|
2574
|
+
for (const symbol of parsedFile.symbols) {
|
|
2575
|
+
const nodeId = `${parsedFile.filePath}::${symbol.name}`;
|
|
2576
|
+
try {
|
|
2577
|
+
graph.addNode(nodeId, {
|
|
2578
|
+
name: symbol.name,
|
|
2579
|
+
kind: symbol.kind,
|
|
2580
|
+
filePath: parsedFile.filePath,
|
|
2581
|
+
startLine: symbol.location.startLine,
|
|
2582
|
+
endLine: symbol.location.endLine,
|
|
2583
|
+
exported: symbol.exported,
|
|
2584
|
+
scope: symbol.scope
|
|
2585
|
+
});
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
for (const edge of parsedFile.edges) {
|
|
2590
|
+
try {
|
|
2591
|
+
graph.mergeEdge(edge.source, edge.target, {
|
|
2592
|
+
kind: edge.kind,
|
|
2593
|
+
sourceFile: edge.sourceFile,
|
|
2594
|
+
targetFile: edge.targetFile
|
|
2595
|
+
});
|
|
2596
|
+
} catch (error) {
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
2601
|
+
removeFileFromGraph(graph, relativeFilePath);
|
|
2602
|
+
const absolutePath = join8(projectRoot, relativeFilePath);
|
|
2603
|
+
try {
|
|
2604
|
+
const parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
|
|
2605
|
+
addFileToGraph(graph, parsedFile);
|
|
2606
|
+
} catch (error) {
|
|
2607
|
+
console.error(`Failed to parse file ${relativeFilePath}:`, error);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/docs/generator.ts
|
|
2612
|
+
import { writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync6 } from "fs";
|
|
2613
|
+
import { join as join10 } from "path";
|
|
2614
|
+
|
|
2615
|
+
// src/docs/architecture.ts
|
|
2616
|
+
import { dirname as dirname6 } from "path";
|
|
2617
|
+
|
|
2618
|
+
// src/docs/templates.ts
|
|
2619
|
+
function header(text, level = 1) {
|
|
2620
|
+
return `${"#".repeat(level)} ${text}
|
|
2621
|
+
|
|
2622
|
+
`;
|
|
2623
|
+
}
|
|
2624
|
+
function code(text) {
|
|
2625
|
+
return `\`${text}\``;
|
|
2626
|
+
}
|
|
2627
|
+
function codeBlock(code2, lang = "") {
|
|
2628
|
+
return `\`\`\`${lang}
|
|
2629
|
+
${code2}
|
|
2630
|
+
\`\`\`
|
|
2631
|
+
|
|
2632
|
+
`;
|
|
2633
|
+
}
|
|
2634
|
+
function unorderedList(items) {
|
|
2635
|
+
return items.map((item) => `- ${item}`).join("\n") + "\n\n";
|
|
2636
|
+
}
|
|
2637
|
+
function orderedList(items) {
|
|
2638
|
+
return items.map((item, i) => `${i + 1}. ${item}`).join("\n") + "\n\n";
|
|
2639
|
+
}
|
|
2640
|
+
function table(headers, rows) {
|
|
2641
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
2642
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
2643
|
+
const dataRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
2644
|
+
return `${headerRow}
|
|
2645
|
+
${separator}
|
|
2646
|
+
${dataRows}
|
|
2647
|
+
|
|
2648
|
+
`;
|
|
2649
|
+
}
|
|
2650
|
+
function blockquote(text) {
|
|
2651
|
+
return `> ${text}
|
|
2652
|
+
|
|
2653
|
+
`;
|
|
2654
|
+
}
|
|
2655
|
+
function timestamp(version, date, fileCount, symbolCount) {
|
|
2656
|
+
return blockquote(`Auto-generated by Depwire ${version} on ${date} | ${fileCount.toLocaleString()} files, ${symbolCount.toLocaleString()} symbols`);
|
|
2657
|
+
}
|
|
2658
|
+
function formatNumber(n) {
|
|
2659
|
+
return n.toLocaleString();
|
|
2660
|
+
}
|
|
2661
|
+
function formatPercent(value, total) {
|
|
2662
|
+
if (total === 0) return "0.0%";
|
|
2663
|
+
return `${(value / total * 100).toFixed(1)}%`;
|
|
2664
|
+
}
|
|
2665
|
+
function impactEmoji(count) {
|
|
2666
|
+
if (count >= 20) return "\u{1F534}";
|
|
2667
|
+
if (count >= 10) return "\u{1F7E1}";
|
|
2668
|
+
if (count >= 5) return "\u{1F7E2}";
|
|
2669
|
+
return "\u26AA";
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/docs/architecture.ts
|
|
2673
|
+
function generateArchitecture(graph, projectRoot, version, parseTime) {
|
|
2674
|
+
const startTime = Date.now();
|
|
2675
|
+
let output = "";
|
|
2676
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2677
|
+
output += timestamp(version, now, getFileCount(graph), graph.order);
|
|
2678
|
+
output += header("Architecture Overview");
|
|
2679
|
+
output += header("Project Summary", 2);
|
|
2680
|
+
output += generateProjectSummary(graph, parseTime);
|
|
2681
|
+
output += header("Module Structure", 2);
|
|
2682
|
+
output += generateModuleStructure(graph);
|
|
2683
|
+
output += header("Entry Points", 2);
|
|
2684
|
+
output += generateEntryPoints(graph);
|
|
2685
|
+
output += header("Hub Files", 2);
|
|
2686
|
+
output += generateHubFiles(graph);
|
|
2687
|
+
output += header("Layer Analysis", 2);
|
|
2688
|
+
output += generateLayerAnalysis(graph);
|
|
2689
|
+
output += header("Circular Dependencies", 2);
|
|
2690
|
+
output += generateCircularDependencies(graph);
|
|
2691
|
+
return output;
|
|
2692
|
+
}
|
|
2693
|
+
function getFileCount(graph) {
|
|
2694
|
+
const files = /* @__PURE__ */ new Set();
|
|
2695
|
+
graph.forEachNode((node, attrs) => {
|
|
2696
|
+
files.add(attrs.filePath);
|
|
2697
|
+
});
|
|
2698
|
+
return files.size;
|
|
2699
|
+
}
|
|
2700
|
+
function getLanguageStats(graph) {
|
|
2701
|
+
const stats = {};
|
|
2702
|
+
const files = /* @__PURE__ */ new Set();
|
|
2703
|
+
graph.forEachNode((node, attrs) => {
|
|
2704
|
+
if (!files.has(attrs.filePath)) {
|
|
2705
|
+
files.add(attrs.filePath);
|
|
2706
|
+
const ext = attrs.filePath.toLowerCase();
|
|
2707
|
+
let lang;
|
|
2708
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
2709
|
+
lang = "TypeScript";
|
|
2710
|
+
} else if (ext.endsWith(".py")) {
|
|
2711
|
+
lang = "Python";
|
|
2712
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
2713
|
+
lang = "JavaScript";
|
|
2714
|
+
} else if (ext.endsWith(".go")) {
|
|
2715
|
+
lang = "Go";
|
|
2716
|
+
} else {
|
|
2717
|
+
lang = "Other";
|
|
2718
|
+
}
|
|
2719
|
+
stats[lang] = (stats[lang] || 0) + 1;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
return stats;
|
|
2723
|
+
}
|
|
2724
|
+
function generateProjectSummary(graph, parseTime) {
|
|
2725
|
+
const fileCount = getFileCount(graph);
|
|
2726
|
+
const symbolCount = graph.order;
|
|
2727
|
+
const edgeCount = graph.size;
|
|
2728
|
+
const languages = getLanguageStats(graph);
|
|
2729
|
+
let output = "";
|
|
2730
|
+
output += `- **Total Files:** ${formatNumber(fileCount)}
|
|
2731
|
+
`;
|
|
2732
|
+
output += `- **Total Symbols:** ${formatNumber(symbolCount)}
|
|
2733
|
+
`;
|
|
2734
|
+
output += `- **Total Edges:** ${formatNumber(edgeCount)}
|
|
2735
|
+
`;
|
|
2736
|
+
output += `- **Parse Time:** ${parseTime.toFixed(1)}s
|
|
2737
|
+
`;
|
|
2738
|
+
if (Object.keys(languages).length > 1) {
|
|
2739
|
+
output += "\n**Languages:**\n\n";
|
|
2740
|
+
const totalFiles = fileCount;
|
|
2741
|
+
for (const [lang, count] of Object.entries(languages).sort((a, b) => b[1] - a[1])) {
|
|
2742
|
+
output += `- ${lang}: ${count} files (${formatPercent(count, totalFiles)})
|
|
2743
|
+
`;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
output += "\n";
|
|
2747
|
+
return output;
|
|
2748
|
+
}
|
|
2749
|
+
function generateModuleStructure(graph) {
|
|
2750
|
+
const dirStats = getDirectoryStats(graph);
|
|
2751
|
+
if (dirStats.length === 0) {
|
|
2752
|
+
return "No module structure detected (single file or flat structure).\n\n";
|
|
2753
|
+
}
|
|
2754
|
+
const headers = ["Directory", "Files", "Symbols", "Connections", "Role"];
|
|
2755
|
+
const rows = dirStats.slice(0, 15).map((dir) => [
|
|
2756
|
+
`\`${dir.name}\``,
|
|
2757
|
+
formatNumber(dir.fileCount),
|
|
2758
|
+
formatNumber(dir.symbolCount),
|
|
2759
|
+
formatNumber(dir.connectionCount),
|
|
2760
|
+
dir.role
|
|
2761
|
+
]);
|
|
2762
|
+
return table(headers, rows);
|
|
2763
|
+
}
|
|
2764
|
+
function getDirectoryStats(graph) {
|
|
2765
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
2766
|
+
graph.forEachNode((node, attrs) => {
|
|
2767
|
+
const dir = dirname6(attrs.filePath);
|
|
2768
|
+
if (dir === ".") return;
|
|
2769
|
+
if (!dirMap.has(dir)) {
|
|
2770
|
+
dirMap.set(dir, {
|
|
2771
|
+
name: dir,
|
|
2772
|
+
fileCount: 0,
|
|
2773
|
+
symbolCount: 0,
|
|
2774
|
+
connectionCount: 0,
|
|
2775
|
+
role: "",
|
|
2776
|
+
typeCount: 0,
|
|
2777
|
+
functionCount: 0,
|
|
2778
|
+
outboundEdges: 0,
|
|
2779
|
+
inboundEdges: 0
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
const dirStat = dirMap.get(dir);
|
|
2783
|
+
dirStat.symbolCount++;
|
|
2784
|
+
if (attrs.kind === "interface" || attrs.kind === "type_alias") {
|
|
2785
|
+
dirStat.typeCount++;
|
|
2786
|
+
} else if (attrs.kind === "function" || attrs.kind === "method") {
|
|
2787
|
+
dirStat.functionCount++;
|
|
2788
|
+
}
|
|
2789
|
+
});
|
|
2790
|
+
const filesPerDir = /* @__PURE__ */ new Map();
|
|
2791
|
+
graph.forEachNode((node, attrs) => {
|
|
2792
|
+
const dir = dirname6(attrs.filePath);
|
|
2793
|
+
if (!filesPerDir.has(dir)) {
|
|
2794
|
+
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
2795
|
+
}
|
|
2796
|
+
filesPerDir.get(dir).add(attrs.filePath);
|
|
2797
|
+
});
|
|
2798
|
+
filesPerDir.forEach((files, dir) => {
|
|
2799
|
+
if (dirMap.has(dir)) {
|
|
2800
|
+
dirMap.get(dir).fileCount = files.size;
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
2804
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2805
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2806
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2807
|
+
const sourceDir = dirname6(sourceAttrs.filePath);
|
|
2808
|
+
const targetDir = dirname6(targetAttrs.filePath);
|
|
2809
|
+
if (sourceDir !== targetDir) {
|
|
2810
|
+
if (!dirEdges.has(sourceDir)) {
|
|
2811
|
+
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
2812
|
+
}
|
|
2813
|
+
if (!dirEdges.has(targetDir)) {
|
|
2814
|
+
dirEdges.set(targetDir, { in: 0, out: 0 });
|
|
2815
|
+
}
|
|
2816
|
+
dirEdges.get(sourceDir).out++;
|
|
2817
|
+
dirEdges.get(targetDir).in++;
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
dirEdges.forEach((edges, dir) => {
|
|
2821
|
+
if (dirMap.has(dir)) {
|
|
2822
|
+
const stat = dirMap.get(dir);
|
|
2823
|
+
stat.inboundEdges = edges.in;
|
|
2824
|
+
stat.outboundEdges = edges.out;
|
|
2825
|
+
stat.connectionCount = edges.in + edges.out;
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
dirMap.forEach((dir) => {
|
|
2829
|
+
const typeRatio = dir.symbolCount > 0 ? dir.typeCount / dir.symbolCount : 0;
|
|
2830
|
+
const outboundRatio = dir.connectionCount > 0 ? dir.outboundEdges / dir.connectionCount : 0;
|
|
2831
|
+
const inboundRatio = dir.connectionCount > 0 ? dir.inboundEdges / dir.connectionCount : 0;
|
|
2832
|
+
if (typeRatio > 0.7) {
|
|
2833
|
+
dir.role = "Type definitions";
|
|
2834
|
+
} else if (outboundRatio > 0.7) {
|
|
2835
|
+
dir.role = "Orchestration / Entry points";
|
|
2836
|
+
} else if (inboundRatio > 0.7) {
|
|
2837
|
+
dir.role = "Shared utilities / Foundation";
|
|
2838
|
+
} else {
|
|
2839
|
+
dir.role = "Core logic";
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
return Array.from(dirMap.values()).sort((a, b) => b.symbolCount - a.symbolCount);
|
|
2843
|
+
}
|
|
2844
|
+
function generateEntryPoints(graph) {
|
|
2845
|
+
const fileStats = getFileStats(graph);
|
|
2846
|
+
const entryPoints = fileStats.filter((f) => f.outgoingRefs > 0).map((f) => ({
|
|
2847
|
+
...f,
|
|
2848
|
+
ratio: f.incomingRefs === 0 ? Infinity : f.outgoingRefs / (f.incomingRefs + 1)
|
|
2849
|
+
})).sort((a, b) => b.ratio - a.ratio).slice(0, 5);
|
|
2850
|
+
if (entryPoints.length === 0) {
|
|
2851
|
+
return "No clear entry points detected.\n\n";
|
|
2852
|
+
}
|
|
2853
|
+
const headers = ["File", "Outgoing", "Incoming", "Ratio"];
|
|
2854
|
+
const rows = entryPoints.map((f) => [
|
|
2855
|
+
`\`${f.filePath}\``,
|
|
2856
|
+
formatNumber(f.outgoingRefs),
|
|
2857
|
+
formatNumber(f.incomingRefs),
|
|
2858
|
+
f.ratio === Infinity ? "\u221E" : f.ratio.toFixed(1)
|
|
2859
|
+
]);
|
|
2860
|
+
return table(headers, rows);
|
|
2861
|
+
}
|
|
2862
|
+
function generateHubFiles(graph) {
|
|
2863
|
+
const fileStats = getFileStats(graph);
|
|
2864
|
+
const hubFiles = fileStats.sort((a, b) => b.incomingRefs - a.incomingRefs).slice(0, 10);
|
|
2865
|
+
if (hubFiles.length === 0 || hubFiles[0].incomingRefs === 0) {
|
|
2866
|
+
return "No hub files detected.\n\n";
|
|
2867
|
+
}
|
|
2868
|
+
const headers = ["File", "Dependents", "Symbols"];
|
|
2869
|
+
const rows = hubFiles.map((f) => [
|
|
2870
|
+
`\`${f.filePath}\``,
|
|
2871
|
+
formatNumber(f.incomingRefs),
|
|
2872
|
+
formatNumber(f.symbolCount)
|
|
2873
|
+
]);
|
|
2874
|
+
return table(headers, rows);
|
|
2875
|
+
}
|
|
2876
|
+
function getFileStats(graph) {
|
|
2877
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
2878
|
+
graph.forEachNode((node, attrs) => {
|
|
2879
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
2880
|
+
fileMap.set(attrs.filePath, {
|
|
2881
|
+
symbolCount: 0,
|
|
2882
|
+
incomingRefs: /* @__PURE__ */ new Set(),
|
|
2883
|
+
outgoingRefs: /* @__PURE__ */ new Set()
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
fileMap.get(attrs.filePath).symbolCount++;
|
|
2887
|
+
});
|
|
2888
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2889
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2890
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2891
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
2892
|
+
const sourceFile = fileMap.get(sourceAttrs.filePath);
|
|
2893
|
+
const targetFile = fileMap.get(targetAttrs.filePath);
|
|
2894
|
+
if (sourceFile) {
|
|
2895
|
+
sourceFile.outgoingRefs.add(targetAttrs.filePath);
|
|
2896
|
+
}
|
|
2897
|
+
if (targetFile) {
|
|
2898
|
+
targetFile.incomingRefs.add(sourceAttrs.filePath);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
const result = [];
|
|
2903
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
2904
|
+
result.push({
|
|
2905
|
+
filePath,
|
|
2906
|
+
symbolCount: data.symbolCount,
|
|
2907
|
+
incomingRefs: data.incomingRefs.size,
|
|
2908
|
+
outgoingRefs: data.outgoingRefs.size
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
return result;
|
|
2912
|
+
}
|
|
2913
|
+
function generateLayerAnalysis(graph) {
|
|
2914
|
+
const dirStats = getDirectoryStats(graph);
|
|
2915
|
+
if (dirStats.length === 0) {
|
|
2916
|
+
return "No layered architecture detected (flat or single-file project).\n\n";
|
|
2917
|
+
}
|
|
2918
|
+
const foundation = dirStats.filter((d) => d.inboundEdges > d.outboundEdges * 2);
|
|
2919
|
+
const orchestration = dirStats.filter((d) => d.outboundEdges > d.inboundEdges * 2);
|
|
2920
|
+
const core = dirStats.filter((d) => !foundation.includes(d) && !orchestration.includes(d));
|
|
2921
|
+
let output = "";
|
|
2922
|
+
if (foundation.length > 0) {
|
|
2923
|
+
output += "**Foundation Layer** (mostly imported by others):\n\n";
|
|
2924
|
+
output += unorderedList(foundation.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2925
|
+
}
|
|
2926
|
+
if (core.length > 0) {
|
|
2927
|
+
output += "**Core Layer** (balanced dependencies):\n\n";
|
|
2928
|
+
output += unorderedList(core.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2929
|
+
}
|
|
2930
|
+
if (orchestration.length > 0) {
|
|
2931
|
+
output += "**Orchestration Layer** (mostly imports from others):\n\n";
|
|
2932
|
+
output += unorderedList(orchestration.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2933
|
+
}
|
|
2934
|
+
return output;
|
|
2935
|
+
}
|
|
2936
|
+
function generateCircularDependencies(graph) {
|
|
2937
|
+
const cycles = detectCycles(graph);
|
|
2938
|
+
if (cycles.length === 0) {
|
|
2939
|
+
return "\u2705 No circular dependencies detected.\n\n";
|
|
2940
|
+
}
|
|
2941
|
+
let output = `\u26A0\uFE0F Found ${cycles.length} circular ${cycles.length === 1 ? "dependency" : "dependencies"}:
|
|
2942
|
+
|
|
2943
|
+
`;
|
|
2944
|
+
for (let i = 0; i < Math.min(cycles.length, 10); i++) {
|
|
2945
|
+
const cycle = cycles[i];
|
|
2946
|
+
output += `**Cycle ${i + 1}:**
|
|
2947
|
+
|
|
2948
|
+
`;
|
|
2949
|
+
output += codeBlock(cycle.path.join(" \u2192\n"), "");
|
|
2950
|
+
output += `**Suggested fix:** ${cycle.suggestion}
|
|
2951
|
+
|
|
2952
|
+
`;
|
|
2953
|
+
}
|
|
2954
|
+
if (cycles.length > 10) {
|
|
2955
|
+
output += `... and ${cycles.length - 10} more cycles.
|
|
2956
|
+
|
|
2957
|
+
`;
|
|
2958
|
+
}
|
|
2959
|
+
return output;
|
|
2960
|
+
}
|
|
2961
|
+
function detectCycles(graph) {
|
|
2962
|
+
const cycles = [];
|
|
2963
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2964
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
2965
|
+
const pathStack = [];
|
|
2966
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
2967
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2968
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
2969
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
2970
|
+
if (sourceFile !== targetFile) {
|
|
2971
|
+
if (!fileGraph.has(sourceFile)) {
|
|
2972
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
|
|
2973
|
+
}
|
|
2974
|
+
fileGraph.get(sourceFile).add(targetFile);
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
function dfs(file) {
|
|
2978
|
+
visited.add(file);
|
|
2979
|
+
recStack.add(file);
|
|
2980
|
+
pathStack.push(file);
|
|
2981
|
+
const neighbors = fileGraph.get(file);
|
|
2982
|
+
if (neighbors) {
|
|
2983
|
+
for (const neighbor of neighbors) {
|
|
2984
|
+
if (!visited.has(neighbor)) {
|
|
2985
|
+
if (dfs(neighbor)) {
|
|
2986
|
+
return true;
|
|
2987
|
+
}
|
|
2988
|
+
} else if (recStack.has(neighbor)) {
|
|
2989
|
+
const cycleStart = pathStack.indexOf(neighbor);
|
|
2990
|
+
const cyclePath = pathStack.slice(cycleStart);
|
|
2991
|
+
cyclePath.push(neighbor);
|
|
2992
|
+
cycles.push({
|
|
2993
|
+
path: cyclePath,
|
|
2994
|
+
suggestion: "Extract shared types/interfaces to a common file"
|
|
2995
|
+
});
|
|
2996
|
+
return true;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
recStack.delete(file);
|
|
3001
|
+
pathStack.pop();
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
for (const file of fileGraph.keys()) {
|
|
3005
|
+
if (!visited.has(file)) {
|
|
3006
|
+
dfs(file);
|
|
3007
|
+
recStack.clear();
|
|
3008
|
+
pathStack.length = 0;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return cycles;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/docs/conventions.ts
|
|
3015
|
+
import { basename as basename2, extname as extname4 } from "path";
|
|
3016
|
+
function generateConventions(graph, projectRoot, version) {
|
|
3017
|
+
let output = "";
|
|
3018
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3019
|
+
const fileCount = getFileCount2(graph);
|
|
3020
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
3021
|
+
output += header("Code Conventions");
|
|
3022
|
+
output += "Auto-detected coding patterns and conventions in this codebase.\n\n";
|
|
3023
|
+
output += header("File Organization", 2);
|
|
3024
|
+
output += generateFileOrganization(graph);
|
|
3025
|
+
output += header("Naming Patterns", 2);
|
|
3026
|
+
output += generateNamingPatterns(graph);
|
|
3027
|
+
output += header("Import Style", 2);
|
|
3028
|
+
output += generateImportStyle(graph);
|
|
3029
|
+
output += header("Export Patterns", 2);
|
|
3030
|
+
output += generateExportPatterns(graph);
|
|
3031
|
+
output += header("Symbol Distribution", 2);
|
|
3032
|
+
output += generateSymbolDistribution(graph);
|
|
3033
|
+
output += header("Detected Design Patterns", 2);
|
|
3034
|
+
output += generateDesignPatterns(graph);
|
|
3035
|
+
return output;
|
|
3036
|
+
}
|
|
3037
|
+
function getFileCount2(graph) {
|
|
3038
|
+
const files = /* @__PURE__ */ new Set();
|
|
3039
|
+
graph.forEachNode((node, attrs) => {
|
|
3040
|
+
files.add(attrs.filePath);
|
|
3041
|
+
});
|
|
3042
|
+
return files.size;
|
|
3043
|
+
}
|
|
3044
|
+
function generateFileOrganization(graph) {
|
|
3045
|
+
const files = /* @__PURE__ */ new Set();
|
|
3046
|
+
let barrelFileCount = 0;
|
|
3047
|
+
let testFileCount = 0;
|
|
3048
|
+
let totalLines = 0;
|
|
3049
|
+
const fileSizes = [];
|
|
3050
|
+
graph.forEachNode((node, attrs) => {
|
|
3051
|
+
if (!files.has(attrs.filePath)) {
|
|
3052
|
+
files.add(attrs.filePath);
|
|
3053
|
+
const fileName = basename2(attrs.filePath);
|
|
3054
|
+
if (fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx") {
|
|
3055
|
+
barrelFileCount++;
|
|
3056
|
+
}
|
|
3057
|
+
if (fileName.includes(".test.") || fileName.includes(".spec.") || attrs.filePath.includes("__tests__")) {
|
|
3058
|
+
testFileCount++;
|
|
3059
|
+
}
|
|
3060
|
+
const maxLine = getMaxLineNumber(graph, attrs.filePath);
|
|
3061
|
+
if (maxLine > 0) {
|
|
3062
|
+
fileSizes.push(maxLine);
|
|
3063
|
+
totalLines += maxLine;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
const avgFileSize = fileSizes.length > 0 ? Math.round(totalLines / fileSizes.length) : 0;
|
|
3068
|
+
const medianFileSize = fileSizes.length > 0 ? getMedian(fileSizes) : 0;
|
|
3069
|
+
let output = "";
|
|
3070
|
+
output += `- **Total Files:** ${formatNumber(files.size)}
|
|
3071
|
+
`;
|
|
3072
|
+
output += `- **Barrel Files (index.*):** ${formatNumber(barrelFileCount)} (${formatPercent(barrelFileCount, files.size)})
|
|
3073
|
+
`;
|
|
3074
|
+
output += `- **Test Files:** ${formatNumber(testFileCount)} (${formatPercent(testFileCount, files.size)})
|
|
3075
|
+
`;
|
|
3076
|
+
if (avgFileSize > 0) {
|
|
3077
|
+
output += `- **Average File Size:** ${formatNumber(avgFileSize)} lines
|
|
3078
|
+
`;
|
|
3079
|
+
output += `- **Median File Size:** ${formatNumber(medianFileSize)} lines
|
|
3080
|
+
`;
|
|
3081
|
+
}
|
|
3082
|
+
output += "\n";
|
|
3083
|
+
return output;
|
|
3084
|
+
}
|
|
3085
|
+
function getMaxLineNumber(graph, filePath) {
|
|
3086
|
+
let maxLine = 0;
|
|
3087
|
+
graph.forEachNode((node, attrs) => {
|
|
3088
|
+
if (attrs.filePath === filePath) {
|
|
3089
|
+
maxLine = Math.max(maxLine, attrs.endLine);
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
return maxLine;
|
|
3093
|
+
}
|
|
3094
|
+
function getMedian(numbers) {
|
|
3095
|
+
const sorted = [...numbers].sort((a, b) => a - b);
|
|
3096
|
+
const mid = Math.floor(sorted.length / 2);
|
|
3097
|
+
return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
|
|
3098
|
+
}
|
|
3099
|
+
function generateNamingPatterns(graph) {
|
|
3100
|
+
const patterns = {
|
|
3101
|
+
files: { camelCase: 0, PascalCase: 0, kebabCase: 0, snakeCase: 0, total: 0 },
|
|
3102
|
+
functions: { camelCase: 0, PascalCase: 0, snakeCase: 0, total: 0 },
|
|
3103
|
+
classes: { PascalCase: 0, other: 0, total: 0 },
|
|
3104
|
+
interfaces: { IPrefixed: 0, PascalCase: 0, other: 0, total: 0 },
|
|
3105
|
+
constants: { UPPER_SNAKE: 0, other: 0, total: 0 },
|
|
3106
|
+
types: { PascalCase: 0, camelCase: 0, other: 0, total: 0 }
|
|
3107
|
+
};
|
|
3108
|
+
const files = /* @__PURE__ */ new Set();
|
|
3109
|
+
graph.forEachNode((node, attrs) => {
|
|
3110
|
+
if (!files.has(attrs.filePath)) {
|
|
3111
|
+
files.add(attrs.filePath);
|
|
3112
|
+
const fileName = basename2(attrs.filePath, extname4(attrs.filePath));
|
|
3113
|
+
if (isCamelCase(fileName)) patterns.files.camelCase++;
|
|
3114
|
+
else if (isPascalCase(fileName)) patterns.files.PascalCase++;
|
|
3115
|
+
else if (isKebabCase(fileName)) patterns.files.kebabCase++;
|
|
3116
|
+
else if (isSnakeCase(fileName)) patterns.files.snakeCase++;
|
|
3117
|
+
patterns.files.total++;
|
|
3118
|
+
}
|
|
3119
|
+
const name = attrs.name;
|
|
3120
|
+
const kind = attrs.kind;
|
|
3121
|
+
if (kind === "function" || kind === "method") {
|
|
3122
|
+
if (isCamelCase(name)) patterns.functions.camelCase++;
|
|
3123
|
+
else if (isPascalCase(name)) patterns.functions.PascalCase++;
|
|
3124
|
+
else if (isSnakeCase(name)) patterns.functions.snakeCase++;
|
|
3125
|
+
patterns.functions.total++;
|
|
3126
|
+
} else if (kind === "class") {
|
|
3127
|
+
if (isPascalCase(name)) patterns.classes.PascalCase++;
|
|
3128
|
+
else patterns.classes.other++;
|
|
3129
|
+
patterns.classes.total++;
|
|
3130
|
+
} else if (kind === "interface") {
|
|
3131
|
+
if (name.startsWith("I") && isPascalCase(name.slice(1))) patterns.interfaces.IPrefixed++;
|
|
3132
|
+
else if (isPascalCase(name)) patterns.interfaces.PascalCase++;
|
|
3133
|
+
else patterns.interfaces.other++;
|
|
3134
|
+
patterns.interfaces.total++;
|
|
3135
|
+
} else if (kind === "constant") {
|
|
3136
|
+
if (isUpperSnakeCase(name)) patterns.constants.UPPER_SNAKE++;
|
|
3137
|
+
else patterns.constants.other++;
|
|
3138
|
+
patterns.constants.total++;
|
|
3139
|
+
} else if (kind === "type_alias") {
|
|
3140
|
+
if (isPascalCase(name)) patterns.types.PascalCase++;
|
|
3141
|
+
else if (isCamelCase(name)) patterns.types.camelCase++;
|
|
3142
|
+
else patterns.types.other++;
|
|
3143
|
+
patterns.types.total++;
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
let output = "";
|
|
3147
|
+
if (patterns.files.total > 0) {
|
|
3148
|
+
output += "**File Naming:**\n\n";
|
|
3149
|
+
if (patterns.files.kebabCase > 0) {
|
|
3150
|
+
output += `- kebab-case: ${formatPercent(patterns.files.kebabCase, patterns.files.total)}
|
|
3151
|
+
`;
|
|
3152
|
+
}
|
|
3153
|
+
if (patterns.files.camelCase > 0) {
|
|
3154
|
+
output += `- camelCase: ${formatPercent(patterns.files.camelCase, patterns.files.total)}
|
|
3155
|
+
`;
|
|
3156
|
+
}
|
|
3157
|
+
if (patterns.files.PascalCase > 0) {
|
|
3158
|
+
output += `- PascalCase: ${formatPercent(patterns.files.PascalCase, patterns.files.total)}
|
|
3159
|
+
`;
|
|
3160
|
+
}
|
|
3161
|
+
if (patterns.files.snakeCase > 0) {
|
|
3162
|
+
output += `- snake_case: ${formatPercent(patterns.files.snakeCase, patterns.files.total)}
|
|
3163
|
+
`;
|
|
3164
|
+
}
|
|
3165
|
+
output += "\n";
|
|
3166
|
+
}
|
|
3167
|
+
if (patterns.functions.total > 0) {
|
|
3168
|
+
output += "**Function Naming:**\n\n";
|
|
3169
|
+
if (patterns.functions.camelCase > 0) {
|
|
3170
|
+
output += `- camelCase: ${formatPercent(patterns.functions.camelCase, patterns.functions.total)}
|
|
3171
|
+
`;
|
|
3172
|
+
}
|
|
3173
|
+
if (patterns.functions.snakeCase > 0) {
|
|
3174
|
+
output += `- snake_case: ${formatPercent(patterns.functions.snakeCase, patterns.functions.total)}
|
|
3175
|
+
`;
|
|
3176
|
+
}
|
|
3177
|
+
if (patterns.functions.PascalCase > 0) {
|
|
3178
|
+
output += `- PascalCase: ${formatPercent(patterns.functions.PascalCase, patterns.functions.total)}
|
|
3179
|
+
`;
|
|
3180
|
+
}
|
|
3181
|
+
output += "\n";
|
|
3182
|
+
}
|
|
3183
|
+
if (patterns.classes.total > 0) {
|
|
3184
|
+
output += "**Class Naming:**\n\n";
|
|
3185
|
+
output += `- PascalCase: ${formatPercent(patterns.classes.PascalCase, patterns.classes.total)}
|
|
3186
|
+
`;
|
|
3187
|
+
if (patterns.classes.other > 0) {
|
|
3188
|
+
output += `- Other: ${formatPercent(patterns.classes.other, patterns.classes.total)}
|
|
3189
|
+
`;
|
|
3190
|
+
}
|
|
3191
|
+
output += "\n";
|
|
3192
|
+
}
|
|
3193
|
+
if (patterns.interfaces.total > 0) {
|
|
3194
|
+
output += "**Interface Naming:**\n\n";
|
|
3195
|
+
if (patterns.interfaces.IPrefixed > 0) {
|
|
3196
|
+
output += `- I-prefix (IPerson): ${formatPercent(patterns.interfaces.IPrefixed, patterns.interfaces.total)}
|
|
3197
|
+
`;
|
|
3198
|
+
}
|
|
3199
|
+
if (patterns.interfaces.PascalCase > 0) {
|
|
3200
|
+
output += `- PascalCase (Person): ${formatPercent(patterns.interfaces.PascalCase, patterns.interfaces.total)}
|
|
3201
|
+
`;
|
|
3202
|
+
}
|
|
3203
|
+
if (patterns.interfaces.other > 0) {
|
|
3204
|
+
output += `- Other: ${formatPercent(patterns.interfaces.other, patterns.interfaces.total)}
|
|
3205
|
+
`;
|
|
3206
|
+
}
|
|
3207
|
+
output += "\n";
|
|
3208
|
+
}
|
|
3209
|
+
if (patterns.types.total > 0) {
|
|
3210
|
+
output += "**Type Naming:**\n\n";
|
|
3211
|
+
if (patterns.types.PascalCase > 0) {
|
|
3212
|
+
output += `- PascalCase: ${formatPercent(patterns.types.PascalCase, patterns.types.total)}
|
|
3213
|
+
`;
|
|
3214
|
+
}
|
|
3215
|
+
if (patterns.types.camelCase > 0) {
|
|
3216
|
+
output += `- camelCase: ${formatPercent(patterns.types.camelCase, patterns.types.total)}
|
|
3217
|
+
`;
|
|
3218
|
+
}
|
|
3219
|
+
if (patterns.types.other > 0) {
|
|
3220
|
+
output += `- Other: ${formatPercent(patterns.types.other, patterns.types.total)}
|
|
3221
|
+
`;
|
|
3222
|
+
}
|
|
3223
|
+
output += "\n";
|
|
3224
|
+
}
|
|
3225
|
+
if (patterns.constants.total > 0) {
|
|
3226
|
+
output += "**Constant Naming:**\n\n";
|
|
3227
|
+
output += `- UPPER_SNAKE_CASE: ${formatPercent(patterns.constants.UPPER_SNAKE, patterns.constants.total)}
|
|
3228
|
+
`;
|
|
3229
|
+
if (patterns.constants.other > 0) {
|
|
3230
|
+
output += `- Other: ${formatPercent(patterns.constants.other, patterns.constants.total)}
|
|
3231
|
+
`;
|
|
3232
|
+
}
|
|
3233
|
+
output += "\n";
|
|
3234
|
+
}
|
|
3235
|
+
return output;
|
|
3236
|
+
}
|
|
3237
|
+
function generateImportStyle(graph) {
|
|
3238
|
+
let barrelImportCount = 0;
|
|
3239
|
+
let pathAliasCount = 0;
|
|
3240
|
+
let totalImports = 0;
|
|
3241
|
+
let namedExportCount = 0;
|
|
3242
|
+
let defaultExportCount = 0;
|
|
3243
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3244
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3245
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3246
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath && attrs.kind === "imports") {
|
|
3247
|
+
totalImports++;
|
|
3248
|
+
if (targetAttrs.filePath.endsWith("/index.ts") || targetAttrs.filePath.endsWith("/index.js")) {
|
|
3249
|
+
barrelImportCount++;
|
|
3250
|
+
}
|
|
3251
|
+
if (targetAttrs.filePath.startsWith("@/") || targetAttrs.filePath.startsWith("~/") || targetAttrs.filePath.startsWith("src/")) {
|
|
3252
|
+
pathAliasCount++;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
3256
|
+
graph.forEachNode((node, attrs) => {
|
|
3257
|
+
if (attrs.exported) {
|
|
3258
|
+
if (attrs.name === "default") {
|
|
3259
|
+
defaultExportCount++;
|
|
3260
|
+
} else {
|
|
3261
|
+
namedExportCount++;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
let output = "";
|
|
3266
|
+
if (totalImports > 0) {
|
|
3267
|
+
output += `- **Total Cross-File Imports:** ${formatNumber(totalImports)}
|
|
3268
|
+
`;
|
|
3269
|
+
if (barrelImportCount > 0) {
|
|
3270
|
+
output += `- **Barrel Imports (from index files):** ${formatPercent(barrelImportCount, totalImports)}
|
|
3271
|
+
`;
|
|
3272
|
+
}
|
|
3273
|
+
if (pathAliasCount > 0) {
|
|
3274
|
+
output += `- **Path Alias Usage (@/ or ~/):** ${formatPercent(pathAliasCount, totalImports)}
|
|
3275
|
+
`;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
output += "\n";
|
|
3279
|
+
return output;
|
|
3280
|
+
}
|
|
3281
|
+
function generateExportPatterns(graph) {
|
|
3282
|
+
let namedExportCount = 0;
|
|
3283
|
+
let defaultExportCount = 0;
|
|
3284
|
+
let reExportCount = 0;
|
|
3285
|
+
graph.forEachNode((node, attrs) => {
|
|
3286
|
+
if (attrs.exported) {
|
|
3287
|
+
if (attrs.name === "default") {
|
|
3288
|
+
defaultExportCount++;
|
|
3289
|
+
} else {
|
|
3290
|
+
namedExportCount++;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (attrs.kind === "export") {
|
|
3294
|
+
reExportCount++;
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
const totalExports = namedExportCount + defaultExportCount;
|
|
3298
|
+
let output = "";
|
|
3299
|
+
if (totalExports > 0) {
|
|
3300
|
+
output += `- **Named Exports:** ${formatNumber(namedExportCount)} (${formatPercent(namedExportCount, totalExports)})
|
|
3301
|
+
`;
|
|
3302
|
+
output += `- **Default Exports:** ${formatNumber(defaultExportCount)} (${formatPercent(defaultExportCount, totalExports)})
|
|
3303
|
+
`;
|
|
3304
|
+
if (reExportCount > 0) {
|
|
3305
|
+
output += `- **Re-exports:** ${formatNumber(reExportCount)}
|
|
3306
|
+
`;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
output += "\n";
|
|
3310
|
+
return output;
|
|
3311
|
+
}
|
|
3312
|
+
function generateSymbolDistribution(graph) {
|
|
3313
|
+
const symbolCounts = {
|
|
3314
|
+
function: 0,
|
|
3315
|
+
class: 0,
|
|
3316
|
+
variable: 0,
|
|
3317
|
+
constant: 0,
|
|
3318
|
+
type_alias: 0,
|
|
3319
|
+
interface: 0,
|
|
3320
|
+
enum: 0,
|
|
3321
|
+
import: 0,
|
|
3322
|
+
export: 0,
|
|
3323
|
+
method: 0,
|
|
3324
|
+
property: 0,
|
|
3325
|
+
decorator: 0,
|
|
3326
|
+
module: 0
|
|
3327
|
+
};
|
|
3328
|
+
graph.forEachNode((node, attrs) => {
|
|
3329
|
+
symbolCounts[attrs.kind]++;
|
|
3330
|
+
});
|
|
3331
|
+
const total = graph.order;
|
|
3332
|
+
const rows = [];
|
|
3333
|
+
for (const [kind, count] of Object.entries(symbolCounts)) {
|
|
3334
|
+
if (count > 0) {
|
|
3335
|
+
rows.push([kind, formatNumber(count), formatPercent(count, total)]);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
rows.sort((a, b) => parseInt(b[1].replace(/,/g, "")) - parseInt(a[1].replace(/,/g, "")));
|
|
3339
|
+
return table(["Symbol Kind", "Count", "Percentage"], rows);
|
|
3340
|
+
}
|
|
3341
|
+
function generateDesignPatterns(graph) {
|
|
3342
|
+
const patterns = {
|
|
3343
|
+
service: 0,
|
|
3344
|
+
factory: 0,
|
|
3345
|
+
hook: 0,
|
|
3346
|
+
middleware: 0,
|
|
3347
|
+
controller: 0,
|
|
3348
|
+
repository: 0,
|
|
3349
|
+
handler: 0
|
|
3350
|
+
};
|
|
3351
|
+
graph.forEachNode((node, attrs) => {
|
|
3352
|
+
const name = attrs.name;
|
|
3353
|
+
const file = attrs.filePath.toLowerCase();
|
|
3354
|
+
if (attrs.kind === "class" && name.endsWith("Service")) {
|
|
3355
|
+
patterns.service++;
|
|
3356
|
+
}
|
|
3357
|
+
if (attrs.kind === "function" && name.startsWith("create")) {
|
|
3358
|
+
patterns.factory++;
|
|
3359
|
+
}
|
|
3360
|
+
if (attrs.kind === "function" && name.startsWith("use") && name.length > 3) {
|
|
3361
|
+
patterns.hook++;
|
|
3362
|
+
}
|
|
3363
|
+
if (file.includes("middleware")) {
|
|
3364
|
+
patterns.middleware++;
|
|
3365
|
+
}
|
|
3366
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Controller")) {
|
|
3367
|
+
patterns.controller++;
|
|
3368
|
+
}
|
|
3369
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Repository")) {
|
|
3370
|
+
patterns.repository++;
|
|
3371
|
+
}
|
|
3372
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Handler")) {
|
|
3373
|
+
patterns.handler++;
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
const detected = Object.entries(patterns).filter(([, count]) => count > 0);
|
|
3377
|
+
if (detected.length === 0) {
|
|
3378
|
+
return "No common design patterns detected.\n\n";
|
|
3379
|
+
}
|
|
3380
|
+
let output = "";
|
|
3381
|
+
for (const [pattern, count] of detected) {
|
|
3382
|
+
const description = getPatternDescription(pattern);
|
|
3383
|
+
output += `- **${capitalizeFirst(pattern)} Pattern:** ${count} occurrences \u2014 ${description}
|
|
3384
|
+
`;
|
|
3385
|
+
}
|
|
3386
|
+
output += "\n";
|
|
3387
|
+
return output;
|
|
3388
|
+
}
|
|
3389
|
+
function getPatternDescription(pattern) {
|
|
3390
|
+
switch (pattern) {
|
|
3391
|
+
case "service":
|
|
3392
|
+
return 'Classes ending in "Service"';
|
|
3393
|
+
case "factory":
|
|
3394
|
+
return 'Functions starting with "create"';
|
|
3395
|
+
case "hook":
|
|
3396
|
+
return 'Functions starting with "use" (React hooks)';
|
|
3397
|
+
case "middleware":
|
|
3398
|
+
return "Files in middleware directories";
|
|
3399
|
+
case "controller":
|
|
3400
|
+
return "Controllers for handling requests";
|
|
3401
|
+
case "repository":
|
|
3402
|
+
return "Data access layer pattern";
|
|
3403
|
+
case "handler":
|
|
3404
|
+
return "Event/request handlers";
|
|
3405
|
+
default:
|
|
3406
|
+
return "";
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
function capitalizeFirst(str) {
|
|
3410
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3411
|
+
}
|
|
3412
|
+
function isCamelCase(name) {
|
|
3413
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name);
|
|
3414
|
+
}
|
|
3415
|
+
function isPascalCase(name) {
|
|
3416
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
3417
|
+
}
|
|
3418
|
+
function isKebabCase(name) {
|
|
3419
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name);
|
|
3420
|
+
}
|
|
3421
|
+
function isSnakeCase(name) {
|
|
3422
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
3423
|
+
}
|
|
3424
|
+
function isUpperSnakeCase(name) {
|
|
3425
|
+
return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// src/docs/dependencies.ts
|
|
3429
|
+
function generateDependencies(graph, projectRoot, version) {
|
|
3430
|
+
let output = "";
|
|
3431
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3432
|
+
const fileCount = getFileCount3(graph);
|
|
3433
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
3434
|
+
output += header("Dependency Map");
|
|
3435
|
+
output += "Complete dependency mapping showing what connects to what.\n\n";
|
|
3436
|
+
output += header("Module Dependency Matrix", 2);
|
|
3437
|
+
output += generateModuleDependencyMatrix(graph);
|
|
3438
|
+
output += header("High-Impact Symbols", 2);
|
|
3439
|
+
output += generateHighImpactSymbols(graph);
|
|
3440
|
+
output += header("Isolated Files", 2);
|
|
3441
|
+
output += generateIsolatedFiles(graph);
|
|
3442
|
+
output += header("Most Connected File Pairs", 2);
|
|
3443
|
+
output += generateConnectedFilePairs(graph);
|
|
3444
|
+
output += header("Longest Dependency Chains", 2);
|
|
3445
|
+
output += generateDependencyChains(graph);
|
|
3446
|
+
output += header("Circular Dependencies (Detailed)", 2);
|
|
3447
|
+
output += generateCircularDependenciesDetailed(graph);
|
|
3448
|
+
return output;
|
|
3449
|
+
}
|
|
3450
|
+
function getFileCount3(graph) {
|
|
3451
|
+
const files = /* @__PURE__ */ new Set();
|
|
3452
|
+
graph.forEachNode((node, attrs) => {
|
|
3453
|
+
files.add(attrs.filePath);
|
|
3454
|
+
});
|
|
3455
|
+
return files.size;
|
|
3456
|
+
}
|
|
3457
|
+
function generateModuleDependencyMatrix(graph) {
|
|
3458
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
3459
|
+
const allDirs = /* @__PURE__ */ new Set();
|
|
3460
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3461
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3462
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3463
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3464
|
+
const sourceDir = getTopLevelDir(sourceAttrs.filePath);
|
|
3465
|
+
const targetDir = getTopLevelDir(targetAttrs.filePath);
|
|
3466
|
+
if (sourceDir && targetDir && sourceDir !== targetDir) {
|
|
3467
|
+
allDirs.add(sourceDir);
|
|
3468
|
+
allDirs.add(targetDir);
|
|
3469
|
+
if (!dirEdges.has(sourceDir)) {
|
|
3470
|
+
dirEdges.set(sourceDir, /* @__PURE__ */ new Map());
|
|
3471
|
+
}
|
|
3472
|
+
const targetMap = dirEdges.get(sourceDir);
|
|
3473
|
+
targetMap.set(targetDir, (targetMap.get(targetDir) || 0) + 1);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
});
|
|
3477
|
+
if (allDirs.size === 0) {
|
|
3478
|
+
return "No module structure detected (flat or single-directory project).\n\n";
|
|
3479
|
+
}
|
|
3480
|
+
const dirTotalEdges = /* @__PURE__ */ new Map();
|
|
3481
|
+
for (const [sourceDir, targets] of dirEdges.entries()) {
|
|
3482
|
+
let total = 0;
|
|
3483
|
+
for (const count of targets.values()) {
|
|
3484
|
+
total += count;
|
|
3485
|
+
}
|
|
3486
|
+
dirTotalEdges.set(sourceDir, total);
|
|
3487
|
+
}
|
|
3488
|
+
const sortedDirs = Array.from(allDirs).sort((a, b) => (dirTotalEdges.get(b) || 0) - (dirTotalEdges.get(a) || 0)).slice(0, 15);
|
|
3489
|
+
if (sortedDirs.length === 0) {
|
|
3490
|
+
return "No cross-module dependencies detected.\n\n";
|
|
3491
|
+
}
|
|
3492
|
+
const headers = ["From / To", ...sortedDirs];
|
|
3493
|
+
const rows = [];
|
|
3494
|
+
for (const sourceDir of sortedDirs) {
|
|
3495
|
+
const row = [sourceDir];
|
|
3496
|
+
for (const targetDir of sortedDirs) {
|
|
3497
|
+
if (sourceDir === targetDir) {
|
|
3498
|
+
row.push("-");
|
|
3499
|
+
} else {
|
|
3500
|
+
const count = dirEdges.get(sourceDir)?.get(targetDir) || 0;
|
|
3501
|
+
row.push(count > 0 ? count.toString() : "\u2717");
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
rows.push(row);
|
|
3505
|
+
}
|
|
3506
|
+
return table(headers, rows);
|
|
3507
|
+
}
|
|
3508
|
+
function getTopLevelDir(filePath) {
|
|
3509
|
+
const parts = filePath.split("/");
|
|
3510
|
+
if (parts.length < 2) {
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
if (parts[0] === "src" && parts.length >= 3) {
|
|
3514
|
+
return `${parts[0]}/${parts[1]}`;
|
|
3515
|
+
}
|
|
3516
|
+
if (parts[0] === "src" && parts.length === 2) {
|
|
3517
|
+
return null;
|
|
3518
|
+
}
|
|
3519
|
+
const firstDir = parts[0];
|
|
3520
|
+
if (firstDir.includes("test") || firstDir.includes("fixture") || firstDir.includes("example") || firstDir.includes("__tests__") || firstDir === "node_modules" || firstDir === "dist" || firstDir === "build") {
|
|
3521
|
+
return null;
|
|
3522
|
+
}
|
|
3523
|
+
if (parts.length >= 2) {
|
|
3524
|
+
return `${parts[0]}/${parts[1]}`;
|
|
3525
|
+
}
|
|
3526
|
+
return parts[0];
|
|
3527
|
+
}
|
|
3528
|
+
function generateHighImpactSymbols(graph) {
|
|
3529
|
+
const symbolImpact = [];
|
|
3530
|
+
graph.forEachNode((node, attrs) => {
|
|
3531
|
+
const inDegree = graph.inDegree(node);
|
|
3532
|
+
if (inDegree > 0 && attrs.name !== "__file__") {
|
|
3533
|
+
symbolImpact.push({
|
|
3534
|
+
name: attrs.name,
|
|
3535
|
+
filePath: attrs.filePath,
|
|
3536
|
+
kind: attrs.kind,
|
|
3537
|
+
dependentCount: inDegree
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
symbolImpact.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
3542
|
+
const top = symbolImpact.slice(0, 15);
|
|
3543
|
+
if (top.length === 0) {
|
|
3544
|
+
return "No high-impact symbols detected.\n\n";
|
|
3545
|
+
}
|
|
3546
|
+
const headers = ["Symbol", "File", "Kind", "Dependents", "Impact"];
|
|
3547
|
+
const rows = top.map((s) => {
|
|
3548
|
+
const impact = s.dependentCount >= 20 ? `${impactEmoji(s.dependentCount)} Critical` : s.dependentCount >= 10 ? `${impactEmoji(s.dependentCount)} High` : s.dependentCount >= 5 ? `${impactEmoji(s.dependentCount)} Medium` : `${impactEmoji(s.dependentCount)} Low`;
|
|
3549
|
+
return [
|
|
3550
|
+
`\`${s.name}\``,
|
|
3551
|
+
`\`${s.filePath}\``,
|
|
3552
|
+
s.kind,
|
|
3553
|
+
formatNumber(s.dependentCount),
|
|
3554
|
+
impact
|
|
3555
|
+
];
|
|
3556
|
+
});
|
|
3557
|
+
return table(headers, rows);
|
|
3558
|
+
}
|
|
3559
|
+
function generateIsolatedFiles(graph) {
|
|
3560
|
+
const fileConnections = /* @__PURE__ */ new Map();
|
|
3561
|
+
graph.forEachNode((node, attrs) => {
|
|
3562
|
+
if (!fileConnections.has(attrs.filePath)) {
|
|
3563
|
+
fileConnections.set(attrs.filePath, { incoming: 0, outgoing: 0 });
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3567
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3568
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3569
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3570
|
+
const sourceConn = fileConnections.get(sourceAttrs.filePath);
|
|
3571
|
+
const targetConn = fileConnections.get(targetAttrs.filePath);
|
|
3572
|
+
if (sourceConn) sourceConn.outgoing++;
|
|
3573
|
+
if (targetConn) targetConn.incoming++;
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
const isolated = [];
|
|
3577
|
+
for (const [file, conn] of fileConnections.entries()) {
|
|
3578
|
+
if (conn.incoming === 0) {
|
|
3579
|
+
isolated.push(file);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
if (isolated.length === 0) {
|
|
3583
|
+
return "No isolated files detected. All files are connected.\n\n";
|
|
3584
|
+
}
|
|
3585
|
+
let output = `Found ${isolated.length} file${isolated.length === 1 ? "" : "s"} with no incoming dependencies:
|
|
3586
|
+
|
|
3587
|
+
`;
|
|
3588
|
+
if (isolated.length <= 20) {
|
|
3589
|
+
output += unorderedList(isolated.map((f) => `\`${f}\``));
|
|
3590
|
+
} else {
|
|
3591
|
+
output += unorderedList(isolated.slice(0, 20).map((f) => `\`${f}\``));
|
|
3592
|
+
output += `... and ${isolated.length - 20} more.
|
|
3593
|
+
|
|
3594
|
+
`;
|
|
3595
|
+
}
|
|
3596
|
+
output += "These files could be entry points, standalone scripts, or dead code.\n\n";
|
|
3597
|
+
return output;
|
|
3598
|
+
}
|
|
3599
|
+
function generateConnectedFilePairs(graph) {
|
|
3600
|
+
const filePairEdges = /* @__PURE__ */ new Map();
|
|
3601
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3602
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3603
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3604
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3605
|
+
const pair = [sourceAttrs.filePath, targetAttrs.filePath].sort().join(" <-> ");
|
|
3606
|
+
filePairEdges.set(pair, (filePairEdges.get(pair) || 0) + 1);
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
const pairs = Array.from(filePairEdges.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
3610
|
+
if (pairs.length === 0) {
|
|
3611
|
+
return "No cross-file dependencies detected.\n\n";
|
|
3612
|
+
}
|
|
3613
|
+
const headers = ["File 1", "File 2", "Edges"];
|
|
3614
|
+
const rows = pairs.map(([pair, count]) => {
|
|
3615
|
+
const [file1, file2] = pair.split(" <-> ");
|
|
3616
|
+
return [`\`${file1}\``, `\`${file2}\``, formatNumber(count)];
|
|
3617
|
+
});
|
|
3618
|
+
return table(headers, rows);
|
|
3619
|
+
}
|
|
3620
|
+
function generateDependencyChains(graph) {
|
|
3621
|
+
const chains = findLongestPaths(graph, 5);
|
|
3622
|
+
if (chains.length === 0) {
|
|
3623
|
+
return "No significant dependency chains detected.\n\n";
|
|
3624
|
+
}
|
|
3625
|
+
let output = "";
|
|
3626
|
+
for (let i = 0; i < chains.length; i++) {
|
|
3627
|
+
const chain = chains[i];
|
|
3628
|
+
output += `**Chain ${i + 1}** (${chain.length} files):
|
|
3629
|
+
|
|
3630
|
+
`;
|
|
3631
|
+
output += codeBlock(chain.join(" \u2192\n"), "");
|
|
3632
|
+
}
|
|
3633
|
+
return output;
|
|
3634
|
+
}
|
|
3635
|
+
function findLongestPaths(graph, limit) {
|
|
3636
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
3637
|
+
const fileInDegree = /* @__PURE__ */ new Map();
|
|
3638
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3639
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
3640
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
3641
|
+
if (sourceFile !== targetFile) {
|
|
3642
|
+
if (!fileGraph.has(sourceFile)) {
|
|
3643
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
|
|
3644
|
+
}
|
|
3645
|
+
fileGraph.get(sourceFile).add(targetFile);
|
|
3646
|
+
fileInDegree.set(targetFile, (fileInDegree.get(targetFile) || 0) + 1);
|
|
3647
|
+
if (!fileInDegree.has(sourceFile)) {
|
|
3648
|
+
fileInDegree.set(sourceFile, 0);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
const roots = [];
|
|
3653
|
+
for (const [file, inDegree] of fileInDegree.entries()) {
|
|
3654
|
+
if (inDegree === 0) {
|
|
3655
|
+
roots.push(file);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
const allPaths = [];
|
|
3659
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3660
|
+
function dfs(file, path) {
|
|
3661
|
+
visited.add(file);
|
|
3662
|
+
path.push(file);
|
|
3663
|
+
const neighbors = fileGraph.get(file);
|
|
3664
|
+
if (!neighbors || neighbors.size === 0) {
|
|
3665
|
+
allPaths.push([...path]);
|
|
3666
|
+
} else {
|
|
3667
|
+
for (const neighbor of neighbors) {
|
|
3668
|
+
if (!visited.has(neighbor)) {
|
|
3669
|
+
dfs(neighbor, path);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
path.pop();
|
|
3674
|
+
visited.delete(file);
|
|
3675
|
+
}
|
|
3676
|
+
for (const root of roots.slice(0, 10)) {
|
|
3677
|
+
dfs(root, []);
|
|
3678
|
+
}
|
|
3679
|
+
allPaths.sort((a, b) => b.length - a.length);
|
|
3680
|
+
return allPaths.slice(0, limit);
|
|
3681
|
+
}
|
|
3682
|
+
function generateCircularDependenciesDetailed(graph) {
|
|
3683
|
+
const cycles = detectCyclesDetailed(graph);
|
|
3684
|
+
if (cycles.length === 0) {
|
|
3685
|
+
return "\u2705 No circular dependencies detected.\n\n";
|
|
3686
|
+
}
|
|
3687
|
+
let output = `\u26A0\uFE0F Found ${cycles.length} circular ${cycles.length === 1 ? "dependency" : "dependencies"}:
|
|
3688
|
+
|
|
3689
|
+
`;
|
|
3690
|
+
for (let i = 0; i < Math.min(cycles.length, 5); i++) {
|
|
3691
|
+
const cycle = cycles[i];
|
|
3692
|
+
output += `**Cycle ${i + 1}:**
|
|
3693
|
+
|
|
3694
|
+
`;
|
|
3695
|
+
output += codeBlock(cycle.files.join(" \u2192\n") + " \u2192 " + cycle.files[0], "");
|
|
3696
|
+
if (cycle.symbols.length > 0) {
|
|
3697
|
+
output += "**Symbols involved:**\n\n";
|
|
3698
|
+
output += unorderedList(cycle.symbols.map((s) => `\`${s.name}\` (${s.kind}) at \`${s.filePath}:${s.line}\``));
|
|
3699
|
+
}
|
|
3700
|
+
output += `**Suggested fix:** ${cycle.suggestion}
|
|
3701
|
+
|
|
3702
|
+
`;
|
|
3703
|
+
}
|
|
3704
|
+
if (cycles.length > 5) {
|
|
3705
|
+
output += `... and ${cycles.length - 5} more cycles.
|
|
3706
|
+
|
|
3707
|
+
`;
|
|
3708
|
+
}
|
|
3709
|
+
return output;
|
|
3710
|
+
}
|
|
3711
|
+
function detectCyclesDetailed(graph) {
|
|
3712
|
+
const cycles = [];
|
|
3713
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3714
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
3715
|
+
const pathStack = [];
|
|
3716
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
3717
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3718
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3719
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3720
|
+
const sourceFile = sourceAttrs.filePath;
|
|
3721
|
+
const targetFile = targetAttrs.filePath;
|
|
3722
|
+
if (sourceFile !== targetFile) {
|
|
3723
|
+
if (!fileGraph.has(sourceFile)) {
|
|
3724
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Map());
|
|
3725
|
+
}
|
|
3726
|
+
const targetMap = fileGraph.get(sourceFile);
|
|
3727
|
+
if (!targetMap.has(targetFile)) {
|
|
3728
|
+
targetMap.set(targetFile, []);
|
|
3729
|
+
}
|
|
3730
|
+
targetMap.get(targetFile).push({
|
|
3731
|
+
symbolName: targetAttrs.name,
|
|
3732
|
+
symbolKind: targetAttrs.kind,
|
|
3733
|
+
line: attrs.line || sourceAttrs.startLine
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
3737
|
+
function dfs(file) {
|
|
3738
|
+
visited.add(file);
|
|
3739
|
+
recStack.add(file);
|
|
3740
|
+
pathStack.push(file);
|
|
3741
|
+
const neighbors = fileGraph.get(file);
|
|
3742
|
+
if (neighbors) {
|
|
3743
|
+
for (const [neighbor, symbols] of neighbors.entries()) {
|
|
3744
|
+
if (!visited.has(neighbor)) {
|
|
3745
|
+
if (dfs(neighbor)) {
|
|
3746
|
+
return true;
|
|
3747
|
+
}
|
|
3748
|
+
} else if (recStack.has(neighbor)) {
|
|
3749
|
+
const cycleStart = pathStack.indexOf(neighbor);
|
|
3750
|
+
const cyclePath = pathStack.slice(cycleStart);
|
|
3751
|
+
const cycleSymbols = [];
|
|
3752
|
+
for (let i = 0; i < cyclePath.length; i++) {
|
|
3753
|
+
const currentFile = cyclePath[i];
|
|
3754
|
+
const nextFile = cyclePath[(i + 1) % cyclePath.length];
|
|
3755
|
+
const edgeSymbols = fileGraph.get(currentFile)?.get(nextFile) || [];
|
|
3756
|
+
for (const sym of edgeSymbols.slice(0, 3)) {
|
|
3757
|
+
cycleSymbols.push({
|
|
3758
|
+
name: sym.symbolName,
|
|
3759
|
+
kind: sym.symbolKind,
|
|
3760
|
+
filePath: currentFile,
|
|
3761
|
+
line: sym.line
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
cycles.push({
|
|
3766
|
+
files: cyclePath,
|
|
3767
|
+
symbols: cycleSymbols,
|
|
3768
|
+
suggestion: "Extract shared types/interfaces to a common file"
|
|
3769
|
+
});
|
|
3770
|
+
return true;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
recStack.delete(file);
|
|
3775
|
+
pathStack.pop();
|
|
3776
|
+
return false;
|
|
3777
|
+
}
|
|
3778
|
+
for (const file of fileGraph.keys()) {
|
|
3779
|
+
if (!visited.has(file)) {
|
|
3780
|
+
dfs(file);
|
|
3781
|
+
recStack.clear();
|
|
3782
|
+
pathStack.length = 0;
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
return cycles;
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
// src/docs/onboarding.ts
|
|
3789
|
+
import { dirname as dirname7 } from "path";
|
|
3790
|
+
function generateOnboarding(graph, projectRoot, version) {
|
|
3791
|
+
let output = "";
|
|
3792
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3793
|
+
const fileCount = getFileCount4(graph);
|
|
3794
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
3795
|
+
output += header("Onboarding Guide");
|
|
3796
|
+
output += "A guide for developers new to this codebase.\n\n";
|
|
3797
|
+
output += header("Quick Orientation", 2);
|
|
3798
|
+
output += generateQuickOrientation(graph);
|
|
3799
|
+
output += header("Where to Start Reading", 2);
|
|
3800
|
+
output += generateReadingOrder(graph);
|
|
3801
|
+
output += header("Module Map", 2);
|
|
3802
|
+
output += generateModuleMap(graph);
|
|
3803
|
+
output += header("Key Concepts", 2);
|
|
3804
|
+
output += generateKeyConcepts(graph);
|
|
3805
|
+
output += header("High-Impact Files", 2);
|
|
3806
|
+
output += generateHighImpactWarning(graph);
|
|
3807
|
+
output += header("Using Depwire with This Project", 2);
|
|
3808
|
+
output += generateDepwireUsage(projectRoot);
|
|
3809
|
+
return output;
|
|
3810
|
+
}
|
|
3811
|
+
function getFileCount4(graph) {
|
|
3812
|
+
const files = /* @__PURE__ */ new Set();
|
|
3813
|
+
graph.forEachNode((node, attrs) => {
|
|
3814
|
+
files.add(attrs.filePath);
|
|
3815
|
+
});
|
|
3816
|
+
return files.size;
|
|
3817
|
+
}
|
|
3818
|
+
function getLanguageStats2(graph) {
|
|
3819
|
+
const stats = {};
|
|
3820
|
+
const files = /* @__PURE__ */ new Set();
|
|
3821
|
+
graph.forEachNode((node, attrs) => {
|
|
3822
|
+
if (!files.has(attrs.filePath)) {
|
|
3823
|
+
files.add(attrs.filePath);
|
|
3824
|
+
const ext = attrs.filePath.toLowerCase();
|
|
3825
|
+
let lang;
|
|
3826
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
3827
|
+
lang = "TypeScript";
|
|
3828
|
+
} else if (ext.endsWith(".py")) {
|
|
3829
|
+
lang = "Python";
|
|
3830
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
3831
|
+
lang = "JavaScript";
|
|
3832
|
+
} else if (ext.endsWith(".go")) {
|
|
3833
|
+
lang = "Go";
|
|
3834
|
+
} else {
|
|
3835
|
+
lang = "Other";
|
|
3836
|
+
}
|
|
3837
|
+
stats[lang] = (stats[lang] || 0) + 1;
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
return stats;
|
|
3841
|
+
}
|
|
3842
|
+
function generateQuickOrientation(graph) {
|
|
3843
|
+
const fileCount = getFileCount4(graph);
|
|
3844
|
+
const languages = getLanguageStats2(graph);
|
|
3845
|
+
const primaryLang = Object.entries(languages).sort((a, b) => b[1] - a[1])[0];
|
|
3846
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3847
|
+
graph.forEachNode((node, attrs) => {
|
|
3848
|
+
const dir = dirname7(attrs.filePath);
|
|
3849
|
+
if (dir !== ".") {
|
|
3850
|
+
const topLevel = dir.split("/")[0];
|
|
3851
|
+
dirs.add(topLevel);
|
|
3852
|
+
}
|
|
3853
|
+
});
|
|
3854
|
+
const mainAreas = Array.from(dirs).sort().join(", ");
|
|
3855
|
+
let output = "";
|
|
3856
|
+
if (primaryLang) {
|
|
3857
|
+
output += `This is a **${primaryLang[0]}** project with **${fileCount} files** and **${graph.order} symbols**. `;
|
|
3858
|
+
} else {
|
|
3859
|
+
output += `This project has **${fileCount} files** and **${graph.order} symbols**. `;
|
|
3860
|
+
}
|
|
3861
|
+
if (dirs.size > 0) {
|
|
3862
|
+
output += `The main areas are: ${mainAreas}.`;
|
|
3863
|
+
} else {
|
|
3864
|
+
output += "The project has a flat file structure.";
|
|
3865
|
+
}
|
|
3866
|
+
output += "\n\n";
|
|
3867
|
+
return output;
|
|
3868
|
+
}
|
|
3869
|
+
function generateReadingOrder(graph) {
|
|
3870
|
+
const fileStats = getFileStatsWithDeps(graph);
|
|
3871
|
+
if (fileStats.length === 0) {
|
|
3872
|
+
return "No files to analyze.\n\n";
|
|
3873
|
+
}
|
|
3874
|
+
const foundation = fileStats.filter((f) => f.incomingRefs > 0 && f.incomingRefs >= f.outgoingRefs * 2).sort((a, b) => b.incomingRefs - a.incomingRefs).slice(0, 3);
|
|
3875
|
+
const core = fileStats.filter((f) => !foundation.includes(f)).filter((f) => f.incomingRefs > 0 && f.outgoingRefs > 0).filter((f) => {
|
|
3876
|
+
const ratio = f.incomingRefs / (f.outgoingRefs + 0.1);
|
|
3877
|
+
return ratio > 0.3 && ratio < 3;
|
|
3878
|
+
}).sort((a, b) => b.incomingRefs + b.outgoingRefs - (a.incomingRefs + a.outgoingRefs)).slice(0, 5);
|
|
3879
|
+
const orchestration = fileStats.filter((f) => !foundation.includes(f) && !core.includes(f)).filter((f) => f.outgoingRefs > 0 && f.outgoingRefs >= f.incomingRefs * 2).sort((a, b) => b.outgoingRefs - a.outgoingRefs).slice(0, 3);
|
|
3880
|
+
if (foundation.length === 0 && core.length === 0 && orchestration.length === 0) {
|
|
3881
|
+
return "No clear reading order detected. Start with any file.\n\n";
|
|
3882
|
+
}
|
|
3883
|
+
let output = "Recommended reading order for understanding the codebase:\n\n";
|
|
3884
|
+
if (foundation.length > 0) {
|
|
3885
|
+
output += "**Foundation** (start here \u2014 these are building blocks):\n\n";
|
|
3886
|
+
output += orderedList(foundation.map((f) => `${code(f.filePath)} \u2014 Shared foundation (${f.incomingRefs} dependents)`));
|
|
3887
|
+
}
|
|
3888
|
+
if (core.length > 0) {
|
|
3889
|
+
output += "**Core Logic** (read these next):\n\n";
|
|
3890
|
+
output += orderedList(core.map((f) => `${code(f.filePath)} \u2014 Core logic (${f.symbolCount} symbols)`));
|
|
3891
|
+
}
|
|
3892
|
+
if (orchestration.length > 0) {
|
|
3893
|
+
output += "**Entry Points** (read these last to see how it all fits together):\n\n";
|
|
3894
|
+
output += orderedList(orchestration.map((f) => `${code(f.filePath)} \u2014 Entry point (imports from ${f.outgoingRefs} files)`));
|
|
3895
|
+
}
|
|
3896
|
+
return output;
|
|
3897
|
+
}
|
|
3898
|
+
function getFileStatsWithDeps(graph) {
|
|
3899
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
3900
|
+
graph.forEachNode((node, attrs) => {
|
|
3901
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
3902
|
+
fileMap.set(attrs.filePath, {
|
|
3903
|
+
symbolCount: 0,
|
|
3904
|
+
incomingRefs: /* @__PURE__ */ new Set(),
|
|
3905
|
+
outgoingRefs: /* @__PURE__ */ new Set()
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
fileMap.get(attrs.filePath).symbolCount++;
|
|
3909
|
+
});
|
|
3910
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3911
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3912
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3913
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3914
|
+
const sourceFile = fileMap.get(sourceAttrs.filePath);
|
|
3915
|
+
const targetFile = fileMap.get(targetAttrs.filePath);
|
|
3916
|
+
if (sourceFile) {
|
|
3917
|
+
sourceFile.outgoingRefs.add(targetAttrs.filePath);
|
|
3918
|
+
}
|
|
3919
|
+
if (targetFile) {
|
|
3920
|
+
targetFile.incomingRefs.add(sourceAttrs.filePath);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
const result = [];
|
|
3925
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
3926
|
+
result.push({
|
|
3927
|
+
filePath,
|
|
3928
|
+
symbolCount: data.symbolCount,
|
|
3929
|
+
incomingRefs: data.incomingRefs.size,
|
|
3930
|
+
outgoingRefs: data.outgoingRefs.size
|
|
2501
3931
|
});
|
|
3932
|
+
}
|
|
3933
|
+
return result;
|
|
3934
|
+
}
|
|
3935
|
+
function generateModuleMap(graph) {
|
|
3936
|
+
const dirStats = getDirectoryStats2(graph);
|
|
3937
|
+
if (dirStats.length === 0) {
|
|
3938
|
+
return "Flat file structure (no subdirectories).\n\n";
|
|
3939
|
+
}
|
|
3940
|
+
let output = "";
|
|
3941
|
+
for (const dir of dirStats) {
|
|
3942
|
+
const description = inferDirectoryDescription(dir, graph);
|
|
3943
|
+
output += `- ${code(dir.name)} \u2014 ${description}
|
|
3944
|
+
`;
|
|
3945
|
+
}
|
|
3946
|
+
output += "\n";
|
|
3947
|
+
return output;
|
|
3948
|
+
}
|
|
3949
|
+
function getDirectoryStats2(graph) {
|
|
3950
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
3951
|
+
graph.forEachNode((node, attrs) => {
|
|
3952
|
+
const dir = dirname7(attrs.filePath);
|
|
3953
|
+
if (dir === ".") return;
|
|
3954
|
+
if (!dirMap.has(dir)) {
|
|
3955
|
+
dirMap.set(dir, {
|
|
3956
|
+
name: dir,
|
|
3957
|
+
fileCount: 0,
|
|
3958
|
+
symbolCount: 0,
|
|
3959
|
+
inboundEdges: 0,
|
|
3960
|
+
outboundEdges: 0
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
dirMap.get(dir).symbolCount++;
|
|
2502
3964
|
});
|
|
2503
|
-
const
|
|
2504
|
-
|
|
3965
|
+
const filesPerDir = /* @__PURE__ */ new Map();
|
|
3966
|
+
graph.forEachNode((node, attrs) => {
|
|
3967
|
+
const dir = dirname7(attrs.filePath);
|
|
3968
|
+
if (!filesPerDir.has(dir)) {
|
|
3969
|
+
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
3970
|
+
}
|
|
3971
|
+
filesPerDir.get(dir).add(attrs.filePath);
|
|
3972
|
+
});
|
|
3973
|
+
filesPerDir.forEach((files, dir) => {
|
|
3974
|
+
if (dirMap.has(dir)) {
|
|
3975
|
+
dirMap.get(dir).fileCount = files.size;
|
|
3976
|
+
}
|
|
3977
|
+
});
|
|
3978
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
3979
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3980
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3981
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3982
|
+
const sourceDir = dirname7(sourceAttrs.filePath);
|
|
3983
|
+
const targetDir = dirname7(targetAttrs.filePath);
|
|
3984
|
+
if (sourceDir !== targetDir) {
|
|
3985
|
+
if (!dirEdges.has(sourceDir)) {
|
|
3986
|
+
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
3987
|
+
}
|
|
3988
|
+
if (!dirEdges.has(targetDir)) {
|
|
3989
|
+
dirEdges.set(targetDir, { in: 0, out: 0 });
|
|
3990
|
+
}
|
|
3991
|
+
dirEdges.get(sourceDir).out++;
|
|
3992
|
+
dirEdges.get(targetDir).in++;
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
dirEdges.forEach((edges, dir) => {
|
|
3996
|
+
if (dirMap.has(dir)) {
|
|
3997
|
+
const stat = dirMap.get(dir);
|
|
3998
|
+
stat.inboundEdges = edges.in;
|
|
3999
|
+
stat.outboundEdges = edges.out;
|
|
4000
|
+
}
|
|
4001
|
+
});
|
|
4002
|
+
return Array.from(dirMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
2505
4003
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
4004
|
+
function inferDirectoryDescription(dir, graph) {
|
|
4005
|
+
const name = dir.name.toLowerCase();
|
|
4006
|
+
if (name.includes("types") || name.includes("interfaces")) {
|
|
4007
|
+
return "Type definitions and interfaces";
|
|
4008
|
+
}
|
|
4009
|
+
if (name.includes("utils") || name.includes("helpers")) {
|
|
4010
|
+
return "Utility functions and helpers";
|
|
4011
|
+
}
|
|
4012
|
+
if (name.includes("services")) {
|
|
4013
|
+
return "Business logic and services";
|
|
4014
|
+
}
|
|
4015
|
+
if (name.includes("components")) {
|
|
4016
|
+
return "UI components";
|
|
4017
|
+
}
|
|
4018
|
+
if (name.includes("api") || name.includes("routes")) {
|
|
4019
|
+
return "API routes and endpoints";
|
|
4020
|
+
}
|
|
4021
|
+
if (name.includes("models") || name.includes("entities")) {
|
|
4022
|
+
return "Data models and entities";
|
|
4023
|
+
}
|
|
4024
|
+
if (name.includes("config")) {
|
|
4025
|
+
return "Configuration files";
|
|
4026
|
+
}
|
|
4027
|
+
if (name.includes("test")) {
|
|
4028
|
+
return "Test files";
|
|
4029
|
+
}
|
|
4030
|
+
const totalEdges = dir.inboundEdges + dir.outboundEdges;
|
|
4031
|
+
if (totalEdges === 0) {
|
|
4032
|
+
return "Isolated module";
|
|
4033
|
+
}
|
|
4034
|
+
const inboundRatio = dir.inboundEdges / totalEdges;
|
|
4035
|
+
if (inboundRatio > 0.7) {
|
|
4036
|
+
return "Shared foundation \u2014 heavily imported by other modules";
|
|
4037
|
+
} else if (inboundRatio < 0.3) {
|
|
4038
|
+
return "Orchestration \u2014 imports from many other modules";
|
|
4039
|
+
} else {
|
|
4040
|
+
return `Core logic \u2014 ${dir.fileCount} files, ${dir.symbolCount} symbols`;
|
|
4041
|
+
}
|
|
2515
4042
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
4043
|
+
function generateKeyConcepts(graph) {
|
|
4044
|
+
const clusters = detectClusters(graph);
|
|
4045
|
+
if (clusters.length === 0) {
|
|
4046
|
+
return "No distinct concept clusters detected.\n\n";
|
|
4047
|
+
}
|
|
4048
|
+
let output = "The codebase is organized around these key concepts:\n\n";
|
|
4049
|
+
for (const cluster of clusters.slice(0, 5)) {
|
|
4050
|
+
output += `- **${cluster.name}** \u2014 ${cluster.files.length} tightly-connected files: `;
|
|
4051
|
+
output += cluster.files.slice(0, 3).map((f) => code(f)).join(", ");
|
|
4052
|
+
if (cluster.files.length > 3) {
|
|
4053
|
+
output += `, and ${cluster.files.length - 3} more`;
|
|
4054
|
+
}
|
|
4055
|
+
output += "\n";
|
|
4056
|
+
}
|
|
4057
|
+
output += "\n";
|
|
4058
|
+
return output;
|
|
2518
4059
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
function removeFileFromGraph(graph, filePath) {
|
|
2523
|
-
const nodesToRemove = [];
|
|
4060
|
+
function detectClusters(graph) {
|
|
4061
|
+
const dirFiles = /* @__PURE__ */ new Map();
|
|
4062
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
2524
4063
|
graph.forEachNode((node, attrs) => {
|
|
2525
|
-
|
|
2526
|
-
|
|
4064
|
+
const dir = dirname7(attrs.filePath);
|
|
4065
|
+
if (!dirFiles.has(dir)) {
|
|
4066
|
+
dirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
2527
4067
|
}
|
|
4068
|
+
dirFiles.get(dir).add(attrs.filePath);
|
|
2528
4069
|
});
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
4070
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4071
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
4072
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
4073
|
+
if (sourceFile !== targetFile) {
|
|
4074
|
+
if (!fileEdges.has(sourceFile)) {
|
|
4075
|
+
fileEdges.set(sourceFile, /* @__PURE__ */ new Set());
|
|
4076
|
+
}
|
|
4077
|
+
fileEdges.get(sourceFile).add(targetFile);
|
|
2533
4078
|
}
|
|
2534
4079
|
});
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
4080
|
+
const clusters = [];
|
|
4081
|
+
for (const [dir, files] of dirFiles.entries()) {
|
|
4082
|
+
if (dir === "." || files.size < 2) continue;
|
|
4083
|
+
const fileArray = Array.from(files);
|
|
4084
|
+
let internalEdgeCount = 0;
|
|
4085
|
+
for (const file of fileArray) {
|
|
4086
|
+
const targets = fileEdges.get(file);
|
|
4087
|
+
if (targets) {
|
|
4088
|
+
for (const target of targets) {
|
|
4089
|
+
if (files.has(target)) {
|
|
4090
|
+
internalEdgeCount++;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
if (internalEdgeCount >= 2) {
|
|
4096
|
+
const clusterName = inferClusterName(fileArray);
|
|
4097
|
+
clusters.push({
|
|
4098
|
+
name: clusterName,
|
|
4099
|
+
files: fileArray
|
|
2548
4100
|
});
|
|
2549
|
-
} catch (error) {
|
|
2550
4101
|
}
|
|
2551
4102
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
4103
|
+
return clusters.sort((a, b) => b.files.length - a.files.length);
|
|
4104
|
+
}
|
|
4105
|
+
function inferClusterName(files) {
|
|
4106
|
+
const words = /* @__PURE__ */ new Map();
|
|
4107
|
+
for (const file of files) {
|
|
4108
|
+
const fileName = file.toLowerCase();
|
|
4109
|
+
const parts = fileName.split(/[\/\-\_\.]/).filter((p) => p.length > 3);
|
|
4110
|
+
for (const part of parts) {
|
|
4111
|
+
words.set(part, (words.get(part) || 0) + 1);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
const sortedWords = Array.from(words.entries()).sort((a, b) => b[1] - a[1]);
|
|
4115
|
+
if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
|
|
4116
|
+
return capitalizeFirst2(sortedWords[0][0]);
|
|
4117
|
+
}
|
|
4118
|
+
const commonDir = dirname7(files[0]);
|
|
4119
|
+
if (files.every((f) => dirname7(f) === commonDir)) {
|
|
4120
|
+
return capitalizeFirst2(commonDir.split("/").pop() || "Core");
|
|
4121
|
+
}
|
|
4122
|
+
return "Core";
|
|
4123
|
+
}
|
|
4124
|
+
function capitalizeFirst2(str) {
|
|
4125
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4126
|
+
}
|
|
4127
|
+
function generateHighImpactWarning(graph) {
|
|
4128
|
+
const highImpactFiles = [];
|
|
4129
|
+
const fileInDegree = /* @__PURE__ */ new Map();
|
|
4130
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4131
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
4132
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
4133
|
+
if (sourceFile !== targetFile) {
|
|
4134
|
+
fileInDegree.set(targetFile, (fileInDegree.get(targetFile) || 0) + 1);
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
for (const [file, count] of fileInDegree.entries()) {
|
|
4138
|
+
if (count >= 5) {
|
|
4139
|
+
highImpactFiles.push({ file, dependents: count });
|
|
2560
4140
|
}
|
|
2561
4141
|
}
|
|
4142
|
+
highImpactFiles.sort((a, b) => b.dependents - a.dependents);
|
|
4143
|
+
if (highImpactFiles.length === 0) {
|
|
4144
|
+
return "No high-impact files detected. Changes should be relatively isolated.\n\n";
|
|
4145
|
+
}
|
|
4146
|
+
let output = "\u26A0\uFE0F **Before modifying these files, check the blast radius:**\n\n";
|
|
4147
|
+
const topFiles = highImpactFiles.slice(0, 5);
|
|
4148
|
+
for (const { file, dependents } of topFiles) {
|
|
4149
|
+
output += `- ${code(file)} \u2014 ${dependents} dependent files (run \`depwire impact_analysis ${file}\`)
|
|
4150
|
+
`;
|
|
4151
|
+
}
|
|
4152
|
+
output += "\n";
|
|
4153
|
+
return output;
|
|
2562
4154
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
4155
|
+
function generateDepwireUsage(projectRoot) {
|
|
4156
|
+
let output = "Use Depwire to explore this codebase:\n\n";
|
|
4157
|
+
output += "**Visualize the dependency graph:**\n\n";
|
|
4158
|
+
output += "```bash\n";
|
|
4159
|
+
output += "depwire viz .\n";
|
|
4160
|
+
output += "```\n\n";
|
|
4161
|
+
output += "**Connect to AI coding tools (MCP):**\n\n";
|
|
4162
|
+
output += "```bash\n";
|
|
4163
|
+
output += "depwire mcp .\n";
|
|
4164
|
+
output += "```\n\n";
|
|
4165
|
+
output += "**Analyze impact of changes:**\n\n";
|
|
4166
|
+
output += "```bash\n";
|
|
4167
|
+
output += "depwire query . <symbol-name>\n";
|
|
4168
|
+
output += "```\n\n";
|
|
4169
|
+
output += "**Update documentation:**\n\n";
|
|
4170
|
+
output += "```bash\n";
|
|
4171
|
+
output += "depwire docs . --update\n";
|
|
4172
|
+
output += "```\n\n";
|
|
4173
|
+
return output;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// src/docs/metadata.ts
|
|
4177
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
4178
|
+
import { join as join9 } from "path";
|
|
4179
|
+
function loadMetadata(outputDir) {
|
|
4180
|
+
const metadataPath = join9(outputDir, "metadata.json");
|
|
4181
|
+
if (!existsSync5(metadataPath)) {
|
|
4182
|
+
return null;
|
|
4183
|
+
}
|
|
2566
4184
|
try {
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
} catch (
|
|
2570
|
-
console.error(
|
|
4185
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
4186
|
+
return JSON.parse(content);
|
|
4187
|
+
} catch (err) {
|
|
4188
|
+
console.error("Failed to load metadata:", err);
|
|
4189
|
+
return null;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
function saveMetadata(outputDir, metadata) {
|
|
4193
|
+
const metadataPath = join9(outputDir, "metadata.json");
|
|
4194
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
4195
|
+
}
|
|
4196
|
+
function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
|
|
4197
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4198
|
+
const documents = {};
|
|
4199
|
+
for (const docType of docTypes) {
|
|
4200
|
+
const fileName = docType === "architecture" ? "ARCHITECTURE.md" : docType === "conventions" ? "CONVENTIONS.md" : docType === "dependencies" ? "DEPENDENCIES.md" : docType === "onboarding" ? "ONBOARDING.md" : `${docType.toUpperCase()}.md`;
|
|
4201
|
+
documents[docType] = {
|
|
4202
|
+
generated_at: now,
|
|
4203
|
+
file: fileName
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
return {
|
|
4207
|
+
version,
|
|
4208
|
+
generated_at: now,
|
|
4209
|
+
project_path: projectPath,
|
|
4210
|
+
file_count: fileCount,
|
|
4211
|
+
symbol_count: symbolCount,
|
|
4212
|
+
edge_count: edgeCount,
|
|
4213
|
+
documents
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
function updateMetadata(existing, docTypes, fileCount, symbolCount, edgeCount) {
|
|
4217
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4218
|
+
for (const docType of docTypes) {
|
|
4219
|
+
if (existing.documents[docType]) {
|
|
4220
|
+
existing.documents[docType].generated_at = now;
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
existing.file_count = fileCount;
|
|
4224
|
+
existing.symbol_count = symbolCount;
|
|
4225
|
+
existing.edge_count = edgeCount;
|
|
4226
|
+
existing.generated_at = now;
|
|
4227
|
+
return existing;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/docs/generator.ts
|
|
4231
|
+
async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
4232
|
+
const startTime = Date.now();
|
|
4233
|
+
const generated = [];
|
|
4234
|
+
const errors = [];
|
|
4235
|
+
try {
|
|
4236
|
+
if (!existsSync6(options.outputDir)) {
|
|
4237
|
+
mkdirSync(options.outputDir, { recursive: true });
|
|
4238
|
+
if (options.verbose) {
|
|
4239
|
+
console.log(`Created output directory: ${options.outputDir}`);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
let docsToGenerate = options.include;
|
|
4243
|
+
if (options.update && options.only) {
|
|
4244
|
+
docsToGenerate = options.only;
|
|
4245
|
+
}
|
|
4246
|
+
if (docsToGenerate.includes("all")) {
|
|
4247
|
+
docsToGenerate = ["architecture", "conventions", "dependencies", "onboarding"];
|
|
4248
|
+
}
|
|
4249
|
+
let metadata = null;
|
|
4250
|
+
if (options.update) {
|
|
4251
|
+
metadata = loadMetadata(options.outputDir);
|
|
4252
|
+
}
|
|
4253
|
+
const fileCount = getFileCount5(graph);
|
|
4254
|
+
const symbolCount = graph.order;
|
|
4255
|
+
const edgeCount = graph.size;
|
|
4256
|
+
if (options.format === "markdown") {
|
|
4257
|
+
if (docsToGenerate.includes("architecture")) {
|
|
4258
|
+
try {
|
|
4259
|
+
if (options.verbose) console.log("Generating ARCHITECTURE.md...");
|
|
4260
|
+
const content = generateArchitecture(graph, projectRoot, version, parseTime);
|
|
4261
|
+
const filePath = join10(options.outputDir, "ARCHITECTURE.md");
|
|
4262
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4263
|
+
generated.push("ARCHITECTURE.md");
|
|
4264
|
+
} catch (err) {
|
|
4265
|
+
errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
if (docsToGenerate.includes("conventions")) {
|
|
4269
|
+
try {
|
|
4270
|
+
if (options.verbose) console.log("Generating CONVENTIONS.md...");
|
|
4271
|
+
const content = generateConventions(graph, projectRoot, version);
|
|
4272
|
+
const filePath = join10(options.outputDir, "CONVENTIONS.md");
|
|
4273
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4274
|
+
generated.push("CONVENTIONS.md");
|
|
4275
|
+
} catch (err) {
|
|
4276
|
+
errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
if (docsToGenerate.includes("dependencies")) {
|
|
4280
|
+
try {
|
|
4281
|
+
if (options.verbose) console.log("Generating DEPENDENCIES.md...");
|
|
4282
|
+
const content = generateDependencies(graph, projectRoot, version);
|
|
4283
|
+
const filePath = join10(options.outputDir, "DEPENDENCIES.md");
|
|
4284
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4285
|
+
generated.push("DEPENDENCIES.md");
|
|
4286
|
+
} catch (err) {
|
|
4287
|
+
errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
if (docsToGenerate.includes("onboarding")) {
|
|
4291
|
+
try {
|
|
4292
|
+
if (options.verbose) console.log("Generating ONBOARDING.md...");
|
|
4293
|
+
const content = generateOnboarding(graph, projectRoot, version);
|
|
4294
|
+
const filePath = join10(options.outputDir, "ONBOARDING.md");
|
|
4295
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4296
|
+
generated.push("ONBOARDING.md");
|
|
4297
|
+
} catch (err) {
|
|
4298
|
+
errors.push(`Failed to generate ONBOARDING.md: ${err}`);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
} else if (options.format === "json") {
|
|
4302
|
+
errors.push("JSON format not yet supported");
|
|
4303
|
+
}
|
|
4304
|
+
if (metadata && options.update) {
|
|
4305
|
+
metadata = updateMetadata(metadata, docsToGenerate, fileCount, symbolCount, edgeCount);
|
|
4306
|
+
} else {
|
|
4307
|
+
metadata = createMetadata(version, projectRoot, fileCount, symbolCount, edgeCount, docsToGenerate);
|
|
4308
|
+
}
|
|
4309
|
+
saveMetadata(options.outputDir, metadata);
|
|
4310
|
+
if (options.verbose) console.log("Saved metadata.json");
|
|
4311
|
+
const totalTime = Date.now() - startTime;
|
|
4312
|
+
return {
|
|
4313
|
+
success: errors.length === 0,
|
|
4314
|
+
generated,
|
|
4315
|
+
errors,
|
|
4316
|
+
stats: options.stats ? {
|
|
4317
|
+
totalTime,
|
|
4318
|
+
filesGenerated: generated.length
|
|
4319
|
+
} : void 0
|
|
4320
|
+
};
|
|
4321
|
+
} catch (err) {
|
|
4322
|
+
return {
|
|
4323
|
+
success: false,
|
|
4324
|
+
generated,
|
|
4325
|
+
errors: [`Fatal error: ${err}`]
|
|
4326
|
+
};
|
|
2571
4327
|
}
|
|
2572
4328
|
}
|
|
4329
|
+
function getFileCount5(graph) {
|
|
4330
|
+
const files = /* @__PURE__ */ new Set();
|
|
4331
|
+
graph.forEachNode((node, attrs) => {
|
|
4332
|
+
files.add(attrs.filePath);
|
|
4333
|
+
});
|
|
4334
|
+
return files.size;
|
|
4335
|
+
}
|
|
2573
4336
|
|
|
2574
4337
|
// src/mcp/server.ts
|
|
2575
4338
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2576
4339
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2577
4340
|
|
|
2578
4341
|
// src/mcp/tools.ts
|
|
2579
|
-
import { dirname as
|
|
4342
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
4343
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
2580
4344
|
|
|
2581
4345
|
// src/mcp/connect.ts
|
|
2582
4346
|
import simpleGit from "simple-git";
|
|
2583
|
-
import { existsSync as
|
|
2584
|
-
import { join as
|
|
4347
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4348
|
+
import { join as join11, basename as basename3, resolve as resolve2 } from "path";
|
|
2585
4349
|
import { tmpdir, homedir } from "os";
|
|
2586
4350
|
function validateProjectPath(source) {
|
|
2587
4351
|
const resolved = resolve2(source);
|
|
@@ -2594,11 +4358,11 @@ function validateProjectPath(source) {
|
|
|
2594
4358
|
"/boot",
|
|
2595
4359
|
"/proc",
|
|
2596
4360
|
"/sys",
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
4361
|
+
join11(homedir(), ".ssh"),
|
|
4362
|
+
join11(homedir(), ".gnupg"),
|
|
4363
|
+
join11(homedir(), ".aws"),
|
|
4364
|
+
join11(homedir(), ".config"),
|
|
4365
|
+
join11(homedir(), ".env")
|
|
2602
4366
|
];
|
|
2603
4367
|
for (const blocked of blockedPaths) {
|
|
2604
4368
|
if (resolved.startsWith(blocked)) {
|
|
@@ -2621,11 +4385,11 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2621
4385
|
};
|
|
2622
4386
|
}
|
|
2623
4387
|
projectName = match[1];
|
|
2624
|
-
const reposDir =
|
|
2625
|
-
const cloneDir =
|
|
4388
|
+
const reposDir = join11(tmpdir(), "depwire-repos");
|
|
4389
|
+
const cloneDir = join11(reposDir, projectName);
|
|
2626
4390
|
console.error(`Connecting to GitHub repo: ${source}`);
|
|
2627
4391
|
const git = simpleGit();
|
|
2628
|
-
if (
|
|
4392
|
+
if (existsSync7(cloneDir)) {
|
|
2629
4393
|
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
2630
4394
|
try {
|
|
2631
4395
|
await git.cwd(cloneDir).pull();
|
|
@@ -2643,7 +4407,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2643
4407
|
};
|
|
2644
4408
|
}
|
|
2645
4409
|
}
|
|
2646
|
-
projectRoot = subdirectory ?
|
|
4410
|
+
projectRoot = subdirectory ? join11(cloneDir, subdirectory) : cloneDir;
|
|
2647
4411
|
} else {
|
|
2648
4412
|
const validation2 = validateProjectPath(source);
|
|
2649
4413
|
if (!validation2.valid) {
|
|
@@ -2652,14 +4416,14 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2652
4416
|
message: validation2.error
|
|
2653
4417
|
};
|
|
2654
4418
|
}
|
|
2655
|
-
if (!
|
|
4419
|
+
if (!existsSync7(source)) {
|
|
2656
4420
|
return {
|
|
2657
4421
|
error: "Directory not found",
|
|
2658
4422
|
message: `Directory does not exist: ${source}`
|
|
2659
4423
|
};
|
|
2660
4424
|
}
|
|
2661
|
-
projectRoot = subdirectory ?
|
|
2662
|
-
projectName =
|
|
4425
|
+
projectRoot = subdirectory ? join11(source, subdirectory) : source;
|
|
4426
|
+
projectName = basename3(projectRoot);
|
|
2663
4427
|
}
|
|
2664
4428
|
const validation = validateProjectPath(projectRoot);
|
|
2665
4429
|
if (!validation.valid) {
|
|
@@ -2668,7 +4432,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2668
4432
|
message: validation.error
|
|
2669
4433
|
};
|
|
2670
4434
|
}
|
|
2671
|
-
if (!
|
|
4435
|
+
if (!existsSync7(projectRoot)) {
|
|
2672
4436
|
return {
|
|
2673
4437
|
error: "Project root not found",
|
|
2674
4438
|
message: `Directory does not exist: ${projectRoot}`
|
|
@@ -2793,13 +4557,13 @@ function getToolsList() {
|
|
|
2793
4557
|
},
|
|
2794
4558
|
{
|
|
2795
4559
|
name: "get_symbol_info",
|
|
2796
|
-
description: "Look up detailed information about a symbol (function, class, variable, type, etc.) by name.
|
|
4560
|
+
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.",
|
|
2797
4561
|
inputSchema: {
|
|
2798
4562
|
type: "object",
|
|
2799
4563
|
properties: {
|
|
2800
4564
|
name: {
|
|
2801
4565
|
type: "string",
|
|
2802
|
-
description: "The symbol name to look up (e.g., 'UserService'
|
|
4566
|
+
description: "The symbol name to look up (e.g., 'UserService') or full ID (e.g., 'src/services/UserService.ts::UserService')"
|
|
2803
4567
|
}
|
|
2804
4568
|
},
|
|
2805
4569
|
required: ["name"]
|
|
@@ -2807,13 +4571,13 @@ function getToolsList() {
|
|
|
2807
4571
|
},
|
|
2808
4572
|
{
|
|
2809
4573
|
name: "get_dependencies",
|
|
2810
|
-
description: "Get all symbols that a given symbol depends on (what does this symbol use/import/call?).",
|
|
4574
|
+
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.",
|
|
2811
4575
|
inputSchema: {
|
|
2812
4576
|
type: "object",
|
|
2813
4577
|
properties: {
|
|
2814
4578
|
symbol: {
|
|
2815
4579
|
type: "string",
|
|
2816
|
-
description: "Symbol name or ID
|
|
4580
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
2817
4581
|
}
|
|
2818
4582
|
},
|
|
2819
4583
|
required: ["symbol"]
|
|
@@ -2821,13 +4585,13 @@ function getToolsList() {
|
|
|
2821
4585
|
},
|
|
2822
4586
|
{
|
|
2823
4587
|
name: "get_dependents",
|
|
2824
|
-
description: "Get all symbols that depend on a given symbol (what uses this symbol?).",
|
|
4588
|
+
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.",
|
|
2825
4589
|
inputSchema: {
|
|
2826
4590
|
type: "object",
|
|
2827
4591
|
properties: {
|
|
2828
4592
|
symbol: {
|
|
2829
4593
|
type: "string",
|
|
2830
|
-
description: "Symbol name or ID
|
|
4594
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
2831
4595
|
}
|
|
2832
4596
|
},
|
|
2833
4597
|
required: ["symbol"]
|
|
@@ -2835,13 +4599,13 @@ function getToolsList() {
|
|
|
2835
4599
|
},
|
|
2836
4600
|
{
|
|
2837
4601
|
name: "impact_analysis",
|
|
2838
|
-
description: "Analyze what would break if a symbol is changed, renamed, or removed. Shows direct dependents, transitive dependents (chain reaction), and all affected files. Use this before making changes to understand the blast radius.",
|
|
4602
|
+
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.",
|
|
2839
4603
|
inputSchema: {
|
|
2840
4604
|
type: "object",
|
|
2841
4605
|
properties: {
|
|
2842
4606
|
symbol: {
|
|
2843
4607
|
type: "string",
|
|
2844
|
-
description: "
|
|
4608
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
2845
4609
|
}
|
|
2846
4610
|
},
|
|
2847
4611
|
required: ["symbol"]
|
|
@@ -2916,6 +4680,32 @@ function getToolsList() {
|
|
|
2916
4680
|
}
|
|
2917
4681
|
}
|
|
2918
4682
|
}
|
|
4683
|
+
},
|
|
4684
|
+
{
|
|
4685
|
+
name: "get_project_docs",
|
|
4686
|
+
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.",
|
|
4687
|
+
inputSchema: {
|
|
4688
|
+
type: "object",
|
|
4689
|
+
properties: {
|
|
4690
|
+
doc_type: {
|
|
4691
|
+
type: "string",
|
|
4692
|
+
description: "Document type to retrieve: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
},
|
|
4697
|
+
{
|
|
4698
|
+
name: "update_project_docs",
|
|
4699
|
+
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.",
|
|
4700
|
+
inputSchema: {
|
|
4701
|
+
type: "object",
|
|
4702
|
+
properties: {
|
|
4703
|
+
doc_type: {
|
|
4704
|
+
type: "string",
|
|
4705
|
+
description: "Document type to update: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
2919
4709
|
}
|
|
2920
4710
|
];
|
|
2921
4711
|
}
|
|
@@ -2942,6 +4732,24 @@ async function handleToolCall(name, args, state) {
|
|
|
2942
4732
|
} else {
|
|
2943
4733
|
result = await handleVisualizeGraph(args.highlight, args.maxFiles, state);
|
|
2944
4734
|
}
|
|
4735
|
+
} else if (name === "get_project_docs") {
|
|
4736
|
+
if (!isProjectLoaded(state)) {
|
|
4737
|
+
result = {
|
|
4738
|
+
error: "No project loaded",
|
|
4739
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
4740
|
+
};
|
|
4741
|
+
} else {
|
|
4742
|
+
result = await handleGetProjectDocs(args.doc_type || "all", state);
|
|
4743
|
+
}
|
|
4744
|
+
} else if (name === "update_project_docs") {
|
|
4745
|
+
if (!isProjectLoaded(state)) {
|
|
4746
|
+
result = {
|
|
4747
|
+
error: "No project loaded",
|
|
4748
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
4749
|
+
};
|
|
4750
|
+
} else {
|
|
4751
|
+
result = await handleUpdateProjectDocs(args.doc_type || "all", state);
|
|
4752
|
+
}
|
|
2945
4753
|
} else {
|
|
2946
4754
|
if (!isProjectLoaded(state)) {
|
|
2947
4755
|
result = {
|
|
@@ -3019,12 +4827,39 @@ async function handleToolCall(name, args, state) {
|
|
|
3019
4827
|
};
|
|
3020
4828
|
}
|
|
3021
4829
|
}
|
|
4830
|
+
function createDisambiguationResponse(matches, queryName) {
|
|
4831
|
+
const suggestion = matches.length > 0 ? matches[0].id : "";
|
|
4832
|
+
return {
|
|
4833
|
+
ambiguous: true,
|
|
4834
|
+
message: `Found ${matches.length} symbols named '${queryName}'. Please specify which one by using the full ID (e.g., '${suggestion}').`,
|
|
4835
|
+
matches: matches.map((m, index) => ({
|
|
4836
|
+
id: m.id,
|
|
4837
|
+
kind: m.kind,
|
|
4838
|
+
filePath: m.filePath,
|
|
4839
|
+
line: m.startLine,
|
|
4840
|
+
dependents: m.dependentCount,
|
|
4841
|
+
hint: index === 0 && m.dependentCount > 0 ? "Most dependents \u2014 likely the one you want" : ""
|
|
4842
|
+
})),
|
|
4843
|
+
suggestion
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
3022
4846
|
function handleGetSymbolInfo(name, graph) {
|
|
3023
|
-
const matches =
|
|
3024
|
-
|
|
3025
|
-
|
|
4847
|
+
const matches = findSymbols(graph, name);
|
|
4848
|
+
if (matches.length === 0) {
|
|
4849
|
+
const fuzzyMatches = searchSymbols(graph, name).slice(0, 10);
|
|
4850
|
+
return {
|
|
4851
|
+
error: `Symbol '${name}' not found`,
|
|
4852
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols",
|
|
4853
|
+
fuzzyMatches: fuzzyMatches.map((m) => ({
|
|
4854
|
+
id: m.id,
|
|
4855
|
+
name: m.name,
|
|
4856
|
+
kind: m.kind,
|
|
4857
|
+
filePath: m.filePath
|
|
4858
|
+
}))
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
3026
4861
|
return {
|
|
3027
|
-
matches:
|
|
4862
|
+
matches: matches.map((m) => ({
|
|
3028
4863
|
id: m.id,
|
|
3029
4864
|
name: m.name,
|
|
3030
4865
|
kind: m.kind,
|
|
@@ -3032,19 +4867,24 @@ function handleGetSymbolInfo(name, graph) {
|
|
|
3032
4867
|
startLine: m.startLine,
|
|
3033
4868
|
endLine: m.endLine,
|
|
3034
4869
|
exported: m.exported,
|
|
3035
|
-
scope: m.scope
|
|
4870
|
+
scope: m.scope,
|
|
4871
|
+
dependents: m.dependentCount
|
|
3036
4872
|
})),
|
|
3037
|
-
count:
|
|
4873
|
+
count: matches.length
|
|
3038
4874
|
};
|
|
3039
4875
|
}
|
|
3040
4876
|
function handleGetDependencies(symbol, graph) {
|
|
3041
|
-
const matches =
|
|
4877
|
+
const matches = findSymbols(graph, symbol);
|
|
3042
4878
|
if (matches.length === 0) {
|
|
4879
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
3043
4880
|
return {
|
|
3044
4881
|
error: `Symbol '${symbol}' not found`,
|
|
3045
|
-
suggestion: "Try using search_symbols to find available symbols"
|
|
4882
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
3046
4883
|
};
|
|
3047
4884
|
}
|
|
4885
|
+
if (matches.length > 1) {
|
|
4886
|
+
return createDisambiguationResponse(matches, symbol);
|
|
4887
|
+
}
|
|
3048
4888
|
const target = matches[0];
|
|
3049
4889
|
const deps = getDependencies(graph, target.id);
|
|
3050
4890
|
const grouped = {};
|
|
@@ -3062,19 +4902,23 @@ function handleGetDependencies(symbol, graph) {
|
|
|
3062
4902
|
});
|
|
3063
4903
|
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
3064
4904
|
return {
|
|
3065
|
-
symbol:
|
|
4905
|
+
symbol: target.id,
|
|
3066
4906
|
dependencies: grouped,
|
|
3067
4907
|
totalCount
|
|
3068
4908
|
};
|
|
3069
4909
|
}
|
|
3070
4910
|
function handleGetDependents(symbol, graph) {
|
|
3071
|
-
const matches =
|
|
4911
|
+
const matches = findSymbols(graph, symbol);
|
|
3072
4912
|
if (matches.length === 0) {
|
|
4913
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
3073
4914
|
return {
|
|
3074
4915
|
error: `Symbol '${symbol}' not found`,
|
|
3075
|
-
suggestion: "Try using search_symbols to find available symbols"
|
|
4916
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
3076
4917
|
};
|
|
3077
4918
|
}
|
|
4919
|
+
if (matches.length > 1) {
|
|
4920
|
+
return createDisambiguationResponse(matches, symbol);
|
|
4921
|
+
}
|
|
3078
4922
|
const target = matches[0];
|
|
3079
4923
|
const deps = getDependents(graph, target.id);
|
|
3080
4924
|
const grouped = {};
|
|
@@ -3092,19 +4936,23 @@ function handleGetDependents(symbol, graph) {
|
|
|
3092
4936
|
});
|
|
3093
4937
|
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
3094
4938
|
return {
|
|
3095
|
-
symbol:
|
|
4939
|
+
symbol: target.id,
|
|
3096
4940
|
dependents: grouped,
|
|
3097
4941
|
totalCount
|
|
3098
4942
|
};
|
|
3099
4943
|
}
|
|
3100
4944
|
function handleImpactAnalysis(symbol, graph) {
|
|
3101
|
-
const matches =
|
|
4945
|
+
const matches = findSymbols(graph, symbol);
|
|
3102
4946
|
if (matches.length === 0) {
|
|
4947
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
3103
4948
|
return {
|
|
3104
4949
|
error: `Symbol '${symbol}' not found`,
|
|
3105
|
-
suggestion: "Try using search_symbols to find available symbols"
|
|
4950
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
3106
4951
|
};
|
|
3107
4952
|
}
|
|
4953
|
+
if (matches.length > 1) {
|
|
4954
|
+
return createDisambiguationResponse(matches, symbol);
|
|
4955
|
+
}
|
|
3108
4956
|
const target = matches[0];
|
|
3109
4957
|
const impact = getImpact(graph, target.id);
|
|
3110
4958
|
const directWithKinds = impact.directDependents.map((dep) => {
|
|
@@ -3127,6 +4975,7 @@ function handleImpactAnalysis(symbol, graph) {
|
|
|
3127
4975
|
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).`;
|
|
3128
4976
|
return {
|
|
3129
4977
|
symbol: {
|
|
4978
|
+
id: target.id,
|
|
3130
4979
|
name: target.name,
|
|
3131
4980
|
filePath: target.filePath,
|
|
3132
4981
|
kind: target.kind
|
|
@@ -3238,7 +5087,7 @@ function handleGetArchitectureSummary(graph) {
|
|
|
3238
5087
|
const dirMap = /* @__PURE__ */ new Map();
|
|
3239
5088
|
const languageBreakdown = {};
|
|
3240
5089
|
fileSummary.forEach((f) => {
|
|
3241
|
-
const dir = f.filePath.includes("/") ?
|
|
5090
|
+
const dir = f.filePath.includes("/") ? dirname8(f.filePath) : ".";
|
|
3242
5091
|
if (!dirMap.has(dir)) {
|
|
3243
5092
|
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
3244
5093
|
}
|
|
@@ -3324,6 +5173,112 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
|
3324
5173
|
content: [{ type: "text", text: message }]
|
|
3325
5174
|
};
|
|
3326
5175
|
}
|
|
5176
|
+
async function handleGetProjectDocs(docType, state) {
|
|
5177
|
+
const docsDir = join12(state.projectRoot, ".depwire");
|
|
5178
|
+
if (!existsSync8(docsDir)) {
|
|
5179
|
+
const errorMessage = `Project documentation has not been generated yet.
|
|
5180
|
+
|
|
5181
|
+
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
5182
|
+
|
|
5183
|
+
Once generated, this tool will return the requested documentation.
|
|
5184
|
+
|
|
5185
|
+
Available document types:
|
|
5186
|
+
- architecture: High-level structural overview
|
|
5187
|
+
- conventions: Auto-detected coding patterns
|
|
5188
|
+
- dependencies: Complete dependency mapping
|
|
5189
|
+
- onboarding: Guide for new developers`;
|
|
5190
|
+
return {
|
|
5191
|
+
content: [{ type: "text", text: errorMessage }]
|
|
5192
|
+
};
|
|
5193
|
+
}
|
|
5194
|
+
const metadata = loadMetadata(docsDir);
|
|
5195
|
+
if (!metadata) {
|
|
5196
|
+
return {
|
|
5197
|
+
content: [{ type: "text", text: "Documentation directory exists but metadata is missing. Please regenerate with `depwire docs`." }]
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
5200
|
+
const docsToReturn = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
5201
|
+
let output = "";
|
|
5202
|
+
const missing = [];
|
|
5203
|
+
for (const doc of docsToReturn) {
|
|
5204
|
+
if (!metadata.documents[doc]) {
|
|
5205
|
+
missing.push(doc);
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
const filePath = join12(docsDir, metadata.documents[doc].file);
|
|
5209
|
+
if (!existsSync8(filePath)) {
|
|
5210
|
+
missing.push(doc);
|
|
5211
|
+
continue;
|
|
5212
|
+
}
|
|
5213
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
5214
|
+
if (docsToReturn.length > 1) {
|
|
5215
|
+
output += `
|
|
5216
|
+
|
|
5217
|
+
---
|
|
5218
|
+
|
|
5219
|
+
# ${doc.toUpperCase()}
|
|
5220
|
+
|
|
5221
|
+
`;
|
|
5222
|
+
}
|
|
5223
|
+
output += content;
|
|
5224
|
+
}
|
|
5225
|
+
if (missing.length > 0) {
|
|
5226
|
+
output += `
|
|
5227
|
+
|
|
5228
|
+
---
|
|
5229
|
+
|
|
5230
|
+
**Note:** The following documents are missing: ${missing.join(", ")}. Run \`depwire docs ${state.projectRoot} --update\` to generate them.`;
|
|
5231
|
+
}
|
|
5232
|
+
return {
|
|
5233
|
+
content: [{ type: "text", text: output }]
|
|
5234
|
+
};
|
|
5235
|
+
}
|
|
5236
|
+
async function handleUpdateProjectDocs(docType, state) {
|
|
5237
|
+
const startTime = Date.now();
|
|
5238
|
+
const docsDir = join12(state.projectRoot, ".depwire");
|
|
5239
|
+
console.error("Regenerating project documentation...");
|
|
5240
|
+
const parsedFiles = parseProject(state.projectRoot);
|
|
5241
|
+
const graph = buildGraph(parsedFiles);
|
|
5242
|
+
const parseTime = (Date.now() - startTime) / 1e3;
|
|
5243
|
+
state.graph = graph;
|
|
5244
|
+
const packageJsonPath = join12(__dirname, "../../package.json");
|
|
5245
|
+
const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
5246
|
+
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
5247
|
+
const docsExist = existsSync8(docsDir);
|
|
5248
|
+
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
5249
|
+
outputDir: docsDir,
|
|
5250
|
+
format: "markdown",
|
|
5251
|
+
include: docsToGenerate,
|
|
5252
|
+
update: docsExist,
|
|
5253
|
+
only: docsExist ? docsToGenerate : void 0,
|
|
5254
|
+
verbose: false,
|
|
5255
|
+
stats: false
|
|
5256
|
+
});
|
|
5257
|
+
const elapsed = (Date.now() - startTime) / 1e3;
|
|
5258
|
+
if (result.success) {
|
|
5259
|
+
const fileCount = /* @__PURE__ */ new Set();
|
|
5260
|
+
graph.forEachNode((node, attrs) => {
|
|
5261
|
+
fileCount.add(attrs.filePath);
|
|
5262
|
+
});
|
|
5263
|
+
return {
|
|
5264
|
+
status: "success",
|
|
5265
|
+
message: `Updated ${result.generated.join(", ")} (${fileCount.size} files, ${graph.order} symbols, ${elapsed.toFixed(1)}s)`,
|
|
5266
|
+
generated: result.generated,
|
|
5267
|
+
stats: {
|
|
5268
|
+
files: fileCount.size,
|
|
5269
|
+
symbols: graph.order,
|
|
5270
|
+
edges: graph.size,
|
|
5271
|
+
time: elapsed
|
|
5272
|
+
}
|
|
5273
|
+
};
|
|
5274
|
+
} else {
|
|
5275
|
+
return {
|
|
5276
|
+
status: "error",
|
|
5277
|
+
message: `Failed to update documentation: ${result.errors.join(", ")}`,
|
|
5278
|
+
errors: result.errors
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
3327
5282
|
|
|
3328
5283
|
// src/mcp/server.ts
|
|
3329
5284
|
async function startMcpServer(state) {
|
|
@@ -3369,5 +5324,6 @@ export {
|
|
|
3369
5324
|
startVizServer,
|
|
3370
5325
|
createEmptyState,
|
|
3371
5326
|
updateFileInGraph,
|
|
5327
|
+
generateDocs,
|
|
3372
5328
|
startMcpServer
|
|
3373
5329
|
};
|