depwire-cli 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -2
- package/dist/{chunk-V3SB37U3.js → chunk-C3LAKUAJ.js} +1897 -19
- package/dist/index.js +101 -1
- package/dist/mcpb-entry.js +1 -1
- package/package.json +2 -2
|
@@ -2364,7 +2364,7 @@ import { fileURLToPath } from "url";
|
|
|
2364
2364
|
import { dirname as dirname5, join as join7 } from "path";
|
|
2365
2365
|
import { WebSocketServer } from "ws";
|
|
2366
2366
|
var __filename = fileURLToPath(import.meta.url);
|
|
2367
|
-
var
|
|
2367
|
+
var __dirname2 = dirname5(__filename);
|
|
2368
2368
|
var activeServer = null;
|
|
2369
2369
|
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
2370
2370
|
const net = await import("net");
|
|
@@ -2402,7 +2402,7 @@ async function startVizServer(initialVizData, graph, projectRoot, port = 3333, s
|
|
|
2402
2402
|
const availablePort = await findAvailablePort(port);
|
|
2403
2403
|
const app = express();
|
|
2404
2404
|
let vizData = initialVizData;
|
|
2405
|
-
const publicDir = join7(
|
|
2405
|
+
const publicDir = join7(__dirname2, "viz", "public");
|
|
2406
2406
|
app.use(express.static(publicDir));
|
|
2407
2407
|
app.get("/api/graph", (req, res) => {
|
|
2408
2408
|
res.json(vizData);
|
|
@@ -2571,17 +2571,1744 @@ async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
|
2571
2571
|
}
|
|
2572
2572
|
}
|
|
2573
2573
|
|
|
2574
|
+
// src/docs/generator.ts
|
|
2575
|
+
import { writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync6 } from "fs";
|
|
2576
|
+
import { join as join10 } from "path";
|
|
2577
|
+
|
|
2578
|
+
// src/docs/architecture.ts
|
|
2579
|
+
import { dirname as dirname6 } from "path";
|
|
2580
|
+
|
|
2581
|
+
// src/docs/templates.ts
|
|
2582
|
+
function header(text, level = 1) {
|
|
2583
|
+
return `${"#".repeat(level)} ${text}
|
|
2584
|
+
|
|
2585
|
+
`;
|
|
2586
|
+
}
|
|
2587
|
+
function code(text) {
|
|
2588
|
+
return `\`${text}\``;
|
|
2589
|
+
}
|
|
2590
|
+
function codeBlock(code2, lang = "") {
|
|
2591
|
+
return `\`\`\`${lang}
|
|
2592
|
+
${code2}
|
|
2593
|
+
\`\`\`
|
|
2594
|
+
|
|
2595
|
+
`;
|
|
2596
|
+
}
|
|
2597
|
+
function unorderedList(items) {
|
|
2598
|
+
return items.map((item) => `- ${item}`).join("\n") + "\n\n";
|
|
2599
|
+
}
|
|
2600
|
+
function orderedList(items) {
|
|
2601
|
+
return items.map((item, i) => `${i + 1}. ${item}`).join("\n") + "\n\n";
|
|
2602
|
+
}
|
|
2603
|
+
function table(headers, rows) {
|
|
2604
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
2605
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
2606
|
+
const dataRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
|
|
2607
|
+
return `${headerRow}
|
|
2608
|
+
${separator}
|
|
2609
|
+
${dataRows}
|
|
2610
|
+
|
|
2611
|
+
`;
|
|
2612
|
+
}
|
|
2613
|
+
function blockquote(text) {
|
|
2614
|
+
return `> ${text}
|
|
2615
|
+
|
|
2616
|
+
`;
|
|
2617
|
+
}
|
|
2618
|
+
function timestamp(version, date, fileCount, symbolCount) {
|
|
2619
|
+
return blockquote(`Auto-generated by Depwire ${version} on ${date} | ${fileCount.toLocaleString()} files, ${symbolCount.toLocaleString()} symbols`);
|
|
2620
|
+
}
|
|
2621
|
+
function formatNumber(n) {
|
|
2622
|
+
return n.toLocaleString();
|
|
2623
|
+
}
|
|
2624
|
+
function formatPercent(value, total) {
|
|
2625
|
+
if (total === 0) return "0.0%";
|
|
2626
|
+
return `${(value / total * 100).toFixed(1)}%`;
|
|
2627
|
+
}
|
|
2628
|
+
function impactEmoji(count) {
|
|
2629
|
+
if (count >= 20) return "\u{1F534}";
|
|
2630
|
+
if (count >= 10) return "\u{1F7E1}";
|
|
2631
|
+
if (count >= 5) return "\u{1F7E2}";
|
|
2632
|
+
return "\u26AA";
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/docs/architecture.ts
|
|
2636
|
+
function generateArchitecture(graph, projectRoot, version, parseTime) {
|
|
2637
|
+
const startTime = Date.now();
|
|
2638
|
+
let output = "";
|
|
2639
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2640
|
+
output += timestamp(version, now, getFileCount(graph), graph.order);
|
|
2641
|
+
output += header("Architecture Overview");
|
|
2642
|
+
output += header("Project Summary", 2);
|
|
2643
|
+
output += generateProjectSummary(graph, parseTime);
|
|
2644
|
+
output += header("Module Structure", 2);
|
|
2645
|
+
output += generateModuleStructure(graph);
|
|
2646
|
+
output += header("Entry Points", 2);
|
|
2647
|
+
output += generateEntryPoints(graph);
|
|
2648
|
+
output += header("Hub Files", 2);
|
|
2649
|
+
output += generateHubFiles(graph);
|
|
2650
|
+
output += header("Layer Analysis", 2);
|
|
2651
|
+
output += generateLayerAnalysis(graph);
|
|
2652
|
+
output += header("Circular Dependencies", 2);
|
|
2653
|
+
output += generateCircularDependencies(graph);
|
|
2654
|
+
return output;
|
|
2655
|
+
}
|
|
2656
|
+
function getFileCount(graph) {
|
|
2657
|
+
const files = /* @__PURE__ */ new Set();
|
|
2658
|
+
graph.forEachNode((node, attrs) => {
|
|
2659
|
+
files.add(attrs.filePath);
|
|
2660
|
+
});
|
|
2661
|
+
return files.size;
|
|
2662
|
+
}
|
|
2663
|
+
function getLanguageStats(graph) {
|
|
2664
|
+
const stats = {};
|
|
2665
|
+
const files = /* @__PURE__ */ new Set();
|
|
2666
|
+
graph.forEachNode((node, attrs) => {
|
|
2667
|
+
if (!files.has(attrs.filePath)) {
|
|
2668
|
+
files.add(attrs.filePath);
|
|
2669
|
+
const ext = attrs.filePath.toLowerCase();
|
|
2670
|
+
let lang;
|
|
2671
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
2672
|
+
lang = "TypeScript";
|
|
2673
|
+
} else if (ext.endsWith(".py")) {
|
|
2674
|
+
lang = "Python";
|
|
2675
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
2676
|
+
lang = "JavaScript";
|
|
2677
|
+
} else if (ext.endsWith(".go")) {
|
|
2678
|
+
lang = "Go";
|
|
2679
|
+
} else {
|
|
2680
|
+
lang = "Other";
|
|
2681
|
+
}
|
|
2682
|
+
stats[lang] = (stats[lang] || 0) + 1;
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
return stats;
|
|
2686
|
+
}
|
|
2687
|
+
function generateProjectSummary(graph, parseTime) {
|
|
2688
|
+
const fileCount = getFileCount(graph);
|
|
2689
|
+
const symbolCount = graph.order;
|
|
2690
|
+
const edgeCount = graph.size;
|
|
2691
|
+
const languages = getLanguageStats(graph);
|
|
2692
|
+
let output = "";
|
|
2693
|
+
output += `- **Total Files:** ${formatNumber(fileCount)}
|
|
2694
|
+
`;
|
|
2695
|
+
output += `- **Total Symbols:** ${formatNumber(symbolCount)}
|
|
2696
|
+
`;
|
|
2697
|
+
output += `- **Total Edges:** ${formatNumber(edgeCount)}
|
|
2698
|
+
`;
|
|
2699
|
+
output += `- **Parse Time:** ${parseTime.toFixed(1)}s
|
|
2700
|
+
`;
|
|
2701
|
+
if (Object.keys(languages).length > 1) {
|
|
2702
|
+
output += "\n**Languages:**\n\n";
|
|
2703
|
+
const totalFiles = fileCount;
|
|
2704
|
+
for (const [lang, count] of Object.entries(languages).sort((a, b) => b[1] - a[1])) {
|
|
2705
|
+
output += `- ${lang}: ${count} files (${formatPercent(count, totalFiles)})
|
|
2706
|
+
`;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
output += "\n";
|
|
2710
|
+
return output;
|
|
2711
|
+
}
|
|
2712
|
+
function generateModuleStructure(graph) {
|
|
2713
|
+
const dirStats = getDirectoryStats(graph);
|
|
2714
|
+
if (dirStats.length === 0) {
|
|
2715
|
+
return "No module structure detected (single file or flat structure).\n\n";
|
|
2716
|
+
}
|
|
2717
|
+
const headers = ["Directory", "Files", "Symbols", "Connections", "Role"];
|
|
2718
|
+
const rows = dirStats.slice(0, 15).map((dir) => [
|
|
2719
|
+
`\`${dir.name}\``,
|
|
2720
|
+
formatNumber(dir.fileCount),
|
|
2721
|
+
formatNumber(dir.symbolCount),
|
|
2722
|
+
formatNumber(dir.connectionCount),
|
|
2723
|
+
dir.role
|
|
2724
|
+
]);
|
|
2725
|
+
return table(headers, rows);
|
|
2726
|
+
}
|
|
2727
|
+
function getDirectoryStats(graph) {
|
|
2728
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
2729
|
+
graph.forEachNode((node, attrs) => {
|
|
2730
|
+
const dir = dirname6(attrs.filePath);
|
|
2731
|
+
if (dir === ".") return;
|
|
2732
|
+
if (!dirMap.has(dir)) {
|
|
2733
|
+
dirMap.set(dir, {
|
|
2734
|
+
name: dir,
|
|
2735
|
+
fileCount: 0,
|
|
2736
|
+
symbolCount: 0,
|
|
2737
|
+
connectionCount: 0,
|
|
2738
|
+
role: "",
|
|
2739
|
+
typeCount: 0,
|
|
2740
|
+
functionCount: 0,
|
|
2741
|
+
outboundEdges: 0,
|
|
2742
|
+
inboundEdges: 0
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
const dirStat = dirMap.get(dir);
|
|
2746
|
+
dirStat.symbolCount++;
|
|
2747
|
+
if (attrs.kind === "interface" || attrs.kind === "type_alias") {
|
|
2748
|
+
dirStat.typeCount++;
|
|
2749
|
+
} else if (attrs.kind === "function" || attrs.kind === "method") {
|
|
2750
|
+
dirStat.functionCount++;
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
const filesPerDir = /* @__PURE__ */ new Map();
|
|
2754
|
+
graph.forEachNode((node, attrs) => {
|
|
2755
|
+
const dir = dirname6(attrs.filePath);
|
|
2756
|
+
if (!filesPerDir.has(dir)) {
|
|
2757
|
+
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
2758
|
+
}
|
|
2759
|
+
filesPerDir.get(dir).add(attrs.filePath);
|
|
2760
|
+
});
|
|
2761
|
+
filesPerDir.forEach((files, dir) => {
|
|
2762
|
+
if (dirMap.has(dir)) {
|
|
2763
|
+
dirMap.get(dir).fileCount = files.size;
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
2767
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2768
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2769
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2770
|
+
const sourceDir = dirname6(sourceAttrs.filePath);
|
|
2771
|
+
const targetDir = dirname6(targetAttrs.filePath);
|
|
2772
|
+
if (sourceDir !== targetDir) {
|
|
2773
|
+
if (!dirEdges.has(sourceDir)) {
|
|
2774
|
+
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
2775
|
+
}
|
|
2776
|
+
if (!dirEdges.has(targetDir)) {
|
|
2777
|
+
dirEdges.set(targetDir, { in: 0, out: 0 });
|
|
2778
|
+
}
|
|
2779
|
+
dirEdges.get(sourceDir).out++;
|
|
2780
|
+
dirEdges.get(targetDir).in++;
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
dirEdges.forEach((edges, dir) => {
|
|
2784
|
+
if (dirMap.has(dir)) {
|
|
2785
|
+
const stat = dirMap.get(dir);
|
|
2786
|
+
stat.inboundEdges = edges.in;
|
|
2787
|
+
stat.outboundEdges = edges.out;
|
|
2788
|
+
stat.connectionCount = edges.in + edges.out;
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
dirMap.forEach((dir) => {
|
|
2792
|
+
const typeRatio = dir.symbolCount > 0 ? dir.typeCount / dir.symbolCount : 0;
|
|
2793
|
+
const outboundRatio = dir.connectionCount > 0 ? dir.outboundEdges / dir.connectionCount : 0;
|
|
2794
|
+
const inboundRatio = dir.connectionCount > 0 ? dir.inboundEdges / dir.connectionCount : 0;
|
|
2795
|
+
if (typeRatio > 0.7) {
|
|
2796
|
+
dir.role = "Type definitions";
|
|
2797
|
+
} else if (outboundRatio > 0.7) {
|
|
2798
|
+
dir.role = "Orchestration / Entry points";
|
|
2799
|
+
} else if (inboundRatio > 0.7) {
|
|
2800
|
+
dir.role = "Shared utilities / Foundation";
|
|
2801
|
+
} else {
|
|
2802
|
+
dir.role = "Core logic";
|
|
2803
|
+
}
|
|
2804
|
+
});
|
|
2805
|
+
return Array.from(dirMap.values()).sort((a, b) => b.symbolCount - a.symbolCount);
|
|
2806
|
+
}
|
|
2807
|
+
function generateEntryPoints(graph) {
|
|
2808
|
+
const fileStats = getFileStats(graph);
|
|
2809
|
+
const entryPoints = fileStats.filter((f) => f.outgoingRefs > 0).map((f) => ({
|
|
2810
|
+
...f,
|
|
2811
|
+
ratio: f.incomingRefs === 0 ? Infinity : f.outgoingRefs / (f.incomingRefs + 1)
|
|
2812
|
+
})).sort((a, b) => b.ratio - a.ratio).slice(0, 5);
|
|
2813
|
+
if (entryPoints.length === 0) {
|
|
2814
|
+
return "No clear entry points detected.\n\n";
|
|
2815
|
+
}
|
|
2816
|
+
const headers = ["File", "Outgoing", "Incoming", "Ratio"];
|
|
2817
|
+
const rows = entryPoints.map((f) => [
|
|
2818
|
+
`\`${f.filePath}\``,
|
|
2819
|
+
formatNumber(f.outgoingRefs),
|
|
2820
|
+
formatNumber(f.incomingRefs),
|
|
2821
|
+
f.ratio === Infinity ? "\u221E" : f.ratio.toFixed(1)
|
|
2822
|
+
]);
|
|
2823
|
+
return table(headers, rows);
|
|
2824
|
+
}
|
|
2825
|
+
function generateHubFiles(graph) {
|
|
2826
|
+
const fileStats = getFileStats(graph);
|
|
2827
|
+
const hubFiles = fileStats.sort((a, b) => b.incomingRefs - a.incomingRefs).slice(0, 10);
|
|
2828
|
+
if (hubFiles.length === 0 || hubFiles[0].incomingRefs === 0) {
|
|
2829
|
+
return "No hub files detected.\n\n";
|
|
2830
|
+
}
|
|
2831
|
+
const headers = ["File", "Dependents", "Symbols"];
|
|
2832
|
+
const rows = hubFiles.map((f) => [
|
|
2833
|
+
`\`${f.filePath}\``,
|
|
2834
|
+
formatNumber(f.incomingRefs),
|
|
2835
|
+
formatNumber(f.symbolCount)
|
|
2836
|
+
]);
|
|
2837
|
+
return table(headers, rows);
|
|
2838
|
+
}
|
|
2839
|
+
function getFileStats(graph) {
|
|
2840
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
2841
|
+
graph.forEachNode((node, attrs) => {
|
|
2842
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
2843
|
+
fileMap.set(attrs.filePath, {
|
|
2844
|
+
symbolCount: 0,
|
|
2845
|
+
incomingRefs: /* @__PURE__ */ new Set(),
|
|
2846
|
+
outgoingRefs: /* @__PURE__ */ new Set()
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
fileMap.get(attrs.filePath).symbolCount++;
|
|
2850
|
+
});
|
|
2851
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2852
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2853
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
2854
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
2855
|
+
const sourceFile = fileMap.get(sourceAttrs.filePath);
|
|
2856
|
+
const targetFile = fileMap.get(targetAttrs.filePath);
|
|
2857
|
+
if (sourceFile) {
|
|
2858
|
+
sourceFile.outgoingRefs.add(targetAttrs.filePath);
|
|
2859
|
+
}
|
|
2860
|
+
if (targetFile) {
|
|
2861
|
+
targetFile.incomingRefs.add(sourceAttrs.filePath);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
});
|
|
2865
|
+
const result = [];
|
|
2866
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
2867
|
+
result.push({
|
|
2868
|
+
filePath,
|
|
2869
|
+
symbolCount: data.symbolCount,
|
|
2870
|
+
incomingRefs: data.incomingRefs.size,
|
|
2871
|
+
outgoingRefs: data.outgoingRefs.size
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
return result;
|
|
2875
|
+
}
|
|
2876
|
+
function generateLayerAnalysis(graph) {
|
|
2877
|
+
const dirStats = getDirectoryStats(graph);
|
|
2878
|
+
if (dirStats.length === 0) {
|
|
2879
|
+
return "No layered architecture detected (flat or single-file project).\n\n";
|
|
2880
|
+
}
|
|
2881
|
+
const foundation = dirStats.filter((d) => d.inboundEdges > d.outboundEdges * 2);
|
|
2882
|
+
const orchestration = dirStats.filter((d) => d.outboundEdges > d.inboundEdges * 2);
|
|
2883
|
+
const core = dirStats.filter((d) => !foundation.includes(d) && !orchestration.includes(d));
|
|
2884
|
+
let output = "";
|
|
2885
|
+
if (foundation.length > 0) {
|
|
2886
|
+
output += "**Foundation Layer** (mostly imported by others):\n\n";
|
|
2887
|
+
output += unorderedList(foundation.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2888
|
+
}
|
|
2889
|
+
if (core.length > 0) {
|
|
2890
|
+
output += "**Core Layer** (balanced dependencies):\n\n";
|
|
2891
|
+
output += unorderedList(core.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2892
|
+
}
|
|
2893
|
+
if (orchestration.length > 0) {
|
|
2894
|
+
output += "**Orchestration Layer** (mostly imports from others):\n\n";
|
|
2895
|
+
output += unorderedList(orchestration.map((d) => `\`${d.name}\` \u2014 ${d.role}`));
|
|
2896
|
+
}
|
|
2897
|
+
return output;
|
|
2898
|
+
}
|
|
2899
|
+
function generateCircularDependencies(graph) {
|
|
2900
|
+
const cycles = detectCycles(graph);
|
|
2901
|
+
if (cycles.length === 0) {
|
|
2902
|
+
return "\u2705 No circular dependencies detected.\n\n";
|
|
2903
|
+
}
|
|
2904
|
+
let output = `\u26A0\uFE0F Found ${cycles.length} circular ${cycles.length === 1 ? "dependency" : "dependencies"}:
|
|
2905
|
+
|
|
2906
|
+
`;
|
|
2907
|
+
for (let i = 0; i < Math.min(cycles.length, 10); i++) {
|
|
2908
|
+
const cycle = cycles[i];
|
|
2909
|
+
output += `**Cycle ${i + 1}:**
|
|
2910
|
+
|
|
2911
|
+
`;
|
|
2912
|
+
output += codeBlock(cycle.path.join(" \u2192\n"), "");
|
|
2913
|
+
output += `**Suggested fix:** ${cycle.suggestion}
|
|
2914
|
+
|
|
2915
|
+
`;
|
|
2916
|
+
}
|
|
2917
|
+
if (cycles.length > 10) {
|
|
2918
|
+
output += `... and ${cycles.length - 10} more cycles.
|
|
2919
|
+
|
|
2920
|
+
`;
|
|
2921
|
+
}
|
|
2922
|
+
return output;
|
|
2923
|
+
}
|
|
2924
|
+
function detectCycles(graph) {
|
|
2925
|
+
const cycles = [];
|
|
2926
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2927
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
2928
|
+
const pathStack = [];
|
|
2929
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
2930
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
2931
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
2932
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
2933
|
+
if (sourceFile !== targetFile) {
|
|
2934
|
+
if (!fileGraph.has(sourceFile)) {
|
|
2935
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
|
|
2936
|
+
}
|
|
2937
|
+
fileGraph.get(sourceFile).add(targetFile);
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
2940
|
+
function dfs(file) {
|
|
2941
|
+
visited.add(file);
|
|
2942
|
+
recStack.add(file);
|
|
2943
|
+
pathStack.push(file);
|
|
2944
|
+
const neighbors = fileGraph.get(file);
|
|
2945
|
+
if (neighbors) {
|
|
2946
|
+
for (const neighbor of neighbors) {
|
|
2947
|
+
if (!visited.has(neighbor)) {
|
|
2948
|
+
if (dfs(neighbor)) {
|
|
2949
|
+
return true;
|
|
2950
|
+
}
|
|
2951
|
+
} else if (recStack.has(neighbor)) {
|
|
2952
|
+
const cycleStart = pathStack.indexOf(neighbor);
|
|
2953
|
+
const cyclePath = pathStack.slice(cycleStart);
|
|
2954
|
+
cyclePath.push(neighbor);
|
|
2955
|
+
cycles.push({
|
|
2956
|
+
path: cyclePath,
|
|
2957
|
+
suggestion: "Extract shared types/interfaces to a common file"
|
|
2958
|
+
});
|
|
2959
|
+
return true;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
recStack.delete(file);
|
|
2964
|
+
pathStack.pop();
|
|
2965
|
+
return false;
|
|
2966
|
+
}
|
|
2967
|
+
for (const file of fileGraph.keys()) {
|
|
2968
|
+
if (!visited.has(file)) {
|
|
2969
|
+
dfs(file);
|
|
2970
|
+
recStack.clear();
|
|
2971
|
+
pathStack.length = 0;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return cycles;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// src/docs/conventions.ts
|
|
2978
|
+
import { basename as basename2, extname as extname4 } from "path";
|
|
2979
|
+
function generateConventions(graph, projectRoot, version) {
|
|
2980
|
+
let output = "";
|
|
2981
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2982
|
+
const fileCount = getFileCount2(graph);
|
|
2983
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
2984
|
+
output += header("Code Conventions");
|
|
2985
|
+
output += "Auto-detected coding patterns and conventions in this codebase.\n\n";
|
|
2986
|
+
output += header("File Organization", 2);
|
|
2987
|
+
output += generateFileOrganization(graph);
|
|
2988
|
+
output += header("Naming Patterns", 2);
|
|
2989
|
+
output += generateNamingPatterns(graph);
|
|
2990
|
+
output += header("Import Style", 2);
|
|
2991
|
+
output += generateImportStyle(graph);
|
|
2992
|
+
output += header("Export Patterns", 2);
|
|
2993
|
+
output += generateExportPatterns(graph);
|
|
2994
|
+
output += header("Symbol Distribution", 2);
|
|
2995
|
+
output += generateSymbolDistribution(graph);
|
|
2996
|
+
output += header("Detected Design Patterns", 2);
|
|
2997
|
+
output += generateDesignPatterns(graph);
|
|
2998
|
+
return output;
|
|
2999
|
+
}
|
|
3000
|
+
function getFileCount2(graph) {
|
|
3001
|
+
const files = /* @__PURE__ */ new Set();
|
|
3002
|
+
graph.forEachNode((node, attrs) => {
|
|
3003
|
+
files.add(attrs.filePath);
|
|
3004
|
+
});
|
|
3005
|
+
return files.size;
|
|
3006
|
+
}
|
|
3007
|
+
function generateFileOrganization(graph) {
|
|
3008
|
+
const files = /* @__PURE__ */ new Set();
|
|
3009
|
+
let barrelFileCount = 0;
|
|
3010
|
+
let testFileCount = 0;
|
|
3011
|
+
let totalLines = 0;
|
|
3012
|
+
const fileSizes = [];
|
|
3013
|
+
graph.forEachNode((node, attrs) => {
|
|
3014
|
+
if (!files.has(attrs.filePath)) {
|
|
3015
|
+
files.add(attrs.filePath);
|
|
3016
|
+
const fileName = basename2(attrs.filePath);
|
|
3017
|
+
if (fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx") {
|
|
3018
|
+
barrelFileCount++;
|
|
3019
|
+
}
|
|
3020
|
+
if (fileName.includes(".test.") || fileName.includes(".spec.") || attrs.filePath.includes("__tests__")) {
|
|
3021
|
+
testFileCount++;
|
|
3022
|
+
}
|
|
3023
|
+
const maxLine = getMaxLineNumber(graph, attrs.filePath);
|
|
3024
|
+
if (maxLine > 0) {
|
|
3025
|
+
fileSizes.push(maxLine);
|
|
3026
|
+
totalLines += maxLine;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
const avgFileSize = fileSizes.length > 0 ? Math.round(totalLines / fileSizes.length) : 0;
|
|
3031
|
+
const medianFileSize = fileSizes.length > 0 ? getMedian(fileSizes) : 0;
|
|
3032
|
+
let output = "";
|
|
3033
|
+
output += `- **Total Files:** ${formatNumber(files.size)}
|
|
3034
|
+
`;
|
|
3035
|
+
output += `- **Barrel Files (index.*):** ${formatNumber(barrelFileCount)} (${formatPercent(barrelFileCount, files.size)})
|
|
3036
|
+
`;
|
|
3037
|
+
output += `- **Test Files:** ${formatNumber(testFileCount)} (${formatPercent(testFileCount, files.size)})
|
|
3038
|
+
`;
|
|
3039
|
+
if (avgFileSize > 0) {
|
|
3040
|
+
output += `- **Average File Size:** ${formatNumber(avgFileSize)} lines
|
|
3041
|
+
`;
|
|
3042
|
+
output += `- **Median File Size:** ${formatNumber(medianFileSize)} lines
|
|
3043
|
+
`;
|
|
3044
|
+
}
|
|
3045
|
+
output += "\n";
|
|
3046
|
+
return output;
|
|
3047
|
+
}
|
|
3048
|
+
function getMaxLineNumber(graph, filePath) {
|
|
3049
|
+
let maxLine = 0;
|
|
3050
|
+
graph.forEachNode((node, attrs) => {
|
|
3051
|
+
if (attrs.filePath === filePath) {
|
|
3052
|
+
maxLine = Math.max(maxLine, attrs.endLine);
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
return maxLine;
|
|
3056
|
+
}
|
|
3057
|
+
function getMedian(numbers) {
|
|
3058
|
+
const sorted = [...numbers].sort((a, b) => a - b);
|
|
3059
|
+
const mid = Math.floor(sorted.length / 2);
|
|
3060
|
+
return sorted.length % 2 === 0 ? Math.round((sorted[mid - 1] + sorted[mid]) / 2) : sorted[mid];
|
|
3061
|
+
}
|
|
3062
|
+
function generateNamingPatterns(graph) {
|
|
3063
|
+
const patterns = {
|
|
3064
|
+
files: { camelCase: 0, PascalCase: 0, kebabCase: 0, snakeCase: 0, total: 0 },
|
|
3065
|
+
functions: { camelCase: 0, PascalCase: 0, snakeCase: 0, total: 0 },
|
|
3066
|
+
classes: { PascalCase: 0, other: 0, total: 0 },
|
|
3067
|
+
interfaces: { IPrefixed: 0, PascalCase: 0, other: 0, total: 0 },
|
|
3068
|
+
constants: { UPPER_SNAKE: 0, other: 0, total: 0 },
|
|
3069
|
+
types: { PascalCase: 0, camelCase: 0, other: 0, total: 0 }
|
|
3070
|
+
};
|
|
3071
|
+
const files = /* @__PURE__ */ new Set();
|
|
3072
|
+
graph.forEachNode((node, attrs) => {
|
|
3073
|
+
if (!files.has(attrs.filePath)) {
|
|
3074
|
+
files.add(attrs.filePath);
|
|
3075
|
+
const fileName = basename2(attrs.filePath, extname4(attrs.filePath));
|
|
3076
|
+
if (isCamelCase(fileName)) patterns.files.camelCase++;
|
|
3077
|
+
else if (isPascalCase(fileName)) patterns.files.PascalCase++;
|
|
3078
|
+
else if (isKebabCase(fileName)) patterns.files.kebabCase++;
|
|
3079
|
+
else if (isSnakeCase(fileName)) patterns.files.snakeCase++;
|
|
3080
|
+
patterns.files.total++;
|
|
3081
|
+
}
|
|
3082
|
+
const name = attrs.name;
|
|
3083
|
+
const kind = attrs.kind;
|
|
3084
|
+
if (kind === "function" || kind === "method") {
|
|
3085
|
+
if (isCamelCase(name)) patterns.functions.camelCase++;
|
|
3086
|
+
else if (isPascalCase(name)) patterns.functions.PascalCase++;
|
|
3087
|
+
else if (isSnakeCase(name)) patterns.functions.snakeCase++;
|
|
3088
|
+
patterns.functions.total++;
|
|
3089
|
+
} else if (kind === "class") {
|
|
3090
|
+
if (isPascalCase(name)) patterns.classes.PascalCase++;
|
|
3091
|
+
else patterns.classes.other++;
|
|
3092
|
+
patterns.classes.total++;
|
|
3093
|
+
} else if (kind === "interface") {
|
|
3094
|
+
if (name.startsWith("I") && isPascalCase(name.slice(1))) patterns.interfaces.IPrefixed++;
|
|
3095
|
+
else if (isPascalCase(name)) patterns.interfaces.PascalCase++;
|
|
3096
|
+
else patterns.interfaces.other++;
|
|
3097
|
+
patterns.interfaces.total++;
|
|
3098
|
+
} else if (kind === "constant") {
|
|
3099
|
+
if (isUpperSnakeCase(name)) patterns.constants.UPPER_SNAKE++;
|
|
3100
|
+
else patterns.constants.other++;
|
|
3101
|
+
patterns.constants.total++;
|
|
3102
|
+
} else if (kind === "type_alias") {
|
|
3103
|
+
if (isPascalCase(name)) patterns.types.PascalCase++;
|
|
3104
|
+
else if (isCamelCase(name)) patterns.types.camelCase++;
|
|
3105
|
+
else patterns.types.other++;
|
|
3106
|
+
patterns.types.total++;
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
let output = "";
|
|
3110
|
+
if (patterns.files.total > 0) {
|
|
3111
|
+
output += "**File Naming:**\n\n";
|
|
3112
|
+
if (patterns.files.kebabCase > 0) {
|
|
3113
|
+
output += `- kebab-case: ${formatPercent(patterns.files.kebabCase, patterns.files.total)}
|
|
3114
|
+
`;
|
|
3115
|
+
}
|
|
3116
|
+
if (patterns.files.camelCase > 0) {
|
|
3117
|
+
output += `- camelCase: ${formatPercent(patterns.files.camelCase, patterns.files.total)}
|
|
3118
|
+
`;
|
|
3119
|
+
}
|
|
3120
|
+
if (patterns.files.PascalCase > 0) {
|
|
3121
|
+
output += `- PascalCase: ${formatPercent(patterns.files.PascalCase, patterns.files.total)}
|
|
3122
|
+
`;
|
|
3123
|
+
}
|
|
3124
|
+
if (patterns.files.snakeCase > 0) {
|
|
3125
|
+
output += `- snake_case: ${formatPercent(patterns.files.snakeCase, patterns.files.total)}
|
|
3126
|
+
`;
|
|
3127
|
+
}
|
|
3128
|
+
output += "\n";
|
|
3129
|
+
}
|
|
3130
|
+
if (patterns.functions.total > 0) {
|
|
3131
|
+
output += "**Function Naming:**\n\n";
|
|
3132
|
+
if (patterns.functions.camelCase > 0) {
|
|
3133
|
+
output += `- camelCase: ${formatPercent(patterns.functions.camelCase, patterns.functions.total)}
|
|
3134
|
+
`;
|
|
3135
|
+
}
|
|
3136
|
+
if (patterns.functions.snakeCase > 0) {
|
|
3137
|
+
output += `- snake_case: ${formatPercent(patterns.functions.snakeCase, patterns.functions.total)}
|
|
3138
|
+
`;
|
|
3139
|
+
}
|
|
3140
|
+
if (patterns.functions.PascalCase > 0) {
|
|
3141
|
+
output += `- PascalCase: ${formatPercent(patterns.functions.PascalCase, patterns.functions.total)}
|
|
3142
|
+
`;
|
|
3143
|
+
}
|
|
3144
|
+
output += "\n";
|
|
3145
|
+
}
|
|
3146
|
+
if (patterns.classes.total > 0) {
|
|
3147
|
+
output += "**Class Naming:**\n\n";
|
|
3148
|
+
output += `- PascalCase: ${formatPercent(patterns.classes.PascalCase, patterns.classes.total)}
|
|
3149
|
+
`;
|
|
3150
|
+
if (patterns.classes.other > 0) {
|
|
3151
|
+
output += `- Other: ${formatPercent(patterns.classes.other, patterns.classes.total)}
|
|
3152
|
+
`;
|
|
3153
|
+
}
|
|
3154
|
+
output += "\n";
|
|
3155
|
+
}
|
|
3156
|
+
if (patterns.interfaces.total > 0) {
|
|
3157
|
+
output += "**Interface Naming:**\n\n";
|
|
3158
|
+
if (patterns.interfaces.IPrefixed > 0) {
|
|
3159
|
+
output += `- I-prefix (IPerson): ${formatPercent(patterns.interfaces.IPrefixed, patterns.interfaces.total)}
|
|
3160
|
+
`;
|
|
3161
|
+
}
|
|
3162
|
+
if (patterns.interfaces.PascalCase > 0) {
|
|
3163
|
+
output += `- PascalCase (Person): ${formatPercent(patterns.interfaces.PascalCase, patterns.interfaces.total)}
|
|
3164
|
+
`;
|
|
3165
|
+
}
|
|
3166
|
+
if (patterns.interfaces.other > 0) {
|
|
3167
|
+
output += `- Other: ${formatPercent(patterns.interfaces.other, patterns.interfaces.total)}
|
|
3168
|
+
`;
|
|
3169
|
+
}
|
|
3170
|
+
output += "\n";
|
|
3171
|
+
}
|
|
3172
|
+
if (patterns.types.total > 0) {
|
|
3173
|
+
output += "**Type Naming:**\n\n";
|
|
3174
|
+
if (patterns.types.PascalCase > 0) {
|
|
3175
|
+
output += `- PascalCase: ${formatPercent(patterns.types.PascalCase, patterns.types.total)}
|
|
3176
|
+
`;
|
|
3177
|
+
}
|
|
3178
|
+
if (patterns.types.camelCase > 0) {
|
|
3179
|
+
output += `- camelCase: ${formatPercent(patterns.types.camelCase, patterns.types.total)}
|
|
3180
|
+
`;
|
|
3181
|
+
}
|
|
3182
|
+
if (patterns.types.other > 0) {
|
|
3183
|
+
output += `- Other: ${formatPercent(patterns.types.other, patterns.types.total)}
|
|
3184
|
+
`;
|
|
3185
|
+
}
|
|
3186
|
+
output += "\n";
|
|
3187
|
+
}
|
|
3188
|
+
if (patterns.constants.total > 0) {
|
|
3189
|
+
output += "**Constant Naming:**\n\n";
|
|
3190
|
+
output += `- UPPER_SNAKE_CASE: ${formatPercent(patterns.constants.UPPER_SNAKE, patterns.constants.total)}
|
|
3191
|
+
`;
|
|
3192
|
+
if (patterns.constants.other > 0) {
|
|
3193
|
+
output += `- Other: ${formatPercent(patterns.constants.other, patterns.constants.total)}
|
|
3194
|
+
`;
|
|
3195
|
+
}
|
|
3196
|
+
output += "\n";
|
|
3197
|
+
}
|
|
3198
|
+
return output;
|
|
3199
|
+
}
|
|
3200
|
+
function generateImportStyle(graph) {
|
|
3201
|
+
let barrelImportCount = 0;
|
|
3202
|
+
let pathAliasCount = 0;
|
|
3203
|
+
let totalImports = 0;
|
|
3204
|
+
let namedExportCount = 0;
|
|
3205
|
+
let defaultExportCount = 0;
|
|
3206
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3207
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3208
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3209
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath && attrs.kind === "imports") {
|
|
3210
|
+
totalImports++;
|
|
3211
|
+
if (targetAttrs.filePath.endsWith("/index.ts") || targetAttrs.filePath.endsWith("/index.js")) {
|
|
3212
|
+
barrelImportCount++;
|
|
3213
|
+
}
|
|
3214
|
+
if (targetAttrs.filePath.startsWith("@/") || targetAttrs.filePath.startsWith("~/") || targetAttrs.filePath.startsWith("src/")) {
|
|
3215
|
+
pathAliasCount++;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
graph.forEachNode((node, attrs) => {
|
|
3220
|
+
if (attrs.exported) {
|
|
3221
|
+
if (attrs.name === "default") {
|
|
3222
|
+
defaultExportCount++;
|
|
3223
|
+
} else {
|
|
3224
|
+
namedExportCount++;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
let output = "";
|
|
3229
|
+
if (totalImports > 0) {
|
|
3230
|
+
output += `- **Total Cross-File Imports:** ${formatNumber(totalImports)}
|
|
3231
|
+
`;
|
|
3232
|
+
if (barrelImportCount > 0) {
|
|
3233
|
+
output += `- **Barrel Imports (from index files):** ${formatPercent(barrelImportCount, totalImports)}
|
|
3234
|
+
`;
|
|
3235
|
+
}
|
|
3236
|
+
if (pathAliasCount > 0) {
|
|
3237
|
+
output += `- **Path Alias Usage (@/ or ~/):** ${formatPercent(pathAliasCount, totalImports)}
|
|
3238
|
+
`;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
output += "\n";
|
|
3242
|
+
return output;
|
|
3243
|
+
}
|
|
3244
|
+
function generateExportPatterns(graph) {
|
|
3245
|
+
let namedExportCount = 0;
|
|
3246
|
+
let defaultExportCount = 0;
|
|
3247
|
+
let reExportCount = 0;
|
|
3248
|
+
graph.forEachNode((node, attrs) => {
|
|
3249
|
+
if (attrs.exported) {
|
|
3250
|
+
if (attrs.name === "default") {
|
|
3251
|
+
defaultExportCount++;
|
|
3252
|
+
} else {
|
|
3253
|
+
namedExportCount++;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
if (attrs.kind === "export") {
|
|
3257
|
+
reExportCount++;
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
const totalExports = namedExportCount + defaultExportCount;
|
|
3261
|
+
let output = "";
|
|
3262
|
+
if (totalExports > 0) {
|
|
3263
|
+
output += `- **Named Exports:** ${formatNumber(namedExportCount)} (${formatPercent(namedExportCount, totalExports)})
|
|
3264
|
+
`;
|
|
3265
|
+
output += `- **Default Exports:** ${formatNumber(defaultExportCount)} (${formatPercent(defaultExportCount, totalExports)})
|
|
3266
|
+
`;
|
|
3267
|
+
if (reExportCount > 0) {
|
|
3268
|
+
output += `- **Re-exports:** ${formatNumber(reExportCount)}
|
|
3269
|
+
`;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
output += "\n";
|
|
3273
|
+
return output;
|
|
3274
|
+
}
|
|
3275
|
+
function generateSymbolDistribution(graph) {
|
|
3276
|
+
const symbolCounts = {
|
|
3277
|
+
function: 0,
|
|
3278
|
+
class: 0,
|
|
3279
|
+
variable: 0,
|
|
3280
|
+
constant: 0,
|
|
3281
|
+
type_alias: 0,
|
|
3282
|
+
interface: 0,
|
|
3283
|
+
enum: 0,
|
|
3284
|
+
import: 0,
|
|
3285
|
+
export: 0,
|
|
3286
|
+
method: 0,
|
|
3287
|
+
property: 0,
|
|
3288
|
+
decorator: 0,
|
|
3289
|
+
module: 0
|
|
3290
|
+
};
|
|
3291
|
+
graph.forEachNode((node, attrs) => {
|
|
3292
|
+
symbolCounts[attrs.kind]++;
|
|
3293
|
+
});
|
|
3294
|
+
const total = graph.order;
|
|
3295
|
+
const rows = [];
|
|
3296
|
+
for (const [kind, count] of Object.entries(symbolCounts)) {
|
|
3297
|
+
if (count > 0) {
|
|
3298
|
+
rows.push([kind, formatNumber(count), formatPercent(count, total)]);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
rows.sort((a, b) => parseInt(b[1].replace(/,/g, "")) - parseInt(a[1].replace(/,/g, "")));
|
|
3302
|
+
return table(["Symbol Kind", "Count", "Percentage"], rows);
|
|
3303
|
+
}
|
|
3304
|
+
function generateDesignPatterns(graph) {
|
|
3305
|
+
const patterns = {
|
|
3306
|
+
service: 0,
|
|
3307
|
+
factory: 0,
|
|
3308
|
+
hook: 0,
|
|
3309
|
+
middleware: 0,
|
|
3310
|
+
controller: 0,
|
|
3311
|
+
repository: 0,
|
|
3312
|
+
handler: 0
|
|
3313
|
+
};
|
|
3314
|
+
graph.forEachNode((node, attrs) => {
|
|
3315
|
+
const name = attrs.name;
|
|
3316
|
+
const file = attrs.filePath.toLowerCase();
|
|
3317
|
+
if (attrs.kind === "class" && name.endsWith("Service")) {
|
|
3318
|
+
patterns.service++;
|
|
3319
|
+
}
|
|
3320
|
+
if (attrs.kind === "function" && name.startsWith("create")) {
|
|
3321
|
+
patterns.factory++;
|
|
3322
|
+
}
|
|
3323
|
+
if (attrs.kind === "function" && name.startsWith("use") && name.length > 3) {
|
|
3324
|
+
patterns.hook++;
|
|
3325
|
+
}
|
|
3326
|
+
if (file.includes("middleware")) {
|
|
3327
|
+
patterns.middleware++;
|
|
3328
|
+
}
|
|
3329
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Controller")) {
|
|
3330
|
+
patterns.controller++;
|
|
3331
|
+
}
|
|
3332
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Repository")) {
|
|
3333
|
+
patterns.repository++;
|
|
3334
|
+
}
|
|
3335
|
+
if ((attrs.kind === "class" || attrs.kind === "function") && name.endsWith("Handler")) {
|
|
3336
|
+
patterns.handler++;
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
const detected = Object.entries(patterns).filter(([, count]) => count > 0);
|
|
3340
|
+
if (detected.length === 0) {
|
|
3341
|
+
return "No common design patterns detected.\n\n";
|
|
3342
|
+
}
|
|
3343
|
+
let output = "";
|
|
3344
|
+
for (const [pattern, count] of detected) {
|
|
3345
|
+
const description = getPatternDescription(pattern);
|
|
3346
|
+
output += `- **${capitalizeFirst(pattern)} Pattern:** ${count} occurrences \u2014 ${description}
|
|
3347
|
+
`;
|
|
3348
|
+
}
|
|
3349
|
+
output += "\n";
|
|
3350
|
+
return output;
|
|
3351
|
+
}
|
|
3352
|
+
function getPatternDescription(pattern) {
|
|
3353
|
+
switch (pattern) {
|
|
3354
|
+
case "service":
|
|
3355
|
+
return 'Classes ending in "Service"';
|
|
3356
|
+
case "factory":
|
|
3357
|
+
return 'Functions starting with "create"';
|
|
3358
|
+
case "hook":
|
|
3359
|
+
return 'Functions starting with "use" (React hooks)';
|
|
3360
|
+
case "middleware":
|
|
3361
|
+
return "Files in middleware directories";
|
|
3362
|
+
case "controller":
|
|
3363
|
+
return "Controllers for handling requests";
|
|
3364
|
+
case "repository":
|
|
3365
|
+
return "Data access layer pattern";
|
|
3366
|
+
case "handler":
|
|
3367
|
+
return "Event/request handlers";
|
|
3368
|
+
default:
|
|
3369
|
+
return "";
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
function capitalizeFirst(str) {
|
|
3373
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3374
|
+
}
|
|
3375
|
+
function isCamelCase(name) {
|
|
3376
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name);
|
|
3377
|
+
}
|
|
3378
|
+
function isPascalCase(name) {
|
|
3379
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
3380
|
+
}
|
|
3381
|
+
function isKebabCase(name) {
|
|
3382
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name);
|
|
3383
|
+
}
|
|
3384
|
+
function isSnakeCase(name) {
|
|
3385
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
3386
|
+
}
|
|
3387
|
+
function isUpperSnakeCase(name) {
|
|
3388
|
+
return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name);
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// src/docs/dependencies.ts
|
|
3392
|
+
function generateDependencies(graph, projectRoot, version) {
|
|
3393
|
+
let output = "";
|
|
3394
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3395
|
+
const fileCount = getFileCount3(graph);
|
|
3396
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
3397
|
+
output += header("Dependency Map");
|
|
3398
|
+
output += "Complete dependency mapping showing what connects to what.\n\n";
|
|
3399
|
+
output += header("Module Dependency Matrix", 2);
|
|
3400
|
+
output += generateModuleDependencyMatrix(graph);
|
|
3401
|
+
output += header("High-Impact Symbols", 2);
|
|
3402
|
+
output += generateHighImpactSymbols(graph);
|
|
3403
|
+
output += header("Isolated Files", 2);
|
|
3404
|
+
output += generateIsolatedFiles(graph);
|
|
3405
|
+
output += header("Most Connected File Pairs", 2);
|
|
3406
|
+
output += generateConnectedFilePairs(graph);
|
|
3407
|
+
output += header("Longest Dependency Chains", 2);
|
|
3408
|
+
output += generateDependencyChains(graph);
|
|
3409
|
+
output += header("Circular Dependencies (Detailed)", 2);
|
|
3410
|
+
output += generateCircularDependenciesDetailed(graph);
|
|
3411
|
+
return output;
|
|
3412
|
+
}
|
|
3413
|
+
function getFileCount3(graph) {
|
|
3414
|
+
const files = /* @__PURE__ */ new Set();
|
|
3415
|
+
graph.forEachNode((node, attrs) => {
|
|
3416
|
+
files.add(attrs.filePath);
|
|
3417
|
+
});
|
|
3418
|
+
return files.size;
|
|
3419
|
+
}
|
|
3420
|
+
function generateModuleDependencyMatrix(graph) {
|
|
3421
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
3422
|
+
const allDirs = /* @__PURE__ */ new Set();
|
|
3423
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3424
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3425
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3426
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3427
|
+
const sourceDir = getTopLevelDir(sourceAttrs.filePath);
|
|
3428
|
+
const targetDir = getTopLevelDir(targetAttrs.filePath);
|
|
3429
|
+
if (sourceDir && targetDir && sourceDir !== targetDir) {
|
|
3430
|
+
allDirs.add(sourceDir);
|
|
3431
|
+
allDirs.add(targetDir);
|
|
3432
|
+
if (!dirEdges.has(sourceDir)) {
|
|
3433
|
+
dirEdges.set(sourceDir, /* @__PURE__ */ new Map());
|
|
3434
|
+
}
|
|
3435
|
+
const targetMap = dirEdges.get(sourceDir);
|
|
3436
|
+
targetMap.set(targetDir, (targetMap.get(targetDir) || 0) + 1);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
});
|
|
3440
|
+
if (allDirs.size === 0) {
|
|
3441
|
+
return "No module structure detected (flat or single-directory project).\n\n";
|
|
3442
|
+
}
|
|
3443
|
+
const dirTotalEdges = /* @__PURE__ */ new Map();
|
|
3444
|
+
for (const [sourceDir, targets] of dirEdges.entries()) {
|
|
3445
|
+
let total = 0;
|
|
3446
|
+
for (const count of targets.values()) {
|
|
3447
|
+
total += count;
|
|
3448
|
+
}
|
|
3449
|
+
dirTotalEdges.set(sourceDir, total);
|
|
3450
|
+
}
|
|
3451
|
+
const sortedDirs = Array.from(allDirs).sort((a, b) => (dirTotalEdges.get(b) || 0) - (dirTotalEdges.get(a) || 0)).slice(0, 15);
|
|
3452
|
+
if (sortedDirs.length === 0) {
|
|
3453
|
+
return "No cross-module dependencies detected.\n\n";
|
|
3454
|
+
}
|
|
3455
|
+
const headers = ["From / To", ...sortedDirs];
|
|
3456
|
+
const rows = [];
|
|
3457
|
+
for (const sourceDir of sortedDirs) {
|
|
3458
|
+
const row = [sourceDir];
|
|
3459
|
+
for (const targetDir of sortedDirs) {
|
|
3460
|
+
if (sourceDir === targetDir) {
|
|
3461
|
+
row.push("-");
|
|
3462
|
+
} else {
|
|
3463
|
+
const count = dirEdges.get(sourceDir)?.get(targetDir) || 0;
|
|
3464
|
+
row.push(count > 0 ? count.toString() : "\u2717");
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
rows.push(row);
|
|
3468
|
+
}
|
|
3469
|
+
return table(headers, rows);
|
|
3470
|
+
}
|
|
3471
|
+
function getTopLevelDir(filePath) {
|
|
3472
|
+
const parts = filePath.split("/");
|
|
3473
|
+
if (parts.length < 2) {
|
|
3474
|
+
return null;
|
|
3475
|
+
}
|
|
3476
|
+
if (parts[0] === "src" && parts.length >= 3) {
|
|
3477
|
+
return `${parts[0]}/${parts[1]}`;
|
|
3478
|
+
}
|
|
3479
|
+
if (parts[0] === "src" && parts.length === 2) {
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
const firstDir = parts[0];
|
|
3483
|
+
if (firstDir.includes("test") || firstDir.includes("fixture") || firstDir.includes("example") || firstDir.includes("__tests__") || firstDir === "node_modules" || firstDir === "dist" || firstDir === "build") {
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
if (parts.length >= 2) {
|
|
3487
|
+
return `${parts[0]}/${parts[1]}`;
|
|
3488
|
+
}
|
|
3489
|
+
return parts[0];
|
|
3490
|
+
}
|
|
3491
|
+
function generateHighImpactSymbols(graph) {
|
|
3492
|
+
const symbolImpact = [];
|
|
3493
|
+
graph.forEachNode((node, attrs) => {
|
|
3494
|
+
const inDegree = graph.inDegree(node);
|
|
3495
|
+
if (inDegree > 0 && attrs.name !== "__file__") {
|
|
3496
|
+
symbolImpact.push({
|
|
3497
|
+
name: attrs.name,
|
|
3498
|
+
filePath: attrs.filePath,
|
|
3499
|
+
kind: attrs.kind,
|
|
3500
|
+
dependentCount: inDegree
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
symbolImpact.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
3505
|
+
const top = symbolImpact.slice(0, 15);
|
|
3506
|
+
if (top.length === 0) {
|
|
3507
|
+
return "No high-impact symbols detected.\n\n";
|
|
3508
|
+
}
|
|
3509
|
+
const headers = ["Symbol", "File", "Kind", "Dependents", "Impact"];
|
|
3510
|
+
const rows = top.map((s) => {
|
|
3511
|
+
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`;
|
|
3512
|
+
return [
|
|
3513
|
+
`\`${s.name}\``,
|
|
3514
|
+
`\`${s.filePath}\``,
|
|
3515
|
+
s.kind,
|
|
3516
|
+
formatNumber(s.dependentCount),
|
|
3517
|
+
impact
|
|
3518
|
+
];
|
|
3519
|
+
});
|
|
3520
|
+
return table(headers, rows);
|
|
3521
|
+
}
|
|
3522
|
+
function generateIsolatedFiles(graph) {
|
|
3523
|
+
const fileConnections = /* @__PURE__ */ new Map();
|
|
3524
|
+
graph.forEachNode((node, attrs) => {
|
|
3525
|
+
if (!fileConnections.has(attrs.filePath)) {
|
|
3526
|
+
fileConnections.set(attrs.filePath, { incoming: 0, outgoing: 0 });
|
|
3527
|
+
}
|
|
3528
|
+
});
|
|
3529
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3530
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3531
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3532
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3533
|
+
const sourceConn = fileConnections.get(sourceAttrs.filePath);
|
|
3534
|
+
const targetConn = fileConnections.get(targetAttrs.filePath);
|
|
3535
|
+
if (sourceConn) sourceConn.outgoing++;
|
|
3536
|
+
if (targetConn) targetConn.incoming++;
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
const isolated = [];
|
|
3540
|
+
for (const [file, conn] of fileConnections.entries()) {
|
|
3541
|
+
if (conn.incoming === 0) {
|
|
3542
|
+
isolated.push(file);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
if (isolated.length === 0) {
|
|
3546
|
+
return "No isolated files detected. All files are connected.\n\n";
|
|
3547
|
+
}
|
|
3548
|
+
let output = `Found ${isolated.length} file${isolated.length === 1 ? "" : "s"} with no incoming dependencies:
|
|
3549
|
+
|
|
3550
|
+
`;
|
|
3551
|
+
if (isolated.length <= 20) {
|
|
3552
|
+
output += unorderedList(isolated.map((f) => `\`${f}\``));
|
|
3553
|
+
} else {
|
|
3554
|
+
output += unorderedList(isolated.slice(0, 20).map((f) => `\`${f}\``));
|
|
3555
|
+
output += `... and ${isolated.length - 20} more.
|
|
3556
|
+
|
|
3557
|
+
`;
|
|
3558
|
+
}
|
|
3559
|
+
output += "These files could be entry points, standalone scripts, or dead code.\n\n";
|
|
3560
|
+
return output;
|
|
3561
|
+
}
|
|
3562
|
+
function generateConnectedFilePairs(graph) {
|
|
3563
|
+
const filePairEdges = /* @__PURE__ */ new Map();
|
|
3564
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3565
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3566
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3567
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3568
|
+
const pair = [sourceAttrs.filePath, targetAttrs.filePath].sort().join(" <-> ");
|
|
3569
|
+
filePairEdges.set(pair, (filePairEdges.get(pair) || 0) + 1);
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
const pairs = Array.from(filePairEdges.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
3573
|
+
if (pairs.length === 0) {
|
|
3574
|
+
return "No cross-file dependencies detected.\n\n";
|
|
3575
|
+
}
|
|
3576
|
+
const headers = ["File 1", "File 2", "Edges"];
|
|
3577
|
+
const rows = pairs.map(([pair, count]) => {
|
|
3578
|
+
const [file1, file2] = pair.split(" <-> ");
|
|
3579
|
+
return [`\`${file1}\``, `\`${file2}\``, formatNumber(count)];
|
|
3580
|
+
});
|
|
3581
|
+
return table(headers, rows);
|
|
3582
|
+
}
|
|
3583
|
+
function generateDependencyChains(graph) {
|
|
3584
|
+
const chains = findLongestPaths(graph, 5);
|
|
3585
|
+
if (chains.length === 0) {
|
|
3586
|
+
return "No significant dependency chains detected.\n\n";
|
|
3587
|
+
}
|
|
3588
|
+
let output = "";
|
|
3589
|
+
for (let i = 0; i < chains.length; i++) {
|
|
3590
|
+
const chain = chains[i];
|
|
3591
|
+
output += `**Chain ${i + 1}** (${chain.length} files):
|
|
3592
|
+
|
|
3593
|
+
`;
|
|
3594
|
+
output += codeBlock(chain.join(" \u2192\n"), "");
|
|
3595
|
+
}
|
|
3596
|
+
return output;
|
|
3597
|
+
}
|
|
3598
|
+
function findLongestPaths(graph, limit) {
|
|
3599
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
3600
|
+
const fileInDegree = /* @__PURE__ */ new Map();
|
|
3601
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3602
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
3603
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
3604
|
+
if (sourceFile !== targetFile) {
|
|
3605
|
+
if (!fileGraph.has(sourceFile)) {
|
|
3606
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
|
|
3607
|
+
}
|
|
3608
|
+
fileGraph.get(sourceFile).add(targetFile);
|
|
3609
|
+
fileInDegree.set(targetFile, (fileInDegree.get(targetFile) || 0) + 1);
|
|
3610
|
+
if (!fileInDegree.has(sourceFile)) {
|
|
3611
|
+
fileInDegree.set(sourceFile, 0);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
});
|
|
3615
|
+
const roots = [];
|
|
3616
|
+
for (const [file, inDegree] of fileInDegree.entries()) {
|
|
3617
|
+
if (inDegree === 0) {
|
|
3618
|
+
roots.push(file);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const allPaths = [];
|
|
3622
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3623
|
+
function dfs(file, path) {
|
|
3624
|
+
visited.add(file);
|
|
3625
|
+
path.push(file);
|
|
3626
|
+
const neighbors = fileGraph.get(file);
|
|
3627
|
+
if (!neighbors || neighbors.size === 0) {
|
|
3628
|
+
allPaths.push([...path]);
|
|
3629
|
+
} else {
|
|
3630
|
+
for (const neighbor of neighbors) {
|
|
3631
|
+
if (!visited.has(neighbor)) {
|
|
3632
|
+
dfs(neighbor, path);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
path.pop();
|
|
3637
|
+
visited.delete(file);
|
|
3638
|
+
}
|
|
3639
|
+
for (const root of roots.slice(0, 10)) {
|
|
3640
|
+
dfs(root, []);
|
|
3641
|
+
}
|
|
3642
|
+
allPaths.sort((a, b) => b.length - a.length);
|
|
3643
|
+
return allPaths.slice(0, limit);
|
|
3644
|
+
}
|
|
3645
|
+
function generateCircularDependenciesDetailed(graph) {
|
|
3646
|
+
const cycles = detectCyclesDetailed(graph);
|
|
3647
|
+
if (cycles.length === 0) {
|
|
3648
|
+
return "\u2705 No circular dependencies detected.\n\n";
|
|
3649
|
+
}
|
|
3650
|
+
let output = `\u26A0\uFE0F Found ${cycles.length} circular ${cycles.length === 1 ? "dependency" : "dependencies"}:
|
|
3651
|
+
|
|
3652
|
+
`;
|
|
3653
|
+
for (let i = 0; i < Math.min(cycles.length, 5); i++) {
|
|
3654
|
+
const cycle = cycles[i];
|
|
3655
|
+
output += `**Cycle ${i + 1}:**
|
|
3656
|
+
|
|
3657
|
+
`;
|
|
3658
|
+
output += codeBlock(cycle.files.join(" \u2192\n") + " \u2192 " + cycle.files[0], "");
|
|
3659
|
+
if (cycle.symbols.length > 0) {
|
|
3660
|
+
output += "**Symbols involved:**\n\n";
|
|
3661
|
+
output += unorderedList(cycle.symbols.map((s) => `\`${s.name}\` (${s.kind}) at \`${s.filePath}:${s.line}\``));
|
|
3662
|
+
}
|
|
3663
|
+
output += `**Suggested fix:** ${cycle.suggestion}
|
|
3664
|
+
|
|
3665
|
+
`;
|
|
3666
|
+
}
|
|
3667
|
+
if (cycles.length > 5) {
|
|
3668
|
+
output += `... and ${cycles.length - 5} more cycles.
|
|
3669
|
+
|
|
3670
|
+
`;
|
|
3671
|
+
}
|
|
3672
|
+
return output;
|
|
3673
|
+
}
|
|
3674
|
+
function detectCyclesDetailed(graph) {
|
|
3675
|
+
const cycles = [];
|
|
3676
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3677
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
3678
|
+
const pathStack = [];
|
|
3679
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
3680
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3681
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3682
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3683
|
+
const sourceFile = sourceAttrs.filePath;
|
|
3684
|
+
const targetFile = targetAttrs.filePath;
|
|
3685
|
+
if (sourceFile !== targetFile) {
|
|
3686
|
+
if (!fileGraph.has(sourceFile)) {
|
|
3687
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Map());
|
|
3688
|
+
}
|
|
3689
|
+
const targetMap = fileGraph.get(sourceFile);
|
|
3690
|
+
if (!targetMap.has(targetFile)) {
|
|
3691
|
+
targetMap.set(targetFile, []);
|
|
3692
|
+
}
|
|
3693
|
+
targetMap.get(targetFile).push({
|
|
3694
|
+
symbolName: targetAttrs.name,
|
|
3695
|
+
symbolKind: targetAttrs.kind,
|
|
3696
|
+
line: attrs.line || sourceAttrs.startLine
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
});
|
|
3700
|
+
function dfs(file) {
|
|
3701
|
+
visited.add(file);
|
|
3702
|
+
recStack.add(file);
|
|
3703
|
+
pathStack.push(file);
|
|
3704
|
+
const neighbors = fileGraph.get(file);
|
|
3705
|
+
if (neighbors) {
|
|
3706
|
+
for (const [neighbor, symbols] of neighbors.entries()) {
|
|
3707
|
+
if (!visited.has(neighbor)) {
|
|
3708
|
+
if (dfs(neighbor)) {
|
|
3709
|
+
return true;
|
|
3710
|
+
}
|
|
3711
|
+
} else if (recStack.has(neighbor)) {
|
|
3712
|
+
const cycleStart = pathStack.indexOf(neighbor);
|
|
3713
|
+
const cyclePath = pathStack.slice(cycleStart);
|
|
3714
|
+
const cycleSymbols = [];
|
|
3715
|
+
for (let i = 0; i < cyclePath.length; i++) {
|
|
3716
|
+
const currentFile = cyclePath[i];
|
|
3717
|
+
const nextFile = cyclePath[(i + 1) % cyclePath.length];
|
|
3718
|
+
const edgeSymbols = fileGraph.get(currentFile)?.get(nextFile) || [];
|
|
3719
|
+
for (const sym of edgeSymbols.slice(0, 3)) {
|
|
3720
|
+
cycleSymbols.push({
|
|
3721
|
+
name: sym.symbolName,
|
|
3722
|
+
kind: sym.symbolKind,
|
|
3723
|
+
filePath: currentFile,
|
|
3724
|
+
line: sym.line
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
cycles.push({
|
|
3729
|
+
files: cyclePath,
|
|
3730
|
+
symbols: cycleSymbols,
|
|
3731
|
+
suggestion: "Extract shared types/interfaces to a common file"
|
|
3732
|
+
});
|
|
3733
|
+
return true;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
recStack.delete(file);
|
|
3738
|
+
pathStack.pop();
|
|
3739
|
+
return false;
|
|
3740
|
+
}
|
|
3741
|
+
for (const file of fileGraph.keys()) {
|
|
3742
|
+
if (!visited.has(file)) {
|
|
3743
|
+
dfs(file);
|
|
3744
|
+
recStack.clear();
|
|
3745
|
+
pathStack.length = 0;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
return cycles;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// src/docs/onboarding.ts
|
|
3752
|
+
import { dirname as dirname7 } from "path";
|
|
3753
|
+
function generateOnboarding(graph, projectRoot, version) {
|
|
3754
|
+
let output = "";
|
|
3755
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3756
|
+
const fileCount = getFileCount4(graph);
|
|
3757
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
3758
|
+
output += header("Onboarding Guide");
|
|
3759
|
+
output += "A guide for developers new to this codebase.\n\n";
|
|
3760
|
+
output += header("Quick Orientation", 2);
|
|
3761
|
+
output += generateQuickOrientation(graph);
|
|
3762
|
+
output += header("Where to Start Reading", 2);
|
|
3763
|
+
output += generateReadingOrder(graph);
|
|
3764
|
+
output += header("Module Map", 2);
|
|
3765
|
+
output += generateModuleMap(graph);
|
|
3766
|
+
output += header("Key Concepts", 2);
|
|
3767
|
+
output += generateKeyConcepts(graph);
|
|
3768
|
+
output += header("High-Impact Files", 2);
|
|
3769
|
+
output += generateHighImpactWarning(graph);
|
|
3770
|
+
output += header("Using Depwire with This Project", 2);
|
|
3771
|
+
output += generateDepwireUsage(projectRoot);
|
|
3772
|
+
return output;
|
|
3773
|
+
}
|
|
3774
|
+
function getFileCount4(graph) {
|
|
3775
|
+
const files = /* @__PURE__ */ new Set();
|
|
3776
|
+
graph.forEachNode((node, attrs) => {
|
|
3777
|
+
files.add(attrs.filePath);
|
|
3778
|
+
});
|
|
3779
|
+
return files.size;
|
|
3780
|
+
}
|
|
3781
|
+
function getLanguageStats2(graph) {
|
|
3782
|
+
const stats = {};
|
|
3783
|
+
const files = /* @__PURE__ */ new Set();
|
|
3784
|
+
graph.forEachNode((node, attrs) => {
|
|
3785
|
+
if (!files.has(attrs.filePath)) {
|
|
3786
|
+
files.add(attrs.filePath);
|
|
3787
|
+
const ext = attrs.filePath.toLowerCase();
|
|
3788
|
+
let lang;
|
|
3789
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
3790
|
+
lang = "TypeScript";
|
|
3791
|
+
} else if (ext.endsWith(".py")) {
|
|
3792
|
+
lang = "Python";
|
|
3793
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
3794
|
+
lang = "JavaScript";
|
|
3795
|
+
} else if (ext.endsWith(".go")) {
|
|
3796
|
+
lang = "Go";
|
|
3797
|
+
} else {
|
|
3798
|
+
lang = "Other";
|
|
3799
|
+
}
|
|
3800
|
+
stats[lang] = (stats[lang] || 0) + 1;
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
return stats;
|
|
3804
|
+
}
|
|
3805
|
+
function generateQuickOrientation(graph) {
|
|
3806
|
+
const fileCount = getFileCount4(graph);
|
|
3807
|
+
const languages = getLanguageStats2(graph);
|
|
3808
|
+
const primaryLang = Object.entries(languages).sort((a, b) => b[1] - a[1])[0];
|
|
3809
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3810
|
+
graph.forEachNode((node, attrs) => {
|
|
3811
|
+
const dir = dirname7(attrs.filePath);
|
|
3812
|
+
if (dir !== ".") {
|
|
3813
|
+
const topLevel = dir.split("/")[0];
|
|
3814
|
+
dirs.add(topLevel);
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
const mainAreas = Array.from(dirs).sort().join(", ");
|
|
3818
|
+
let output = "";
|
|
3819
|
+
if (primaryLang) {
|
|
3820
|
+
output += `This is a **${primaryLang[0]}** project with **${fileCount} files** and **${graph.order} symbols**. `;
|
|
3821
|
+
} else {
|
|
3822
|
+
output += `This project has **${fileCount} files** and **${graph.order} symbols**. `;
|
|
3823
|
+
}
|
|
3824
|
+
if (dirs.size > 0) {
|
|
3825
|
+
output += `The main areas are: ${mainAreas}.`;
|
|
3826
|
+
} else {
|
|
3827
|
+
output += "The project has a flat file structure.";
|
|
3828
|
+
}
|
|
3829
|
+
output += "\n\n";
|
|
3830
|
+
return output;
|
|
3831
|
+
}
|
|
3832
|
+
function generateReadingOrder(graph) {
|
|
3833
|
+
const fileStats = getFileStatsWithDeps(graph);
|
|
3834
|
+
if (fileStats.length === 0) {
|
|
3835
|
+
return "No files to analyze.\n\n";
|
|
3836
|
+
}
|
|
3837
|
+
const foundation = fileStats.filter((f) => f.incomingRefs > 0 && f.incomingRefs >= f.outgoingRefs * 2).sort((a, b) => b.incomingRefs - a.incomingRefs).slice(0, 3);
|
|
3838
|
+
const core = fileStats.filter((f) => !foundation.includes(f)).filter((f) => f.incomingRefs > 0 && f.outgoingRefs > 0).filter((f) => {
|
|
3839
|
+
const ratio = f.incomingRefs / (f.outgoingRefs + 0.1);
|
|
3840
|
+
return ratio > 0.3 && ratio < 3;
|
|
3841
|
+
}).sort((a, b) => b.incomingRefs + b.outgoingRefs - (a.incomingRefs + a.outgoingRefs)).slice(0, 5);
|
|
3842
|
+
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);
|
|
3843
|
+
if (foundation.length === 0 && core.length === 0 && orchestration.length === 0) {
|
|
3844
|
+
return "No clear reading order detected. Start with any file.\n\n";
|
|
3845
|
+
}
|
|
3846
|
+
let output = "Recommended reading order for understanding the codebase:\n\n";
|
|
3847
|
+
if (foundation.length > 0) {
|
|
3848
|
+
output += "**Foundation** (start here \u2014 these are building blocks):\n\n";
|
|
3849
|
+
output += orderedList(foundation.map((f) => `${code(f.filePath)} \u2014 Shared foundation (${f.incomingRefs} dependents)`));
|
|
3850
|
+
}
|
|
3851
|
+
if (core.length > 0) {
|
|
3852
|
+
output += "**Core Logic** (read these next):\n\n";
|
|
3853
|
+
output += orderedList(core.map((f) => `${code(f.filePath)} \u2014 Core logic (${f.symbolCount} symbols)`));
|
|
3854
|
+
}
|
|
3855
|
+
if (orchestration.length > 0) {
|
|
3856
|
+
output += "**Entry Points** (read these last to see how it all fits together):\n\n";
|
|
3857
|
+
output += orderedList(orchestration.map((f) => `${code(f.filePath)} \u2014 Entry point (imports from ${f.outgoingRefs} files)`));
|
|
3858
|
+
}
|
|
3859
|
+
return output;
|
|
3860
|
+
}
|
|
3861
|
+
function getFileStatsWithDeps(graph) {
|
|
3862
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
3863
|
+
graph.forEachNode((node, attrs) => {
|
|
3864
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
3865
|
+
fileMap.set(attrs.filePath, {
|
|
3866
|
+
symbolCount: 0,
|
|
3867
|
+
incomingRefs: /* @__PURE__ */ new Set(),
|
|
3868
|
+
outgoingRefs: /* @__PURE__ */ new Set()
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
fileMap.get(attrs.filePath).symbolCount++;
|
|
3872
|
+
});
|
|
3873
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3874
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3875
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3876
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
3877
|
+
const sourceFile = fileMap.get(sourceAttrs.filePath);
|
|
3878
|
+
const targetFile = fileMap.get(targetAttrs.filePath);
|
|
3879
|
+
if (sourceFile) {
|
|
3880
|
+
sourceFile.outgoingRefs.add(targetAttrs.filePath);
|
|
3881
|
+
}
|
|
3882
|
+
if (targetFile) {
|
|
3883
|
+
targetFile.incomingRefs.add(sourceAttrs.filePath);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
const result = [];
|
|
3888
|
+
for (const [filePath, data] of fileMap.entries()) {
|
|
3889
|
+
result.push({
|
|
3890
|
+
filePath,
|
|
3891
|
+
symbolCount: data.symbolCount,
|
|
3892
|
+
incomingRefs: data.incomingRefs.size,
|
|
3893
|
+
outgoingRefs: data.outgoingRefs.size
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
return result;
|
|
3897
|
+
}
|
|
3898
|
+
function generateModuleMap(graph) {
|
|
3899
|
+
const dirStats = getDirectoryStats2(graph);
|
|
3900
|
+
if (dirStats.length === 0) {
|
|
3901
|
+
return "Flat file structure (no subdirectories).\n\n";
|
|
3902
|
+
}
|
|
3903
|
+
let output = "";
|
|
3904
|
+
for (const dir of dirStats) {
|
|
3905
|
+
const description = inferDirectoryDescription(dir, graph);
|
|
3906
|
+
output += `- ${code(dir.name)} \u2014 ${description}
|
|
3907
|
+
`;
|
|
3908
|
+
}
|
|
3909
|
+
output += "\n";
|
|
3910
|
+
return output;
|
|
3911
|
+
}
|
|
3912
|
+
function getDirectoryStats2(graph) {
|
|
3913
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
3914
|
+
graph.forEachNode((node, attrs) => {
|
|
3915
|
+
const dir = dirname7(attrs.filePath);
|
|
3916
|
+
if (dir === ".") return;
|
|
3917
|
+
if (!dirMap.has(dir)) {
|
|
3918
|
+
dirMap.set(dir, {
|
|
3919
|
+
name: dir,
|
|
3920
|
+
fileCount: 0,
|
|
3921
|
+
symbolCount: 0,
|
|
3922
|
+
inboundEdges: 0,
|
|
3923
|
+
outboundEdges: 0
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
dirMap.get(dir).symbolCount++;
|
|
3927
|
+
});
|
|
3928
|
+
const filesPerDir = /* @__PURE__ */ new Map();
|
|
3929
|
+
graph.forEachNode((node, attrs) => {
|
|
3930
|
+
const dir = dirname7(attrs.filePath);
|
|
3931
|
+
if (!filesPerDir.has(dir)) {
|
|
3932
|
+
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
3933
|
+
}
|
|
3934
|
+
filesPerDir.get(dir).add(attrs.filePath);
|
|
3935
|
+
});
|
|
3936
|
+
filesPerDir.forEach((files, dir) => {
|
|
3937
|
+
if (dirMap.has(dir)) {
|
|
3938
|
+
dirMap.get(dir).fileCount = files.size;
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
3942
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3943
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3944
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
3945
|
+
const sourceDir = dirname7(sourceAttrs.filePath);
|
|
3946
|
+
const targetDir = dirname7(targetAttrs.filePath);
|
|
3947
|
+
if (sourceDir !== targetDir) {
|
|
3948
|
+
if (!dirEdges.has(sourceDir)) {
|
|
3949
|
+
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
3950
|
+
}
|
|
3951
|
+
if (!dirEdges.has(targetDir)) {
|
|
3952
|
+
dirEdges.set(targetDir, { in: 0, out: 0 });
|
|
3953
|
+
}
|
|
3954
|
+
dirEdges.get(sourceDir).out++;
|
|
3955
|
+
dirEdges.get(targetDir).in++;
|
|
3956
|
+
}
|
|
3957
|
+
});
|
|
3958
|
+
dirEdges.forEach((edges, dir) => {
|
|
3959
|
+
if (dirMap.has(dir)) {
|
|
3960
|
+
const stat = dirMap.get(dir);
|
|
3961
|
+
stat.inboundEdges = edges.in;
|
|
3962
|
+
stat.outboundEdges = edges.out;
|
|
3963
|
+
}
|
|
3964
|
+
});
|
|
3965
|
+
return Array.from(dirMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
3966
|
+
}
|
|
3967
|
+
function inferDirectoryDescription(dir, graph) {
|
|
3968
|
+
const name = dir.name.toLowerCase();
|
|
3969
|
+
if (name.includes("types") || name.includes("interfaces")) {
|
|
3970
|
+
return "Type definitions and interfaces";
|
|
3971
|
+
}
|
|
3972
|
+
if (name.includes("utils") || name.includes("helpers")) {
|
|
3973
|
+
return "Utility functions and helpers";
|
|
3974
|
+
}
|
|
3975
|
+
if (name.includes("services")) {
|
|
3976
|
+
return "Business logic and services";
|
|
3977
|
+
}
|
|
3978
|
+
if (name.includes("components")) {
|
|
3979
|
+
return "UI components";
|
|
3980
|
+
}
|
|
3981
|
+
if (name.includes("api") || name.includes("routes")) {
|
|
3982
|
+
return "API routes and endpoints";
|
|
3983
|
+
}
|
|
3984
|
+
if (name.includes("models") || name.includes("entities")) {
|
|
3985
|
+
return "Data models and entities";
|
|
3986
|
+
}
|
|
3987
|
+
if (name.includes("config")) {
|
|
3988
|
+
return "Configuration files";
|
|
3989
|
+
}
|
|
3990
|
+
if (name.includes("test")) {
|
|
3991
|
+
return "Test files";
|
|
3992
|
+
}
|
|
3993
|
+
const totalEdges = dir.inboundEdges + dir.outboundEdges;
|
|
3994
|
+
if (totalEdges === 0) {
|
|
3995
|
+
return "Isolated module";
|
|
3996
|
+
}
|
|
3997
|
+
const inboundRatio = dir.inboundEdges / totalEdges;
|
|
3998
|
+
if (inboundRatio > 0.7) {
|
|
3999
|
+
return "Shared foundation \u2014 heavily imported by other modules";
|
|
4000
|
+
} else if (inboundRatio < 0.3) {
|
|
4001
|
+
return "Orchestration \u2014 imports from many other modules";
|
|
4002
|
+
} else {
|
|
4003
|
+
return `Core logic \u2014 ${dir.fileCount} files, ${dir.symbolCount} symbols`;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function generateKeyConcepts(graph) {
|
|
4007
|
+
const clusters = detectClusters(graph);
|
|
4008
|
+
if (clusters.length === 0) {
|
|
4009
|
+
return "No distinct concept clusters detected.\n\n";
|
|
4010
|
+
}
|
|
4011
|
+
let output = "The codebase is organized around these key concepts:\n\n";
|
|
4012
|
+
for (const cluster of clusters.slice(0, 5)) {
|
|
4013
|
+
output += `- **${cluster.name}** \u2014 ${cluster.files.length} tightly-connected files: `;
|
|
4014
|
+
output += cluster.files.slice(0, 3).map((f) => code(f)).join(", ");
|
|
4015
|
+
if (cluster.files.length > 3) {
|
|
4016
|
+
output += `, and ${cluster.files.length - 3} more`;
|
|
4017
|
+
}
|
|
4018
|
+
output += "\n";
|
|
4019
|
+
}
|
|
4020
|
+
output += "\n";
|
|
4021
|
+
return output;
|
|
4022
|
+
}
|
|
4023
|
+
function detectClusters(graph) {
|
|
4024
|
+
const dirFiles = /* @__PURE__ */ new Map();
|
|
4025
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
4026
|
+
graph.forEachNode((node, attrs) => {
|
|
4027
|
+
const dir = dirname7(attrs.filePath);
|
|
4028
|
+
if (!dirFiles.has(dir)) {
|
|
4029
|
+
dirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
4030
|
+
}
|
|
4031
|
+
dirFiles.get(dir).add(attrs.filePath);
|
|
4032
|
+
});
|
|
4033
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4034
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
4035
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
4036
|
+
if (sourceFile !== targetFile) {
|
|
4037
|
+
if (!fileEdges.has(sourceFile)) {
|
|
4038
|
+
fileEdges.set(sourceFile, /* @__PURE__ */ new Set());
|
|
4039
|
+
}
|
|
4040
|
+
fileEdges.get(sourceFile).add(targetFile);
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
const clusters = [];
|
|
4044
|
+
for (const [dir, files] of dirFiles.entries()) {
|
|
4045
|
+
if (dir === "." || files.size < 2) continue;
|
|
4046
|
+
const fileArray = Array.from(files);
|
|
4047
|
+
let internalEdgeCount = 0;
|
|
4048
|
+
for (const file of fileArray) {
|
|
4049
|
+
const targets = fileEdges.get(file);
|
|
4050
|
+
if (targets) {
|
|
4051
|
+
for (const target of targets) {
|
|
4052
|
+
if (files.has(target)) {
|
|
4053
|
+
internalEdgeCount++;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
if (internalEdgeCount >= 2) {
|
|
4059
|
+
const clusterName = inferClusterName(fileArray);
|
|
4060
|
+
clusters.push({
|
|
4061
|
+
name: clusterName,
|
|
4062
|
+
files: fileArray
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
return clusters.sort((a, b) => b.files.length - a.files.length);
|
|
4067
|
+
}
|
|
4068
|
+
function inferClusterName(files) {
|
|
4069
|
+
const words = /* @__PURE__ */ new Map();
|
|
4070
|
+
for (const file of files) {
|
|
4071
|
+
const fileName = file.toLowerCase();
|
|
4072
|
+
const parts = fileName.split(/[\/\-\_\.]/).filter((p) => p.length > 3);
|
|
4073
|
+
for (const part of parts) {
|
|
4074
|
+
words.set(part, (words.get(part) || 0) + 1);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
const sortedWords = Array.from(words.entries()).sort((a, b) => b[1] - a[1]);
|
|
4078
|
+
if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
|
|
4079
|
+
return capitalizeFirst2(sortedWords[0][0]);
|
|
4080
|
+
}
|
|
4081
|
+
const commonDir = dirname7(files[0]);
|
|
4082
|
+
if (files.every((f) => dirname7(f) === commonDir)) {
|
|
4083
|
+
return capitalizeFirst2(commonDir.split("/").pop() || "Core");
|
|
4084
|
+
}
|
|
4085
|
+
return "Core";
|
|
4086
|
+
}
|
|
4087
|
+
function capitalizeFirst2(str) {
|
|
4088
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4089
|
+
}
|
|
4090
|
+
function generateHighImpactWarning(graph) {
|
|
4091
|
+
const highImpactFiles = [];
|
|
4092
|
+
const fileInDegree = /* @__PURE__ */ new Map();
|
|
4093
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4094
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
4095
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
4096
|
+
if (sourceFile !== targetFile) {
|
|
4097
|
+
fileInDegree.set(targetFile, (fileInDegree.get(targetFile) || 0) + 1);
|
|
4098
|
+
}
|
|
4099
|
+
});
|
|
4100
|
+
for (const [file, count] of fileInDegree.entries()) {
|
|
4101
|
+
if (count >= 5) {
|
|
4102
|
+
highImpactFiles.push({ file, dependents: count });
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
highImpactFiles.sort((a, b) => b.dependents - a.dependents);
|
|
4106
|
+
if (highImpactFiles.length === 0) {
|
|
4107
|
+
return "No high-impact files detected. Changes should be relatively isolated.\n\n";
|
|
4108
|
+
}
|
|
4109
|
+
let output = "\u26A0\uFE0F **Before modifying these files, check the blast radius:**\n\n";
|
|
4110
|
+
const topFiles = highImpactFiles.slice(0, 5);
|
|
4111
|
+
for (const { file, dependents } of topFiles) {
|
|
4112
|
+
output += `- ${code(file)} \u2014 ${dependents} dependent files (run \`depwire impact_analysis ${file}\`)
|
|
4113
|
+
`;
|
|
4114
|
+
}
|
|
4115
|
+
output += "\n";
|
|
4116
|
+
return output;
|
|
4117
|
+
}
|
|
4118
|
+
function generateDepwireUsage(projectRoot) {
|
|
4119
|
+
let output = "Use Depwire to explore this codebase:\n\n";
|
|
4120
|
+
output += "**Visualize the dependency graph:**\n\n";
|
|
4121
|
+
output += "```bash\n";
|
|
4122
|
+
output += "depwire viz .\n";
|
|
4123
|
+
output += "```\n\n";
|
|
4124
|
+
output += "**Connect to AI coding tools (MCP):**\n\n";
|
|
4125
|
+
output += "```bash\n";
|
|
4126
|
+
output += "depwire mcp .\n";
|
|
4127
|
+
output += "```\n\n";
|
|
4128
|
+
output += "**Analyze impact of changes:**\n\n";
|
|
4129
|
+
output += "```bash\n";
|
|
4130
|
+
output += "depwire query . <symbol-name>\n";
|
|
4131
|
+
output += "```\n\n";
|
|
4132
|
+
output += "**Update documentation:**\n\n";
|
|
4133
|
+
output += "```bash\n";
|
|
4134
|
+
output += "depwire docs . --update\n";
|
|
4135
|
+
output += "```\n\n";
|
|
4136
|
+
return output;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// src/docs/metadata.ts
|
|
4140
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
4141
|
+
import { join as join9 } from "path";
|
|
4142
|
+
function loadMetadata(outputDir) {
|
|
4143
|
+
const metadataPath = join9(outputDir, "metadata.json");
|
|
4144
|
+
if (!existsSync5(metadataPath)) {
|
|
4145
|
+
return null;
|
|
4146
|
+
}
|
|
4147
|
+
try {
|
|
4148
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
4149
|
+
return JSON.parse(content);
|
|
4150
|
+
} catch (err) {
|
|
4151
|
+
console.error("Failed to load metadata:", err);
|
|
4152
|
+
return null;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
function saveMetadata(outputDir, metadata) {
|
|
4156
|
+
const metadataPath = join9(outputDir, "metadata.json");
|
|
4157
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
4158
|
+
}
|
|
4159
|
+
function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
|
|
4160
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4161
|
+
const documents = {};
|
|
4162
|
+
for (const docType of docTypes) {
|
|
4163
|
+
const fileName = docType === "architecture" ? "ARCHITECTURE.md" : docType === "conventions" ? "CONVENTIONS.md" : docType === "dependencies" ? "DEPENDENCIES.md" : docType === "onboarding" ? "ONBOARDING.md" : `${docType.toUpperCase()}.md`;
|
|
4164
|
+
documents[docType] = {
|
|
4165
|
+
generated_at: now,
|
|
4166
|
+
file: fileName
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
return {
|
|
4170
|
+
version,
|
|
4171
|
+
generated_at: now,
|
|
4172
|
+
project_path: projectPath,
|
|
4173
|
+
file_count: fileCount,
|
|
4174
|
+
symbol_count: symbolCount,
|
|
4175
|
+
edge_count: edgeCount,
|
|
4176
|
+
documents
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
function updateMetadata(existing, docTypes, fileCount, symbolCount, edgeCount) {
|
|
4180
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4181
|
+
for (const docType of docTypes) {
|
|
4182
|
+
if (existing.documents[docType]) {
|
|
4183
|
+
existing.documents[docType].generated_at = now;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
existing.file_count = fileCount;
|
|
4187
|
+
existing.symbol_count = symbolCount;
|
|
4188
|
+
existing.edge_count = edgeCount;
|
|
4189
|
+
existing.generated_at = now;
|
|
4190
|
+
return existing;
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// src/docs/generator.ts
|
|
4194
|
+
async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
4195
|
+
const startTime = Date.now();
|
|
4196
|
+
const generated = [];
|
|
4197
|
+
const errors = [];
|
|
4198
|
+
try {
|
|
4199
|
+
if (!existsSync6(options.outputDir)) {
|
|
4200
|
+
mkdirSync(options.outputDir, { recursive: true });
|
|
4201
|
+
if (options.verbose) {
|
|
4202
|
+
console.log(`Created output directory: ${options.outputDir}`);
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
let docsToGenerate = options.include;
|
|
4206
|
+
if (options.update && options.only) {
|
|
4207
|
+
docsToGenerate = options.only;
|
|
4208
|
+
}
|
|
4209
|
+
if (docsToGenerate.includes("all")) {
|
|
4210
|
+
docsToGenerate = ["architecture", "conventions", "dependencies", "onboarding"];
|
|
4211
|
+
}
|
|
4212
|
+
let metadata = null;
|
|
4213
|
+
if (options.update) {
|
|
4214
|
+
metadata = loadMetadata(options.outputDir);
|
|
4215
|
+
}
|
|
4216
|
+
const fileCount = getFileCount5(graph);
|
|
4217
|
+
const symbolCount = graph.order;
|
|
4218
|
+
const edgeCount = graph.size;
|
|
4219
|
+
if (options.format === "markdown") {
|
|
4220
|
+
if (docsToGenerate.includes("architecture")) {
|
|
4221
|
+
try {
|
|
4222
|
+
if (options.verbose) console.log("Generating ARCHITECTURE.md...");
|
|
4223
|
+
const content = generateArchitecture(graph, projectRoot, version, parseTime);
|
|
4224
|
+
const filePath = join10(options.outputDir, "ARCHITECTURE.md");
|
|
4225
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4226
|
+
generated.push("ARCHITECTURE.md");
|
|
4227
|
+
} catch (err) {
|
|
4228
|
+
errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
if (docsToGenerate.includes("conventions")) {
|
|
4232
|
+
try {
|
|
4233
|
+
if (options.verbose) console.log("Generating CONVENTIONS.md...");
|
|
4234
|
+
const content = generateConventions(graph, projectRoot, version);
|
|
4235
|
+
const filePath = join10(options.outputDir, "CONVENTIONS.md");
|
|
4236
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4237
|
+
generated.push("CONVENTIONS.md");
|
|
4238
|
+
} catch (err) {
|
|
4239
|
+
errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
if (docsToGenerate.includes("dependencies")) {
|
|
4243
|
+
try {
|
|
4244
|
+
if (options.verbose) console.log("Generating DEPENDENCIES.md...");
|
|
4245
|
+
const content = generateDependencies(graph, projectRoot, version);
|
|
4246
|
+
const filePath = join10(options.outputDir, "DEPENDENCIES.md");
|
|
4247
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4248
|
+
generated.push("DEPENDENCIES.md");
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
if (docsToGenerate.includes("onboarding")) {
|
|
4254
|
+
try {
|
|
4255
|
+
if (options.verbose) console.log("Generating ONBOARDING.md...");
|
|
4256
|
+
const content = generateOnboarding(graph, projectRoot, version);
|
|
4257
|
+
const filePath = join10(options.outputDir, "ONBOARDING.md");
|
|
4258
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
4259
|
+
generated.push("ONBOARDING.md");
|
|
4260
|
+
} catch (err) {
|
|
4261
|
+
errors.push(`Failed to generate ONBOARDING.md: ${err}`);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
} else if (options.format === "json") {
|
|
4265
|
+
errors.push("JSON format not yet supported");
|
|
4266
|
+
}
|
|
4267
|
+
if (metadata && options.update) {
|
|
4268
|
+
metadata = updateMetadata(metadata, docsToGenerate, fileCount, symbolCount, edgeCount);
|
|
4269
|
+
} else {
|
|
4270
|
+
metadata = createMetadata(version, projectRoot, fileCount, symbolCount, edgeCount, docsToGenerate);
|
|
4271
|
+
}
|
|
4272
|
+
saveMetadata(options.outputDir, metadata);
|
|
4273
|
+
if (options.verbose) console.log("Saved metadata.json");
|
|
4274
|
+
const totalTime = Date.now() - startTime;
|
|
4275
|
+
return {
|
|
4276
|
+
success: errors.length === 0,
|
|
4277
|
+
generated,
|
|
4278
|
+
errors,
|
|
4279
|
+
stats: options.stats ? {
|
|
4280
|
+
totalTime,
|
|
4281
|
+
filesGenerated: generated.length
|
|
4282
|
+
} : void 0
|
|
4283
|
+
};
|
|
4284
|
+
} catch (err) {
|
|
4285
|
+
return {
|
|
4286
|
+
success: false,
|
|
4287
|
+
generated,
|
|
4288
|
+
errors: [`Fatal error: ${err}`]
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
function getFileCount5(graph) {
|
|
4293
|
+
const files = /* @__PURE__ */ new Set();
|
|
4294
|
+
graph.forEachNode((node, attrs) => {
|
|
4295
|
+
files.add(attrs.filePath);
|
|
4296
|
+
});
|
|
4297
|
+
return files.size;
|
|
4298
|
+
}
|
|
4299
|
+
|
|
2574
4300
|
// src/mcp/server.ts
|
|
2575
4301
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2576
4302
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2577
4303
|
|
|
2578
4304
|
// src/mcp/tools.ts
|
|
2579
|
-
import { dirname as
|
|
4305
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
4306
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
2580
4307
|
|
|
2581
4308
|
// src/mcp/connect.ts
|
|
2582
4309
|
import simpleGit from "simple-git";
|
|
2583
|
-
import { existsSync as
|
|
2584
|
-
import { join as
|
|
4310
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4311
|
+
import { join as join11, basename as basename3, resolve as resolve2 } from "path";
|
|
2585
4312
|
import { tmpdir, homedir } from "os";
|
|
2586
4313
|
function validateProjectPath(source) {
|
|
2587
4314
|
const resolved = resolve2(source);
|
|
@@ -2594,11 +4321,11 @@ function validateProjectPath(source) {
|
|
|
2594
4321
|
"/boot",
|
|
2595
4322
|
"/proc",
|
|
2596
4323
|
"/sys",
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
4324
|
+
join11(homedir(), ".ssh"),
|
|
4325
|
+
join11(homedir(), ".gnupg"),
|
|
4326
|
+
join11(homedir(), ".aws"),
|
|
4327
|
+
join11(homedir(), ".config"),
|
|
4328
|
+
join11(homedir(), ".env")
|
|
2602
4329
|
];
|
|
2603
4330
|
for (const blocked of blockedPaths) {
|
|
2604
4331
|
if (resolved.startsWith(blocked)) {
|
|
@@ -2621,11 +4348,11 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2621
4348
|
};
|
|
2622
4349
|
}
|
|
2623
4350
|
projectName = match[1];
|
|
2624
|
-
const reposDir =
|
|
2625
|
-
const cloneDir =
|
|
4351
|
+
const reposDir = join11(tmpdir(), "depwire-repos");
|
|
4352
|
+
const cloneDir = join11(reposDir, projectName);
|
|
2626
4353
|
console.error(`Connecting to GitHub repo: ${source}`);
|
|
2627
4354
|
const git = simpleGit();
|
|
2628
|
-
if (
|
|
4355
|
+
if (existsSync7(cloneDir)) {
|
|
2629
4356
|
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
2630
4357
|
try {
|
|
2631
4358
|
await git.cwd(cloneDir).pull();
|
|
@@ -2643,7 +4370,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2643
4370
|
};
|
|
2644
4371
|
}
|
|
2645
4372
|
}
|
|
2646
|
-
projectRoot = subdirectory ?
|
|
4373
|
+
projectRoot = subdirectory ? join11(cloneDir, subdirectory) : cloneDir;
|
|
2647
4374
|
} else {
|
|
2648
4375
|
const validation2 = validateProjectPath(source);
|
|
2649
4376
|
if (!validation2.valid) {
|
|
@@ -2652,14 +4379,14 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2652
4379
|
message: validation2.error
|
|
2653
4380
|
};
|
|
2654
4381
|
}
|
|
2655
|
-
if (!
|
|
4382
|
+
if (!existsSync7(source)) {
|
|
2656
4383
|
return {
|
|
2657
4384
|
error: "Directory not found",
|
|
2658
4385
|
message: `Directory does not exist: ${source}`
|
|
2659
4386
|
};
|
|
2660
4387
|
}
|
|
2661
|
-
projectRoot = subdirectory ?
|
|
2662
|
-
projectName =
|
|
4388
|
+
projectRoot = subdirectory ? join11(source, subdirectory) : source;
|
|
4389
|
+
projectName = basename3(projectRoot);
|
|
2663
4390
|
}
|
|
2664
4391
|
const validation = validateProjectPath(projectRoot);
|
|
2665
4392
|
if (!validation.valid) {
|
|
@@ -2668,7 +4395,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
2668
4395
|
message: validation.error
|
|
2669
4396
|
};
|
|
2670
4397
|
}
|
|
2671
|
-
if (!
|
|
4398
|
+
if (!existsSync7(projectRoot)) {
|
|
2672
4399
|
return {
|
|
2673
4400
|
error: "Project root not found",
|
|
2674
4401
|
message: `Directory does not exist: ${projectRoot}`
|
|
@@ -2916,6 +4643,32 @@ function getToolsList() {
|
|
|
2916
4643
|
}
|
|
2917
4644
|
}
|
|
2918
4645
|
}
|
|
4646
|
+
},
|
|
4647
|
+
{
|
|
4648
|
+
name: "get_project_docs",
|
|
4649
|
+
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.",
|
|
4650
|
+
inputSchema: {
|
|
4651
|
+
type: "object",
|
|
4652
|
+
properties: {
|
|
4653
|
+
doc_type: {
|
|
4654
|
+
type: "string",
|
|
4655
|
+
description: "Document type to retrieve: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
},
|
|
4660
|
+
{
|
|
4661
|
+
name: "update_project_docs",
|
|
4662
|
+
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.",
|
|
4663
|
+
inputSchema: {
|
|
4664
|
+
type: "object",
|
|
4665
|
+
properties: {
|
|
4666
|
+
doc_type: {
|
|
4667
|
+
type: "string",
|
|
4668
|
+
description: "Document type to update: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
2919
4672
|
}
|
|
2920
4673
|
];
|
|
2921
4674
|
}
|
|
@@ -2942,6 +4695,24 @@ async function handleToolCall(name, args, state) {
|
|
|
2942
4695
|
} else {
|
|
2943
4696
|
result = await handleVisualizeGraph(args.highlight, args.maxFiles, state);
|
|
2944
4697
|
}
|
|
4698
|
+
} else if (name === "get_project_docs") {
|
|
4699
|
+
if (!isProjectLoaded(state)) {
|
|
4700
|
+
result = {
|
|
4701
|
+
error: "No project loaded",
|
|
4702
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
4703
|
+
};
|
|
4704
|
+
} else {
|
|
4705
|
+
result = await handleGetProjectDocs(args.doc_type || "all", state);
|
|
4706
|
+
}
|
|
4707
|
+
} else if (name === "update_project_docs") {
|
|
4708
|
+
if (!isProjectLoaded(state)) {
|
|
4709
|
+
result = {
|
|
4710
|
+
error: "No project loaded",
|
|
4711
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
4712
|
+
};
|
|
4713
|
+
} else {
|
|
4714
|
+
result = await handleUpdateProjectDocs(args.doc_type || "all", state);
|
|
4715
|
+
}
|
|
2945
4716
|
} else {
|
|
2946
4717
|
if (!isProjectLoaded(state)) {
|
|
2947
4718
|
result = {
|
|
@@ -3238,7 +5009,7 @@ function handleGetArchitectureSummary(graph) {
|
|
|
3238
5009
|
const dirMap = /* @__PURE__ */ new Map();
|
|
3239
5010
|
const languageBreakdown = {};
|
|
3240
5011
|
fileSummary.forEach((f) => {
|
|
3241
|
-
const dir = f.filePath.includes("/") ?
|
|
5012
|
+
const dir = f.filePath.includes("/") ? dirname8(f.filePath) : ".";
|
|
3242
5013
|
if (!dirMap.has(dir)) {
|
|
3243
5014
|
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
3244
5015
|
}
|
|
@@ -3324,6 +5095,112 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
|
3324
5095
|
content: [{ type: "text", text: message }]
|
|
3325
5096
|
};
|
|
3326
5097
|
}
|
|
5098
|
+
async function handleGetProjectDocs(docType, state) {
|
|
5099
|
+
const docsDir = join12(state.projectRoot, ".depwire");
|
|
5100
|
+
if (!existsSync8(docsDir)) {
|
|
5101
|
+
const errorMessage = `Project documentation has not been generated yet.
|
|
5102
|
+
|
|
5103
|
+
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
5104
|
+
|
|
5105
|
+
Once generated, this tool will return the requested documentation.
|
|
5106
|
+
|
|
5107
|
+
Available document types:
|
|
5108
|
+
- architecture: High-level structural overview
|
|
5109
|
+
- conventions: Auto-detected coding patterns
|
|
5110
|
+
- dependencies: Complete dependency mapping
|
|
5111
|
+
- onboarding: Guide for new developers`;
|
|
5112
|
+
return {
|
|
5113
|
+
content: [{ type: "text", text: errorMessage }]
|
|
5114
|
+
};
|
|
5115
|
+
}
|
|
5116
|
+
const metadata = loadMetadata(docsDir);
|
|
5117
|
+
if (!metadata) {
|
|
5118
|
+
return {
|
|
5119
|
+
content: [{ type: "text", text: "Documentation directory exists but metadata is missing. Please regenerate with `depwire docs`." }]
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
const docsToReturn = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
5123
|
+
let output = "";
|
|
5124
|
+
const missing = [];
|
|
5125
|
+
for (const doc of docsToReturn) {
|
|
5126
|
+
if (!metadata.documents[doc]) {
|
|
5127
|
+
missing.push(doc);
|
|
5128
|
+
continue;
|
|
5129
|
+
}
|
|
5130
|
+
const filePath = join12(docsDir, metadata.documents[doc].file);
|
|
5131
|
+
if (!existsSync8(filePath)) {
|
|
5132
|
+
missing.push(doc);
|
|
5133
|
+
continue;
|
|
5134
|
+
}
|
|
5135
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
5136
|
+
if (docsToReturn.length > 1) {
|
|
5137
|
+
output += `
|
|
5138
|
+
|
|
5139
|
+
---
|
|
5140
|
+
|
|
5141
|
+
# ${doc.toUpperCase()}
|
|
5142
|
+
|
|
5143
|
+
`;
|
|
5144
|
+
}
|
|
5145
|
+
output += content;
|
|
5146
|
+
}
|
|
5147
|
+
if (missing.length > 0) {
|
|
5148
|
+
output += `
|
|
5149
|
+
|
|
5150
|
+
---
|
|
5151
|
+
|
|
5152
|
+
**Note:** The following documents are missing: ${missing.join(", ")}. Run \`depwire docs ${state.projectRoot} --update\` to generate them.`;
|
|
5153
|
+
}
|
|
5154
|
+
return {
|
|
5155
|
+
content: [{ type: "text", text: output }]
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
async function handleUpdateProjectDocs(docType, state) {
|
|
5159
|
+
const startTime = Date.now();
|
|
5160
|
+
const docsDir = join12(state.projectRoot, ".depwire");
|
|
5161
|
+
console.error("Regenerating project documentation...");
|
|
5162
|
+
const parsedFiles = parseProject(state.projectRoot);
|
|
5163
|
+
const graph = buildGraph(parsedFiles);
|
|
5164
|
+
const parseTime = (Date.now() - startTime) / 1e3;
|
|
5165
|
+
state.graph = graph;
|
|
5166
|
+
const packageJsonPath = join12(__dirname, "../../package.json");
|
|
5167
|
+
const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
5168
|
+
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
5169
|
+
const docsExist = existsSync8(docsDir);
|
|
5170
|
+
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
5171
|
+
outputDir: docsDir,
|
|
5172
|
+
format: "markdown",
|
|
5173
|
+
include: docsToGenerate,
|
|
5174
|
+
update: docsExist,
|
|
5175
|
+
only: docsExist ? docsToGenerate : void 0,
|
|
5176
|
+
verbose: false,
|
|
5177
|
+
stats: false
|
|
5178
|
+
});
|
|
5179
|
+
const elapsed = (Date.now() - startTime) / 1e3;
|
|
5180
|
+
if (result.success) {
|
|
5181
|
+
const fileCount = /* @__PURE__ */ new Set();
|
|
5182
|
+
graph.forEachNode((node, attrs) => {
|
|
5183
|
+
fileCount.add(attrs.filePath);
|
|
5184
|
+
});
|
|
5185
|
+
return {
|
|
5186
|
+
status: "success",
|
|
5187
|
+
message: `Updated ${result.generated.join(", ")} (${fileCount.size} files, ${graph.order} symbols, ${elapsed.toFixed(1)}s)`,
|
|
5188
|
+
generated: result.generated,
|
|
5189
|
+
stats: {
|
|
5190
|
+
files: fileCount.size,
|
|
5191
|
+
symbols: graph.order,
|
|
5192
|
+
edges: graph.size,
|
|
5193
|
+
time: elapsed
|
|
5194
|
+
}
|
|
5195
|
+
};
|
|
5196
|
+
} else {
|
|
5197
|
+
return {
|
|
5198
|
+
status: "error",
|
|
5199
|
+
message: `Failed to update documentation: ${result.errors.join(", ")}`,
|
|
5200
|
+
errors: result.errors
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
3327
5204
|
|
|
3328
5205
|
// src/mcp/server.ts
|
|
3329
5206
|
async function startMcpServer(state) {
|
|
@@ -3369,5 +5246,6 @@ export {
|
|
|
3369
5246
|
startVizServer,
|
|
3370
5247
|
createEmptyState,
|
|
3371
5248
|
updateFileInGraph,
|
|
5249
|
+
generateDocs,
|
|
3372
5250
|
startMcpServer
|
|
3373
5251
|
};
|