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.
@@ -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 __dirname = dirname5(__filename);
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(__dirname, "viz", "public");
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 url = `http://127.0.0.1:${availablePort}`;
2504
- return { server, url, alreadyRunning: false };
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
- // src/mcp/state.ts
2508
- function createEmptyState() {
2509
- return {
2510
- graph: null,
2511
- projectRoot: null,
2512
- projectName: null,
2513
- watcher: null
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 isProjectLoaded(state) {
2517
- return state.graph !== null && state.projectRoot !== null;
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
- // src/graph/updater.ts
2521
- import { join as join8 } from "path";
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
- if (attrs.filePath === filePath) {
2526
- nodesToRemove.push(node);
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
- nodesToRemove.forEach((node) => {
2530
- try {
2531
- graph.dropNode(node);
2532
- } catch (error) {
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
- function addFileToGraph(graph, parsedFile) {
2537
- for (const symbol of parsedFile.symbols) {
2538
- const nodeId = `${parsedFile.filePath}::${symbol.name}`;
2539
- try {
2540
- graph.addNode(nodeId, {
2541
- name: symbol.name,
2542
- kind: symbol.kind,
2543
- filePath: parsedFile.filePath,
2544
- startLine: symbol.location.startLine,
2545
- endLine: symbol.location.endLine,
2546
- exported: symbol.exported,
2547
- scope: symbol.scope
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
- for (const edge of parsedFile.edges) {
2553
- try {
2554
- graph.mergeEdge(edge.source, edge.target, {
2555
- kind: edge.kind,
2556
- sourceFile: edge.sourceFile,
2557
- targetFile: edge.targetFile
2558
- });
2559
- } catch (error) {
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
- async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
2564
- removeFileFromGraph(graph, relativeFilePath);
2565
- const absolutePath = join8(projectRoot, relativeFilePath);
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 parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
2568
- addFileToGraph(graph, parsedFile);
2569
- } catch (error) {
2570
- console.error(`Failed to parse file ${relativeFilePath}:`, 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 dirname6 } from "path";
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 existsSync5 } from "fs";
2584
- import { join as join9, basename as basename2, resolve as resolve2 } from "path";
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
- join9(homedir(), ".ssh"),
2598
- join9(homedir(), ".gnupg"),
2599
- join9(homedir(), ".aws"),
2600
- join9(homedir(), ".config"),
2601
- join9(homedir(), ".env")
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 = join9(tmpdir(), "depwire-repos");
2625
- const cloneDir = join9(reposDir, projectName);
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 (existsSync5(cloneDir)) {
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 ? join9(cloneDir, subdirectory) : cloneDir;
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 (!existsSync5(source)) {
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 ? join9(source, subdirectory) : source;
2662
- projectName = basename2(projectRoot);
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 (!existsSync5(projectRoot)) {
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. Returns file location, type, line numbers, and export status.",
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', 'handleAuth')"
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 to analyze"
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 to analyze"
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: "The symbol name or ID to analyze"
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 = searchSymbols(graph, name);
3024
- const exactMatches = matches.filter((m) => m.name.toLowerCase() === name.toLowerCase());
3025
- const results = exactMatches.length > 0 ? exactMatches : matches.slice(0, 10);
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: results.map((m) => ({
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: results.length
4873
+ count: matches.length
3038
4874
  };
3039
4875
  }
3040
4876
  function handleGetDependencies(symbol, graph) {
3041
- const matches = searchSymbols(graph, symbol);
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: `${target.filePath}::${target.name}`,
4905
+ symbol: target.id,
3066
4906
  dependencies: grouped,
3067
4907
  totalCount
3068
4908
  };
3069
4909
  }
3070
4910
  function handleGetDependents(symbol, graph) {
3071
- const matches = searchSymbols(graph, symbol);
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: `${target.filePath}::${target.name}`,
4939
+ symbol: target.id,
3096
4940
  dependents: grouped,
3097
4941
  totalCount
3098
4942
  };
3099
4943
  }
3100
4944
  function handleImpactAnalysis(symbol, graph) {
3101
- const matches = searchSymbols(graph, symbol);
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("/") ? dirname6(f.filePath) : ".";
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
  };