depwire-cli 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2638,12 +2638,501 @@ 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, mkdirSync } from "fs";
2992
+ import { join as join9, dirname as dirname7 } 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
+ mkdirSync(dirname7(historyFile), { recursive: true });
3115
+ writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
3116
+ }
3117
+ function loadHealthHistory(projectRoot) {
3118
+ const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3119
+ if (!existsSync6(historyFile)) {
3120
+ return [];
3121
+ }
3122
+ try {
3123
+ const content = readFileSync4(historyFile, "utf-8");
3124
+ return JSON.parse(content);
3125
+ } catch {
3126
+ return [];
3127
+ }
3128
+ }
3129
+
2641
3130
  // src/docs/generator.ts
2642
- import { writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync8 } from "fs";
2643
- import { join as join11 } from "path";
3131
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
3132
+ import { join as join12 } from "path";
2644
3133
 
2645
3134
  // src/docs/architecture.ts
2646
- import { dirname as dirname6 } from "path";
3135
+ import { dirname as dirname8 } from "path";
2647
3136
 
2648
3137
  // src/docs/templates.ts
2649
3138
  function header(text, level = 1) {
@@ -2654,9 +3143,9 @@ function header(text, level = 1) {
2654
3143
  function code(text) {
2655
3144
  return `\`${text}\``;
2656
3145
  }
2657
- function codeBlock(code2, lang = "") {
3146
+ function codeBlock(code3, lang = "") {
2658
3147
  return `\`\`\`${lang}
2659
- ${code2}
3148
+ ${code3}
2660
3149
  \`\`\`
2661
3150
 
2662
3151
  `;
@@ -2794,7 +3283,7 @@ function generateModuleStructure(graph) {
2794
3283
  function getDirectoryStats(graph) {
2795
3284
  const dirMap = /* @__PURE__ */ new Map();
2796
3285
  graph.forEachNode((node, attrs) => {
2797
- const dir = dirname6(attrs.filePath);
3286
+ const dir = dirname8(attrs.filePath);
2798
3287
  if (dir === ".") return;
2799
3288
  if (!dirMap.has(dir)) {
2800
3289
  dirMap.set(dir, {
@@ -2819,7 +3308,7 @@ function getDirectoryStats(graph) {
2819
3308
  });
2820
3309
  const filesPerDir = /* @__PURE__ */ new Map();
2821
3310
  graph.forEachNode((node, attrs) => {
2822
- const dir = dirname6(attrs.filePath);
3311
+ const dir = dirname8(attrs.filePath);
2823
3312
  if (!filesPerDir.has(dir)) {
2824
3313
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
2825
3314
  }
@@ -2834,8 +3323,8 @@ function getDirectoryStats(graph) {
2834
3323
  graph.forEachEdge((edge, attrs, source, target) => {
2835
3324
  const sourceAttrs = graph.getNodeAttributes(source);
2836
3325
  const targetAttrs = graph.getNodeAttributes(target);
2837
- const sourceDir = dirname6(sourceAttrs.filePath);
2838
- const targetDir = dirname6(targetAttrs.filePath);
3326
+ const sourceDir = dirname8(sourceAttrs.filePath);
3327
+ const targetDir = dirname8(targetAttrs.filePath);
2839
3328
  if (sourceDir !== targetDir) {
2840
3329
  if (!dirEdges.has(sourceDir)) {
2841
3330
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -3816,7 +4305,7 @@ function detectCyclesDetailed(graph) {
3816
4305
  }
3817
4306
 
3818
4307
  // src/docs/onboarding.ts
3819
- import { dirname as dirname7 } from "path";
4308
+ import { dirname as dirname9 } from "path";
3820
4309
  function generateOnboarding(graph, projectRoot, version) {
3821
4310
  let output = "";
3822
4311
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -3875,7 +4364,7 @@ function generateQuickOrientation(graph) {
3875
4364
  const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
3876
4365
  const dirs = /* @__PURE__ */ new Set();
3877
4366
  graph.forEachNode((node, attrs) => {
3878
- const dir = dirname7(attrs.filePath);
4367
+ const dir = dirname9(attrs.filePath);
3879
4368
  if (dir !== ".") {
3880
4369
  const topLevel = dir.split("/")[0];
3881
4370
  dirs.add(topLevel);
@@ -3979,7 +4468,7 @@ function generateModuleMap(graph) {
3979
4468
  function getDirectoryStats2(graph) {
3980
4469
  const dirMap = /* @__PURE__ */ new Map();
3981
4470
  graph.forEachNode((node, attrs) => {
3982
- const dir = dirname7(attrs.filePath);
4471
+ const dir = dirname9(attrs.filePath);
3983
4472
  if (dir === ".") return;
3984
4473
  if (!dirMap.has(dir)) {
3985
4474
  dirMap.set(dir, {
@@ -3994,7 +4483,7 @@ function getDirectoryStats2(graph) {
3994
4483
  });
3995
4484
  const filesPerDir = /* @__PURE__ */ new Map();
3996
4485
  graph.forEachNode((node, attrs) => {
3997
- const dir = dirname7(attrs.filePath);
4486
+ const dir = dirname9(attrs.filePath);
3998
4487
  if (!filesPerDir.has(dir)) {
3999
4488
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
4000
4489
  }
@@ -4009,8 +4498,8 @@ function getDirectoryStats2(graph) {
4009
4498
  graph.forEachEdge((edge, attrs, source, target) => {
4010
4499
  const sourceAttrs = graph.getNodeAttributes(source);
4011
4500
  const targetAttrs = graph.getNodeAttributes(target);
4012
- const sourceDir = dirname7(sourceAttrs.filePath);
4013
- const targetDir = dirname7(targetAttrs.filePath);
4501
+ const sourceDir = dirname9(sourceAttrs.filePath);
4502
+ const targetDir = dirname9(targetAttrs.filePath);
4014
4503
  if (sourceDir !== targetDir) {
4015
4504
  if (!dirEdges.has(sourceDir)) {
4016
4505
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -4091,7 +4580,7 @@ function detectClusters(graph) {
4091
4580
  const dirFiles = /* @__PURE__ */ new Map();
4092
4581
  const fileEdges = /* @__PURE__ */ new Map();
4093
4582
  graph.forEachNode((node, attrs) => {
4094
- const dir = dirname7(attrs.filePath);
4583
+ const dir = dirname9(attrs.filePath);
4095
4584
  if (!dirFiles.has(dir)) {
4096
4585
  dirFiles.set(dir, /* @__PURE__ */ new Set());
4097
4586
  }
@@ -4145,8 +4634,8 @@ function inferClusterName(files) {
4145
4634
  if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
4146
4635
  return capitalizeFirst2(sortedWords[0][0]);
4147
4636
  }
4148
- const commonDir = dirname7(files[0]);
4149
- if (files.every((f) => dirname7(f) === commonDir)) {
4637
+ const commonDir = dirname9(files[0]);
4638
+ if (files.every((f) => dirname9(f) === commonDir)) {
4150
4639
  return capitalizeFirst2(commonDir.split("/").pop() || "Core");
4151
4640
  }
4152
4641
  return "Core";
@@ -4204,7 +4693,7 @@ function generateDepwireUsage(projectRoot) {
4204
4693
  }
4205
4694
 
4206
4695
  // src/docs/files.ts
4207
- import { dirname as dirname8, basename as basename3 } from "path";
4696
+ import { dirname as dirname10, basename as basename3 } from "path";
4208
4697
  function generateFiles(graph, projectRoot, version) {
4209
4698
  let output = "";
4210
4699
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -4308,7 +4797,7 @@ function generateDirectoryBreakdown(graph) {
4308
4797
  const fileStats = getFileStats2(graph);
4309
4798
  const dirMap = /* @__PURE__ */ new Map();
4310
4799
  for (const file of fileStats) {
4311
- const dir = dirname8(file.filePath);
4800
+ const dir = dirname10(file.filePath);
4312
4801
  const topDir = dir === "." ? "." : dir.split("/")[0];
4313
4802
  if (!dirMap.has(topDir)) {
4314
4803
  dirMap.set(topDir, {
@@ -4951,7 +5440,7 @@ function generateRecommendations(graph) {
4951
5440
  }
4952
5441
 
4953
5442
  // src/docs/tests.ts
4954
- import { basename as basename4, dirname as dirname9 } from "path";
5443
+ import { basename as basename4, dirname as dirname11 } from "path";
4955
5444
  function generateTests(graph, projectRoot, version) {
4956
5445
  let output = "";
4957
5446
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -4980,7 +5469,7 @@ function getFileCount8(graph) {
4980
5469
  }
4981
5470
  function isTestFile(filePath) {
4982
5471
  const fileName = basename4(filePath).toLowerCase();
4983
- const dirPath = dirname9(filePath).toLowerCase();
5472
+ const dirPath = dirname11(filePath).toLowerCase();
4984
5473
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
4985
5474
  return true;
4986
5475
  }
@@ -5039,12 +5528,12 @@ function generateTestFileInventory(graph) {
5039
5528
  }
5040
5529
  function matchTestToSource(testFile) {
5041
5530
  const testFileName = basename4(testFile);
5042
- const testDir = dirname9(testFile);
5531
+ const testDir = dirname11(testFile);
5043
5532
  let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
5044
5533
  const possiblePaths = [];
5045
5534
  possiblePaths.push(testDir + "/" + sourceFileName);
5046
5535
  if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
5047
- const parentDir = dirname9(testDir);
5536
+ const parentDir = dirname11(testDir);
5048
5537
  possiblePaths.push(parentDir + "/" + sourceFileName);
5049
5538
  }
5050
5539
  if (testDir.includes("test")) {
@@ -5241,7 +5730,7 @@ function generateTestStatistics(graph) {
5241
5730
  `;
5242
5731
  const dirTestCoverage = /* @__PURE__ */ new Map();
5243
5732
  for (const sourceFile of sourceFiles) {
5244
- const dir = dirname9(sourceFile).split("/")[0];
5733
+ const dir = dirname11(sourceFile).split("/")[0];
5245
5734
  if (!dirTestCoverage.has(dir)) {
5246
5735
  dirTestCoverage.set(dir, { total: 0, tested: 0 });
5247
5736
  }
@@ -5264,7 +5753,7 @@ function generateTestStatistics(graph) {
5264
5753
  }
5265
5754
 
5266
5755
  // src/docs/history.ts
5267
- import { dirname as dirname10 } from "path";
5756
+ import { dirname as dirname12 } from "path";
5268
5757
  import { execSync } from "child_process";
5269
5758
  function generateHistory(graph, projectRoot, version) {
5270
5759
  let output = "";
@@ -5545,7 +6034,7 @@ function generateFeatureClusters(graph) {
5545
6034
  const dirFiles = /* @__PURE__ */ new Map();
5546
6035
  const fileEdges = /* @__PURE__ */ new Map();
5547
6036
  graph.forEachNode((node, attrs) => {
5548
- const dir = dirname10(attrs.filePath);
6037
+ const dir = dirname12(attrs.filePath);
5549
6038
  if (!dirFiles.has(dir)) {
5550
6039
  dirFiles.set(dir, /* @__PURE__ */ new Set());
5551
6040
  }
@@ -5627,7 +6116,7 @@ function capitalizeFirst3(str) {
5627
6116
  }
5628
6117
 
5629
6118
  // src/docs/current.ts
5630
- import { dirname as dirname11 } from "path";
6119
+ import { dirname as dirname13 } from "path";
5631
6120
  function generateCurrent(graph, projectRoot, version) {
5632
6121
  let output = "";
5633
6122
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -5765,7 +6254,7 @@ function generateCompleteFileIndex(graph) {
5765
6254
  fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
5766
6255
  const dirGroups = /* @__PURE__ */ new Map();
5767
6256
  for (const info of fileInfos) {
5768
- const dir = dirname11(info.filePath);
6257
+ const dir = dirname13(info.filePath);
5769
6258
  const topDir = dir === "." ? "root" : dir.split("/")[0];
5770
6259
  if (!dirGroups.has(topDir)) {
5771
6260
  dirGroups.set(topDir, []);
@@ -5976,8 +6465,8 @@ function getTopLevelDir2(filePath) {
5976
6465
  }
5977
6466
 
5978
6467
  // src/docs/status.ts
5979
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
5980
- import { join as join9 } from "path";
6468
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
6469
+ import { join as join10 } from "path";
5981
6470
  function generateStatus(graph, projectRoot, version) {
5982
6471
  let output = "";
5983
6472
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -6010,12 +6499,12 @@ function getFileCount11(graph) {
6010
6499
  }
6011
6500
  function extractComments(projectRoot, filePath) {
6012
6501
  const comments = [];
6013
- const fullPath = join9(projectRoot, filePath);
6014
- if (!existsSync6(fullPath)) {
6502
+ const fullPath = join10(projectRoot, filePath);
6503
+ if (!existsSync7(fullPath)) {
6015
6504
  return comments;
6016
6505
  }
6017
6506
  try {
6018
- const content = readFileSync4(fullPath, "utf-8");
6507
+ const content = readFileSync5(fullPath, "utf-8");
6019
6508
  const lines = content.split("\n");
6020
6509
  const patterns = [
6021
6510
  { type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
@@ -6329,16 +6818,171 @@ function generateCompleteness(projectRoot, graph) {
6329
6818
  return output;
6330
6819
  }
6331
6820
 
6821
+ // src/docs/health.ts
6822
+ function generateHealth(graph, projectRoot, version) {
6823
+ let output = "";
6824
+ const report = calculateHealthScore(graph, projectRoot);
6825
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6826
+ const fileCount = getFileCount12(graph);
6827
+ output += timestamp(version, now, fileCount, graph.order);
6828
+ output += header("Dependency Health Score");
6829
+ output += "Analysis of dependency architecture quality across 6 dimensions.\n\n";
6830
+ output += header("Overall Score", 2);
6831
+ output += generateOverallScore(report);
6832
+ output += header("Dimension Breakdown", 2);
6833
+ output += generateDimensionsBreakdown(report.dimensions);
6834
+ output += header("Recommendations", 2);
6835
+ output += generateRecommendations2(report.recommendations);
6836
+ output += header("Historical Trend", 2);
6837
+ output += generateHistoricalTrend(projectRoot, report);
6838
+ output += header("Detailed Metrics", 2);
6839
+ output += generateDetailedMetrics(report.dimensions);
6840
+ return output;
6841
+ }
6842
+ function getFileCount12(graph) {
6843
+ const files = /* @__PURE__ */ new Set();
6844
+ graph.forEachNode((node, attrs) => {
6845
+ files.add(attrs.filePath);
6846
+ });
6847
+ return files.size;
6848
+ }
6849
+ function generateOverallScore(report) {
6850
+ let output = "";
6851
+ const gradeEmoji = {
6852
+ "A": "\u{1F7E2}",
6853
+ "B": "\u{1F535}",
6854
+ "C": "\u{1F7E1}",
6855
+ "D": "\u{1F7E0}",
6856
+ "F": "\u{1F534}"
6857
+ };
6858
+ output += `**Score:** ${report.overall}/100
6859
+
6860
+ `;
6861
+ output += `**Grade:** ${gradeEmoji[report.grade]} ${report.grade}
6862
+
6863
+ `;
6864
+ output += `**Summary:** ${report.summary}
6865
+
6866
+ `;
6867
+ output += `**Project Statistics:**
6868
+
6869
+ `;
6870
+ output += `- Files: ${formatNumber(report.projectStats.files)}
6871
+ `;
6872
+ output += `- Symbols: ${formatNumber(report.projectStats.symbols)}
6873
+ `;
6874
+ output += `- Edges: ${formatNumber(report.projectStats.edges)}
6875
+ `;
6876
+ const langs = Object.entries(report.projectStats.languages).sort((a, b) => b[1] - a[1]).map(([lang, count]) => `${lang} (${count})`).join(", ");
6877
+ output += `- Languages: ${langs}
6878
+
6879
+ `;
6880
+ return output;
6881
+ }
6882
+ function generateDimensionsBreakdown(dimensions) {
6883
+ let output = "";
6884
+ const headers = ["Dimension", "Score", "Grade", "Weight", "Details"];
6885
+ const rows = dimensions.map((d) => [
6886
+ d.name,
6887
+ `${d.score}/100`,
6888
+ d.grade,
6889
+ `${(d.weight * 100).toFixed(0)}%`,
6890
+ d.details
6891
+ ]);
6892
+ output += table(headers, rows);
6893
+ return output;
6894
+ }
6895
+ function generateRecommendations2(recommendations) {
6896
+ if (recommendations.length === 0) {
6897
+ return "\u2705 No critical issues detected.\n\n";
6898
+ }
6899
+ return unorderedList(recommendations);
6900
+ }
6901
+ function generateHistoricalTrend(projectRoot, currentReport) {
6902
+ const history = loadHealthHistory(projectRoot);
6903
+ if (history.length < 2) {
6904
+ return "No historical data available. Run `depwire health` regularly to track trends.\n\n";
6905
+ }
6906
+ let output = `Showing last ${Math.min(history.length, 10)} health checks:
6907
+
6908
+ `;
6909
+ const headers = ["Date", "Score", "Grade", "Trend"];
6910
+ const recent = history.slice(-10);
6911
+ const rows = recent.map((entry, idx) => {
6912
+ let trend = "\u2014";
6913
+ if (idx > 0) {
6914
+ const prev = recent[idx - 1];
6915
+ const delta = entry.score - prev.score;
6916
+ if (delta > 0) {
6917
+ trend = `\u2191 +${delta}`;
6918
+ } else if (delta < 0) {
6919
+ trend = `\u2193 ${delta}`;
6920
+ } else {
6921
+ trend = "\u2192 0";
6922
+ }
6923
+ }
6924
+ return [
6925
+ entry.timestamp.split("T")[0],
6926
+ entry.score.toString(),
6927
+ entry.grade,
6928
+ trend
6929
+ ];
6930
+ });
6931
+ output += table(headers, rows);
6932
+ const first = recent[0];
6933
+ const last = recent[recent.length - 1];
6934
+ const totalDelta = last.score - first.score;
6935
+ output += `
6936
+ **Trend:** `;
6937
+ if (totalDelta > 0) {
6938
+ output += `\u{1F4C8} Improved by ${totalDelta} points over ${recent.length} checks
6939
+
6940
+ `;
6941
+ } else if (totalDelta < 0) {
6942
+ output += `\u{1F4C9} Declined by ${Math.abs(totalDelta)} points over ${recent.length} checks
6943
+
6944
+ `;
6945
+ } else {
6946
+ output += `\u{1F4CA} Stable at ${last.score} points over ${recent.length} checks
6947
+
6948
+ `;
6949
+ }
6950
+ return output;
6951
+ }
6952
+ function generateDetailedMetrics(dimensions) {
6953
+ let output = "";
6954
+ for (const dim of dimensions) {
6955
+ output += header(dim.name, 3);
6956
+ output += `**Score:** ${dim.score}/100 (${dim.grade})
6957
+
6958
+ `;
6959
+ output += `**Details:** ${dim.details}
6960
+
6961
+ `;
6962
+ if (Object.keys(dim.metrics).length > 0) {
6963
+ output += `**Metrics:**
6964
+
6965
+ `;
6966
+ for (const [key, value] of Object.entries(dim.metrics)) {
6967
+ output += `- ${key}: ${typeof value === "number" ? formatNumber(value) : value}
6968
+ `;
6969
+ }
6970
+ output += "\n";
6971
+ }
6972
+ }
6973
+ return output;
6974
+ }
6975
+
6332
6976
  // src/docs/metadata.ts
6333
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync } from "fs";
6334
- import { join as join10 } from "path";
6977
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
6978
+ import { join as join11 } from "path";
6335
6979
  function loadMetadata(outputDir) {
6336
- const metadataPath = join10(outputDir, "metadata.json");
6337
- if (!existsSync7(metadataPath)) {
6980
+ const metadataPath = join11(outputDir, "metadata.json");
6981
+ if (!existsSync8(metadataPath)) {
6338
6982
  return null;
6339
6983
  }
6340
6984
  try {
6341
- const content = readFileSync5(metadataPath, "utf-8");
6985
+ const content = readFileSync6(metadataPath, "utf-8");
6342
6986
  return JSON.parse(content);
6343
6987
  } catch (err) {
6344
6988
  console.error("Failed to load metadata:", err);
@@ -6346,14 +6990,14 @@ function loadMetadata(outputDir) {
6346
6990
  }
6347
6991
  }
6348
6992
  function saveMetadata(outputDir, metadata) {
6349
- const metadataPath = join10(outputDir, "metadata.json");
6350
- writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
6993
+ const metadataPath = join11(outputDir, "metadata.json");
6994
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
6351
6995
  }
6352
6996
  function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
6353
6997
  const now = (/* @__PURE__ */ new Date()).toISOString();
6354
6998
  const documents = {};
6355
6999
  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`;
7000
+ 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
7001
  documents[docType] = {
6358
7002
  generated_at: now,
6359
7003
  file: fileName
@@ -6389,8 +7033,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6389
7033
  const generated = [];
6390
7034
  const errors = [];
6391
7035
  try {
6392
- if (!existsSync8(options.outputDir)) {
6393
- mkdirSync(options.outputDir, { recursive: true });
7036
+ if (!existsSync9(options.outputDir)) {
7037
+ mkdirSync2(options.outputDir, { recursive: true });
6394
7038
  if (options.verbose) {
6395
7039
  console.log(`Created output directory: ${options.outputDir}`);
6396
7040
  }
@@ -6411,14 +7055,15 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6411
7055
  "tests",
6412
7056
  "history",
6413
7057
  "current",
6414
- "status"
7058
+ "status",
7059
+ "health"
6415
7060
  ];
6416
7061
  }
6417
7062
  let metadata = null;
6418
7063
  if (options.update) {
6419
7064
  metadata = loadMetadata(options.outputDir);
6420
7065
  }
6421
- const fileCount = getFileCount12(graph);
7066
+ const fileCount = getFileCount13(graph);
6422
7067
  const symbolCount = graph.order;
6423
7068
  const edgeCount = graph.size;
6424
7069
  if (options.format === "markdown") {
@@ -6426,8 +7071,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6426
7071
  try {
6427
7072
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
6428
7073
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
6429
- const filePath = join11(options.outputDir, "ARCHITECTURE.md");
6430
- writeFileSync2(filePath, content, "utf-8");
7074
+ const filePath = join12(options.outputDir, "ARCHITECTURE.md");
7075
+ writeFileSync3(filePath, content, "utf-8");
6431
7076
  generated.push("ARCHITECTURE.md");
6432
7077
  } catch (err) {
6433
7078
  errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
@@ -6437,8 +7082,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6437
7082
  try {
6438
7083
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
6439
7084
  const content = generateConventions(graph, projectRoot, version);
6440
- const filePath = join11(options.outputDir, "CONVENTIONS.md");
6441
- writeFileSync2(filePath, content, "utf-8");
7085
+ const filePath = join12(options.outputDir, "CONVENTIONS.md");
7086
+ writeFileSync3(filePath, content, "utf-8");
6442
7087
  generated.push("CONVENTIONS.md");
6443
7088
  } catch (err) {
6444
7089
  errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
@@ -6448,8 +7093,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6448
7093
  try {
6449
7094
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
6450
7095
  const content = generateDependencies(graph, projectRoot, version);
6451
- const filePath = join11(options.outputDir, "DEPENDENCIES.md");
6452
- writeFileSync2(filePath, content, "utf-8");
7096
+ const filePath = join12(options.outputDir, "DEPENDENCIES.md");
7097
+ writeFileSync3(filePath, content, "utf-8");
6453
7098
  generated.push("DEPENDENCIES.md");
6454
7099
  } catch (err) {
6455
7100
  errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
@@ -6459,8 +7104,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6459
7104
  try {
6460
7105
  if (options.verbose) console.log("Generating ONBOARDING.md...");
6461
7106
  const content = generateOnboarding(graph, projectRoot, version);
6462
- const filePath = join11(options.outputDir, "ONBOARDING.md");
6463
- writeFileSync2(filePath, content, "utf-8");
7107
+ const filePath = join12(options.outputDir, "ONBOARDING.md");
7108
+ writeFileSync3(filePath, content, "utf-8");
6464
7109
  generated.push("ONBOARDING.md");
6465
7110
  } catch (err) {
6466
7111
  errors.push(`Failed to generate ONBOARDING.md: ${err}`);
@@ -6470,8 +7115,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6470
7115
  try {
6471
7116
  if (options.verbose) console.log("Generating FILES.md...");
6472
7117
  const content = generateFiles(graph, projectRoot, version);
6473
- const filePath = join11(options.outputDir, "FILES.md");
6474
- writeFileSync2(filePath, content, "utf-8");
7118
+ const filePath = join12(options.outputDir, "FILES.md");
7119
+ writeFileSync3(filePath, content, "utf-8");
6475
7120
  generated.push("FILES.md");
6476
7121
  } catch (err) {
6477
7122
  errors.push(`Failed to generate FILES.md: ${err}`);
@@ -6481,8 +7126,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6481
7126
  try {
6482
7127
  if (options.verbose) console.log("Generating API_SURFACE.md...");
6483
7128
  const content = generateApiSurface(graph, projectRoot, version);
6484
- const filePath = join11(options.outputDir, "API_SURFACE.md");
6485
- writeFileSync2(filePath, content, "utf-8");
7129
+ const filePath = join12(options.outputDir, "API_SURFACE.md");
7130
+ writeFileSync3(filePath, content, "utf-8");
6486
7131
  generated.push("API_SURFACE.md");
6487
7132
  } catch (err) {
6488
7133
  errors.push(`Failed to generate API_SURFACE.md: ${err}`);
@@ -6492,8 +7137,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6492
7137
  try {
6493
7138
  if (options.verbose) console.log("Generating ERRORS.md...");
6494
7139
  const content = generateErrors(graph, projectRoot, version);
6495
- const filePath = join11(options.outputDir, "ERRORS.md");
6496
- writeFileSync2(filePath, content, "utf-8");
7140
+ const filePath = join12(options.outputDir, "ERRORS.md");
7141
+ writeFileSync3(filePath, content, "utf-8");
6497
7142
  generated.push("ERRORS.md");
6498
7143
  } catch (err) {
6499
7144
  errors.push(`Failed to generate ERRORS.md: ${err}`);
@@ -6503,8 +7148,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6503
7148
  try {
6504
7149
  if (options.verbose) console.log("Generating TESTS.md...");
6505
7150
  const content = generateTests(graph, projectRoot, version);
6506
- const filePath = join11(options.outputDir, "TESTS.md");
6507
- writeFileSync2(filePath, content, "utf-8");
7151
+ const filePath = join12(options.outputDir, "TESTS.md");
7152
+ writeFileSync3(filePath, content, "utf-8");
6508
7153
  generated.push("TESTS.md");
6509
7154
  } catch (err) {
6510
7155
  errors.push(`Failed to generate TESTS.md: ${err}`);
@@ -6514,8 +7159,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6514
7159
  try {
6515
7160
  if (options.verbose) console.log("Generating HISTORY.md...");
6516
7161
  const content = generateHistory(graph, projectRoot, version);
6517
- const filePath = join11(options.outputDir, "HISTORY.md");
6518
- writeFileSync2(filePath, content, "utf-8");
7162
+ const filePath = join12(options.outputDir, "HISTORY.md");
7163
+ writeFileSync3(filePath, content, "utf-8");
6519
7164
  generated.push("HISTORY.md");
6520
7165
  } catch (err) {
6521
7166
  errors.push(`Failed to generate HISTORY.md: ${err}`);
@@ -6525,8 +7170,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6525
7170
  try {
6526
7171
  if (options.verbose) console.log("Generating CURRENT.md...");
6527
7172
  const content = generateCurrent(graph, projectRoot, version);
6528
- const filePath = join11(options.outputDir, "CURRENT.md");
6529
- writeFileSync2(filePath, content, "utf-8");
7173
+ const filePath = join12(options.outputDir, "CURRENT.md");
7174
+ writeFileSync3(filePath, content, "utf-8");
6530
7175
  generated.push("CURRENT.md");
6531
7176
  } catch (err) {
6532
7177
  errors.push(`Failed to generate CURRENT.md: ${err}`);
@@ -6536,13 +7181,24 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6536
7181
  try {
6537
7182
  if (options.verbose) console.log("Generating STATUS.md...");
6538
7183
  const content = generateStatus(graph, projectRoot, version);
6539
- const filePath = join11(options.outputDir, "STATUS.md");
6540
- writeFileSync2(filePath, content, "utf-8");
7184
+ const filePath = join12(options.outputDir, "STATUS.md");
7185
+ writeFileSync3(filePath, content, "utf-8");
6541
7186
  generated.push("STATUS.md");
6542
7187
  } catch (err) {
6543
7188
  errors.push(`Failed to generate STATUS.md: ${err}`);
6544
7189
  }
6545
7190
  }
7191
+ if (docsToGenerate.includes("health")) {
7192
+ try {
7193
+ if (options.verbose) console.log("Generating HEALTH.md...");
7194
+ const content = generateHealth(graph, projectRoot, version);
7195
+ const filePath = join12(options.outputDir, "HEALTH.md");
7196
+ writeFileSync3(filePath, content, "utf-8");
7197
+ generated.push("HEALTH.md");
7198
+ } catch (err) {
7199
+ errors.push(`Failed to generate HEALTH.md: ${err}`);
7200
+ }
7201
+ }
6546
7202
  } else if (options.format === "json") {
6547
7203
  errors.push("JSON format not yet supported");
6548
7204
  }
@@ -6571,7 +7227,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
6571
7227
  };
6572
7228
  }
6573
7229
  }
6574
- function getFileCount12(graph) {
7230
+ function getFileCount13(graph) {
6575
7231
  const files = /* @__PURE__ */ new Set();
6576
7232
  graph.forEachNode((node, attrs) => {
6577
7233
  files.add(attrs.filePath);
@@ -6584,13 +7240,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6584
7240
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6585
7241
 
6586
7242
  // src/mcp/tools.ts
6587
- import { dirname as dirname12, join as join13 } from "path";
6588
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
7243
+ import { dirname as dirname14, join as join14 } from "path";
7244
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
6589
7245
 
6590
7246
  // src/mcp/connect.ts
6591
7247
  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";
7248
+ import { existsSync as existsSync10 } from "fs";
7249
+ import { join as join13, basename as basename5, resolve as resolve2 } from "path";
6594
7250
  import { tmpdir, homedir } from "os";
6595
7251
  function validateProjectPath(source) {
6596
7252
  const resolved = resolve2(source);
@@ -6603,11 +7259,11 @@ function validateProjectPath(source) {
6603
7259
  "/boot",
6604
7260
  "/proc",
6605
7261
  "/sys",
6606
- join12(homedir(), ".ssh"),
6607
- join12(homedir(), ".gnupg"),
6608
- join12(homedir(), ".aws"),
6609
- join12(homedir(), ".config"),
6610
- join12(homedir(), ".env")
7262
+ join13(homedir(), ".ssh"),
7263
+ join13(homedir(), ".gnupg"),
7264
+ join13(homedir(), ".aws"),
7265
+ join13(homedir(), ".config"),
7266
+ join13(homedir(), ".env")
6611
7267
  ];
6612
7268
  for (const blocked of blockedPaths) {
6613
7269
  if (resolved.startsWith(blocked)) {
@@ -6630,11 +7286,11 @@ async function connectToRepo(source, subdirectory, state) {
6630
7286
  };
6631
7287
  }
6632
7288
  projectName = match[1];
6633
- const reposDir = join12(tmpdir(), "depwire-repos");
6634
- const cloneDir = join12(reposDir, projectName);
7289
+ const reposDir = join13(tmpdir(), "depwire-repos");
7290
+ const cloneDir = join13(reposDir, projectName);
6635
7291
  console.error(`Connecting to GitHub repo: ${source}`);
6636
7292
  const git = simpleGit();
6637
- if (existsSync9(cloneDir)) {
7293
+ if (existsSync10(cloneDir)) {
6638
7294
  console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
6639
7295
  try {
6640
7296
  await git.cwd(cloneDir).pull();
@@ -6652,7 +7308,7 @@ async function connectToRepo(source, subdirectory, state) {
6652
7308
  };
6653
7309
  }
6654
7310
  }
6655
- projectRoot = subdirectory ? join12(cloneDir, subdirectory) : cloneDir;
7311
+ projectRoot = subdirectory ? join13(cloneDir, subdirectory) : cloneDir;
6656
7312
  } else {
6657
7313
  const validation2 = validateProjectPath(source);
6658
7314
  if (!validation2.valid) {
@@ -6661,13 +7317,13 @@ async function connectToRepo(source, subdirectory, state) {
6661
7317
  message: validation2.error
6662
7318
  };
6663
7319
  }
6664
- if (!existsSync9(source)) {
7320
+ if (!existsSync10(source)) {
6665
7321
  return {
6666
7322
  error: "Directory not found",
6667
7323
  message: `Directory does not exist: ${source}`
6668
7324
  };
6669
7325
  }
6670
- projectRoot = subdirectory ? join12(source, subdirectory) : source;
7326
+ projectRoot = subdirectory ? join13(source, subdirectory) : source;
6671
7327
  projectName = basename5(projectRoot);
6672
7328
  }
6673
7329
  const validation = validateProjectPath(projectRoot);
@@ -6677,7 +7333,7 @@ async function connectToRepo(source, subdirectory, state) {
6677
7333
  message: validation.error
6678
7334
  };
6679
7335
  }
6680
- if (!existsSync9(projectRoot)) {
7336
+ if (!existsSync10(projectRoot)) {
6681
7337
  return {
6682
7338
  error: "Project root not found",
6683
7339
  message: `Directory does not exist: ${projectRoot}`
@@ -6951,6 +7607,14 @@ function getToolsList() {
6951
7607
  }
6952
7608
  }
6953
7609
  }
7610
+ },
7611
+ {
7612
+ name: "get_health_score",
7613
+ 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.",
7614
+ inputSchema: {
7615
+ type: "object",
7616
+ properties: {}
7617
+ }
6954
7618
  }
6955
7619
  ];
6956
7620
  }
@@ -6995,6 +7659,15 @@ async function handleToolCall(name, args, state) {
6995
7659
  } else {
6996
7660
  result = await handleUpdateProjectDocs(args.doc_type || "all", state);
6997
7661
  }
7662
+ } else if (name === "get_health_score") {
7663
+ if (!isProjectLoaded(state)) {
7664
+ result = {
7665
+ error: "No project loaded",
7666
+ message: "Use connect_repo to connect to a codebase first"
7667
+ };
7668
+ } else {
7669
+ result = handleGetHealthScore(state);
7670
+ }
6998
7671
  } else {
6999
7672
  if (!isProjectLoaded(state)) {
7000
7673
  result = {
@@ -7332,7 +8005,7 @@ function handleGetArchitectureSummary(graph) {
7332
8005
  const dirMap = /* @__PURE__ */ new Map();
7333
8006
  const languageBreakdown = {};
7334
8007
  fileSummary.forEach((f) => {
7335
- const dir = f.filePath.includes("/") ? dirname12(f.filePath) : ".";
8008
+ const dir = f.filePath.includes("/") ? dirname14(f.filePath) : ".";
7336
8009
  if (!dirMap.has(dir)) {
7337
8010
  dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
7338
8011
  }
@@ -7419,8 +8092,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
7419
8092
  };
7420
8093
  }
7421
8094
  async function handleGetProjectDocs(docType, state) {
7422
- const docsDir = join13(state.projectRoot, ".depwire");
7423
- if (!existsSync10(docsDir)) {
8095
+ const docsDir = join14(state.projectRoot, ".depwire");
8096
+ if (!existsSync11(docsDir)) {
7424
8097
  const errorMessage = `Project documentation has not been generated yet.
7425
8098
 
7426
8099
  Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
@@ -7450,12 +8123,12 @@ Available document types:
7450
8123
  missing.push(doc);
7451
8124
  continue;
7452
8125
  }
7453
- const filePath = join13(docsDir, metadata.documents[doc].file);
7454
- if (!existsSync10(filePath)) {
8126
+ const filePath = join14(docsDir, metadata.documents[doc].file);
8127
+ if (!existsSync11(filePath)) {
7455
8128
  missing.push(doc);
7456
8129
  continue;
7457
8130
  }
7458
- const content = readFileSync6(filePath, "utf-8");
8131
+ const content = readFileSync7(filePath, "utf-8");
7459
8132
  if (docsToReturn.length > 1) {
7460
8133
  output += `
7461
8134
 
@@ -7480,16 +8153,16 @@ Available document types:
7480
8153
  }
7481
8154
  async function handleUpdateProjectDocs(docType, state) {
7482
8155
  const startTime = Date.now();
7483
- const docsDir = join13(state.projectRoot, ".depwire");
8156
+ const docsDir = join14(state.projectRoot, ".depwire");
7484
8157
  console.error("Regenerating project documentation...");
7485
8158
  const parsedFiles = await parseProject(state.projectRoot);
7486
8159
  const graph = buildGraph(parsedFiles);
7487
8160
  const parseTime = (Date.now() - startTime) / 1e3;
7488
8161
  state.graph = graph;
7489
- const packageJsonPath = join13(__dirname, "../../package.json");
7490
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
8162
+ const packageJsonPath = join14(__dirname, "../../package.json");
8163
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
7491
8164
  const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
7492
- const docsExist = existsSync10(docsDir);
8165
+ const docsExist = existsSync11(docsDir);
7493
8166
  const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
7494
8167
  outputDir: docsDir,
7495
8168
  format: "markdown",
@@ -7524,6 +8197,12 @@ async function handleUpdateProjectDocs(docType, state) {
7524
8197
  };
7525
8198
  }
7526
8199
  }
8200
+ function handleGetHealthScore(state) {
8201
+ const graph = state.graph;
8202
+ const projectRoot = state.projectRoot;
8203
+ const report = calculateHealthScore(graph, projectRoot);
8204
+ return report;
8205
+ }
7527
8206
 
7528
8207
  // src/mcp/server.ts
7529
8208
  async function startMcpServer(state) {
@@ -7569,6 +8248,8 @@ export {
7569
8248
  startVizServer,
7570
8249
  createEmptyState,
7571
8250
  updateFileInGraph,
8251
+ calculateHealthScore,
8252
+ getHealthTrend,
7572
8253
  generateDocs,
7573
8254
  startMcpServer
7574
8255
  };