depwire-cli 0.5.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 existsSync8 } from "fs";
2643
- import { join as join11 } 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";
@@ -4204,7 +4692,7 @@ function generateDepwireUsage(projectRoot) {
4204
4692
  }
4205
4693
 
4206
4694
  // src/docs/files.ts
4207
- import { dirname as dirname8, basename as basename3 } from "path";
4695
+ import { dirname as dirname9, basename as basename3 } from "path";
4208
4696
  function generateFiles(graph, projectRoot, version) {
4209
4697
  let output = "";
4210
4698
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -4308,7 +4796,7 @@ function generateDirectoryBreakdown(graph) {
4308
4796
  const fileStats = getFileStats2(graph);
4309
4797
  const dirMap = /* @__PURE__ */ new Map();
4310
4798
  for (const file of fileStats) {
4311
- const dir = dirname8(file.filePath);
4799
+ const dir = dirname9(file.filePath);
4312
4800
  const topDir = dir === "." ? "." : dir.split("/")[0];
4313
4801
  if (!dirMap.has(topDir)) {
4314
4802
  dirMap.set(topDir, {
@@ -4951,7 +5439,7 @@ function generateRecommendations(graph) {
4951
5439
  }
4952
5440
 
4953
5441
  // src/docs/tests.ts
4954
- import { basename as basename4, dirname as dirname9 } from "path";
5442
+ import { basename as basename4, dirname as dirname10 } from "path";
4955
5443
  function generateTests(graph, projectRoot, version) {
4956
5444
  let output = "";
4957
5445
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -4980,7 +5468,7 @@ function getFileCount8(graph) {
4980
5468
  }
4981
5469
  function isTestFile(filePath) {
4982
5470
  const fileName = basename4(filePath).toLowerCase();
4983
- const dirPath = dirname9(filePath).toLowerCase();
5471
+ const dirPath = dirname10(filePath).toLowerCase();
4984
5472
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
4985
5473
  return true;
4986
5474
  }
@@ -5039,12 +5527,12 @@ function generateTestFileInventory(graph) {
5039
5527
  }
5040
5528
  function matchTestToSource(testFile) {
5041
5529
  const testFileName = basename4(testFile);
5042
- const testDir = dirname9(testFile);
5530
+ const testDir = dirname10(testFile);
5043
5531
  let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
5044
5532
  const possiblePaths = [];
5045
5533
  possiblePaths.push(testDir + "/" + sourceFileName);
5046
5534
  if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
5047
- const parentDir = dirname9(testDir);
5535
+ const parentDir = dirname10(testDir);
5048
5536
  possiblePaths.push(parentDir + "/" + sourceFileName);
5049
5537
  }
5050
5538
  if (testDir.includes("test")) {
@@ -5241,7 +5729,7 @@ function generateTestStatistics(graph) {
5241
5729
  `;
5242
5730
  const dirTestCoverage = /* @__PURE__ */ new Map();
5243
5731
  for (const sourceFile of sourceFiles) {
5244
- const dir = dirname9(sourceFile).split("/")[0];
5732
+ const dir = dirname10(sourceFile).split("/")[0];
5245
5733
  if (!dirTestCoverage.has(dir)) {
5246
5734
  dirTestCoverage.set(dir, { total: 0, tested: 0 });
5247
5735
  }
@@ -5264,7 +5752,7 @@ function generateTestStatistics(graph) {
5264
5752
  }
5265
5753
 
5266
5754
  // src/docs/history.ts
5267
- import { dirname as dirname10 } from "path";
5755
+ import { dirname as dirname11 } from "path";
5268
5756
  import { execSync } from "child_process";
5269
5757
  function generateHistory(graph, projectRoot, version) {
5270
5758
  let output = "";
@@ -5545,7 +6033,7 @@ function generateFeatureClusters(graph) {
5545
6033
  const dirFiles = /* @__PURE__ */ new Map();
5546
6034
  const fileEdges = /* @__PURE__ */ new Map();
5547
6035
  graph.forEachNode((node, attrs) => {
5548
- const dir = dirname10(attrs.filePath);
6036
+ const dir = dirname11(attrs.filePath);
5549
6037
  if (!dirFiles.has(dir)) {
5550
6038
  dirFiles.set(dir, /* @__PURE__ */ new Set());
5551
6039
  }
@@ -5627,7 +6115,7 @@ function capitalizeFirst3(str) {
5627
6115
  }
5628
6116
 
5629
6117
  // src/docs/current.ts
5630
- import { dirname as dirname11 } from "path";
6118
+ import { dirname as dirname12 } from "path";
5631
6119
  function generateCurrent(graph, projectRoot, version) {
5632
6120
  let output = "";
5633
6121
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -5765,7 +6253,7 @@ function generateCompleteFileIndex(graph) {
5765
6253
  fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
5766
6254
  const dirGroups = /* @__PURE__ */ new Map();
5767
6255
  for (const info of fileInfos) {
5768
- const dir = dirname11(info.filePath);
6256
+ const dir = dirname12(info.filePath);
5769
6257
  const topDir = dir === "." ? "root" : dir.split("/")[0];
5770
6258
  if (!dirGroups.has(topDir)) {
5771
6259
  dirGroups.set(topDir, []);
@@ -5976,8 +6464,8 @@ function getTopLevelDir2(filePath) {
5976
6464
  }
5977
6465
 
5978
6466
  // src/docs/status.ts
5979
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
5980
- import { join as join9 } from "path";
6467
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
6468
+ import { join as join10 } from "path";
5981
6469
  function generateStatus(graph, projectRoot, version) {
5982
6470
  let output = "";
5983
6471
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -6010,12 +6498,12 @@ function getFileCount11(graph) {
6010
6498
  }
6011
6499
  function extractComments(projectRoot, filePath) {
6012
6500
  const comments = [];
6013
- const fullPath = join9(projectRoot, filePath);
6014
- if (!existsSync6(fullPath)) {
6501
+ const fullPath = join10(projectRoot, filePath);
6502
+ if (!existsSync7(fullPath)) {
6015
6503
  return comments;
6016
6504
  }
6017
6505
  try {
6018
- const content = readFileSync4(fullPath, "utf-8");
6506
+ const content = readFileSync5(fullPath, "utf-8");
6019
6507
  const lines = content.split("\n");
6020
6508
  const patterns = [
6021
6509
  { type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
@@ -6329,16 +6817,171 @@ function generateCompleteness(projectRoot, graph) {
6329
6817
  return output;
6330
6818
  }
6331
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
+
6332
6975
  // src/docs/metadata.ts
6333
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync } from "fs";
6334
- import { join as join10 } from "path";
6976
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
6977
+ import { join as join11 } from "path";
6335
6978
  function loadMetadata(outputDir) {
6336
- const metadataPath = join10(outputDir, "metadata.json");
6337
- if (!existsSync7(metadataPath)) {
6979
+ const metadataPath = join11(outputDir, "metadata.json");
6980
+ if (!existsSync8(metadataPath)) {
6338
6981
  return null;
6339
6982
  }
6340
6983
  try {
6341
- const content = readFileSync5(metadataPath, "utf-8");
6984
+ const content = readFileSync6(metadataPath, "utf-8");
6342
6985
  return JSON.parse(content);
6343
6986
  } catch (err) {
6344
6987
  console.error("Failed to load metadata:", err);
@@ -6346,14 +6989,14 @@ function loadMetadata(outputDir) {
6346
6989
  }
6347
6990
  }
6348
6991
  function saveMetadata(outputDir, metadata) {
6349
- const metadataPath = join10(outputDir, "metadata.json");
6350
- writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
6992
+ const metadataPath = join11(outputDir, "metadata.json");
6993
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
6351
6994
  }
6352
6995
  function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
6353
6996
  const now = (/* @__PURE__ */ new Date()).toISOString();
6354
6997
  const documents = {};
6355
6998
  for (const docType of docTypes) {
6356
- 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.toUpperCase()}.md`;
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`;
6357
7000
  documents[docType] = {
6358
7001
  generated_at: now,
6359
7002
  file: fileName
@@ -6389,7 +7032,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6389
7032
  const generated = [];
6390
7033
  const errors = [];
6391
7034
  try {
6392
- if (!existsSync8(options.outputDir)) {
7035
+ if (!existsSync9(options.outputDir)) {
6393
7036
  mkdirSync(options.outputDir, { recursive: true });
6394
7037
  if (options.verbose) {
6395
7038
  console.log(`Created output directory: ${options.outputDir}`);
@@ -6411,14 +7054,15 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6411
7054
  "tests",
6412
7055
  "history",
6413
7056
  "current",
6414
- "status"
7057
+ "status",
7058
+ "health"
6415
7059
  ];
6416
7060
  }
6417
7061
  let metadata = null;
6418
7062
  if (options.update) {
6419
7063
  metadata = loadMetadata(options.outputDir);
6420
7064
  }
6421
- const fileCount = getFileCount12(graph);
7065
+ const fileCount = getFileCount13(graph);
6422
7066
  const symbolCount = graph.order;
6423
7067
  const edgeCount = graph.size;
6424
7068
  if (options.format === "markdown") {
@@ -6426,8 +7070,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6426
7070
  try {
6427
7071
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
6428
7072
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
6429
- const filePath = join11(options.outputDir, "ARCHITECTURE.md");
6430
- writeFileSync2(filePath, content, "utf-8");
7073
+ const filePath = join12(options.outputDir, "ARCHITECTURE.md");
7074
+ writeFileSync3(filePath, content, "utf-8");
6431
7075
  generated.push("ARCHITECTURE.md");
6432
7076
  } catch (err) {
6433
7077
  errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
@@ -6437,8 +7081,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6437
7081
  try {
6438
7082
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
6439
7083
  const content = generateConventions(graph, projectRoot, version);
6440
- const filePath = join11(options.outputDir, "CONVENTIONS.md");
6441
- writeFileSync2(filePath, content, "utf-8");
7084
+ const filePath = join12(options.outputDir, "CONVENTIONS.md");
7085
+ writeFileSync3(filePath, content, "utf-8");
6442
7086
  generated.push("CONVENTIONS.md");
6443
7087
  } catch (err) {
6444
7088
  errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
@@ -6448,8 +7092,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6448
7092
  try {
6449
7093
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
6450
7094
  const content = generateDependencies(graph, projectRoot, version);
6451
- const filePath = join11(options.outputDir, "DEPENDENCIES.md");
6452
- writeFileSync2(filePath, content, "utf-8");
7095
+ const filePath = join12(options.outputDir, "DEPENDENCIES.md");
7096
+ writeFileSync3(filePath, content, "utf-8");
6453
7097
  generated.push("DEPENDENCIES.md");
6454
7098
  } catch (err) {
6455
7099
  errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
@@ -6459,8 +7103,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6459
7103
  try {
6460
7104
  if (options.verbose) console.log("Generating ONBOARDING.md...");
6461
7105
  const content = generateOnboarding(graph, projectRoot, version);
6462
- const filePath = join11(options.outputDir, "ONBOARDING.md");
6463
- writeFileSync2(filePath, content, "utf-8");
7106
+ const filePath = join12(options.outputDir, "ONBOARDING.md");
7107
+ writeFileSync3(filePath, content, "utf-8");
6464
7108
  generated.push("ONBOARDING.md");
6465
7109
  } catch (err) {
6466
7110
  errors.push(`Failed to generate ONBOARDING.md: ${err}`);
@@ -6470,8 +7114,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6470
7114
  try {
6471
7115
  if (options.verbose) console.log("Generating FILES.md...");
6472
7116
  const content = generateFiles(graph, projectRoot, version);
6473
- const filePath = join11(options.outputDir, "FILES.md");
6474
- writeFileSync2(filePath, content, "utf-8");
7117
+ const filePath = join12(options.outputDir, "FILES.md");
7118
+ writeFileSync3(filePath, content, "utf-8");
6475
7119
  generated.push("FILES.md");
6476
7120
  } catch (err) {
6477
7121
  errors.push(`Failed to generate FILES.md: ${err}`);
@@ -6481,8 +7125,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6481
7125
  try {
6482
7126
  if (options.verbose) console.log("Generating API_SURFACE.md...");
6483
7127
  const content = generateApiSurface(graph, projectRoot, version);
6484
- const filePath = join11(options.outputDir, "API_SURFACE.md");
6485
- writeFileSync2(filePath, content, "utf-8");
7128
+ const filePath = join12(options.outputDir, "API_SURFACE.md");
7129
+ writeFileSync3(filePath, content, "utf-8");
6486
7130
  generated.push("API_SURFACE.md");
6487
7131
  } catch (err) {
6488
7132
  errors.push(`Failed to generate API_SURFACE.md: ${err}`);
@@ -6492,8 +7136,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6492
7136
  try {
6493
7137
  if (options.verbose) console.log("Generating ERRORS.md...");
6494
7138
  const content = generateErrors(graph, projectRoot, version);
6495
- const filePath = join11(options.outputDir, "ERRORS.md");
6496
- writeFileSync2(filePath, content, "utf-8");
7139
+ const filePath = join12(options.outputDir, "ERRORS.md");
7140
+ writeFileSync3(filePath, content, "utf-8");
6497
7141
  generated.push("ERRORS.md");
6498
7142
  } catch (err) {
6499
7143
  errors.push(`Failed to generate ERRORS.md: ${err}`);
@@ -6503,8 +7147,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6503
7147
  try {
6504
7148
  if (options.verbose) console.log("Generating TESTS.md...");
6505
7149
  const content = generateTests(graph, projectRoot, version);
6506
- const filePath = join11(options.outputDir, "TESTS.md");
6507
- writeFileSync2(filePath, content, "utf-8");
7150
+ const filePath = join12(options.outputDir, "TESTS.md");
7151
+ writeFileSync3(filePath, content, "utf-8");
6508
7152
  generated.push("TESTS.md");
6509
7153
  } catch (err) {
6510
7154
  errors.push(`Failed to generate TESTS.md: ${err}`);
@@ -6514,8 +7158,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6514
7158
  try {
6515
7159
  if (options.verbose) console.log("Generating HISTORY.md...");
6516
7160
  const content = generateHistory(graph, projectRoot, version);
6517
- const filePath = join11(options.outputDir, "HISTORY.md");
6518
- writeFileSync2(filePath, content, "utf-8");
7161
+ const filePath = join12(options.outputDir, "HISTORY.md");
7162
+ writeFileSync3(filePath, content, "utf-8");
6519
7163
  generated.push("HISTORY.md");
6520
7164
  } catch (err) {
6521
7165
  errors.push(`Failed to generate HISTORY.md: ${err}`);
@@ -6525,8 +7169,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6525
7169
  try {
6526
7170
  if (options.verbose) console.log("Generating CURRENT.md...");
6527
7171
  const content = generateCurrent(graph, projectRoot, version);
6528
- const filePath = join11(options.outputDir, "CURRENT.md");
6529
- writeFileSync2(filePath, content, "utf-8");
7172
+ const filePath = join12(options.outputDir, "CURRENT.md");
7173
+ writeFileSync3(filePath, content, "utf-8");
6530
7174
  generated.push("CURRENT.md");
6531
7175
  } catch (err) {
6532
7176
  errors.push(`Failed to generate CURRENT.md: ${err}`);
@@ -6536,13 +7180,24 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6536
7180
  try {
6537
7181
  if (options.verbose) console.log("Generating STATUS.md...");
6538
7182
  const content = generateStatus(graph, projectRoot, version);
6539
- const filePath = join11(options.outputDir, "STATUS.md");
6540
- writeFileSync2(filePath, content, "utf-8");
7183
+ const filePath = join12(options.outputDir, "STATUS.md");
7184
+ writeFileSync3(filePath, content, "utf-8");
6541
7185
  generated.push("STATUS.md");
6542
7186
  } catch (err) {
6543
7187
  errors.push(`Failed to generate STATUS.md: ${err}`);
6544
7188
  }
6545
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
+ }
6546
7201
  } else if (options.format === "json") {
6547
7202
  errors.push("JSON format not yet supported");
6548
7203
  }
@@ -6571,7 +7226,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6571
7226
  };
6572
7227
  }
6573
7228
  }
6574
- function getFileCount12(graph) {
7229
+ function getFileCount13(graph) {
6575
7230
  const files = /* @__PURE__ */ new Set();
6576
7231
  graph.forEachNode((node, attrs) => {
6577
7232
  files.add(attrs.filePath);
@@ -6584,13 +7239,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6584
7239
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6585
7240
 
6586
7241
  // src/mcp/tools.ts
6587
- import { dirname as dirname12, join as join13 } from "path";
6588
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
7242
+ import { dirname as dirname13, join as join14 } from "path";
7243
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
6589
7244
 
6590
7245
  // src/mcp/connect.ts
6591
7246
  import simpleGit from "simple-git";
6592
- import { existsSync as existsSync9 } from "fs";
6593
- import { join as join12, basename as basename5, 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";
6594
7249
  import { tmpdir, homedir } from "os";
6595
7250
  function validateProjectPath(source) {
6596
7251
  const resolved = resolve2(source);
@@ -6603,11 +7258,11 @@ function validateProjectPath(source) {
6603
7258
  "/boot",
6604
7259
  "/proc",
6605
7260
  "/sys",
6606
- join12(homedir(), ".ssh"),
6607
- join12(homedir(), ".gnupg"),
6608
- join12(homedir(), ".aws"),
6609
- join12(homedir(), ".config"),
6610
- join12(homedir(), ".env")
7261
+ join13(homedir(), ".ssh"),
7262
+ join13(homedir(), ".gnupg"),
7263
+ join13(homedir(), ".aws"),
7264
+ join13(homedir(), ".config"),
7265
+ join13(homedir(), ".env")
6611
7266
  ];
6612
7267
  for (const blocked of blockedPaths) {
6613
7268
  if (resolved.startsWith(blocked)) {
@@ -6630,11 +7285,11 @@ async function connectToRepo(source, subdirectory, state) {
6630
7285
  };
6631
7286
  }
6632
7287
  projectName = match[1];
6633
- const reposDir = join12(tmpdir(), "depwire-repos");
6634
- const cloneDir = join12(reposDir, projectName);
7288
+ const reposDir = join13(tmpdir(), "depwire-repos");
7289
+ const cloneDir = join13(reposDir, projectName);
6635
7290
  console.error(`Connecting to GitHub repo: ${source}`);
6636
7291
  const git = simpleGit();
6637
- if (existsSync9(cloneDir)) {
7292
+ if (existsSync10(cloneDir)) {
6638
7293
  console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
6639
7294
  try {
6640
7295
  await git.cwd(cloneDir).pull();
@@ -6652,7 +7307,7 @@ async function connectToRepo(source, subdirectory, state) {
6652
7307
  };
6653
7308
  }
6654
7309
  }
6655
- projectRoot = subdirectory ? join12(cloneDir, subdirectory) : cloneDir;
7310
+ projectRoot = subdirectory ? join13(cloneDir, subdirectory) : cloneDir;
6656
7311
  } else {
6657
7312
  const validation2 = validateProjectPath(source);
6658
7313
  if (!validation2.valid) {
@@ -6661,13 +7316,13 @@ async function connectToRepo(source, subdirectory, state) {
6661
7316
  message: validation2.error
6662
7317
  };
6663
7318
  }
6664
- if (!existsSync9(source)) {
7319
+ if (!existsSync10(source)) {
6665
7320
  return {
6666
7321
  error: "Directory not found",
6667
7322
  message: `Directory does not exist: ${source}`
6668
7323
  };
6669
7324
  }
6670
- projectRoot = subdirectory ? join12(source, subdirectory) : source;
7325
+ projectRoot = subdirectory ? join13(source, subdirectory) : source;
6671
7326
  projectName = basename5(projectRoot);
6672
7327
  }
6673
7328
  const validation = validateProjectPath(projectRoot);
@@ -6677,7 +7332,7 @@ async function connectToRepo(source, subdirectory, state) {
6677
7332
  message: validation.error
6678
7333
  };
6679
7334
  }
6680
- if (!existsSync9(projectRoot)) {
7335
+ if (!existsSync10(projectRoot)) {
6681
7336
  return {
6682
7337
  error: "Project root not found",
6683
7338
  message: `Directory does not exist: ${projectRoot}`
@@ -6951,6 +7606,14 @@ function getToolsList() {
6951
7606
  }
6952
7607
  }
6953
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
+ }
6954
7617
  }
6955
7618
  ];
6956
7619
  }
@@ -6995,6 +7658,15 @@ async function handleToolCall(name, args, state) {
6995
7658
  } else {
6996
7659
  result = await handleUpdateProjectDocs(args.doc_type || "all", state);
6997
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
+ }
6998
7670
  } else {
6999
7671
  if (!isProjectLoaded(state)) {
7000
7672
  result = {
@@ -7332,7 +8004,7 @@ function handleGetArchitectureSummary(graph) {
7332
8004
  const dirMap = /* @__PURE__ */ new Map();
7333
8005
  const languageBreakdown = {};
7334
8006
  fileSummary.forEach((f) => {
7335
- const dir = f.filePath.includes("/") ? dirname12(f.filePath) : ".";
8007
+ const dir = f.filePath.includes("/") ? dirname13(f.filePath) : ".";
7336
8008
  if (!dirMap.has(dir)) {
7337
8009
  dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
7338
8010
  }
@@ -7419,8 +8091,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
7419
8091
  };
7420
8092
  }
7421
8093
  async function handleGetProjectDocs(docType, state) {
7422
- const docsDir = join13(state.projectRoot, ".depwire");
7423
- if (!existsSync10(docsDir)) {
8094
+ const docsDir = join14(state.projectRoot, ".depwire");
8095
+ if (!existsSync11(docsDir)) {
7424
8096
  const errorMessage = `Project documentation has not been generated yet.
7425
8097
 
7426
8098
  Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
@@ -7450,12 +8122,12 @@ Available document types:
7450
8122
  missing.push(doc);
7451
8123
  continue;
7452
8124
  }
7453
- const filePath = join13(docsDir, metadata.documents[doc].file);
7454
- if (!existsSync10(filePath)) {
8125
+ const filePath = join14(docsDir, metadata.documents[doc].file);
8126
+ if (!existsSync11(filePath)) {
7455
8127
  missing.push(doc);
7456
8128
  continue;
7457
8129
  }
7458
- const content = readFileSync6(filePath, "utf-8");
8130
+ const content = readFileSync7(filePath, "utf-8");
7459
8131
  if (docsToReturn.length > 1) {
7460
8132
  output += `
7461
8133
 
@@ -7480,16 +8152,16 @@ Available document types:
7480
8152
  }
7481
8153
  async function handleUpdateProjectDocs(docType, state) {
7482
8154
  const startTime = Date.now();
7483
- const docsDir = join13(state.projectRoot, ".depwire");
8155
+ const docsDir = join14(state.projectRoot, ".depwire");
7484
8156
  console.error("Regenerating project documentation...");
7485
8157
  const parsedFiles = await parseProject(state.projectRoot);
7486
8158
  const graph = buildGraph(parsedFiles);
7487
8159
  const parseTime = (Date.now() - startTime) / 1e3;
7488
8160
  state.graph = graph;
7489
- const packageJsonPath = join13(__dirname, "../../package.json");
7490
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
8161
+ const packageJsonPath = join14(__dirname, "../../package.json");
8162
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
7491
8163
  const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
7492
- const docsExist = existsSync10(docsDir);
8164
+ const docsExist = existsSync11(docsDir);
7493
8165
  const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
7494
8166
  outputDir: docsDir,
7495
8167
  format: "markdown",
@@ -7524,6 +8196,12 @@ async function handleUpdateProjectDocs(docType, state) {
7524
8196
  };
7525
8197
  }
7526
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
+ }
7527
8205
 
7528
8206
  // src/mcp/server.ts
7529
8207
  async function startMcpServer(state) {
@@ -7569,6 +8247,8 @@ export {
7569
8247
  startVizServer,
7570
8248
  createEmptyState,
7571
8249
  updateFileInGraph,
8250
+ calculateHealthScore,
8251
+ getHealthTrend,
7572
8252
  generateDocs,
7573
8253
  startMcpServer
7574
8254
  };