depwire-cli 0.4.0 → 0.6.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.
@@ -2638,12 +2638,500 @@ async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
2638
2638
  }
2639
2639
  }
2640
2640
 
2641
+ // src/health/metrics.ts
2642
+ import { dirname as dirname6 } from "path";
2643
+ function scoreToGrade(score) {
2644
+ if (score >= 90) return "A";
2645
+ if (score >= 80) return "B";
2646
+ if (score >= 70) return "C";
2647
+ if (score >= 60) return "D";
2648
+ return "F";
2649
+ }
2650
+ function calculateCouplingScore(graph) {
2651
+ const files = /* @__PURE__ */ new Set();
2652
+ graph.forEachNode((node, attrs) => {
2653
+ files.add(attrs.filePath);
2654
+ });
2655
+ if (files.size === 0) {
2656
+ return {
2657
+ name: "Coupling",
2658
+ score: 100,
2659
+ weight: 0.25,
2660
+ grade: "A",
2661
+ details: "No files to analyze",
2662
+ metrics: { avgConnections: 0, maxConnections: 0, crossDirCoupling: 0 }
2663
+ };
2664
+ }
2665
+ const fileConnections = /* @__PURE__ */ new Map();
2666
+ let crossDirEdges = 0;
2667
+ let totalEdges = 0;
2668
+ graph.forEachEdge((edge, attrs, source, target) => {
2669
+ const sourceAttrs = graph.getNodeAttributes(source);
2670
+ const targetAttrs = graph.getNodeAttributes(target);
2671
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
2672
+ totalEdges++;
2673
+ fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
2674
+ fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
2675
+ const sourceDir = dirname6(sourceAttrs.filePath).split("/")[0];
2676
+ const targetDir = dirname6(targetAttrs.filePath).split("/")[0];
2677
+ if (sourceDir !== targetDir) {
2678
+ crossDirEdges++;
2679
+ }
2680
+ }
2681
+ });
2682
+ const avgConnections = totalEdges / files.size;
2683
+ const maxConnections = Math.max(...Array.from(fileConnections.values()), 0);
2684
+ const crossDirCoupling = totalEdges > 0 ? crossDirEdges / totalEdges : 0;
2685
+ let score = 100;
2686
+ if (avgConnections <= 3) {
2687
+ score = 100;
2688
+ } else if (avgConnections <= 6) {
2689
+ score = 80;
2690
+ } else if (avgConnections <= 10) {
2691
+ score = 60;
2692
+ } else if (avgConnections <= 15) {
2693
+ score = 40;
2694
+ } else {
2695
+ score = 20;
2696
+ }
2697
+ if (maxConnections > avgConnections * 3) {
2698
+ score -= 10;
2699
+ }
2700
+ if (crossDirCoupling > 0.7) {
2701
+ score -= 10;
2702
+ }
2703
+ score = Math.max(0, Math.min(100, score));
2704
+ return {
2705
+ name: "Coupling",
2706
+ score,
2707
+ weight: 0.25,
2708
+ grade: scoreToGrade(score),
2709
+ details: `Average ${avgConnections.toFixed(1)} connections per file, max ${maxConnections}, ${(crossDirCoupling * 100).toFixed(0)}% cross-directory`,
2710
+ metrics: {
2711
+ avgConnections: parseFloat(avgConnections.toFixed(2)),
2712
+ maxConnections,
2713
+ crossDirCoupling: parseFloat((crossDirCoupling * 100).toFixed(1))
2714
+ }
2715
+ };
2716
+ }
2717
+ function calculateCohesionScore(graph) {
2718
+ const dirEdges = /* @__PURE__ */ new Map();
2719
+ graph.forEachEdge((edge, attrs, source, target) => {
2720
+ const sourceAttrs = graph.getNodeAttributes(source);
2721
+ const targetAttrs = graph.getNodeAttributes(target);
2722
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
2723
+ const sourceDir = dirname6(sourceAttrs.filePath);
2724
+ const targetDir = dirname6(targetAttrs.filePath);
2725
+ if (!dirEdges.has(sourceDir)) {
2726
+ dirEdges.set(sourceDir, { internal: 0, total: 0 });
2727
+ }
2728
+ const stats = dirEdges.get(sourceDir);
2729
+ stats.total++;
2730
+ if (sourceDir === targetDir) {
2731
+ stats.internal++;
2732
+ }
2733
+ }
2734
+ });
2735
+ if (dirEdges.size === 0) {
2736
+ return {
2737
+ name: "Cohesion",
2738
+ score: 100,
2739
+ weight: 0.2,
2740
+ grade: "A",
2741
+ details: "No inter-file dependencies",
2742
+ metrics: { avgInternalRatio: 1, directories: 0 }
2743
+ };
2744
+ }
2745
+ let totalRatio = 0;
2746
+ for (const stats of dirEdges.values()) {
2747
+ if (stats.total > 0) {
2748
+ totalRatio += stats.internal / stats.total;
2749
+ }
2750
+ }
2751
+ const avgInternalRatio = totalRatio / dirEdges.size;
2752
+ let score = 100;
2753
+ if (avgInternalRatio >= 0.7) {
2754
+ score = 100;
2755
+ } else if (avgInternalRatio >= 0.5) {
2756
+ score = 80;
2757
+ } else if (avgInternalRatio >= 0.3) {
2758
+ score = 60;
2759
+ } else if (avgInternalRatio >= 0.1) {
2760
+ score = 40;
2761
+ } else {
2762
+ score = 20;
2763
+ }
2764
+ return {
2765
+ name: "Cohesion",
2766
+ score,
2767
+ weight: 0.2,
2768
+ grade: scoreToGrade(score),
2769
+ details: `Average ${(avgInternalRatio * 100).toFixed(0)}% internal dependencies per directory`,
2770
+ metrics: {
2771
+ avgInternalRatio: parseFloat((avgInternalRatio * 100).toFixed(1)),
2772
+ directories: dirEdges.size
2773
+ }
2774
+ };
2775
+ }
2776
+ function calculateCircularDepsScore(graph) {
2777
+ const fileGraph = /* @__PURE__ */ new Map();
2778
+ graph.forEachEdge((edge, attrs, source, target) => {
2779
+ const sourceFile = graph.getNodeAttributes(source).filePath;
2780
+ const targetFile = graph.getNodeAttributes(target).filePath;
2781
+ if (sourceFile !== targetFile) {
2782
+ if (!fileGraph.has(sourceFile)) {
2783
+ fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
2784
+ }
2785
+ fileGraph.get(sourceFile).add(targetFile);
2786
+ }
2787
+ });
2788
+ const visited = /* @__PURE__ */ new Set();
2789
+ const recStack = /* @__PURE__ */ new Set();
2790
+ const cycles = [];
2791
+ function dfs(node, path2) {
2792
+ if (recStack.has(node)) {
2793
+ const cycleStart = path2.indexOf(node);
2794
+ if (cycleStart >= 0) {
2795
+ cycles.push(path2.slice(cycleStart));
2796
+ }
2797
+ return;
2798
+ }
2799
+ if (visited.has(node)) {
2800
+ return;
2801
+ }
2802
+ visited.add(node);
2803
+ recStack.add(node);
2804
+ path2.push(node);
2805
+ const neighbors = fileGraph.get(node);
2806
+ if (neighbors) {
2807
+ for (const neighbor of neighbors) {
2808
+ dfs(neighbor, [...path2]);
2809
+ }
2810
+ }
2811
+ recStack.delete(node);
2812
+ }
2813
+ for (const node of fileGraph.keys()) {
2814
+ if (!visited.has(node)) {
2815
+ dfs(node, []);
2816
+ }
2817
+ }
2818
+ const uniqueCycles = /* @__PURE__ */ new Set();
2819
+ for (const cycle of cycles) {
2820
+ const sorted = [...cycle].sort().join(",");
2821
+ uniqueCycles.add(sorted);
2822
+ }
2823
+ const cycleCount = uniqueCycles.size;
2824
+ let score = 100;
2825
+ if (cycleCount === 0) {
2826
+ score = 100;
2827
+ } else if (cycleCount <= 2) {
2828
+ score = 80;
2829
+ } else if (cycleCount <= 5) {
2830
+ score = 60;
2831
+ } else if (cycleCount <= 10) {
2832
+ score = 40;
2833
+ } else {
2834
+ score = 20;
2835
+ }
2836
+ return {
2837
+ name: "Circular Dependencies",
2838
+ score,
2839
+ weight: 0.2,
2840
+ grade: scoreToGrade(score),
2841
+ details: cycleCount === 0 ? "No circular dependencies detected" : `${cycleCount} circular dependency cycle${cycleCount === 1 ? "" : "s"} detected`,
2842
+ metrics: { cycles: cycleCount }
2843
+ };
2844
+ }
2845
+ function calculateGodFilesScore(graph) {
2846
+ const files = /* @__PURE__ */ new Set();
2847
+ const fileConnections = /* @__PURE__ */ new Map();
2848
+ graph.forEachNode((node, attrs) => {
2849
+ files.add(attrs.filePath);
2850
+ });
2851
+ if (files.size === 0) {
2852
+ return {
2853
+ name: "God Files",
2854
+ score: 100,
2855
+ weight: 0.15,
2856
+ grade: "A",
2857
+ details: "No files to analyze",
2858
+ metrics: { godFiles: 0, threshold: 0 }
2859
+ };
2860
+ }
2861
+ graph.forEachEdge((edge, attrs, source, target) => {
2862
+ const sourceFile = graph.getNodeAttributes(source).filePath;
2863
+ const targetFile = graph.getNodeAttributes(target).filePath;
2864
+ if (sourceFile !== targetFile) {
2865
+ fileConnections.set(sourceFile, (fileConnections.get(sourceFile) || 0) + 1);
2866
+ fileConnections.set(targetFile, (fileConnections.get(targetFile) || 0) + 1);
2867
+ }
2868
+ });
2869
+ const connections = Array.from(fileConnections.values());
2870
+ const avgConnections = connections.length > 0 ? connections.reduce((a, b) => a + b, 0) / connections.length : 0;
2871
+ const godThreshold = avgConnections * 3;
2872
+ const godFiles = connections.filter((c) => c > godThreshold).length;
2873
+ let score = 100;
2874
+ if (godFiles === 0) {
2875
+ score = 100;
2876
+ } else if (godFiles === 1) {
2877
+ score = 80;
2878
+ } else if (godFiles <= 3) {
2879
+ score = 60;
2880
+ } else if (godFiles <= 5) {
2881
+ score = 40;
2882
+ } else {
2883
+ score = 20;
2884
+ }
2885
+ return {
2886
+ name: "God Files",
2887
+ score,
2888
+ weight: 0.15,
2889
+ grade: scoreToGrade(score),
2890
+ details: godFiles === 0 ? "No god files detected" : `${godFiles} god file${godFiles === 1 ? "" : "s"} (>${godThreshold.toFixed(0)} connections)`,
2891
+ metrics: { godFiles, threshold: parseFloat(godThreshold.toFixed(1)) }
2892
+ };
2893
+ }
2894
+ function calculateOrphansScore(graph) {
2895
+ const files = /* @__PURE__ */ new Set();
2896
+ const connectedFiles = /* @__PURE__ */ new Set();
2897
+ graph.forEachNode((node, attrs) => {
2898
+ files.add(attrs.filePath);
2899
+ });
2900
+ graph.forEachEdge((edge, attrs, source, target) => {
2901
+ const sourceFile = graph.getNodeAttributes(source).filePath;
2902
+ const targetFile = graph.getNodeAttributes(target).filePath;
2903
+ if (sourceFile !== targetFile) {
2904
+ connectedFiles.add(sourceFile);
2905
+ connectedFiles.add(targetFile);
2906
+ }
2907
+ });
2908
+ const orphanCount = files.size - connectedFiles.size;
2909
+ const orphanPercent = files.size > 0 ? orphanCount / files.size * 100 : 0;
2910
+ let score = 100;
2911
+ if (orphanPercent === 0) {
2912
+ score = 100;
2913
+ } else if (orphanPercent <= 5) {
2914
+ score = 80;
2915
+ } else if (orphanPercent <= 10) {
2916
+ score = 60;
2917
+ } else if (orphanPercent <= 20) {
2918
+ score = 40;
2919
+ } else {
2920
+ score = 20;
2921
+ }
2922
+ return {
2923
+ name: "Orphan Files",
2924
+ score,
2925
+ weight: 0.1,
2926
+ grade: scoreToGrade(score),
2927
+ details: orphanCount === 0 ? "No orphan files" : `${orphanCount} orphan file${orphanCount === 1 ? "" : "s"} (${orphanPercent.toFixed(0)}%)`,
2928
+ metrics: { orphans: orphanCount, percentage: parseFloat(orphanPercent.toFixed(1)) }
2929
+ };
2930
+ }
2931
+ function calculateDepthScore(graph) {
2932
+ const fileGraph = /* @__PURE__ */ new Map();
2933
+ graph.forEachEdge((edge, attrs, source, target) => {
2934
+ const sourceFile = graph.getNodeAttributes(source).filePath;
2935
+ const targetFile = graph.getNodeAttributes(target).filePath;
2936
+ if (sourceFile !== targetFile) {
2937
+ if (!fileGraph.has(sourceFile)) {
2938
+ fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
2939
+ }
2940
+ fileGraph.get(sourceFile).add(targetFile);
2941
+ }
2942
+ });
2943
+ function findLongestPath(start) {
2944
+ const visited = /* @__PURE__ */ new Set();
2945
+ let maxDepth2 = 0;
2946
+ function dfs(node, depth) {
2947
+ if (visited.has(node)) {
2948
+ return;
2949
+ }
2950
+ visited.add(node);
2951
+ maxDepth2 = Math.max(maxDepth2, depth);
2952
+ const neighbors = fileGraph.get(node);
2953
+ if (neighbors) {
2954
+ for (const neighbor of neighbors) {
2955
+ dfs(neighbor, depth + 1);
2956
+ }
2957
+ }
2958
+ visited.delete(node);
2959
+ }
2960
+ dfs(start, 0);
2961
+ return maxDepth2;
2962
+ }
2963
+ let maxDepth = 0;
2964
+ for (const node of fileGraph.keys()) {
2965
+ const depth = findLongestPath(node);
2966
+ maxDepth = Math.max(maxDepth, depth);
2967
+ }
2968
+ let score = 100;
2969
+ if (maxDepth <= 4) {
2970
+ score = 100;
2971
+ } else if (maxDepth <= 6) {
2972
+ score = 80;
2973
+ } else if (maxDepth <= 8) {
2974
+ score = 60;
2975
+ } else if (maxDepth <= 12) {
2976
+ score = 40;
2977
+ } else {
2978
+ score = 20;
2979
+ }
2980
+ return {
2981
+ name: "Dependency Depth",
2982
+ score,
2983
+ weight: 0.1,
2984
+ grade: scoreToGrade(score),
2985
+ details: `Maximum dependency chain: ${maxDepth} level${maxDepth === 1 ? "" : "s"}`,
2986
+ metrics: { maxDepth }
2987
+ };
2988
+ }
2989
+
2990
+ // src/health/index.ts
2991
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync6 } from "fs";
2992
+ import { join as join9 } from "path";
2993
+ function calculateHealthScore(graph, projectRoot) {
2994
+ const coupling = calculateCouplingScore(graph);
2995
+ const cohesion = calculateCohesionScore(graph);
2996
+ const circular = calculateCircularDepsScore(graph);
2997
+ const godFiles = calculateGodFilesScore(graph);
2998
+ const orphans = calculateOrphansScore(graph);
2999
+ const depth = calculateDepthScore(graph);
3000
+ const dimensions = [coupling, cohesion, circular, godFiles, orphans, depth];
3001
+ const overall = Math.round(
3002
+ dimensions.reduce((sum, dim) => sum + dim.score * dim.weight, 0)
3003
+ );
3004
+ const files = /* @__PURE__ */ new Set();
3005
+ const languages2 = {};
3006
+ graph.forEachNode((node, attrs) => {
3007
+ files.add(attrs.filePath);
3008
+ const ext = attrs.filePath.toLowerCase();
3009
+ let lang;
3010
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
3011
+ lang = "TypeScript";
3012
+ } else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
3013
+ lang = "JavaScript";
3014
+ } else if (ext.endsWith(".py")) {
3015
+ lang = "Python";
3016
+ } else if (ext.endsWith(".go")) {
3017
+ lang = "Go";
3018
+ } else {
3019
+ lang = "Other";
3020
+ }
3021
+ languages2[lang] = (languages2[lang] || 0) + 1;
3022
+ });
3023
+ const grade = scoreToGrade(overall);
3024
+ let summary = `Project health score is ${overall}/100 (Grade: ${grade}). `;
3025
+ if (overall >= 90) {
3026
+ summary += "Excellent architecture with minimal issues.";
3027
+ } else if (overall >= 80) {
3028
+ summary += "Good architecture with some areas for improvement.";
3029
+ } else if (overall >= 70) {
3030
+ summary += "Moderate architecture quality. Consider refactoring high-risk areas.";
3031
+ } else if (overall >= 60) {
3032
+ summary += "Architecture needs improvement. Multiple issues detected.";
3033
+ } else {
3034
+ summary += "Poor architecture quality. Significant refactoring recommended.";
3035
+ }
3036
+ const recommendations = [];
3037
+ if (coupling.score < 70) {
3038
+ recommendations.push(`High coupling detected: Average ${coupling.metrics.avgConnections} connections per file. Consider breaking down large modules.`);
3039
+ }
3040
+ if (cohesion.score < 70) {
3041
+ recommendations.push(`Low cohesion: Only ${cohesion.metrics.avgInternalRatio}% internal dependencies. Reorganize files by feature or domain.`);
3042
+ }
3043
+ if (circular.score < 80 && typeof circular.metrics.cycles === "number" && circular.metrics.cycles > 0) {
3044
+ recommendations.push(`${circular.metrics.cycles} circular dependency cycle${circular.metrics.cycles === 1 ? "" : "s"} detected. Break cycles by introducing interfaces or extracting shared code.`);
3045
+ }
3046
+ if (godFiles.score < 80 && typeof godFiles.metrics.godFiles === "number" && godFiles.metrics.godFiles > 0) {
3047
+ recommendations.push(`${godFiles.metrics.godFiles} god file${godFiles.metrics.godFiles === 1 ? "" : "s"} detected with >${godFiles.metrics.threshold} connections. Split into smaller, focused modules.`);
3048
+ }
3049
+ if (orphans.score < 80 && typeof orphans.metrics.orphans === "number" && orphans.metrics.orphans > 0) {
3050
+ recommendations.push(`${orphans.metrics.orphans} orphan file${orphans.metrics.orphans === 1 ? "" : "s"} detected. Verify they're needed or remove dead code.`);
3051
+ }
3052
+ if (depth.score < 80 && typeof depth.metrics.maxDepth === "number") {
3053
+ recommendations.push(`Maximum dependency depth is ${depth.metrics.maxDepth} levels. Consider flattening the deepest chains.`);
3054
+ }
3055
+ if (recommendations.length === 0) {
3056
+ recommendations.push("No critical issues detected. Maintain current architecture quality.");
3057
+ }
3058
+ const report = {
3059
+ overall,
3060
+ grade,
3061
+ dimensions,
3062
+ summary,
3063
+ recommendations,
3064
+ projectStats: {
3065
+ files: files.size,
3066
+ symbols: graph.order,
3067
+ edges: graph.size,
3068
+ languages: languages2
3069
+ },
3070
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3071
+ };
3072
+ saveHealthHistory(projectRoot, report);
3073
+ return report;
3074
+ }
3075
+ function getHealthTrend(projectRoot, currentScore) {
3076
+ const history = loadHealthHistory(projectRoot);
3077
+ if (history.length < 2) {
3078
+ return null;
3079
+ }
3080
+ const previous = history[history.length - 2];
3081
+ const delta = currentScore - previous.score;
3082
+ if (delta > 0) {
3083
+ return `\u2191 +${delta}`;
3084
+ } else if (delta < 0) {
3085
+ return `\u2193 ${delta}`;
3086
+ } else {
3087
+ return "\u2192 0";
3088
+ }
3089
+ }
3090
+ function saveHealthHistory(projectRoot, report) {
3091
+ const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3092
+ const entry = {
3093
+ timestamp: report.timestamp,
3094
+ score: report.overall,
3095
+ grade: report.grade,
3096
+ dimensions: report.dimensions.map((d) => ({
3097
+ name: d.name,
3098
+ score: d.score,
3099
+ grade: d.grade
3100
+ }))
3101
+ };
3102
+ let history = [];
3103
+ if (existsSync6(historyFile)) {
3104
+ try {
3105
+ const content = readFileSync4(historyFile, "utf-8");
3106
+ history = JSON.parse(content);
3107
+ } catch {
3108
+ }
3109
+ }
3110
+ history.push(entry);
3111
+ if (history.length > 50) {
3112
+ history = history.slice(-50);
3113
+ }
3114
+ writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
3115
+ }
3116
+ function loadHealthHistory(projectRoot) {
3117
+ const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3118
+ if (!existsSync6(historyFile)) {
3119
+ return [];
3120
+ }
3121
+ try {
3122
+ const content = readFileSync4(historyFile, "utf-8");
3123
+ return JSON.parse(content);
3124
+ } catch {
3125
+ return [];
3126
+ }
3127
+ }
3128
+
2641
3129
  // src/docs/generator.ts
2642
- import { writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync7 } from "fs";
2643
- import { join as join10 } from "path";
3130
+ import { writeFileSync as writeFileSync3, mkdirSync, existsSync as existsSync9 } from "fs";
3131
+ import { join as join12 } from "path";
2644
3132
 
2645
3133
  // src/docs/architecture.ts
2646
- import { dirname as dirname6 } from "path";
3134
+ import { dirname as dirname7 } from "path";
2647
3135
 
2648
3136
  // src/docs/templates.ts
2649
3137
  function header(text, level = 1) {
@@ -2654,9 +3142,9 @@ function header(text, level = 1) {
2654
3142
  function code(text) {
2655
3143
  return `\`${text}\``;
2656
3144
  }
2657
- function codeBlock(code2, lang = "") {
3145
+ function codeBlock(code3, lang = "") {
2658
3146
  return `\`\`\`${lang}
2659
- ${code2}
3147
+ ${code3}
2660
3148
  \`\`\`
2661
3149
 
2662
3150
  `;
@@ -2794,7 +3282,7 @@ function generateModuleStructure(graph) {
2794
3282
  function getDirectoryStats(graph) {
2795
3283
  const dirMap = /* @__PURE__ */ new Map();
2796
3284
  graph.forEachNode((node, attrs) => {
2797
- const dir = dirname6(attrs.filePath);
3285
+ const dir = dirname7(attrs.filePath);
2798
3286
  if (dir === ".") return;
2799
3287
  if (!dirMap.has(dir)) {
2800
3288
  dirMap.set(dir, {
@@ -2819,7 +3307,7 @@ function getDirectoryStats(graph) {
2819
3307
  });
2820
3308
  const filesPerDir = /* @__PURE__ */ new Map();
2821
3309
  graph.forEachNode((node, attrs) => {
2822
- const dir = dirname6(attrs.filePath);
3310
+ const dir = dirname7(attrs.filePath);
2823
3311
  if (!filesPerDir.has(dir)) {
2824
3312
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
2825
3313
  }
@@ -2834,8 +3322,8 @@ function getDirectoryStats(graph) {
2834
3322
  graph.forEachEdge((edge, attrs, source, target) => {
2835
3323
  const sourceAttrs = graph.getNodeAttributes(source);
2836
3324
  const targetAttrs = graph.getNodeAttributes(target);
2837
- const sourceDir = dirname6(sourceAttrs.filePath);
2838
- const targetDir = dirname6(targetAttrs.filePath);
3325
+ const sourceDir = dirname7(sourceAttrs.filePath);
3326
+ const targetDir = dirname7(targetAttrs.filePath);
2839
3327
  if (sourceDir !== targetDir) {
2840
3328
  if (!dirEdges.has(sourceDir)) {
2841
3329
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -3816,7 +4304,7 @@ function detectCyclesDetailed(graph) {
3816
4304
  }
3817
4305
 
3818
4306
  // src/docs/onboarding.ts
3819
- import { dirname as dirname7 } from "path";
4307
+ import { dirname as dirname8 } from "path";
3820
4308
  function generateOnboarding(graph, projectRoot, version) {
3821
4309
  let output = "";
3822
4310
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -3875,7 +4363,7 @@ function generateQuickOrientation(graph) {
3875
4363
  const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
3876
4364
  const dirs = /* @__PURE__ */ new Set();
3877
4365
  graph.forEachNode((node, attrs) => {
3878
- const dir = dirname7(attrs.filePath);
4366
+ const dir = dirname8(attrs.filePath);
3879
4367
  if (dir !== ".") {
3880
4368
  const topLevel = dir.split("/")[0];
3881
4369
  dirs.add(topLevel);
@@ -3979,7 +4467,7 @@ function generateModuleMap(graph) {
3979
4467
  function getDirectoryStats2(graph) {
3980
4468
  const dirMap = /* @__PURE__ */ new Map();
3981
4469
  graph.forEachNode((node, attrs) => {
3982
- const dir = dirname7(attrs.filePath);
4470
+ const dir = dirname8(attrs.filePath);
3983
4471
  if (dir === ".") return;
3984
4472
  if (!dirMap.has(dir)) {
3985
4473
  dirMap.set(dir, {
@@ -3994,7 +4482,7 @@ function getDirectoryStats2(graph) {
3994
4482
  });
3995
4483
  const filesPerDir = /* @__PURE__ */ new Map();
3996
4484
  graph.forEachNode((node, attrs) => {
3997
- const dir = dirname7(attrs.filePath);
4485
+ const dir = dirname8(attrs.filePath);
3998
4486
  if (!filesPerDir.has(dir)) {
3999
4487
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
4000
4488
  }
@@ -4009,8 +4497,8 @@ function getDirectoryStats2(graph) {
4009
4497
  graph.forEachEdge((edge, attrs, source, target) => {
4010
4498
  const sourceAttrs = graph.getNodeAttributes(source);
4011
4499
  const targetAttrs = graph.getNodeAttributes(target);
4012
- const sourceDir = dirname7(sourceAttrs.filePath);
4013
- const targetDir = dirname7(targetAttrs.filePath);
4500
+ const sourceDir = dirname8(sourceAttrs.filePath);
4501
+ const targetDir = dirname8(targetAttrs.filePath);
4014
4502
  if (sourceDir !== targetDir) {
4015
4503
  if (!dirEdges.has(sourceDir)) {
4016
4504
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -4091,7 +4579,7 @@ function detectClusters(graph) {
4091
4579
  const dirFiles = /* @__PURE__ */ new Map();
4092
4580
  const fileEdges = /* @__PURE__ */ new Map();
4093
4581
  graph.forEachNode((node, attrs) => {
4094
- const dir = dirname7(attrs.filePath);
4582
+ const dir = dirname8(attrs.filePath);
4095
4583
  if (!dirFiles.has(dir)) {
4096
4584
  dirFiles.set(dir, /* @__PURE__ */ new Set());
4097
4585
  }
@@ -4145,8 +4633,8 @@ function inferClusterName(files) {
4145
4633
  if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
4146
4634
  return capitalizeFirst2(sortedWords[0][0]);
4147
4635
  }
4148
- const commonDir = dirname7(files[0]);
4149
- if (files.every((f) => dirname7(f) === commonDir)) {
4636
+ const commonDir = dirname8(files[0]);
4637
+ if (files.every((f) => dirname8(f) === commonDir)) {
4150
4638
  return capitalizeFirst2(commonDir.split("/").pop() || "Core");
4151
4639
  }
4152
4640
  return "Core";
@@ -4203,84 +4691,2378 @@ function generateDepwireUsage(projectRoot) {
4203
4691
  return output;
4204
4692
  }
4205
4693
 
4206
- // src/docs/metadata.ts
4207
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync } from "fs";
4208
- import { join as join9 } from "path";
4209
- function loadMetadata(outputDir) {
4210
- const metadataPath = join9(outputDir, "metadata.json");
4211
- if (!existsSync6(metadataPath)) {
4212
- return null;
4213
- }
4214
- try {
4215
- const content = readFileSync4(metadataPath, "utf-8");
4216
- return JSON.parse(content);
4217
- } catch (err) {
4218
- console.error("Failed to load metadata:", err);
4219
- return null;
4220
- }
4221
- }
4222
- function saveMetadata(outputDir, metadata) {
4223
- const metadataPath = join9(outputDir, "metadata.json");
4224
- writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
4694
+ // src/docs/files.ts
4695
+ import { dirname as dirname9, basename as basename3 } from "path";
4696
+ function generateFiles(graph, projectRoot, version) {
4697
+ let output = "";
4698
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4699
+ const fileCount = getFileCount5(graph);
4700
+ output += timestamp(version, now, fileCount, graph.order);
4701
+ output += header("File Catalog");
4702
+ output += "Complete catalog of every file in the project with key metrics.\n\n";
4703
+ output += header("File Summary", 2);
4704
+ output += generateFileSummaryTable(graph);
4705
+ output += header("Directory Breakdown", 2);
4706
+ output += generateDirectoryBreakdown(graph);
4707
+ output += header("File Size Distribution", 2);
4708
+ output += generateFileSizeDistribution(graph);
4709
+ output += header("Orphan Files", 2);
4710
+ output += generateOrphanFiles(graph);
4711
+ output += header("Hub Files", 2);
4712
+ output += generateHubFiles2(graph);
4713
+ return output;
4225
4714
  }
4226
- function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
4227
- const now = (/* @__PURE__ */ new Date()).toISOString();
4228
- const documents = {};
4229
- for (const docType of docTypes) {
4230
- const fileName = docType === "architecture" ? "ARCHITECTURE.md" : docType === "conventions" ? "CONVENTIONS.md" : docType === "dependencies" ? "DEPENDENCIES.md" : docType === "onboarding" ? "ONBOARDING.md" : `${docType.toUpperCase()}.md`;
4231
- documents[docType] = {
4232
- generated_at: now,
4233
- file: fileName
4234
- };
4235
- }
4236
- return {
4237
- version,
4238
- generated_at: now,
4239
- project_path: projectPath,
4240
- file_count: fileCount,
4241
- symbol_count: symbolCount,
4242
- edge_count: edgeCount,
4243
- documents
4244
- };
4715
+ function getFileCount5(graph) {
4716
+ const files = /* @__PURE__ */ new Set();
4717
+ graph.forEachNode((node, attrs) => {
4718
+ files.add(attrs.filePath);
4719
+ });
4720
+ return files.size;
4245
4721
  }
4246
- function updateMetadata(existing, docTypes, fileCount, symbolCount, edgeCount) {
4247
- const now = (/* @__PURE__ */ new Date()).toISOString();
4248
- for (const docType of docTypes) {
4249
- if (existing.documents[docType]) {
4250
- existing.documents[docType].generated_at = now;
4722
+ function getFileStats2(graph) {
4723
+ const fileMap = /* @__PURE__ */ new Map();
4724
+ graph.forEachNode((node, attrs) => {
4725
+ if (!fileMap.has(attrs.filePath)) {
4726
+ fileMap.set(attrs.filePath, {
4727
+ filePath: attrs.filePath,
4728
+ language: getLanguageFromPath(attrs.filePath),
4729
+ symbolCount: 0,
4730
+ importCount: 0,
4731
+ exportedSymbolCount: 0,
4732
+ incomingConnections: 0,
4733
+ outgoingConnections: 0,
4734
+ totalConnections: 0,
4735
+ maxLine: 0
4736
+ });
4251
4737
  }
4252
- }
4253
- existing.file_count = fileCount;
4254
- existing.symbol_count = symbolCount;
4255
- existing.edge_count = edgeCount;
4256
- existing.generated_at = now;
4257
- return existing;
4258
- }
4259
-
4260
- // src/docs/generator.ts
4261
- async function generateDocs(graph, projectRoot, version, parseTime, options) {
4262
- const startTime = Date.now();
4263
- const generated = [];
4264
- const errors = [];
4265
- try {
4266
- if (!existsSync7(options.outputDir)) {
4267
- mkdirSync(options.outputDir, { recursive: true });
4268
- if (options.verbose) {
4269
- console.log(`Created output directory: ${options.outputDir}`);
4270
- }
4738
+ const stats = fileMap.get(attrs.filePath);
4739
+ stats.symbolCount++;
4740
+ if (attrs.exported && attrs.name !== "default") {
4741
+ stats.exportedSymbolCount++;
4742
+ }
4743
+ if (attrs.kind === "import") {
4744
+ stats.importCount++;
4745
+ }
4746
+ if (attrs.endLine > stats.maxLine) {
4747
+ stats.maxLine = attrs.endLine;
4748
+ }
4749
+ });
4750
+ graph.forEachEdge((edge, attrs, source, target) => {
4751
+ const sourceAttrs = graph.getNodeAttributes(source);
4752
+ const targetAttrs = graph.getNodeAttributes(target);
4753
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
4754
+ const sourceStats = fileMap.get(sourceAttrs.filePath);
4755
+ const targetStats = fileMap.get(targetAttrs.filePath);
4756
+ if (sourceStats) {
4757
+ sourceStats.outgoingConnections++;
4758
+ }
4759
+ if (targetStats) {
4760
+ targetStats.incomingConnections++;
4761
+ }
4762
+ }
4763
+ });
4764
+ fileMap.forEach((stats) => {
4765
+ stats.totalConnections = stats.incomingConnections + stats.outgoingConnections;
4766
+ });
4767
+ return Array.from(fileMap.values());
4768
+ }
4769
+ function getLanguageFromPath(filePath) {
4770
+ const ext = filePath.toLowerCase();
4771
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
4772
+ if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
4773
+ if (ext.endsWith(".py")) return "Python";
4774
+ if (ext.endsWith(".go")) return "Go";
4775
+ return "Other";
4776
+ }
4777
+ function generateFileSummaryTable(graph) {
4778
+ const fileStats = getFileStats2(graph);
4779
+ if (fileStats.length === 0) {
4780
+ return "No files detected.\n\n";
4781
+ }
4782
+ fileStats.sort((a, b) => a.filePath.localeCompare(b.filePath));
4783
+ const headers = ["File", "Language", "Symbols", "Imports", "Exports", "Connections", "Lines"];
4784
+ const rows = fileStats.map((f) => [
4785
+ `\`${f.filePath}\``,
4786
+ f.language,
4787
+ formatNumber(f.symbolCount),
4788
+ formatNumber(f.importCount),
4789
+ formatNumber(f.exportedSymbolCount),
4790
+ formatNumber(f.totalConnections),
4791
+ formatNumber(f.maxLine)
4792
+ ]);
4793
+ return table(headers, rows);
4794
+ }
4795
+ function generateDirectoryBreakdown(graph) {
4796
+ const fileStats = getFileStats2(graph);
4797
+ const dirMap = /* @__PURE__ */ new Map();
4798
+ for (const file of fileStats) {
4799
+ const dir = dirname9(file.filePath);
4800
+ const topDir = dir === "." ? "." : dir.split("/")[0];
4801
+ if (!dirMap.has(topDir)) {
4802
+ dirMap.set(topDir, {
4803
+ fileCount: 0,
4804
+ symbolCount: 0,
4805
+ mostConnectedFile: "",
4806
+ maxConnections: 0
4807
+ });
4808
+ }
4809
+ const dirStats = dirMap.get(topDir);
4810
+ dirStats.fileCount++;
4811
+ dirStats.symbolCount += file.symbolCount;
4812
+ if (file.totalConnections > dirStats.maxConnections) {
4813
+ dirStats.maxConnections = file.totalConnections;
4814
+ dirStats.mostConnectedFile = basename3(file.filePath);
4815
+ }
4816
+ }
4817
+ if (dirMap.size === 0) {
4818
+ return "No directories detected.\n\n";
4819
+ }
4820
+ let output = "";
4821
+ const sortedDirs = Array.from(dirMap.entries()).sort((a, b) => b[1].fileCount - a[1].fileCount);
4822
+ for (const [dir, stats] of sortedDirs) {
4823
+ output += `**${dir === "." ? "Root" : dir}/**
4824
+
4825
+ `;
4826
+ output += `- **Files:** ${formatNumber(stats.fileCount)}
4827
+ `;
4828
+ output += `- **Symbols:** ${formatNumber(stats.symbolCount)}
4829
+ `;
4830
+ output += `- **Most Connected:** \`${stats.mostConnectedFile}\` (${formatNumber(stats.maxConnections)} connections)
4831
+
4832
+ `;
4833
+ }
4834
+ return output;
4835
+ }
4836
+ function generateFileSizeDistribution(graph) {
4837
+ const fileStats = getFileStats2(graph);
4838
+ if (fileStats.length === 0) {
4839
+ return "No files detected.\n\n";
4840
+ }
4841
+ const bySymbols = [...fileStats].sort((a, b) => b.symbolCount - a.symbolCount);
4842
+ let output = "";
4843
+ output += "**Largest Files (by symbol count):**\n\n";
4844
+ const largest = bySymbols.slice(0, 10);
4845
+ const headers1 = ["File", "Symbols", "Lines"];
4846
+ const rows1 = largest.map((f) => [
4847
+ `\`${f.filePath}\``,
4848
+ formatNumber(f.symbolCount),
4849
+ formatNumber(f.maxLine)
4850
+ ]);
4851
+ output += table(headers1, rows1);
4852
+ if (bySymbols.length > 10) {
4853
+ output += "**Smallest Files (by symbol count):**\n\n";
4854
+ const smallest = bySymbols.slice(-10).reverse();
4855
+ const headers2 = ["File", "Symbols", "Lines"];
4856
+ const rows2 = smallest.map((f) => [
4857
+ `\`${f.filePath}\``,
4858
+ formatNumber(f.symbolCount),
4859
+ formatNumber(f.maxLine)
4860
+ ]);
4861
+ output += table(headers2, rows2);
4862
+ }
4863
+ const avgSymbols = Math.round(fileStats.reduce((sum, f) => sum + f.symbolCount, 0) / fileStats.length);
4864
+ const avgLines = Math.round(fileStats.reduce((sum, f) => sum + f.maxLine, 0) / fileStats.length);
4865
+ output += `**Average File Size:**
4866
+
4867
+ `;
4868
+ output += `- Symbols per file: ${formatNumber(avgSymbols)}
4869
+ `;
4870
+ output += `- Lines per file: ${formatNumber(avgLines)}
4871
+
4872
+ `;
4873
+ return output;
4874
+ }
4875
+ function generateOrphanFiles(graph) {
4876
+ const fileStats = getFileStats2(graph);
4877
+ const orphans = fileStats.filter((f) => f.totalConnections === 0);
4878
+ if (orphans.length === 0) {
4879
+ return "\u2705 No orphan files detected. All files are connected.\n\n";
4880
+ }
4881
+ let output = `Found ${orphans.length} file${orphans.length === 1 ? "" : "s"} with zero connections:
4882
+
4883
+ `;
4884
+ output += unorderedList(orphans.map((f) => `\`${f.filePath}\` (${f.symbolCount} symbols)`));
4885
+ output += "These files may be entry points, standalone scripts, or dead code.\n\n";
4886
+ return output;
4887
+ }
4888
+ function generateHubFiles2(graph) {
4889
+ const fileStats = getFileStats2(graph);
4890
+ const hubs = fileStats.filter((f) => f.totalConnections > 0).sort((a, b) => b.totalConnections - a.totalConnections).slice(0, 10);
4891
+ if (hubs.length === 0) {
4892
+ return "No hub files detected.\n\n";
4893
+ }
4894
+ let output = "Files with the most connections (changing these breaks the most things):\n\n";
4895
+ const headers = ["File", "Total Connections", "Incoming", "Outgoing", "Symbols"];
4896
+ const rows = hubs.map((f) => [
4897
+ `\`${f.filePath}\``,
4898
+ formatNumber(f.totalConnections),
4899
+ formatNumber(f.incomingConnections),
4900
+ formatNumber(f.outgoingConnections),
4901
+ formatNumber(f.symbolCount)
4902
+ ]);
4903
+ output += table(headers, rows);
4904
+ return output;
4905
+ }
4906
+
4907
+ // src/docs/api-surface.ts
4908
+ function generateApiSurface(graph, projectRoot, version) {
4909
+ let output = "";
4910
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4911
+ const fileCount = getFileCount6(graph);
4912
+ output += timestamp(version, now, fileCount, graph.order);
4913
+ output += header("API Surface");
4914
+ output += "Every exported symbol in the project \u2014 the public API.\n\n";
4915
+ output += header("Exports by File", 2);
4916
+ output += generateExportsByFile(graph);
4917
+ output += header("Exports by Kind", 2);
4918
+ output += generateExportsByKind(graph);
4919
+ output += header("Most-Used Exports", 2);
4920
+ output += generateMostUsedExports(graph);
4921
+ output += header("Unused Exports", 2);
4922
+ output += generateUnusedExports(graph);
4923
+ output += header("Re-exports / Barrel Files", 2);
4924
+ output += generateReExports(graph);
4925
+ return output;
4926
+ }
4927
+ function getFileCount6(graph) {
4928
+ const files = /* @__PURE__ */ new Set();
4929
+ graph.forEachNode((node, attrs) => {
4930
+ files.add(attrs.filePath);
4931
+ });
4932
+ return files.size;
4933
+ }
4934
+ function getExportedSymbols(graph) {
4935
+ const exports = [];
4936
+ graph.forEachNode((node, attrs) => {
4937
+ if (attrs.exported && attrs.name !== "__file__") {
4938
+ const dependentCount = graph.inDegree(node);
4939
+ exports.push({
4940
+ name: attrs.name,
4941
+ kind: attrs.kind,
4942
+ filePath: attrs.filePath,
4943
+ line: attrs.startLine,
4944
+ dependentCount
4945
+ });
4946
+ }
4947
+ });
4948
+ return exports;
4949
+ }
4950
+ function generateExportsByFile(graph) {
4951
+ const exports = getExportedSymbols(graph);
4952
+ if (exports.length === 0) {
4953
+ return "No exported symbols detected.\n\n";
4954
+ }
4955
+ const fileExports = /* @__PURE__ */ new Map();
4956
+ for (const exp of exports) {
4957
+ if (!fileExports.has(exp.filePath)) {
4958
+ fileExports.set(exp.filePath, []);
4959
+ }
4960
+ fileExports.get(exp.filePath).push(exp);
4961
+ }
4962
+ const sortedFiles = Array.from(fileExports.entries()).sort((a, b) => b[1].length - a[1].length);
4963
+ let output = "";
4964
+ for (const [filePath, fileExports2] of sortedFiles) {
4965
+ output += header(filePath, 3);
4966
+ const sorted = fileExports2.sort((a, b) => b.dependentCount - a.dependentCount);
4967
+ const items = sorted.map((exp) => {
4968
+ const depInfo = exp.dependentCount > 0 ? ` \u2014 ${formatNumber(exp.dependentCount)} dependents` : "";
4969
+ return `${code(exp.name)} (${exp.kind}, line ${exp.line})${depInfo}`;
4970
+ });
4971
+ output += unorderedList(items);
4972
+ }
4973
+ return output;
4974
+ }
4975
+ function generateExportsByKind(graph) {
4976
+ const exports = getExportedSymbols(graph);
4977
+ if (exports.length === 0) {
4978
+ return "No exported symbols detected.\n\n";
4979
+ }
4980
+ const kindGroups = /* @__PURE__ */ new Map();
4981
+ for (const exp of exports) {
4982
+ if (!kindGroups.has(exp.kind)) {
4983
+ kindGroups.set(exp.kind, []);
4984
+ }
4985
+ kindGroups.get(exp.kind).push(exp);
4986
+ }
4987
+ let output = "";
4988
+ const sortedKinds = Array.from(kindGroups.entries()).sort((a, b) => b[1].length - a[1].length);
4989
+ for (const [kind, kindExports] of sortedKinds) {
4990
+ if (kind === "import" || kind === "export") continue;
4991
+ output += `**${capitalizeKind(kind)}s (${kindExports.length}):**
4992
+
4993
+ `;
4994
+ const sorted = kindExports.sort((a, b) => b.dependentCount - a.dependentCount).slice(0, 20);
4995
+ const items = sorted.map((exp) => {
4996
+ return `${code(exp.name)} \u2014 ${code(exp.filePath)}:${exp.line}`;
4997
+ });
4998
+ output += unorderedList(items);
4999
+ }
5000
+ return output;
5001
+ }
5002
+ function capitalizeKind(kind) {
5003
+ const map = {
5004
+ function: "Function",
5005
+ class: "Class",
5006
+ variable: "Variable",
5007
+ constant: "Constant",
5008
+ type_alias: "Type",
5009
+ interface: "Interface",
5010
+ enum: "Enum",
5011
+ import: "Import",
5012
+ export: "Export",
5013
+ method: "Method",
5014
+ property: "Property",
5015
+ decorator: "Decorator",
5016
+ module: "Module"
5017
+ };
5018
+ return map[kind] || kind;
5019
+ }
5020
+ function generateMostUsedExports(graph) {
5021
+ const exports = getExportedSymbols(graph);
5022
+ if (exports.length === 0) {
5023
+ return "No exported symbols detected.\n\n";
5024
+ }
5025
+ const sorted = exports.filter((exp) => exp.dependentCount > 0).sort((a, b) => b.dependentCount - a.dependentCount).slice(0, 20);
5026
+ if (sorted.length === 0) {
5027
+ return "No exports with dependents detected.\n\n";
5028
+ }
5029
+ let output = "Top 20 exports by dependent count \u2014 these are the most critical symbols:\n\n";
5030
+ const items = sorted.map((exp) => {
5031
+ return `${code(exp.name)} (${exp.kind}) \u2014 ${formatNumber(exp.dependentCount)} dependents \u2014 ${code(exp.filePath)}:${exp.line}`;
5032
+ });
5033
+ output += unorderedList(items);
5034
+ return output;
5035
+ }
5036
+ function generateUnusedExports(graph) {
5037
+ const exports = getExportedSymbols(graph);
5038
+ if (exports.length === 0) {
5039
+ return "No exported symbols detected.\n\n";
5040
+ }
5041
+ const unused = exports.filter((exp) => exp.dependentCount === 0 && exp.kind !== "export");
5042
+ if (unused.length === 0) {
5043
+ return "\u2705 No unused exports detected. All exports are used.\n\n";
5044
+ }
5045
+ let output = `Found ${unused.length} exported symbol${unused.length === 1 ? "" : "s"} with zero dependents:
5046
+
5047
+ `;
5048
+ const fileGroups = /* @__PURE__ */ new Map();
5049
+ for (const exp of unused) {
5050
+ if (!fileGroups.has(exp.filePath)) {
5051
+ fileGroups.set(exp.filePath, []);
5052
+ }
5053
+ fileGroups.get(exp.filePath).push(exp);
5054
+ }
5055
+ for (const [filePath, fileExports] of fileGroups.entries()) {
5056
+ output += `**${filePath}:**
5057
+
5058
+ `;
5059
+ const items = fileExports.map((exp) => `${code(exp.name)} (${exp.kind}, line ${exp.line})`);
5060
+ output += unorderedList(items);
5061
+ }
5062
+ output += "These symbols may be part of the intended public API but are not currently used, or they may be dead code.\n\n";
5063
+ return output;
5064
+ }
5065
+ function generateReExports(graph) {
5066
+ const fileStats = /* @__PURE__ */ new Map();
5067
+ graph.forEachNode((node, attrs) => {
5068
+ if (!fileStats.has(attrs.filePath)) {
5069
+ fileStats.set(attrs.filePath, {
5070
+ exportCount: 0,
5071
+ reExportCount: 0,
5072
+ reExportSources: /* @__PURE__ */ new Set()
5073
+ });
5074
+ }
5075
+ const stats = fileStats.get(attrs.filePath);
5076
+ if (attrs.exported) {
5077
+ stats.exportCount++;
5078
+ }
5079
+ if (attrs.kind === "export") {
5080
+ stats.reExportCount++;
5081
+ }
5082
+ });
5083
+ graph.forEachEdge((edge, attrs, source, target) => {
5084
+ const sourceAttrs = graph.getNodeAttributes(source);
5085
+ const targetAttrs = graph.getNodeAttributes(target);
5086
+ if (sourceAttrs.kind === "export" && sourceAttrs.filePath !== targetAttrs.filePath) {
5087
+ const stats = fileStats.get(sourceAttrs.filePath);
5088
+ if (stats) {
5089
+ stats.reExportSources.add(targetAttrs.filePath);
5090
+ }
5091
+ }
5092
+ });
5093
+ const barrels = [];
5094
+ for (const [filePath, stats] of fileStats.entries()) {
5095
+ if (stats.reExportCount > 0 && stats.reExportCount >= stats.exportCount * 0.5) {
5096
+ barrels.push({
5097
+ filePath,
5098
+ exportCount: stats.exportCount,
5099
+ reExportCount: stats.reExportCount,
5100
+ sources: Array.from(stats.reExportSources)
5101
+ });
5102
+ }
5103
+ }
5104
+ if (barrels.length === 0) {
5105
+ return "No barrel files detected.\n\n";
5106
+ }
5107
+ let output = `Found ${barrels.length} barrel file${barrels.length === 1 ? "" : "s"} (files that primarily re-export from other files):
5108
+
5109
+ `;
5110
+ for (const barrel of barrels) {
5111
+ output += header(barrel.filePath, 3);
5112
+ output += `- **Total exports:** ${formatNumber(barrel.exportCount)}
5113
+ `;
5114
+ output += `- **Re-exports:** ${formatNumber(barrel.reExportCount)}
5115
+ `;
5116
+ if (barrel.sources.length > 0) {
5117
+ output += `- **Sources:**
5118
+
5119
+ `;
5120
+ output += unorderedList(barrel.sources.map((s) => code(s)));
5121
+ } else {
5122
+ output += "\n";
5123
+ }
5124
+ }
5125
+ return output;
5126
+ }
5127
+
5128
+ // src/docs/errors.ts
5129
+ function generateErrors(graph, projectRoot, version) {
5130
+ let output = "";
5131
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5132
+ const fileCount = getFileCount7(graph);
5133
+ output += timestamp(version, now, fileCount, graph.order);
5134
+ output += header("Error Handling Analysis");
5135
+ output += "Analysis of error handling patterns and error-prone areas in the codebase.\n\n";
5136
+ output += header("Error-Related Symbols", 2);
5137
+ output += generateErrorRelatedSymbols(graph);
5138
+ output += header("Custom Error Classes", 2);
5139
+ output += generateCustomErrorClasses(graph);
5140
+ output += header("Error-Prone Files", 2);
5141
+ output += generateErrorProneFiles(graph);
5142
+ output += header("Detected Patterns", 2);
5143
+ output += generateErrorHandlingPatterns(graph);
5144
+ output += header("Recommendations", 2);
5145
+ output += generateRecommendations(graph);
5146
+ return output;
5147
+ }
5148
+ function getFileCount7(graph) {
5149
+ const files = /* @__PURE__ */ new Set();
5150
+ graph.forEachNode((node, attrs) => {
5151
+ files.add(attrs.filePath);
5152
+ });
5153
+ return files.size;
5154
+ }
5155
+ function getErrorRelatedSymbols(graph) {
5156
+ const errorKeywords = [
5157
+ "error",
5158
+ "err",
5159
+ "exception",
5160
+ "throw",
5161
+ "fail",
5162
+ "invalid",
5163
+ "not_found",
5164
+ "notfound",
5165
+ "unauthorized",
5166
+ "forbidden",
5167
+ "timeout",
5168
+ "retry",
5169
+ "catch",
5170
+ "try"
5171
+ ];
5172
+ const symbols = [];
5173
+ graph.forEachNode((node, attrs) => {
5174
+ if (attrs.name === "__file__") return;
5175
+ const nameLower = attrs.name.toLowerCase();
5176
+ for (const keyword of errorKeywords) {
5177
+ if (nameLower.includes(keyword)) {
5178
+ let category = "error_handling";
5179
+ if (nameLower.includes("retry") || nameLower.includes("timeout")) {
5180
+ category = "retry_timeout";
5181
+ } else if (nameLower.includes("invalid") || nameLower.includes("validate")) {
5182
+ category = "validation";
5183
+ } else if (nameLower.includes("unauthorized") || nameLower.includes("forbidden")) {
5184
+ category = "auth_error";
5185
+ } else if (nameLower.includes("notfound") || nameLower.includes("not_found")) {
5186
+ category = "not_found";
5187
+ }
5188
+ symbols.push({
5189
+ name: attrs.name,
5190
+ kind: attrs.kind,
5191
+ filePath: attrs.filePath,
5192
+ line: attrs.startLine,
5193
+ category
5194
+ });
5195
+ break;
5196
+ }
5197
+ }
5198
+ });
5199
+ return symbols;
5200
+ }
5201
+ function generateErrorRelatedSymbols(graph) {
5202
+ const symbols = getErrorRelatedSymbols(graph);
5203
+ if (symbols.length === 0) {
5204
+ return "No error-related symbols detected.\n\n";
5205
+ }
5206
+ let output = `Found ${symbols.length} error-related symbol${symbols.length === 1 ? "" : "s"}:
5207
+
5208
+ `;
5209
+ const categories = /* @__PURE__ */ new Map();
5210
+ for (const sym of symbols) {
5211
+ if (!categories.has(sym.category)) {
5212
+ categories.set(sym.category, []);
5213
+ }
5214
+ categories.get(sym.category).push(sym);
5215
+ }
5216
+ for (const [category, syms] of categories.entries()) {
5217
+ output += `**${formatCategory(category)} (${syms.length}):**
5218
+
5219
+ `;
5220
+ const items = syms.slice(0, 10).map((s) => {
5221
+ return `${code(s.name)} (${s.kind}) \u2014 ${code(s.filePath)}:${s.line}`;
5222
+ });
5223
+ output += unorderedList(items);
5224
+ if (syms.length > 10) {
5225
+ output += `... and ${syms.length - 10} more.
5226
+
5227
+ `;
5228
+ }
5229
+ }
5230
+ return output;
5231
+ }
5232
+ function formatCategory(category) {
5233
+ const map = {
5234
+ "error_handling": "Error Handling",
5235
+ "retry_timeout": "Retry / Timeout",
5236
+ "validation": "Validation",
5237
+ "auth_error": "Authentication Errors",
5238
+ "not_found": "Not Found Errors"
5239
+ };
5240
+ return map[category] || category;
5241
+ }
5242
+ function generateCustomErrorClasses(graph) {
5243
+ const errorClasses = [];
5244
+ graph.forEachNode((node, attrs) => {
5245
+ if (attrs.kind === "class") {
5246
+ const nameLower = attrs.name.toLowerCase();
5247
+ if (nameLower.includes("error") || nameLower.includes("exception")) {
5248
+ errorClasses.push({
5249
+ name: attrs.name,
5250
+ filePath: attrs.filePath,
5251
+ line: attrs.startLine
5252
+ });
5253
+ }
5254
+ }
5255
+ });
5256
+ if (errorClasses.length === 0) {
5257
+ return "No custom error classes detected.\n\n";
5258
+ }
5259
+ let output = `Found ${errorClasses.length} custom error class${errorClasses.length === 1 ? "" : "es"}:
5260
+
5261
+ `;
5262
+ const items = errorClasses.map((c) => {
5263
+ return `${code(c.name)} \u2014 ${code(c.filePath)}:${c.line}`;
5264
+ });
5265
+ output += unorderedList(items);
5266
+ return output;
5267
+ }
5268
+ function generateErrorProneFiles(graph) {
5269
+ const fileStats = /* @__PURE__ */ new Map();
5270
+ graph.forEachNode((node, attrs) => {
5271
+ if (!fileStats.has(attrs.filePath)) {
5272
+ fileStats.set(attrs.filePath, {
5273
+ connectionCount: 0,
5274
+ errorSymbolCount: 0,
5275
+ symbolCount: 0
5276
+ });
5277
+ }
5278
+ fileStats.get(attrs.filePath).symbolCount++;
5279
+ });
5280
+ const errorSymbols = getErrorRelatedSymbols(graph);
5281
+ for (const sym of errorSymbols) {
5282
+ const stats = fileStats.get(sym.filePath);
5283
+ if (stats) {
5284
+ stats.errorSymbolCount++;
5285
+ }
5286
+ }
5287
+ graph.forEachEdge((edge, attrs, source, target) => {
5288
+ const sourceAttrs = graph.getNodeAttributes(source);
5289
+ const targetAttrs = graph.getNodeAttributes(target);
5290
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
5291
+ const sourceStats = fileStats.get(sourceAttrs.filePath);
5292
+ const targetStats = fileStats.get(targetAttrs.filePath);
5293
+ if (sourceStats) sourceStats.connectionCount++;
5294
+ if (targetStats) targetStats.connectionCount++;
5295
+ }
5296
+ });
5297
+ const errorProneFiles = [];
5298
+ for (const [filePath, stats] of fileStats.entries()) {
5299
+ if (stats.connectionCount > 5) {
5300
+ const riskScore = stats.connectionCount * (1 + stats.errorSymbolCount * 0.5);
5301
+ errorProneFiles.push({
5302
+ filePath,
5303
+ connectionCount: stats.connectionCount,
5304
+ errorSymbolCount: stats.errorSymbolCount,
5305
+ riskScore
5306
+ });
5307
+ }
5308
+ }
5309
+ errorProneFiles.sort((a, b) => b.riskScore - a.riskScore);
5310
+ if (errorProneFiles.length === 0) {
5311
+ return "No high-risk files detected.\n\n";
5312
+ }
5313
+ let output = "Files with high complexity and error-related code (riskiest to modify):\n\n";
5314
+ const headers = ["File", "Connections", "Error Symbols", "Risk Score"];
5315
+ const rows = errorProneFiles.slice(0, 15).map((f) => [
5316
+ `\`${f.filePath}\``,
5317
+ formatNumber(f.connectionCount),
5318
+ formatNumber(f.errorSymbolCount),
5319
+ f.riskScore.toFixed(1)
5320
+ ]);
5321
+ output += table(headers, rows);
5322
+ return output;
5323
+ }
5324
+ function generateErrorHandlingPatterns(graph) {
5325
+ const patterns = {
5326
+ custom_errors: 0,
5327
+ retry: 0,
5328
+ timeout: 0,
5329
+ validation: 0,
5330
+ guard: 0
5331
+ };
5332
+ graph.forEachNode((node, attrs) => {
5333
+ const nameLower = attrs.name.toLowerCase();
5334
+ if (attrs.kind === "class" && (nameLower.includes("error") || nameLower.includes("exception"))) {
5335
+ patterns.custom_errors++;
5336
+ }
5337
+ if (nameLower.includes("retry") || nameLower.includes("attempt")) {
5338
+ patterns.retry++;
5339
+ }
5340
+ if (nameLower.includes("timeout")) {
5341
+ patterns.timeout++;
5342
+ }
5343
+ if (nameLower.includes("validate") || nameLower.includes("validator") || nameLower.includes("check")) {
5344
+ patterns.validation++;
5345
+ }
5346
+ if (nameLower.includes("guard") || nameLower.startsWith("is") || nameLower.startsWith("has")) {
5347
+ patterns.guard++;
5348
+ }
5349
+ });
5350
+ const detectedPatterns = Object.entries(patterns).filter(([, count]) => count > 0);
5351
+ if (detectedPatterns.length === 0) {
5352
+ return "No error handling patterns detected.\n\n";
5353
+ }
5354
+ let output = "";
5355
+ for (const [pattern, count] of detectedPatterns) {
5356
+ const description = getPatternDescription2(pattern);
5357
+ output += `- **${formatPatternName(pattern)}:** ${count} occurrences \u2014 ${description}
5358
+ `;
5359
+ }
5360
+ output += "\n";
5361
+ return output;
5362
+ }
5363
+ function formatPatternName(pattern) {
5364
+ const map = {
5365
+ custom_errors: "Custom Error Hierarchy",
5366
+ retry: "Retry Pattern",
5367
+ timeout: "Timeout Handling",
5368
+ validation: "Input Validation",
5369
+ guard: "Guard Clauses"
5370
+ };
5371
+ return map[pattern] || pattern;
5372
+ }
5373
+ function getPatternDescription2(pattern) {
5374
+ const map = {
5375
+ custom_errors: "Custom error classes for domain-specific exceptions",
5376
+ retry: "Retry logic for transient failures",
5377
+ timeout: "Timeout handling for long-running operations",
5378
+ validation: "Input validation to prevent errors",
5379
+ guard: "Guard clauses to check preconditions"
5380
+ };
5381
+ return map[pattern] || "";
5382
+ }
5383
+ function generateRecommendations(graph) {
5384
+ const recommendations = [];
5385
+ const fileStats = /* @__PURE__ */ new Map();
5386
+ graph.forEachNode((node, attrs) => {
5387
+ if (!fileStats.has(attrs.filePath)) {
5388
+ fileStats.set(attrs.filePath, {
5389
+ connectionCount: 0,
5390
+ errorSymbolCount: 0
5391
+ });
5392
+ }
5393
+ });
5394
+ const errorSymbols = getErrorRelatedSymbols(graph);
5395
+ for (const sym of errorSymbols) {
5396
+ const stats = fileStats.get(sym.filePath);
5397
+ if (stats) {
5398
+ stats.errorSymbolCount++;
5399
+ }
5400
+ }
5401
+ graph.forEachEdge((edge, attrs, source, target) => {
5402
+ const sourceAttrs = graph.getNodeAttributes(source);
5403
+ const targetAttrs = graph.getNodeAttributes(target);
5404
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
5405
+ const sourceStats = fileStats.get(sourceAttrs.filePath);
5406
+ const targetStats = fileStats.get(targetAttrs.filePath);
5407
+ if (sourceStats) sourceStats.connectionCount++;
5408
+ if (targetStats) targetStats.connectionCount++;
5409
+ }
5410
+ });
5411
+ const needsErrorHandling = [];
5412
+ for (const [filePath, stats] of fileStats.entries()) {
5413
+ if (stats.connectionCount > 10 && stats.errorSymbolCount === 0) {
5414
+ needsErrorHandling.push(filePath);
5415
+ }
5416
+ }
5417
+ if (needsErrorHandling.length > 0) {
5418
+ recommendations.push(`**Add error handling to high-connection files:** ${needsErrorHandling.slice(0, 5).map((f) => code(f)).join(", ")}`);
5419
+ }
5420
+ const errorClasses = [];
5421
+ graph.forEachNode((node, attrs) => {
5422
+ if (attrs.kind === "class") {
5423
+ const nameLower = attrs.name.toLowerCase();
5424
+ if (nameLower.includes("error") || nameLower.includes("exception")) {
5425
+ const dependents = graph.inDegree(node);
5426
+ if (dependents === 0) {
5427
+ errorClasses.push(attrs.name);
5428
+ }
5429
+ }
5430
+ }
5431
+ });
5432
+ if (errorClasses.length > 0) {
5433
+ recommendations.push(`**Unused error classes detected:** ${errorClasses.slice(0, 5).map((c) => code(c)).join(", ")} \u2014 Consider removing or documenting why they exist`);
5434
+ }
5435
+ if (recommendations.length === 0) {
5436
+ return "\u2705 No specific recommendations. Error handling appears well-distributed.\n\n";
5437
+ }
5438
+ return unorderedList(recommendations);
5439
+ }
5440
+
5441
+ // src/docs/tests.ts
5442
+ import { basename as basename4, dirname as dirname10 } from "path";
5443
+ function generateTests(graph, projectRoot, version) {
5444
+ let output = "";
5445
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5446
+ const fileCount = getFileCount8(graph);
5447
+ output += timestamp(version, now, fileCount, graph.order);
5448
+ output += header("Test Analysis");
5449
+ output += "Test file inventory and coverage mapping.\n\n";
5450
+ output += header("Test File Inventory", 2);
5451
+ output += generateTestFileInventory(graph);
5452
+ output += header("Test-to-Source Mapping", 2);
5453
+ output += generateTestToSourceMapping(graph);
5454
+ output += header("Untested Files", 2);
5455
+ output += generateUntestedFiles(graph);
5456
+ output += header("Test Coverage Map", 2);
5457
+ output += generateTestCoverageMap(graph);
5458
+ output += header("Test Statistics", 2);
5459
+ output += generateTestStatistics(graph);
5460
+ return output;
5461
+ }
5462
+ function getFileCount8(graph) {
5463
+ const files = /* @__PURE__ */ new Set();
5464
+ graph.forEachNode((node, attrs) => {
5465
+ files.add(attrs.filePath);
5466
+ });
5467
+ return files.size;
5468
+ }
5469
+ function isTestFile(filePath) {
5470
+ const fileName = basename4(filePath).toLowerCase();
5471
+ const dirPath = dirname10(filePath).toLowerCase();
5472
+ if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
5473
+ return true;
5474
+ }
5475
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("_test.") || fileName.includes("_spec.")) {
5476
+ return true;
5477
+ }
5478
+ return false;
5479
+ }
5480
+ function getTestFiles(graph) {
5481
+ const testFiles = /* @__PURE__ */ new Map();
5482
+ graph.forEachNode((node, attrs) => {
5483
+ if (isTestFile(attrs.filePath)) {
5484
+ if (!testFiles.has(attrs.filePath)) {
5485
+ testFiles.set(attrs.filePath, {
5486
+ filePath: attrs.filePath,
5487
+ language: getLanguageFromPath2(attrs.filePath),
5488
+ symbolCount: 0,
5489
+ functionCount: 0
5490
+ });
5491
+ }
5492
+ const info = testFiles.get(attrs.filePath);
5493
+ info.symbolCount++;
5494
+ if (attrs.kind === "function" || attrs.kind === "method") {
5495
+ info.functionCount++;
5496
+ }
5497
+ }
5498
+ });
5499
+ return Array.from(testFiles.values());
5500
+ }
5501
+ function getLanguageFromPath2(filePath) {
5502
+ const ext = filePath.toLowerCase();
5503
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
5504
+ if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
5505
+ if (ext.endsWith(".py")) return "Python";
5506
+ if (ext.endsWith(".go")) return "Go";
5507
+ return "Other";
5508
+ }
5509
+ function generateTestFileInventory(graph) {
5510
+ const testFiles = getTestFiles(graph);
5511
+ if (testFiles.length === 0) {
5512
+ return "No test files detected.\n\n";
5513
+ }
5514
+ let output = `Found ${testFiles.length} test file${testFiles.length === 1 ? "" : "s"}:
5515
+
5516
+ `;
5517
+ testFiles.sort((a, b) => a.filePath.localeCompare(b.filePath));
5518
+ const headers = ["Test File", "Language", "Symbols", "Functions"];
5519
+ const rows = testFiles.map((t) => [
5520
+ `\`${t.filePath}\``,
5521
+ t.language,
5522
+ formatNumber(t.symbolCount),
5523
+ formatNumber(t.functionCount)
5524
+ ]);
5525
+ output += table(headers, rows);
5526
+ return output;
5527
+ }
5528
+ function matchTestToSource(testFile) {
5529
+ const testFileName = basename4(testFile);
5530
+ const testDir = dirname10(testFile);
5531
+ let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
5532
+ const possiblePaths = [];
5533
+ possiblePaths.push(testDir + "/" + sourceFileName);
5534
+ if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
5535
+ const parentDir = dirname10(testDir);
5536
+ possiblePaths.push(parentDir + "/" + sourceFileName);
5537
+ }
5538
+ if (testDir.includes("test")) {
5539
+ const srcDir = testDir.replace(/test[s]?/g, "src");
5540
+ possiblePaths.push(srcDir + "/" + sourceFileName);
5541
+ }
5542
+ for (const path2 of possiblePaths) {
5543
+ if (!isTestFile(path2)) {
5544
+ return path2;
5545
+ }
5546
+ }
5547
+ return null;
5548
+ }
5549
+ function generateTestToSourceMapping(graph) {
5550
+ const testFiles = getTestFiles(graph);
5551
+ if (testFiles.length === 0) {
5552
+ return "No test files detected.\n\n";
5553
+ }
5554
+ const allFiles = /* @__PURE__ */ new Set();
5555
+ graph.forEachNode((node, attrs) => {
5556
+ allFiles.add(attrs.filePath);
5557
+ });
5558
+ let output = "";
5559
+ let mappedCount = 0;
5560
+ const mappings = [];
5561
+ for (const testFile of testFiles) {
5562
+ const sourceFile = matchTestToSource(testFile.filePath);
5563
+ const exists = sourceFile && allFiles.has(sourceFile);
5564
+ mappings.push({
5565
+ test: testFile.filePath,
5566
+ source: exists ? sourceFile : null
5567
+ });
5568
+ if (exists) {
5569
+ mappedCount++;
5570
+ }
5571
+ }
5572
+ output += `Matched ${mappedCount} of ${testFiles.length} test files to source files:
5573
+
5574
+ `;
5575
+ for (const mapping of mappings) {
5576
+ if (mapping.source) {
5577
+ output += `- ${code(mapping.source)} \u2190 ${code(mapping.test)}
5578
+ `;
5579
+ }
5580
+ }
5581
+ output += "\n";
5582
+ const unmapped = mappings.filter((m) => !m.source);
5583
+ if (unmapped.length > 0) {
5584
+ output += `**Unmapped test files (${unmapped.length}):**
5585
+
5586
+ `;
5587
+ output += unorderedList(unmapped.map((m) => code(m.test)));
5588
+ }
5589
+ return output;
5590
+ }
5591
+ function generateUntestedFiles(graph) {
5592
+ const testFiles = getTestFiles(graph);
5593
+ const sourceFiles = [];
5594
+ const allFiles = /* @__PURE__ */ new Set();
5595
+ graph.forEachNode((node, attrs) => {
5596
+ allFiles.add(attrs.filePath);
5597
+ });
5598
+ for (const file of allFiles) {
5599
+ if (!isTestFile(file)) {
5600
+ sourceFiles.push(file);
5601
+ }
5602
+ }
5603
+ if (sourceFiles.length === 0) {
5604
+ return "No source files detected.\n\n";
5605
+ }
5606
+ const testedFiles = /* @__PURE__ */ new Set();
5607
+ for (const testFile of testFiles) {
5608
+ const sourceFile = matchTestToSource(testFile.filePath);
5609
+ if (sourceFile && allFiles.has(sourceFile)) {
5610
+ testedFiles.add(sourceFile);
5611
+ }
5612
+ }
5613
+ const untested = sourceFiles.filter((f) => !testedFiles.has(f));
5614
+ if (untested.length === 0) {
5615
+ return "\u2705 All source files have matching test files.\n\n";
5616
+ }
5617
+ const fileConnections = /* @__PURE__ */ new Map();
5618
+ graph.forEachEdge((edge, attrs, source, target) => {
5619
+ const sourceAttrs = graph.getNodeAttributes(source);
5620
+ const targetAttrs = graph.getNodeAttributes(target);
5621
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
5622
+ fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
5623
+ fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
5624
+ }
5625
+ });
5626
+ const untestedWithConnections = untested.map((f) => ({
5627
+ filePath: f,
5628
+ connections: fileConnections.get(f) || 0
5629
+ })).sort((a, b) => b.connections - a.connections);
5630
+ let output = `\u26A0\uFE0F Found ${untested.length} source file${untested.length === 1 ? "" : "s"} without matching test files:
5631
+
5632
+ `;
5633
+ const headers = ["File", "Connections", "Priority"];
5634
+ const rows = untestedWithConnections.slice(0, 20).map((f) => {
5635
+ const priority = f.connections > 10 ? "\u{1F534} High" : f.connections > 5 ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
5636
+ return [
5637
+ `\`${f.filePath}\``,
5638
+ formatNumber(f.connections),
5639
+ priority
5640
+ ];
5641
+ });
5642
+ output += table(headers, rows);
5643
+ if (untested.length > 20) {
5644
+ output += `... and ${untested.length - 20} more.
5645
+
5646
+ `;
5647
+ }
5648
+ return output;
5649
+ }
5650
+ function generateTestCoverageMap(graph) {
5651
+ const testFiles = getTestFiles(graph);
5652
+ const allFiles = /* @__PURE__ */ new Set();
5653
+ const sourceFiles = [];
5654
+ graph.forEachNode((node, attrs) => {
5655
+ allFiles.add(attrs.filePath);
5656
+ });
5657
+ for (const file of allFiles) {
5658
+ if (!isTestFile(file)) {
5659
+ sourceFiles.push(file);
5660
+ }
5661
+ }
5662
+ if (sourceFiles.length === 0) {
5663
+ return "No source files detected.\n\n";
5664
+ }
5665
+ const mappings = [];
5666
+ const testedFiles = /* @__PURE__ */ new Map();
5667
+ for (const testFile of testFiles) {
5668
+ const sourceFile = matchTestToSource(testFile.filePath);
5669
+ if (sourceFile && allFiles.has(sourceFile)) {
5670
+ testedFiles.set(sourceFile, testFile.filePath);
5671
+ }
5672
+ }
5673
+ const fileSymbols = /* @__PURE__ */ new Map();
5674
+ graph.forEachNode((node, attrs) => {
5675
+ fileSymbols.set(attrs.filePath, (fileSymbols.get(attrs.filePath) || 0) + 1);
5676
+ });
5677
+ for (const sourceFile of sourceFiles) {
5678
+ const testFile = testedFiles.get(sourceFile);
5679
+ mappings.push({
5680
+ sourceFile,
5681
+ hasTest: !!testFile,
5682
+ testFile: testFile || null,
5683
+ symbolCount: fileSymbols.get(sourceFile) || 0
5684
+ });
5685
+ }
5686
+ mappings.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile));
5687
+ const headers = ["Source File", "Has Test?", "Test File", "Symbols"];
5688
+ const rows = mappings.slice(0, 30).map((m) => [
5689
+ `\`${m.sourceFile}\``,
5690
+ m.hasTest ? "\u2705" : "\u274C",
5691
+ m.testFile ? `\`${basename4(m.testFile)}\`` : "-",
5692
+ formatNumber(m.symbolCount)
5693
+ ]);
5694
+ let output = table(headers, rows);
5695
+ if (mappings.length > 30) {
5696
+ output += `... and ${mappings.length - 30} more files.
5697
+
5698
+ `;
5699
+ }
5700
+ return output;
5701
+ }
5702
+ function generateTestStatistics(graph) {
5703
+ const testFiles = getTestFiles(graph);
5704
+ const allFiles = /* @__PURE__ */ new Set();
5705
+ const sourceFiles = [];
5706
+ graph.forEachNode((node, attrs) => {
5707
+ allFiles.add(attrs.filePath);
5708
+ });
5709
+ for (const file of allFiles) {
5710
+ if (!isTestFile(file)) {
5711
+ sourceFiles.push(file);
5712
+ }
5713
+ }
5714
+ const testedFiles = /* @__PURE__ */ new Set();
5715
+ for (const testFile of testFiles) {
5716
+ const sourceFile = matchTestToSource(testFile.filePath);
5717
+ if (sourceFile && allFiles.has(sourceFile)) {
5718
+ testedFiles.add(sourceFile);
5719
+ }
5720
+ }
5721
+ let output = "";
5722
+ output += `- **Total test files:** ${formatNumber(testFiles.length)}
5723
+ `;
5724
+ output += `- **Total source files:** ${formatNumber(sourceFiles.length)}
5725
+ `;
5726
+ output += `- **Source files with tests:** ${formatNumber(testedFiles.size)} (${formatPercent(testedFiles.size, sourceFiles.length)})
5727
+ `;
5728
+ output += `- **Source files without tests:** ${formatNumber(sourceFiles.length - testedFiles.size)} (${formatPercent(sourceFiles.length - testedFiles.size, sourceFiles.length)})
5729
+ `;
5730
+ const dirTestCoverage = /* @__PURE__ */ new Map();
5731
+ for (const sourceFile of sourceFiles) {
5732
+ const dir = dirname10(sourceFile).split("/")[0];
5733
+ if (!dirTestCoverage.has(dir)) {
5734
+ dirTestCoverage.set(dir, { total: 0, tested: 0 });
5735
+ }
5736
+ dirTestCoverage.get(dir).total++;
5737
+ if (testedFiles.has(sourceFile)) {
5738
+ dirTestCoverage.get(dir).tested++;
5739
+ }
5740
+ }
5741
+ if (dirTestCoverage.size > 1) {
5742
+ output += "\n**Coverage by directory:**\n\n";
5743
+ const sortedDirs = Array.from(dirTestCoverage.entries()).sort((a, b) => b[1].total - a[1].total);
5744
+ for (const [dir, coverage] of sortedDirs) {
5745
+ const percent = formatPercent(coverage.tested, coverage.total);
5746
+ output += `- **${dir}/**: ${coverage.tested}/${coverage.total} files (${percent})
5747
+ `;
5748
+ }
5749
+ }
5750
+ output += "\n";
5751
+ return output;
5752
+ }
5753
+
5754
+ // src/docs/history.ts
5755
+ import { dirname as dirname11 } from "path";
5756
+ import { execSync } from "child_process";
5757
+ function generateHistory(graph, projectRoot, version) {
5758
+ let output = "";
5759
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
5760
+ const fileCount = getFileCount9(graph);
5761
+ output += timestamp(version, now, fileCount, graph.order);
5762
+ output += header("Development History");
5763
+ output += "Git history combined with graph analysis showing feature evolution.\n\n";
5764
+ const hasGit = isGitAvailable(projectRoot);
5765
+ if (!hasGit) {
5766
+ output += "\u26A0\uFE0F **Git history not available.** This project is not a git repository or git is not installed.\n\n";
5767
+ output += "Showing graph-based analysis only:\n\n";
5768
+ }
5769
+ if (hasGit) {
5770
+ output += header("Development Timeline", 2);
5771
+ output += generateDevelopmentTimeline(projectRoot);
5772
+ }
5773
+ if (hasGit) {
5774
+ output += header("File Change Frequency (Churn)", 2);
5775
+ output += generateFileChurn(projectRoot, graph);
5776
+ }
5777
+ if (hasGit) {
5778
+ output += header("Feature Timeline", 2);
5779
+ output += generateFeatureTimeline(projectRoot);
5780
+ }
5781
+ if (hasGit) {
5782
+ output += header("File Age Analysis", 2);
5783
+ output += generateFileAgeAnalysis(projectRoot, graph);
5784
+ }
5785
+ if (hasGit) {
5786
+ output += header("Contributors", 2);
5787
+ output += generateContributors(projectRoot);
5788
+ }
5789
+ output += header("Feature Clusters (Graph-Based)", 2);
5790
+ output += generateFeatureClusters(graph);
5791
+ return output;
5792
+ }
5793
+ function getFileCount9(graph) {
5794
+ const files = /* @__PURE__ */ new Set();
5795
+ graph.forEachNode((node, attrs) => {
5796
+ files.add(attrs.filePath);
5797
+ });
5798
+ return files.size;
5799
+ }
5800
+ function isGitAvailable(projectRoot) {
5801
+ try {
5802
+ execSync("git rev-parse --git-dir", {
5803
+ cwd: projectRoot,
5804
+ encoding: "utf-8",
5805
+ timeout: 5e3,
5806
+ stdio: "pipe"
5807
+ });
5808
+ return true;
5809
+ } catch {
5810
+ return false;
5811
+ }
5812
+ }
5813
+ function executeGitCommand(projectRoot, command) {
5814
+ try {
5815
+ return execSync(command, {
5816
+ cwd: projectRoot,
5817
+ encoding: "utf-8",
5818
+ timeout: 1e4,
5819
+ stdio: "pipe"
5820
+ }).trim();
5821
+ } catch {
5822
+ return "";
5823
+ }
5824
+ }
5825
+ function generateDevelopmentTimeline(projectRoot) {
5826
+ const log = executeGitCommand(projectRoot, 'git log --format="%ai" --all --no-merges');
5827
+ if (!log) {
5828
+ return "Unable to retrieve git log.\n\n";
5829
+ }
5830
+ const dates = log.split("\n").filter((d) => d.length > 0);
5831
+ if (dates.length === 0) {
5832
+ return "No commits found.\n\n";
5833
+ }
5834
+ const firstCommit = new Date(dates[dates.length - 1]);
5835
+ const lastCommit = new Date(dates[0]);
5836
+ const ageInDays = Math.floor((lastCommit.getTime() - firstCommit.getTime()) / (1e3 * 60 * 60 * 24));
5837
+ const ageInMonths = Math.floor(ageInDays / 30);
5838
+ let output = "";
5839
+ output += `- **First commit:** ${firstCommit.toISOString().split("T")[0]}
5840
+ `;
5841
+ output += `- **Last commit:** ${lastCommit.toISOString().split("T")[0]}
5842
+ `;
5843
+ output += `- **Project age:** ${ageInMonths} months (${ageInDays} days)
5844
+ `;
5845
+ output += `- **Total commits:** ${formatNumber(dates.length)}
5846
+ `;
5847
+ const commitsPerMonth = ageInMonths > 0 ? (dates.length / ageInMonths).toFixed(1) : dates.length.toString();
5848
+ output += `- **Average activity:** ${commitsPerMonth} commits/month
5849
+ `;
5850
+ output += "\n";
5851
+ return output;
5852
+ }
5853
+ function generateFileChurn(projectRoot, graph) {
5854
+ const churnOutput = executeGitCommand(
5855
+ projectRoot,
5856
+ 'git log --all --name-only --format="" | sort | uniq -c | sort -rn | head -20'
5857
+ );
5858
+ if (!churnOutput) {
5859
+ return "Unable to retrieve file churn data.\n\n";
5860
+ }
5861
+ const lines = churnOutput.split("\n").filter((l) => l.trim().length > 0);
5862
+ if (lines.length === 0) {
5863
+ return "No file churn data available.\n\n";
5864
+ }
5865
+ const churnData = [];
5866
+ for (const line of lines) {
5867
+ const match = line.trim().match(/^(\d+)\s+(.+)$/);
5868
+ if (match) {
5869
+ const changes = parseInt(match[1], 10);
5870
+ const file = match[2].trim();
5871
+ if (file && file.length > 0 && !file.startsWith(".")) {
5872
+ churnData.push({ file, changes });
5873
+ }
5874
+ }
5875
+ }
5876
+ if (churnData.length === 0) {
5877
+ return "No valid file churn data.\n\n";
5878
+ }
5879
+ const fileConnections = /* @__PURE__ */ new Map();
5880
+ graph.forEachEdge((edge, attrs, source, target) => {
5881
+ const sourceAttrs = graph.getNodeAttributes(source);
5882
+ const targetAttrs = graph.getNodeAttributes(target);
5883
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
5884
+ fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
5885
+ fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
5886
+ }
5887
+ });
5888
+ let output = "Top 20 most-changed files:\n\n";
5889
+ const headers = ["File", "Changes", "Connections", "Risk"];
5890
+ const rows = churnData.slice(0, 20).map((item) => {
5891
+ const connections = fileConnections.get(item.file) || 0;
5892
+ let risk = "\u{1F7E2} Low";
5893
+ if (item.changes > 50 && connections > 10) {
5894
+ risk = "\u{1F534} High";
5895
+ } else if (item.changes > 20 && connections > 5) {
5896
+ risk = "\u{1F7E1} Medium";
5897
+ } else if (item.changes > 50 || connections > 10) {
5898
+ risk = "\u{1F7E1} Medium";
5899
+ }
5900
+ return [
5901
+ `\`${item.file}\``,
5902
+ formatNumber(item.changes),
5903
+ formatNumber(connections),
5904
+ risk
5905
+ ];
5906
+ });
5907
+ output += table(headers, rows);
5908
+ output += "**Risk levels:**\n\n";
5909
+ output += "- \u{1F534} High churn + high connections = risky hotspot (break often, affect many)\n";
5910
+ output += "- \u{1F7E1} High churn + low connections = actively developed but isolated\n";
5911
+ output += "- \u{1F7E2} Low churn + high connections = stable foundation\n\n";
5912
+ return output;
5913
+ }
5914
+ function generateFeatureTimeline(projectRoot) {
5915
+ const log = executeGitCommand(projectRoot, "git log --oneline --all --no-merges");
5916
+ if (!log) {
5917
+ return "Unable to retrieve commit log.\n\n";
5918
+ }
5919
+ const commits = log.split("\n").filter((c) => c.length > 0);
5920
+ if (commits.length === 0) {
5921
+ return "No commits found.\n\n";
5922
+ }
5923
+ const categories = {
5924
+ features: 0,
5925
+ fixes: 0,
5926
+ refactors: 0,
5927
+ other: 0
5928
+ };
5929
+ const featureKeywords = ["feat", "add", "new", "implement", "create"];
5930
+ const fixKeywords = ["fix", "bug", "patch", "resolve"];
5931
+ const refactorKeywords = ["refactor", "cleanup", "restructure", "improve"];
5932
+ for (const commit of commits) {
5933
+ const messageLower = commit.toLowerCase();
5934
+ if (featureKeywords.some((kw) => messageLower.includes(kw))) {
5935
+ categories.features++;
5936
+ } else if (fixKeywords.some((kw) => messageLower.includes(kw))) {
5937
+ categories.fixes++;
5938
+ } else if (refactorKeywords.some((kw) => messageLower.includes(kw))) {
5939
+ categories.refactors++;
5940
+ } else {
5941
+ categories.other++;
5942
+ }
5943
+ }
5944
+ let output = "Commit breakdown by type:\n\n";
5945
+ output += `- **Features:** ${formatNumber(categories.features)} commits (${(categories.features / commits.length * 100).toFixed(1)}%)
5946
+ `;
5947
+ output += `- **Bug fixes:** ${formatNumber(categories.fixes)} commits (${(categories.fixes / commits.length * 100).toFixed(1)}%)
5948
+ `;
5949
+ output += `- **Refactors:** ${formatNumber(categories.refactors)} commits (${(categories.refactors / commits.length * 100).toFixed(1)}%)
5950
+ `;
5951
+ output += `- **Other:** ${formatNumber(categories.other)} commits (${(categories.other / commits.length * 100).toFixed(1)}%)
5952
+ `;
5953
+ output += "\n";
5954
+ return output;
5955
+ }
5956
+ function generateFileAgeAnalysis(projectRoot, graph) {
5957
+ const files = /* @__PURE__ */ new Set();
5958
+ graph.forEachNode((node, attrs) => {
5959
+ files.add(attrs.filePath);
5960
+ });
5961
+ if (files.size === 0) {
5962
+ return "No files to analyze.\n\n";
5963
+ }
5964
+ const fileAges = [];
5965
+ const sampleFiles = Array.from(files).slice(0, 20);
5966
+ for (const file of sampleFiles) {
5967
+ const dateStr = executeGitCommand(
5968
+ projectRoot,
5969
+ `git log --format="%ai" --diff-filter=A -- "${file}" | tail -1`
5970
+ );
5971
+ if (dateStr) {
5972
+ fileAges.push({
5973
+ file,
5974
+ date: new Date(dateStr)
5975
+ });
5976
+ }
5977
+ }
5978
+ if (fileAges.length === 0) {
5979
+ return "Unable to determine file ages.\n\n";
5980
+ }
5981
+ fileAges.sort((a, b) => a.date.getTime() - b.date.getTime());
5982
+ let output = "";
5983
+ output += "**Oldest files (foundation):**\n\n";
5984
+ const oldest = fileAges.slice(0, 5);
5985
+ output += unorderedList(oldest.map((f) => {
5986
+ return `${code(f.file)} \u2014 added ${f.date.toISOString().split("T")[0]}`;
5987
+ }));
5988
+ output += "**Newest files (recent features):**\n\n";
5989
+ const newest = fileAges.slice(-5).reverse();
5990
+ output += unorderedList(newest.map((f) => {
5991
+ return `${code(f.file)} \u2014 added ${f.date.toISOString().split("T")[0]}`;
5992
+ }));
5993
+ return output;
5994
+ }
5995
+ function generateContributors(projectRoot) {
5996
+ const contributors = executeGitCommand(projectRoot, "git shortlog -sn --all");
5997
+ if (!contributors) {
5998
+ return "Unable to retrieve contributor data.\n\n";
5999
+ }
6000
+ const lines = contributors.split("\n").filter((l) => l.trim().length > 0);
6001
+ if (lines.length === 0) {
6002
+ return "No contributors found.\n\n";
6003
+ }
6004
+ let output = `Found ${lines.length} contributor${lines.length === 1 ? "" : "s"}:
6005
+
6006
+ `;
6007
+ const headers = ["Contributor", "Commits", "Percentage"];
6008
+ const contributorData = [];
6009
+ let totalCommits = 0;
6010
+ for (const line of lines) {
6011
+ const match = line.trim().match(/^(\d+)\s+(.+)$/);
6012
+ if (match) {
6013
+ const commits = parseInt(match[1], 10);
6014
+ const name = match[2].trim();
6015
+ contributorData.push({ name, commits });
6016
+ totalCommits += commits;
6017
+ }
6018
+ }
6019
+ const rows = contributorData.slice(0, 10).map((c) => [
6020
+ c.name,
6021
+ formatNumber(c.commits),
6022
+ `${(c.commits / totalCommits * 100).toFixed(1)}%`
6023
+ ]);
6024
+ output += table(headers, rows);
6025
+ if (contributorData.length > 10) {
6026
+ output += `... and ${contributorData.length - 10} more contributors.
6027
+
6028
+ `;
6029
+ }
6030
+ return output;
6031
+ }
6032
+ function generateFeatureClusters(graph) {
6033
+ const dirFiles = /* @__PURE__ */ new Map();
6034
+ const fileEdges = /* @__PURE__ */ new Map();
6035
+ graph.forEachNode((node, attrs) => {
6036
+ const dir = dirname11(attrs.filePath);
6037
+ if (!dirFiles.has(dir)) {
6038
+ dirFiles.set(dir, /* @__PURE__ */ new Set());
6039
+ }
6040
+ dirFiles.get(dir).add(attrs.filePath);
6041
+ });
6042
+ graph.forEachEdge((edge, attrs, source, target) => {
6043
+ const sourceFile = graph.getNodeAttributes(source).filePath;
6044
+ const targetFile = graph.getNodeAttributes(target).filePath;
6045
+ if (sourceFile !== targetFile) {
6046
+ if (!fileEdges.has(sourceFile)) {
6047
+ fileEdges.set(sourceFile, /* @__PURE__ */ new Set());
6048
+ }
6049
+ fileEdges.get(sourceFile).add(targetFile);
6050
+ }
6051
+ });
6052
+ const clusters = [];
6053
+ for (const [dir, files] of dirFiles.entries()) {
6054
+ if (dir === "." || files.size < 2) continue;
6055
+ const fileArray = Array.from(files);
6056
+ let internalEdgeCount = 0;
6057
+ for (const file of fileArray) {
6058
+ const targets = fileEdges.get(file);
6059
+ if (targets) {
6060
+ for (const target of targets) {
6061
+ if (files.has(target)) {
6062
+ internalEdgeCount++;
6063
+ }
6064
+ }
6065
+ }
6066
+ }
6067
+ if (internalEdgeCount >= 2) {
6068
+ const clusterName = inferClusterName2(fileArray, dir);
6069
+ clusters.push({
6070
+ name: clusterName,
6071
+ files: fileArray,
6072
+ internalEdges: internalEdgeCount
6073
+ });
6074
+ }
6075
+ }
6076
+ if (clusters.length === 0) {
6077
+ return "No distinct feature clusters detected.\n\n";
6078
+ }
6079
+ clusters.sort((a, b) => b.internalEdges - a.internalEdges);
6080
+ let output = `Detected ${clusters.length} feature cluster${clusters.length === 1 ? "" : "s"} (tightly-connected file groups):
6081
+
6082
+ `;
6083
+ for (const cluster of clusters.slice(0, 10)) {
6084
+ output += `**${cluster.name}** (${cluster.files.length} files, ${cluster.internalEdges} internal connections):
6085
+
6086
+ `;
6087
+ const items = cluster.files.slice(0, 5).map((f) => code(f));
6088
+ output += unorderedList(items);
6089
+ if (cluster.files.length > 5) {
6090
+ output += `... and ${cluster.files.length - 5} more files.
6091
+
6092
+ `;
6093
+ }
6094
+ }
6095
+ return output;
6096
+ }
6097
+ function inferClusterName2(files, dir) {
6098
+ const words = /* @__PURE__ */ new Map();
6099
+ for (const file of files) {
6100
+ const fileName = file.toLowerCase();
6101
+ const parts = fileName.split(/[\/\-\_\.]/).filter((p) => p.length > 3);
6102
+ for (const part of parts) {
6103
+ words.set(part, (words.get(part) || 0) + 1);
6104
+ }
6105
+ }
6106
+ const sortedWords = Array.from(words.entries()).sort((a, b) => b[1] - a[1]);
6107
+ if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
6108
+ return capitalizeFirst3(sortedWords[0][0]);
6109
+ }
6110
+ const dirName = dir.split("/").pop() || "Core";
6111
+ return capitalizeFirst3(dirName);
6112
+ }
6113
+ function capitalizeFirst3(str) {
6114
+ return str.charAt(0).toUpperCase() + str.slice(1);
6115
+ }
6116
+
6117
+ // src/docs/current.ts
6118
+ import { dirname as dirname12 } from "path";
6119
+ function generateCurrent(graph, projectRoot, version) {
6120
+ let output = "";
6121
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6122
+ const fileCount = getFileCount10(graph);
6123
+ output += timestamp(version, now, fileCount, graph.order);
6124
+ output += header("Complete Codebase Snapshot");
6125
+ output += "> **Note:** This is a complete snapshot of the entire codebase. For a high-level overview, see ARCHITECTURE.md.\n\n";
6126
+ output += header("Project Overview", 2);
6127
+ output += generateProjectOverview(graph);
6128
+ output += header("Complete File Index", 2);
6129
+ output += generateCompleteFileIndex(graph);
6130
+ output += header("Complete Symbol Index", 2);
6131
+ output += generateCompleteSymbolIndex(graph);
6132
+ output += header("Complete Edge List", 2);
6133
+ output += generateCompleteEdgeList(graph);
6134
+ output += header("Connection Matrix", 2);
6135
+ output += generateConnectionMatrix(graph);
6136
+ return output;
6137
+ }
6138
+ function getFileCount10(graph) {
6139
+ const files = /* @__PURE__ */ new Set();
6140
+ graph.forEachNode((node, attrs) => {
6141
+ files.add(attrs.filePath);
6142
+ });
6143
+ return files.size;
6144
+ }
6145
+ function getLanguageStats3(graph) {
6146
+ const stats = {};
6147
+ const files = /* @__PURE__ */ new Set();
6148
+ graph.forEachNode((node, attrs) => {
6149
+ if (!files.has(attrs.filePath)) {
6150
+ files.add(attrs.filePath);
6151
+ const ext = attrs.filePath.toLowerCase();
6152
+ let lang;
6153
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
6154
+ lang = "TypeScript";
6155
+ } else if (ext.endsWith(".py")) {
6156
+ lang = "Python";
6157
+ } else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
6158
+ lang = "JavaScript";
6159
+ } else if (ext.endsWith(".go")) {
6160
+ lang = "Go";
6161
+ } else {
6162
+ lang = "Other";
6163
+ }
6164
+ stats[lang] = (stats[lang] || 0) + 1;
6165
+ }
6166
+ });
6167
+ return stats;
6168
+ }
6169
+ function generateProjectOverview(graph) {
6170
+ const fileCount = getFileCount10(graph);
6171
+ const symbolCount = graph.order;
6172
+ const edgeCount = graph.size;
6173
+ const languages2 = getLanguageStats3(graph);
6174
+ let output = "";
6175
+ output += `- **Total files:** ${formatNumber(fileCount)}
6176
+ `;
6177
+ output += `- **Total symbols:** ${formatNumber(symbolCount)}
6178
+ `;
6179
+ output += `- **Total edges:** ${formatNumber(edgeCount)}
6180
+ `;
6181
+ if (Object.keys(languages2).length > 0) {
6182
+ output += "\n**Language breakdown:**\n\n";
6183
+ for (const [lang, count] of Object.entries(languages2).sort((a, b) => b[1] - a[1])) {
6184
+ output += `- ${lang}: ${count} files
6185
+ `;
6186
+ }
6187
+ }
6188
+ output += "\n";
6189
+ return output;
6190
+ }
6191
+ function getFileInfo(graph) {
6192
+ const fileMap = /* @__PURE__ */ new Map();
6193
+ graph.forEachNode((node, attrs) => {
6194
+ if (!fileMap.has(attrs.filePath)) {
6195
+ fileMap.set(attrs.filePath, {
6196
+ filePath: attrs.filePath,
6197
+ language: getLanguageFromPath3(attrs.filePath),
6198
+ symbols: [],
6199
+ importsFrom: [],
6200
+ importedBy: [],
6201
+ incomingEdges: 0,
6202
+ outgoingEdges: 0
6203
+ });
6204
+ }
6205
+ const info = fileMap.get(attrs.filePath);
6206
+ if (attrs.name !== "__file__") {
6207
+ info.symbols.push({
6208
+ name: attrs.name,
6209
+ kind: attrs.kind,
6210
+ line: attrs.startLine
6211
+ });
6212
+ }
6213
+ });
6214
+ const fileEdges = /* @__PURE__ */ new Map();
6215
+ const fileEdgesReverse = /* @__PURE__ */ new Map();
6216
+ graph.forEachEdge((edge, attrs, source, target) => {
6217
+ const sourceAttrs = graph.getNodeAttributes(source);
6218
+ const targetAttrs = graph.getNodeAttributes(target);
6219
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
6220
+ if (!fileEdges.has(sourceAttrs.filePath)) {
6221
+ fileEdges.set(sourceAttrs.filePath, /* @__PURE__ */ new Set());
6222
+ }
6223
+ fileEdges.get(sourceAttrs.filePath).add(targetAttrs.filePath);
6224
+ if (!fileEdgesReverse.has(targetAttrs.filePath)) {
6225
+ fileEdgesReverse.set(targetAttrs.filePath, /* @__PURE__ */ new Set());
6226
+ }
6227
+ fileEdgesReverse.get(targetAttrs.filePath).add(sourceAttrs.filePath);
6228
+ }
6229
+ });
6230
+ for (const [filePath, info] of fileMap.entries()) {
6231
+ const importsFrom = fileEdges.get(filePath);
6232
+ const importedBy = fileEdgesReverse.get(filePath);
6233
+ info.importsFrom = importsFrom ? Array.from(importsFrom) : [];
6234
+ info.importedBy = importedBy ? Array.from(importedBy) : [];
6235
+ info.outgoingEdges = info.importsFrom.length;
6236
+ info.incomingEdges = info.importedBy.length;
6237
+ }
6238
+ return Array.from(fileMap.values());
6239
+ }
6240
+ function getLanguageFromPath3(filePath) {
6241
+ const ext = filePath.toLowerCase();
6242
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
6243
+ if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
6244
+ if (ext.endsWith(".py")) return "Python";
6245
+ if (ext.endsWith(".go")) return "Go";
6246
+ return "Other";
6247
+ }
6248
+ function generateCompleteFileIndex(graph) {
6249
+ const fileInfos = getFileInfo(graph);
6250
+ if (fileInfos.length === 0) {
6251
+ return "No files detected.\n\n";
6252
+ }
6253
+ fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
6254
+ const dirGroups = /* @__PURE__ */ new Map();
6255
+ for (const info of fileInfos) {
6256
+ const dir = dirname12(info.filePath);
6257
+ const topDir = dir === "." ? "root" : dir.split("/")[0];
6258
+ if (!dirGroups.has(topDir)) {
6259
+ dirGroups.set(topDir, []);
6260
+ }
6261
+ dirGroups.get(topDir).push(info);
6262
+ }
6263
+ let output = "";
6264
+ for (const [dir, files] of Array.from(dirGroups.entries()).sort()) {
6265
+ output += header(dir === "root" ? "Root Directory" : `${dir}/`, 3);
6266
+ for (const file of files) {
6267
+ output += header(file.filePath, 4);
6268
+ output += `- **Language:** ${file.language}
6269
+ `;
6270
+ output += `- **Symbols (${file.symbols.length}):** `;
6271
+ if (file.symbols.length === 0) {
6272
+ output += "None\n";
6273
+ } else if (file.symbols.length <= 10) {
6274
+ output += file.symbols.map((s) => s.name).join(", ") + "\n";
6275
+ } else {
6276
+ output += file.symbols.slice(0, 10).map((s) => s.name).join(", ");
6277
+ output += `, ... and ${file.symbols.length - 10} more
6278
+ `;
6279
+ }
6280
+ if (file.importsFrom.length > 0) {
6281
+ output += `- **Imports from (${file.importsFrom.length}):** `;
6282
+ if (file.importsFrom.length <= 5) {
6283
+ output += file.importsFrom.map((f) => code(f)).join(", ") + "\n";
6284
+ } else {
6285
+ output += file.importsFrom.slice(0, 5).map((f) => code(f)).join(", ");
6286
+ output += `, ... and ${file.importsFrom.length - 5} more
6287
+ `;
6288
+ }
6289
+ }
6290
+ if (file.importedBy.length > 0) {
6291
+ output += `- **Imported by (${file.importedBy.length}):** `;
6292
+ if (file.importedBy.length <= 5) {
6293
+ output += file.importedBy.map((f) => code(f)).join(", ") + "\n";
6294
+ } else {
6295
+ output += file.importedBy.slice(0, 5).map((f) => code(f)).join(", ");
6296
+ output += `, ... and ${file.importedBy.length - 5} more
6297
+ `;
6298
+ }
6299
+ }
6300
+ output += `- **Connections:** ${file.incomingEdges} inbound, ${file.outgoingEdges} outbound
6301
+
6302
+ `;
6303
+ }
6304
+ }
6305
+ return output;
6306
+ }
6307
+ function generateCompleteSymbolIndex(graph) {
6308
+ const symbolsByKind = /* @__PURE__ */ new Map();
6309
+ graph.forEachNode((node, attrs) => {
6310
+ if (attrs.name === "__file__") return;
6311
+ if (!symbolsByKind.has(attrs.kind)) {
6312
+ symbolsByKind.set(attrs.kind, []);
6313
+ }
6314
+ symbolsByKind.get(attrs.kind).push({
6315
+ name: attrs.name,
6316
+ filePath: attrs.filePath,
6317
+ line: attrs.startLine
6318
+ });
6319
+ });
6320
+ if (symbolsByKind.size === 0) {
6321
+ return "No symbols detected.\n\n";
6322
+ }
6323
+ let output = "";
6324
+ const sortedKinds = Array.from(symbolsByKind.entries()).sort((a, b) => a[0].localeCompare(b[0]));
6325
+ for (const [kind, symbols] of sortedKinds) {
6326
+ output += header(`${capitalizeKind2(kind)}s (${symbols.length})`, 3);
6327
+ const sorted = symbols.sort((a, b) => a.name.localeCompare(b.name));
6328
+ const limit = 100;
6329
+ const items = sorted.slice(0, limit).map((s) => {
6330
+ return `${code(s.name)} \u2014 ${code(s.filePath)}:${s.line}`;
6331
+ });
6332
+ output += unorderedList(items);
6333
+ if (symbols.length > limit) {
6334
+ output += `... and ${symbols.length - limit} more.
6335
+
6336
+ `;
6337
+ }
6338
+ }
6339
+ return output;
6340
+ }
6341
+ function capitalizeKind2(kind) {
6342
+ const map = {
6343
+ function: "Function",
6344
+ class: "Class",
6345
+ variable: "Variable",
6346
+ constant: "Constant",
6347
+ type_alias: "Type",
6348
+ interface: "Interface",
6349
+ enum: "Enum",
6350
+ import: "Import",
6351
+ export: "Export",
6352
+ method: "Method",
6353
+ property: "Property",
6354
+ decorator: "Decorator",
6355
+ module: "Module"
6356
+ };
6357
+ return map[kind] || kind;
6358
+ }
6359
+ function generateCompleteEdgeList(graph) {
6360
+ const fileEdges = /* @__PURE__ */ new Map();
6361
+ graph.forEachEdge((edge, attrs, source, target) => {
6362
+ const sourceAttrs = graph.getNodeAttributes(source);
6363
+ const targetAttrs = graph.getNodeAttributes(target);
6364
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
6365
+ if (!fileEdges.has(sourceAttrs.filePath)) {
6366
+ fileEdges.set(sourceAttrs.filePath, []);
6367
+ }
6368
+ const edgeDesc = `${sourceAttrs.filePath} \u2192 ${targetAttrs.filePath}`;
6369
+ if (!fileEdges.get(sourceAttrs.filePath).includes(edgeDesc)) {
6370
+ fileEdges.get(sourceAttrs.filePath).push(edgeDesc);
6371
+ }
6372
+ }
6373
+ });
6374
+ if (fileEdges.size === 0) {
6375
+ return "No cross-file edges detected.\n\n";
6376
+ }
6377
+ let output = `Total cross-file edges: ${graph.size}
6378
+
6379
+ `;
6380
+ const sortedEdges = Array.from(fileEdges.entries()).sort((a, b) => a[0].localeCompare(b[0]));
6381
+ const limit = 50;
6382
+ for (const [sourceFile, edges] of sortedEdges.slice(0, limit)) {
6383
+ output += header(sourceFile, 3);
6384
+ output += unorderedList(edges.map((e) => e.replace(`${sourceFile} \u2192 `, "")));
6385
+ }
6386
+ if (sortedEdges.length > limit) {
6387
+ output += `... and ${sortedEdges.length - limit} more source files with edges.
6388
+
6389
+ `;
6390
+ }
6391
+ return output;
6392
+ }
6393
+ function generateConnectionMatrix(graph) {
6394
+ const dirEdges = /* @__PURE__ */ new Map();
6395
+ const allDirs = /* @__PURE__ */ new Set();
6396
+ graph.forEachEdge((edge, attrs, source, target) => {
6397
+ const sourceAttrs = graph.getNodeAttributes(source);
6398
+ const targetAttrs = graph.getNodeAttributes(target);
6399
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
6400
+ const sourceDir = getTopLevelDir2(sourceAttrs.filePath);
6401
+ const targetDir = getTopLevelDir2(targetAttrs.filePath);
6402
+ if (sourceDir && targetDir) {
6403
+ allDirs.add(sourceDir);
6404
+ allDirs.add(targetDir);
6405
+ if (!dirEdges.has(sourceDir)) {
6406
+ dirEdges.set(sourceDir, /* @__PURE__ */ new Map());
6407
+ }
6408
+ const targetMap = dirEdges.get(sourceDir);
6409
+ targetMap.set(targetDir, (targetMap.get(targetDir) || 0) + 1);
6410
+ }
6411
+ }
6412
+ });
6413
+ if (allDirs.size === 0) {
6414
+ return "No directory structure detected.\n\n";
6415
+ }
6416
+ const sortedDirs = Array.from(allDirs).sort();
6417
+ let output = "Compact matrix showing which directories depend on which:\n\n";
6418
+ output += codeBlock(buildMatrixString(sortedDirs, dirEdges), "");
6419
+ return output;
6420
+ }
6421
+ function buildMatrixString(dirs, edges) {
6422
+ if (dirs.length === 0) return "No directories";
6423
+ let result = " ";
6424
+ for (const dir of dirs) {
6425
+ result += dir.padEnd(10, " ").substring(0, 10);
6426
+ }
6427
+ result += "\n";
6428
+ for (const sourceDir of dirs) {
6429
+ result += sourceDir.padEnd(10, " ").substring(0, 10) + " ";
6430
+ for (const targetDir of dirs) {
6431
+ if (sourceDir === targetDir) {
6432
+ result += "- ";
6433
+ } else {
6434
+ const count = edges.get(sourceDir)?.get(targetDir) || 0;
6435
+ if (count > 0) {
6436
+ result += "\u2192 ";
6437
+ } else {
6438
+ const reverseCount = edges.get(targetDir)?.get(sourceDir) || 0;
6439
+ if (reverseCount > 0) {
6440
+ result += "\u2190 ";
6441
+ } else {
6442
+ result += " ";
6443
+ }
6444
+ }
6445
+ }
6446
+ }
6447
+ result += "\n";
6448
+ }
6449
+ return result;
6450
+ }
6451
+ function getTopLevelDir2(filePath) {
6452
+ const parts = filePath.split("/");
6453
+ if (parts.length < 2) {
6454
+ return null;
6455
+ }
6456
+ if (parts[0] === "src" && parts.length >= 2) {
6457
+ return parts.length >= 3 ? `${parts[0]}/${parts[1]}` : parts[0];
6458
+ }
6459
+ const firstDir = parts[0];
6460
+ if (firstDir.includes("test") || firstDir.includes("__tests__") || firstDir === "node_modules" || firstDir === "dist" || firstDir === "build") {
6461
+ return null;
6462
+ }
6463
+ return parts[0];
6464
+ }
6465
+
6466
+ // src/docs/status.ts
6467
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
6468
+ import { join as join10 } from "path";
6469
+ function generateStatus(graph, projectRoot, version) {
6470
+ let output = "";
6471
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6472
+ const fileCount = getFileCount11(graph);
6473
+ output += timestamp(version, now, fileCount, graph.order);
6474
+ output += header("Project Status");
6475
+ output += "TODO/FIXME/HACK inventory showing what's implemented vs pending.\n\n";
6476
+ output += header("Status Summary", 2);
6477
+ output += generateStatusSummary(projectRoot, graph);
6478
+ output += header("TODOs by File", 2);
6479
+ output += generateTodosByFile(projectRoot, graph);
6480
+ output += header("FIXMEs (Urgent)", 2);
6481
+ output += generateFixmes(projectRoot, graph);
6482
+ output += header("HACKs (Technical Debt)", 2);
6483
+ output += generateHacks(projectRoot, graph);
6484
+ output += header("Priority Matrix", 2);
6485
+ output += generatePriorityMatrix(projectRoot, graph);
6486
+ output += header("Deprecated Items", 2);
6487
+ output += generateDeprecated(projectRoot, graph);
6488
+ output += header("Implementation Completeness", 2);
6489
+ output += generateCompleteness(projectRoot, graph);
6490
+ return output;
6491
+ }
6492
+ function getFileCount11(graph) {
6493
+ const files = /* @__PURE__ */ new Set();
6494
+ graph.forEachNode((node, attrs) => {
6495
+ files.add(attrs.filePath);
6496
+ });
6497
+ return files.size;
6498
+ }
6499
+ function extractComments(projectRoot, filePath) {
6500
+ const comments = [];
6501
+ const fullPath = join10(projectRoot, filePath);
6502
+ if (!existsSync7(fullPath)) {
6503
+ return comments;
6504
+ }
6505
+ try {
6506
+ const content = readFileSync5(fullPath, "utf-8");
6507
+ const lines = content.split("\n");
6508
+ const patterns = [
6509
+ { type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
6510
+ { type: "FIXME", regex: /(?:\/\/|#|\/\*)\s*FIXME:?\s*(.+)/i },
6511
+ { type: "HACK", regex: /(?:\/\/|#|\/\*)\s*HACK:?\s*(.+)/i },
6512
+ { type: "XXX", regex: /(?:\/\/|#|\/\*)\s*XXX:?\s*(.+)/i },
6513
+ { type: "NOTE", regex: /(?:\/\/|#|\/\*)\s*NOTE:?\s*(.+)/i },
6514
+ { type: "OPTIMIZE", regex: /(?:\/\/|#|\/\*)\s*OPTIMIZE:?\s*(.+)/i },
6515
+ { type: "DEPRECATED", regex: /(?:\/\/|#|\/\*)\s*DEPRECATED:?\s*(.+)/i }
6516
+ ];
6517
+ for (let i = 0; i < lines.length; i++) {
6518
+ const line = lines[i];
6519
+ for (const pattern of patterns) {
6520
+ const match = line.match(pattern.regex);
6521
+ if (match) {
6522
+ comments.push({
6523
+ type: pattern.type,
6524
+ file: filePath,
6525
+ line: i + 1,
6526
+ text: match[1].trim().replace(/\*\/.*$/, "").trim()
6527
+ });
6528
+ break;
6529
+ }
6530
+ }
6531
+ }
6532
+ } catch (err) {
6533
+ return comments;
6534
+ }
6535
+ return comments;
6536
+ }
6537
+ function getAllComments(projectRoot, graph) {
6538
+ const allComments = [];
6539
+ const files = /* @__PURE__ */ new Set();
6540
+ graph.forEachNode((node, attrs) => {
6541
+ files.add(attrs.filePath);
6542
+ });
6543
+ for (const file of files) {
6544
+ const comments = extractComments(projectRoot, file);
6545
+ allComments.push(...comments);
6546
+ }
6547
+ return allComments;
6548
+ }
6549
+ function generateStatusSummary(projectRoot, graph) {
6550
+ const comments = getAllComments(projectRoot, graph);
6551
+ const counts = {
6552
+ TODO: 0,
6553
+ FIXME: 0,
6554
+ HACK: 0,
6555
+ XXX: 0,
6556
+ NOTE: 0,
6557
+ OPTIMIZE: 0,
6558
+ DEPRECATED: 0
6559
+ };
6560
+ for (const comment of comments) {
6561
+ counts[comment.type]++;
6562
+ }
6563
+ let output = "";
6564
+ output += `- **Total TODOs:** ${formatNumber(counts.TODO)}
6565
+ `;
6566
+ output += `- **Total FIXMEs:** ${formatNumber(counts.FIXME)}
6567
+ `;
6568
+ output += `- **Total HACKs:** ${formatNumber(counts.HACK)}
6569
+ `;
6570
+ if (counts.XXX > 0) {
6571
+ output += `- **Total XXXs:** ${formatNumber(counts.XXX)}
6572
+ `;
6573
+ }
6574
+ if (counts.NOTE > 0) {
6575
+ output += `- **Total NOTEs:** ${formatNumber(counts.NOTE)}
6576
+ `;
6577
+ }
6578
+ if (counts.OPTIMIZE > 0) {
6579
+ output += `- **Total OPTIMIZEs:** ${formatNumber(counts.OPTIMIZE)}
6580
+ `;
6581
+ }
6582
+ if (counts.DEPRECATED > 0) {
6583
+ output += `- **Total DEPRECATEDs:** ${formatNumber(counts.DEPRECATED)}
6584
+ `;
6585
+ }
6586
+ output += "\n";
6587
+ return output;
6588
+ }
6589
+ function generateTodosByFile(projectRoot, graph) {
6590
+ const comments = getAllComments(projectRoot, graph);
6591
+ const todos = comments.filter((c) => c.type === "TODO");
6592
+ if (todos.length === 0) {
6593
+ return "\u2705 No TODOs found.\n\n";
6594
+ }
6595
+ const fileGroups = /* @__PURE__ */ new Map();
6596
+ for (const todo of todos) {
6597
+ if (!fileGroups.has(todo.file)) {
6598
+ fileGroups.set(todo.file, []);
6599
+ }
6600
+ fileGroups.get(todo.file).push(todo);
6601
+ }
6602
+ let output = `Found ${todos.length} TODO${todos.length === 1 ? "" : "s"} across ${fileGroups.size} file${fileGroups.size === 1 ? "" : "s"}:
6603
+
6604
+ `;
6605
+ const sortedFiles = Array.from(fileGroups.entries()).sort((a, b) => a[0].localeCompare(b[0]));
6606
+ for (const [file, fileTodos] of sortedFiles) {
6607
+ output += header(file, 3);
6608
+ const items = fileTodos.map((t) => `[ ] TODO: ${t.text} (line ${t.line})`);
6609
+ output += unorderedList(items);
6610
+ }
6611
+ return output;
6612
+ }
6613
+ function generateFixmes(projectRoot, graph) {
6614
+ const comments = getAllComments(projectRoot, graph);
6615
+ const fixmes = comments.filter((c) => c.type === "FIXME");
6616
+ if (fixmes.length === 0) {
6617
+ return "\u2705 No FIXMEs found.\n\n";
6618
+ }
6619
+ let output = `\u26A0\uFE0F Found ${fixmes.length} FIXME${fixmes.length === 1 ? "" : "s"} (known broken or urgent issues):
6620
+
6621
+ `;
6622
+ fixmes.sort((a, b) => a.file.localeCompare(b.file));
6623
+ const items = fixmes.map((f) => {
6624
+ return `[ ] FIXME: ${f.text} (${code(f.file)}:${f.line})`;
6625
+ });
6626
+ output += unorderedList(items);
6627
+ return output;
6628
+ }
6629
+ function generateHacks(projectRoot, graph) {
6630
+ const comments = getAllComments(projectRoot, graph);
6631
+ const hacks = comments.filter((c) => c.type === "HACK");
6632
+ if (hacks.length === 0) {
6633
+ return "\u2705 No HACKs found.\n\n";
6634
+ }
6635
+ let output = `Found ${hacks.length} HACK${hacks.length === 1 ? "" : "s"} (technical debt - works but needs proper implementation):
6636
+
6637
+ `;
6638
+ hacks.sort((a, b) => a.file.localeCompare(b.file));
6639
+ const items = hacks.map((h) => {
6640
+ return `[ ] HACK: ${h.text} (${code(h.file)}:${h.line})`;
6641
+ });
6642
+ output += unorderedList(items);
6643
+ return output;
6644
+ }
6645
+ function generatePriorityMatrix(projectRoot, graph) {
6646
+ const comments = getAllComments(projectRoot, graph);
6647
+ const fileConnections = /* @__PURE__ */ new Map();
6648
+ graph.forEachEdge((edge, attrs, source, target) => {
6649
+ const sourceAttrs = graph.getNodeAttributes(source);
6650
+ const targetAttrs = graph.getNodeAttributes(target);
6651
+ if (sourceAttrs.filePath !== targetAttrs.filePath) {
6652
+ fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
6653
+ fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
6654
+ }
6655
+ });
6656
+ const items = [];
6657
+ for (const comment of comments) {
6658
+ if (comment.type === "TODO" || comment.type === "FIXME" || comment.type === "HACK") {
6659
+ const connections = fileConnections.get(comment.file) || 0;
6660
+ let priority = "Low";
6661
+ let priorityScore = 1;
6662
+ if (comment.type === "FIXME") {
6663
+ if (connections > 10) {
6664
+ priority = "\u{1F534} Critical";
6665
+ priorityScore = 4;
6666
+ } else if (connections > 5) {
6667
+ priority = "\u{1F7E1} High";
6668
+ priorityScore = 3;
6669
+ } else {
6670
+ priority = "\u{1F7E2} Medium";
6671
+ priorityScore = 2;
6672
+ }
6673
+ } else if (comment.type === "TODO") {
6674
+ if (connections > 10) {
6675
+ priority = "\u{1F7E1} High";
6676
+ priorityScore = 3;
6677
+ } else if (connections > 5) {
6678
+ priority = "\u{1F7E2} Medium";
6679
+ priorityScore = 2;
6680
+ } else {
6681
+ priority = "\u26AA Low";
6682
+ priorityScore = 1;
6683
+ }
6684
+ } else if (comment.type === "HACK") {
6685
+ if (connections > 10) {
6686
+ priority = "\u{1F7E1} High";
6687
+ priorityScore = 3;
6688
+ } else {
6689
+ priority = "\u{1F7E2} Medium";
6690
+ priorityScore = 2;
6691
+ }
6692
+ }
6693
+ items.push({
6694
+ comment,
6695
+ connections,
6696
+ priority
6697
+ });
6698
+ }
6699
+ }
6700
+ if (items.length === 0) {
6701
+ return "No items to prioritize.\n\n";
6702
+ }
6703
+ items.sort((a, b) => {
6704
+ const priorityOrder = { "\u{1F534} Critical": 4, "\u{1F7E1} High": 3, "\u{1F7E2} Medium": 2, "\u26AA Low": 1 };
6705
+ const aPriority = priorityOrder[a.priority] || 0;
6706
+ const bPriority = priorityOrder[b.priority] || 0;
6707
+ if (aPriority !== bPriority) {
6708
+ return bPriority - aPriority;
6709
+ }
6710
+ return b.connections - a.connections;
6711
+ });
6712
+ let output = "Items prioritized by type and file connections:\n\n";
6713
+ const headers = ["Type", "File", "Line", "Connections", "Priority"];
6714
+ const rows = items.slice(0, 20).map((item) => [
6715
+ item.comment.type,
6716
+ `\`${item.comment.file}\``,
6717
+ item.comment.line.toString(),
6718
+ formatNumber(item.connections),
6719
+ item.priority
6720
+ ]);
6721
+ output += table(headers, rows);
6722
+ if (items.length > 20) {
6723
+ output += `... and ${items.length - 20} more items.
6724
+
6725
+ `;
6726
+ }
6727
+ return output;
6728
+ }
6729
+ function generateDeprecated(projectRoot, graph) {
6730
+ const comments = getAllComments(projectRoot, graph);
6731
+ const deprecated = comments.filter((c) => c.type === "DEPRECATED");
6732
+ if (deprecated.length === 0) {
6733
+ return "\u2705 No deprecated items found.\n\n";
6734
+ }
6735
+ let output = `Found ${deprecated.length} deprecated item${deprecated.length === 1 ? "" : "s"}:
6736
+
6737
+ `;
6738
+ deprecated.sort((a, b) => a.file.localeCompare(b.file));
6739
+ const items = deprecated.map((d) => {
6740
+ return `DEPRECATED: ${d.text} (${code(d.file)}:${d.line})`;
6741
+ });
6742
+ output += unorderedList(items);
6743
+ return output;
6744
+ }
6745
+ function generateCompleteness(projectRoot, graph) {
6746
+ const comments = getAllComments(projectRoot, graph);
6747
+ const fileTodos = /* @__PURE__ */ new Map();
6748
+ const fileSymbols = /* @__PURE__ */ new Map();
6749
+ for (const comment of comments) {
6750
+ if (comment.type === "TODO") {
6751
+ fileTodos.set(comment.file, (fileTodos.get(comment.file) || 0) + 1);
6752
+ }
6753
+ }
6754
+ graph.forEachNode((node, attrs) => {
6755
+ fileSymbols.set(attrs.filePath, (fileSymbols.get(attrs.filePath) || 0) + 1);
6756
+ });
6757
+ const allFiles = /* @__PURE__ */ new Set();
6758
+ graph.forEachNode((node, attrs) => {
6759
+ allFiles.add(attrs.filePath);
6760
+ });
6761
+ const inProgress = [];
6762
+ const complete = [];
6763
+ for (const file of allFiles) {
6764
+ const todoCount = fileTodos.get(file) || 0;
6765
+ const symbolCount = fileSymbols.get(file) || 0;
6766
+ if (symbolCount === 0) continue;
6767
+ const todoRatio = todoCount / symbolCount;
6768
+ if (todoRatio > 0.1) {
6769
+ inProgress.push(file);
6770
+ } else if (todoCount === 0) {
6771
+ complete.push(file);
6772
+ }
6773
+ }
6774
+ let output = "";
6775
+ const totalFiles = allFiles.size;
6776
+ const completePercent = totalFiles > 0 ? (complete.length / totalFiles * 100).toFixed(1) : "0.0";
6777
+ output += `- **Complete files (no TODOs):** ${formatNumber(complete.length)} (${completePercent}%)
6778
+ `;
6779
+ output += `- **In-progress files (many TODOs):** ${formatNumber(inProgress.length)}
6780
+
6781
+ `;
6782
+ if (inProgress.length > 0) {
6783
+ output += "**Files in progress (high TODO ratio):**\n\n";
6784
+ const items = inProgress.slice(0, 10).map((f) => {
6785
+ const todoCount = fileTodos.get(f) || 0;
6786
+ return `${code(f)} (${todoCount} TODOs)`;
6787
+ });
6788
+ output += unorderedList(items);
6789
+ if (inProgress.length > 10) {
6790
+ output += `... and ${inProgress.length - 10} more.
6791
+
6792
+ `;
6793
+ }
6794
+ }
6795
+ const dirTodos = /* @__PURE__ */ new Map();
6796
+ const dirFiles = /* @__PURE__ */ new Map();
6797
+ for (const file of allFiles) {
6798
+ const dir = file.split("/")[0];
6799
+ dirFiles.set(dir, (dirFiles.get(dir) || 0) + 1);
6800
+ const todoCount = fileTodos.get(file) || 0;
6801
+ if (todoCount > 0) {
6802
+ dirTodos.set(dir, (dirTodos.get(dir) || 0) + 1);
6803
+ }
6804
+ }
6805
+ if (dirFiles.size > 1) {
6806
+ output += "**Completeness by directory:**\n\n";
6807
+ const sortedDirs = Array.from(dirFiles.entries()).sort((a, b) => b[1] - a[1]);
6808
+ for (const [dir, fileCount] of sortedDirs) {
6809
+ const todosInDir = dirTodos.get(dir) || 0;
6810
+ const completeInDir = fileCount - todosInDir;
6811
+ const percent = (completeInDir / fileCount * 100).toFixed(1);
6812
+ output += `- **${dir}/**: ${completeInDir}/${fileCount} files complete (${percent}%)
6813
+ `;
6814
+ }
6815
+ output += "\n";
6816
+ }
6817
+ return output;
6818
+ }
6819
+
6820
+ // src/docs/health.ts
6821
+ function generateHealth(graph, projectRoot, version) {
6822
+ let output = "";
6823
+ const report = calculateHealthScore(graph, projectRoot);
6824
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6825
+ const fileCount = getFileCount12(graph);
6826
+ output += timestamp(version, now, fileCount, graph.order);
6827
+ output += header("Dependency Health Score");
6828
+ output += "Analysis of dependency architecture quality across 6 dimensions.\n\n";
6829
+ output += header("Overall Score", 2);
6830
+ output += generateOverallScore(report);
6831
+ output += header("Dimension Breakdown", 2);
6832
+ output += generateDimensionsBreakdown(report.dimensions);
6833
+ output += header("Recommendations", 2);
6834
+ output += generateRecommendations2(report.recommendations);
6835
+ output += header("Historical Trend", 2);
6836
+ output += generateHistoricalTrend(projectRoot, report);
6837
+ output += header("Detailed Metrics", 2);
6838
+ output += generateDetailedMetrics(report.dimensions);
6839
+ return output;
6840
+ }
6841
+ function getFileCount12(graph) {
6842
+ const files = /* @__PURE__ */ new Set();
6843
+ graph.forEachNode((node, attrs) => {
6844
+ files.add(attrs.filePath);
6845
+ });
6846
+ return files.size;
6847
+ }
6848
+ function generateOverallScore(report) {
6849
+ let output = "";
6850
+ const gradeEmoji = {
6851
+ "A": "\u{1F7E2}",
6852
+ "B": "\u{1F535}",
6853
+ "C": "\u{1F7E1}",
6854
+ "D": "\u{1F7E0}",
6855
+ "F": "\u{1F534}"
6856
+ };
6857
+ output += `**Score:** ${report.overall}/100
6858
+
6859
+ `;
6860
+ output += `**Grade:** ${gradeEmoji[report.grade]} ${report.grade}
6861
+
6862
+ `;
6863
+ output += `**Summary:** ${report.summary}
6864
+
6865
+ `;
6866
+ output += `**Project Statistics:**
6867
+
6868
+ `;
6869
+ output += `- Files: ${formatNumber(report.projectStats.files)}
6870
+ `;
6871
+ output += `- Symbols: ${formatNumber(report.projectStats.symbols)}
6872
+ `;
6873
+ output += `- Edges: ${formatNumber(report.projectStats.edges)}
6874
+ `;
6875
+ const langs = Object.entries(report.projectStats.languages).sort((a, b) => b[1] - a[1]).map(([lang, count]) => `${lang} (${count})`).join(", ");
6876
+ output += `- Languages: ${langs}
6877
+
6878
+ `;
6879
+ return output;
6880
+ }
6881
+ function generateDimensionsBreakdown(dimensions) {
6882
+ let output = "";
6883
+ const headers = ["Dimension", "Score", "Grade", "Weight", "Details"];
6884
+ const rows = dimensions.map((d) => [
6885
+ d.name,
6886
+ `${d.score}/100`,
6887
+ d.grade,
6888
+ `${(d.weight * 100).toFixed(0)}%`,
6889
+ d.details
6890
+ ]);
6891
+ output += table(headers, rows);
6892
+ return output;
6893
+ }
6894
+ function generateRecommendations2(recommendations) {
6895
+ if (recommendations.length === 0) {
6896
+ return "\u2705 No critical issues detected.\n\n";
6897
+ }
6898
+ return unorderedList(recommendations);
6899
+ }
6900
+ function generateHistoricalTrend(projectRoot, currentReport) {
6901
+ const history = loadHealthHistory(projectRoot);
6902
+ if (history.length < 2) {
6903
+ return "No historical data available. Run `depwire health` regularly to track trends.\n\n";
6904
+ }
6905
+ let output = `Showing last ${Math.min(history.length, 10)} health checks:
6906
+
6907
+ `;
6908
+ const headers = ["Date", "Score", "Grade", "Trend"];
6909
+ const recent = history.slice(-10);
6910
+ const rows = recent.map((entry, idx) => {
6911
+ let trend = "\u2014";
6912
+ if (idx > 0) {
6913
+ const prev = recent[idx - 1];
6914
+ const delta = entry.score - prev.score;
6915
+ if (delta > 0) {
6916
+ trend = `\u2191 +${delta}`;
6917
+ } else if (delta < 0) {
6918
+ trend = `\u2193 ${delta}`;
6919
+ } else {
6920
+ trend = "\u2192 0";
6921
+ }
6922
+ }
6923
+ return [
6924
+ entry.timestamp.split("T")[0],
6925
+ entry.score.toString(),
6926
+ entry.grade,
6927
+ trend
6928
+ ];
6929
+ });
6930
+ output += table(headers, rows);
6931
+ const first = recent[0];
6932
+ const last = recent[recent.length - 1];
6933
+ const totalDelta = last.score - first.score;
6934
+ output += `
6935
+ **Trend:** `;
6936
+ if (totalDelta > 0) {
6937
+ output += `\u{1F4C8} Improved by ${totalDelta} points over ${recent.length} checks
6938
+
6939
+ `;
6940
+ } else if (totalDelta < 0) {
6941
+ output += `\u{1F4C9} Declined by ${Math.abs(totalDelta)} points over ${recent.length} checks
6942
+
6943
+ `;
6944
+ } else {
6945
+ output += `\u{1F4CA} Stable at ${last.score} points over ${recent.length} checks
6946
+
6947
+ `;
6948
+ }
6949
+ return output;
6950
+ }
6951
+ function generateDetailedMetrics(dimensions) {
6952
+ let output = "";
6953
+ for (const dim of dimensions) {
6954
+ output += header(dim.name, 3);
6955
+ output += `**Score:** ${dim.score}/100 (${dim.grade})
6956
+
6957
+ `;
6958
+ output += `**Details:** ${dim.details}
6959
+
6960
+ `;
6961
+ if (Object.keys(dim.metrics).length > 0) {
6962
+ output += `**Metrics:**
6963
+
6964
+ `;
6965
+ for (const [key, value] of Object.entries(dim.metrics)) {
6966
+ output += `- ${key}: ${typeof value === "number" ? formatNumber(value) : value}
6967
+ `;
6968
+ }
6969
+ output += "\n";
6970
+ }
6971
+ }
6972
+ return output;
6973
+ }
6974
+
6975
+ // src/docs/metadata.ts
6976
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
6977
+ import { join as join11 } from "path";
6978
+ function loadMetadata(outputDir) {
6979
+ const metadataPath = join11(outputDir, "metadata.json");
6980
+ if (!existsSync8(metadataPath)) {
6981
+ return null;
6982
+ }
6983
+ try {
6984
+ const content = readFileSync6(metadataPath, "utf-8");
6985
+ return JSON.parse(content);
6986
+ } catch (err) {
6987
+ console.error("Failed to load metadata:", err);
6988
+ return null;
6989
+ }
6990
+ }
6991
+ function saveMetadata(outputDir, metadata) {
6992
+ const metadataPath = join11(outputDir, "metadata.json");
6993
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
6994
+ }
6995
+ function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
6996
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6997
+ const documents = {};
6998
+ for (const docType of docTypes) {
6999
+ const fileName = docType === "architecture" ? "ARCHITECTURE.md" : docType === "conventions" ? "CONVENTIONS.md" : docType === "dependencies" ? "DEPENDENCIES.md" : docType === "onboarding" ? "ONBOARDING.md" : docType === "files" ? "FILES.md" : docType === "api_surface" ? "API_SURFACE.md" : docType === "errors" ? "ERRORS.md" : docType === "tests" ? "TESTS.md" : docType === "history" ? "HISTORY.md" : docType === "current" ? "CURRENT.md" : docType === "status" ? "STATUS.md" : docType === "health" ? "HEALTH.md" : `${docType.toUpperCase()}.md`;
7000
+ documents[docType] = {
7001
+ generated_at: now,
7002
+ file: fileName
7003
+ };
7004
+ }
7005
+ return {
7006
+ version,
7007
+ generated_at: now,
7008
+ project_path: projectPath,
7009
+ file_count: fileCount,
7010
+ symbol_count: symbolCount,
7011
+ edge_count: edgeCount,
7012
+ documents
7013
+ };
7014
+ }
7015
+ function updateMetadata(existing, docTypes, fileCount, symbolCount, edgeCount) {
7016
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7017
+ for (const docType of docTypes) {
7018
+ if (existing.documents[docType]) {
7019
+ existing.documents[docType].generated_at = now;
7020
+ }
7021
+ }
7022
+ existing.file_count = fileCount;
7023
+ existing.symbol_count = symbolCount;
7024
+ existing.edge_count = edgeCount;
7025
+ existing.generated_at = now;
7026
+ return existing;
7027
+ }
7028
+
7029
+ // src/docs/generator.ts
7030
+ async function generateDocs(graph, projectRoot, version, parseTime, options) {
7031
+ const startTime = Date.now();
7032
+ const generated = [];
7033
+ const errors = [];
7034
+ try {
7035
+ if (!existsSync9(options.outputDir)) {
7036
+ mkdirSync(options.outputDir, { recursive: true });
7037
+ if (options.verbose) {
7038
+ console.log(`Created output directory: ${options.outputDir}`);
7039
+ }
4271
7040
  }
4272
7041
  let docsToGenerate = options.include;
4273
7042
  if (options.update && options.only) {
4274
7043
  docsToGenerate = options.only;
4275
7044
  }
4276
7045
  if (docsToGenerate.includes("all")) {
4277
- docsToGenerate = ["architecture", "conventions", "dependencies", "onboarding"];
7046
+ docsToGenerate = [
7047
+ "architecture",
7048
+ "conventions",
7049
+ "dependencies",
7050
+ "onboarding",
7051
+ "files",
7052
+ "api_surface",
7053
+ "errors",
7054
+ "tests",
7055
+ "history",
7056
+ "current",
7057
+ "status",
7058
+ "health"
7059
+ ];
4278
7060
  }
4279
7061
  let metadata = null;
4280
7062
  if (options.update) {
4281
7063
  metadata = loadMetadata(options.outputDir);
4282
7064
  }
4283
- const fileCount = getFileCount5(graph);
7065
+ const fileCount = getFileCount13(graph);
4284
7066
  const symbolCount = graph.order;
4285
7067
  const edgeCount = graph.size;
4286
7068
  if (options.format === "markdown") {
@@ -4288,8 +7070,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
4288
7070
  try {
4289
7071
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
4290
7072
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
4291
- const filePath = join10(options.outputDir, "ARCHITECTURE.md");
4292
- writeFileSync2(filePath, content, "utf-8");
7073
+ const filePath = join12(options.outputDir, "ARCHITECTURE.md");
7074
+ writeFileSync3(filePath, content, "utf-8");
4293
7075
  generated.push("ARCHITECTURE.md");
4294
7076
  } catch (err) {
4295
7077
  errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
@@ -4299,8 +7081,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
4299
7081
  try {
4300
7082
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
4301
7083
  const content = generateConventions(graph, projectRoot, version);
4302
- const filePath = join10(options.outputDir, "CONVENTIONS.md");
4303
- writeFileSync2(filePath, content, "utf-8");
7084
+ const filePath = join12(options.outputDir, "CONVENTIONS.md");
7085
+ writeFileSync3(filePath, content, "utf-8");
4304
7086
  generated.push("CONVENTIONS.md");
4305
7087
  } catch (err) {
4306
7088
  errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
@@ -4310,8 +7092,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
4310
7092
  try {
4311
7093
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
4312
7094
  const content = generateDependencies(graph, projectRoot, version);
4313
- const filePath = join10(options.outputDir, "DEPENDENCIES.md");
4314
- writeFileSync2(filePath, content, "utf-8");
7095
+ const filePath = join12(options.outputDir, "DEPENDENCIES.md");
7096
+ writeFileSync3(filePath, content, "utf-8");
4315
7097
  generated.push("DEPENDENCIES.md");
4316
7098
  } catch (err) {
4317
7099
  errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
@@ -4321,13 +7103,101 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
4321
7103
  try {
4322
7104
  if (options.verbose) console.log("Generating ONBOARDING.md...");
4323
7105
  const content = generateOnboarding(graph, projectRoot, version);
4324
- const filePath = join10(options.outputDir, "ONBOARDING.md");
4325
- writeFileSync2(filePath, content, "utf-8");
7106
+ const filePath = join12(options.outputDir, "ONBOARDING.md");
7107
+ writeFileSync3(filePath, content, "utf-8");
4326
7108
  generated.push("ONBOARDING.md");
4327
7109
  } catch (err) {
4328
7110
  errors.push(`Failed to generate ONBOARDING.md: ${err}`);
4329
7111
  }
4330
7112
  }
7113
+ if (docsToGenerate.includes("files")) {
7114
+ try {
7115
+ if (options.verbose) console.log("Generating FILES.md...");
7116
+ const content = generateFiles(graph, projectRoot, version);
7117
+ const filePath = join12(options.outputDir, "FILES.md");
7118
+ writeFileSync3(filePath, content, "utf-8");
7119
+ generated.push("FILES.md");
7120
+ } catch (err) {
7121
+ errors.push(`Failed to generate FILES.md: ${err}`);
7122
+ }
7123
+ }
7124
+ if (docsToGenerate.includes("api_surface")) {
7125
+ try {
7126
+ if (options.verbose) console.log("Generating API_SURFACE.md...");
7127
+ const content = generateApiSurface(graph, projectRoot, version);
7128
+ const filePath = join12(options.outputDir, "API_SURFACE.md");
7129
+ writeFileSync3(filePath, content, "utf-8");
7130
+ generated.push("API_SURFACE.md");
7131
+ } catch (err) {
7132
+ errors.push(`Failed to generate API_SURFACE.md: ${err}`);
7133
+ }
7134
+ }
7135
+ if (docsToGenerate.includes("errors")) {
7136
+ try {
7137
+ if (options.verbose) console.log("Generating ERRORS.md...");
7138
+ const content = generateErrors(graph, projectRoot, version);
7139
+ const filePath = join12(options.outputDir, "ERRORS.md");
7140
+ writeFileSync3(filePath, content, "utf-8");
7141
+ generated.push("ERRORS.md");
7142
+ } catch (err) {
7143
+ errors.push(`Failed to generate ERRORS.md: ${err}`);
7144
+ }
7145
+ }
7146
+ if (docsToGenerate.includes("tests")) {
7147
+ try {
7148
+ if (options.verbose) console.log("Generating TESTS.md...");
7149
+ const content = generateTests(graph, projectRoot, version);
7150
+ const filePath = join12(options.outputDir, "TESTS.md");
7151
+ writeFileSync3(filePath, content, "utf-8");
7152
+ generated.push("TESTS.md");
7153
+ } catch (err) {
7154
+ errors.push(`Failed to generate TESTS.md: ${err}`);
7155
+ }
7156
+ }
7157
+ if (docsToGenerate.includes("history")) {
7158
+ try {
7159
+ if (options.verbose) console.log("Generating HISTORY.md...");
7160
+ const content = generateHistory(graph, projectRoot, version);
7161
+ const filePath = join12(options.outputDir, "HISTORY.md");
7162
+ writeFileSync3(filePath, content, "utf-8");
7163
+ generated.push("HISTORY.md");
7164
+ } catch (err) {
7165
+ errors.push(`Failed to generate HISTORY.md: ${err}`);
7166
+ }
7167
+ }
7168
+ if (docsToGenerate.includes("current")) {
7169
+ try {
7170
+ if (options.verbose) console.log("Generating CURRENT.md...");
7171
+ const content = generateCurrent(graph, projectRoot, version);
7172
+ const filePath = join12(options.outputDir, "CURRENT.md");
7173
+ writeFileSync3(filePath, content, "utf-8");
7174
+ generated.push("CURRENT.md");
7175
+ } catch (err) {
7176
+ errors.push(`Failed to generate CURRENT.md: ${err}`);
7177
+ }
7178
+ }
7179
+ if (docsToGenerate.includes("status")) {
7180
+ try {
7181
+ if (options.verbose) console.log("Generating STATUS.md...");
7182
+ const content = generateStatus(graph, projectRoot, version);
7183
+ const filePath = join12(options.outputDir, "STATUS.md");
7184
+ writeFileSync3(filePath, content, "utf-8");
7185
+ generated.push("STATUS.md");
7186
+ } catch (err) {
7187
+ errors.push(`Failed to generate STATUS.md: ${err}`);
7188
+ }
7189
+ }
7190
+ if (docsToGenerate.includes("health")) {
7191
+ try {
7192
+ if (options.verbose) console.log("Generating HEALTH.md...");
7193
+ const content = generateHealth(graph, projectRoot, version);
7194
+ const filePath = join12(options.outputDir, "HEALTH.md");
7195
+ writeFileSync3(filePath, content, "utf-8");
7196
+ generated.push("HEALTH.md");
7197
+ } catch (err) {
7198
+ errors.push(`Failed to generate HEALTH.md: ${err}`);
7199
+ }
7200
+ }
4331
7201
  } else if (options.format === "json") {
4332
7202
  errors.push("JSON format not yet supported");
4333
7203
  }
@@ -4356,7 +7226,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
4356
7226
  };
4357
7227
  }
4358
7228
  }
4359
- function getFileCount5(graph) {
7229
+ function getFileCount13(graph) {
4360
7230
  const files = /* @__PURE__ */ new Set();
4361
7231
  graph.forEachNode((node, attrs) => {
4362
7232
  files.add(attrs.filePath);
@@ -4369,13 +7239,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4369
7239
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4370
7240
 
4371
7241
  // src/mcp/tools.ts
4372
- import { dirname as dirname8, join as join12 } from "path";
4373
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
7242
+ import { dirname as dirname13, join as join14 } from "path";
7243
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
4374
7244
 
4375
7245
  // src/mcp/connect.ts
4376
7246
  import simpleGit from "simple-git";
4377
- import { existsSync as existsSync8 } from "fs";
4378
- import { join as join11, basename as basename3, resolve as resolve2 } from "path";
7247
+ import { existsSync as existsSync10 } from "fs";
7248
+ import { join as join13, basename as basename5, resolve as resolve2 } from "path";
4379
7249
  import { tmpdir, homedir } from "os";
4380
7250
  function validateProjectPath(source) {
4381
7251
  const resolved = resolve2(source);
@@ -4388,11 +7258,11 @@ function validateProjectPath(source) {
4388
7258
  "/boot",
4389
7259
  "/proc",
4390
7260
  "/sys",
4391
- join11(homedir(), ".ssh"),
4392
- join11(homedir(), ".gnupg"),
4393
- join11(homedir(), ".aws"),
4394
- join11(homedir(), ".config"),
4395
- join11(homedir(), ".env")
7261
+ join13(homedir(), ".ssh"),
7262
+ join13(homedir(), ".gnupg"),
7263
+ join13(homedir(), ".aws"),
7264
+ join13(homedir(), ".config"),
7265
+ join13(homedir(), ".env")
4396
7266
  ];
4397
7267
  for (const blocked of blockedPaths) {
4398
7268
  if (resolved.startsWith(blocked)) {
@@ -4415,11 +7285,11 @@ async function connectToRepo(source, subdirectory, state) {
4415
7285
  };
4416
7286
  }
4417
7287
  projectName = match[1];
4418
- const reposDir = join11(tmpdir(), "depwire-repos");
4419
- const cloneDir = join11(reposDir, projectName);
7288
+ const reposDir = join13(tmpdir(), "depwire-repos");
7289
+ const cloneDir = join13(reposDir, projectName);
4420
7290
  console.error(`Connecting to GitHub repo: ${source}`);
4421
7291
  const git = simpleGit();
4422
- if (existsSync8(cloneDir)) {
7292
+ if (existsSync10(cloneDir)) {
4423
7293
  console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
4424
7294
  try {
4425
7295
  await git.cwd(cloneDir).pull();
@@ -4437,7 +7307,7 @@ async function connectToRepo(source, subdirectory, state) {
4437
7307
  };
4438
7308
  }
4439
7309
  }
4440
- projectRoot = subdirectory ? join11(cloneDir, subdirectory) : cloneDir;
7310
+ projectRoot = subdirectory ? join13(cloneDir, subdirectory) : cloneDir;
4441
7311
  } else {
4442
7312
  const validation2 = validateProjectPath(source);
4443
7313
  if (!validation2.valid) {
@@ -4446,14 +7316,14 @@ async function connectToRepo(source, subdirectory, state) {
4446
7316
  message: validation2.error
4447
7317
  };
4448
7318
  }
4449
- if (!existsSync8(source)) {
7319
+ if (!existsSync10(source)) {
4450
7320
  return {
4451
7321
  error: "Directory not found",
4452
7322
  message: `Directory does not exist: ${source}`
4453
7323
  };
4454
7324
  }
4455
- projectRoot = subdirectory ? join11(source, subdirectory) : source;
4456
- projectName = basename3(projectRoot);
7325
+ projectRoot = subdirectory ? join13(source, subdirectory) : source;
7326
+ projectName = basename5(projectRoot);
4457
7327
  }
4458
7328
  const validation = validateProjectPath(projectRoot);
4459
7329
  if (!validation.valid) {
@@ -4462,7 +7332,7 @@ async function connectToRepo(source, subdirectory, state) {
4462
7332
  message: validation.error
4463
7333
  };
4464
7334
  }
4465
- if (!existsSync8(projectRoot)) {
7335
+ if (!existsSync10(projectRoot)) {
4466
7336
  return {
4467
7337
  error: "Project root not found",
4468
7338
  message: `Directory does not exist: ${projectRoot}`
@@ -4736,6 +7606,14 @@ function getToolsList() {
4736
7606
  }
4737
7607
  }
4738
7608
  }
7609
+ },
7610
+ {
7611
+ name: "get_health_score",
7612
+ description: "Get a 0-100 health score for the project's dependency architecture. Scores coupling, cohesion, circular dependencies, god files, orphan files, and dependency depth. Returns overall score, per-dimension breakdown, and actionable recommendations.",
7613
+ inputSchema: {
7614
+ type: "object",
7615
+ properties: {}
7616
+ }
4739
7617
  }
4740
7618
  ];
4741
7619
  }
@@ -4780,6 +7658,15 @@ async function handleToolCall(name, args, state) {
4780
7658
  } else {
4781
7659
  result = await handleUpdateProjectDocs(args.doc_type || "all", state);
4782
7660
  }
7661
+ } else if (name === "get_health_score") {
7662
+ if (!isProjectLoaded(state)) {
7663
+ result = {
7664
+ error: "No project loaded",
7665
+ message: "Use connect_repo to connect to a codebase first"
7666
+ };
7667
+ } else {
7668
+ result = handleGetHealthScore(state);
7669
+ }
4783
7670
  } else {
4784
7671
  if (!isProjectLoaded(state)) {
4785
7672
  result = {
@@ -5117,7 +8004,7 @@ function handleGetArchitectureSummary(graph) {
5117
8004
  const dirMap = /* @__PURE__ */ new Map();
5118
8005
  const languageBreakdown = {};
5119
8006
  fileSummary.forEach((f) => {
5120
- const dir = f.filePath.includes("/") ? dirname8(f.filePath) : ".";
8007
+ const dir = f.filePath.includes("/") ? dirname13(f.filePath) : ".";
5121
8008
  if (!dirMap.has(dir)) {
5122
8009
  dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
5123
8010
  }
@@ -5204,8 +8091,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
5204
8091
  };
5205
8092
  }
5206
8093
  async function handleGetProjectDocs(docType, state) {
5207
- const docsDir = join12(state.projectRoot, ".depwire");
5208
- if (!existsSync9(docsDir)) {
8094
+ const docsDir = join14(state.projectRoot, ".depwire");
8095
+ if (!existsSync11(docsDir)) {
5209
8096
  const errorMessage = `Project documentation has not been generated yet.
5210
8097
 
5211
8098
  Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
@@ -5235,12 +8122,12 @@ Available document types:
5235
8122
  missing.push(doc);
5236
8123
  continue;
5237
8124
  }
5238
- const filePath = join12(docsDir, metadata.documents[doc].file);
5239
- if (!existsSync9(filePath)) {
8125
+ const filePath = join14(docsDir, metadata.documents[doc].file);
8126
+ if (!existsSync11(filePath)) {
5240
8127
  missing.push(doc);
5241
8128
  continue;
5242
8129
  }
5243
- const content = readFileSync5(filePath, "utf-8");
8130
+ const content = readFileSync7(filePath, "utf-8");
5244
8131
  if (docsToReturn.length > 1) {
5245
8132
  output += `
5246
8133
 
@@ -5265,16 +8152,16 @@ Available document types:
5265
8152
  }
5266
8153
  async function handleUpdateProjectDocs(docType, state) {
5267
8154
  const startTime = Date.now();
5268
- const docsDir = join12(state.projectRoot, ".depwire");
8155
+ const docsDir = join14(state.projectRoot, ".depwire");
5269
8156
  console.error("Regenerating project documentation...");
5270
8157
  const parsedFiles = await parseProject(state.projectRoot);
5271
8158
  const graph = buildGraph(parsedFiles);
5272
8159
  const parseTime = (Date.now() - startTime) / 1e3;
5273
8160
  state.graph = graph;
5274
- const packageJsonPath = join12(__dirname, "../../package.json");
5275
- const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
8161
+ const packageJsonPath = join14(__dirname, "../../package.json");
8162
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
5276
8163
  const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
5277
- const docsExist = existsSync9(docsDir);
8164
+ const docsExist = existsSync11(docsDir);
5278
8165
  const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
5279
8166
  outputDir: docsDir,
5280
8167
  format: "markdown",
@@ -5309,6 +8196,12 @@ async function handleUpdateProjectDocs(docType, state) {
5309
8196
  };
5310
8197
  }
5311
8198
  }
8199
+ function handleGetHealthScore(state) {
8200
+ const graph = state.graph;
8201
+ const projectRoot = state.projectRoot;
8202
+ const report = calculateHealthScore(graph, projectRoot);
8203
+ return report;
8204
+ }
5312
8205
 
5313
8206
  // src/mcp/server.ts
5314
8207
  async function startMcpServer(state) {
@@ -5354,6 +8247,8 @@ export {
5354
8247
  startVizServer,
5355
8248
  createEmptyState,
5356
8249
  updateFileInGraph,
8250
+ calculateHealthScore,
8251
+ getHealthTrend,
5357
8252
  generateDocs,
5358
8253
  startMcpServer
5359
8254
  };