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.
- package/README.md +46 -5
- package/dist/{chunk-Q733EWFA.js → chunk-AFJFZXCI.js} +774 -94
- package/dist/index.js +141 -1
- package/dist/mcpb-entry.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
2643
|
-
import { join as
|
|
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
|
|
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(
|
|
3145
|
+
function codeBlock(code3, lang = "") {
|
|
2658
3146
|
return `\`\`\`${lang}
|
|
2659
|
-
${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2838
|
-
const targetDir =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4013
|
-
const targetDir =
|
|
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 =
|
|
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 =
|
|
4149
|
-
if (files.every((f) =>
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
5980
|
-
import { join as
|
|
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 =
|
|
6014
|
-
if (!
|
|
6501
|
+
const fullPath = join10(projectRoot, filePath);
|
|
6502
|
+
if (!existsSync7(fullPath)) {
|
|
6015
6503
|
return comments;
|
|
6016
6504
|
}
|
|
6017
6505
|
try {
|
|
6018
|
-
const content =
|
|
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
|
|
6334
|
-
import { join as
|
|
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 =
|
|
6337
|
-
if (!
|
|
6979
|
+
const metadataPath = join11(outputDir, "metadata.json");
|
|
6980
|
+
if (!existsSync8(metadataPath)) {
|
|
6338
6981
|
return null;
|
|
6339
6982
|
}
|
|
6340
6983
|
try {
|
|
6341
|
-
const content =
|
|
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 =
|
|
6350
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
6430
|
-
|
|
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 =
|
|
6441
|
-
|
|
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 =
|
|
6452
|
-
|
|
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 =
|
|
6463
|
-
|
|
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 =
|
|
6474
|
-
|
|
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 =
|
|
6485
|
-
|
|
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 =
|
|
6496
|
-
|
|
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 =
|
|
6507
|
-
|
|
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 =
|
|
6518
|
-
|
|
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 =
|
|
6529
|
-
|
|
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 =
|
|
6540
|
-
|
|
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
|
|
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
|
|
6588
|
-
import { existsSync as
|
|
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
|
|
6593
|
-
import { join as
|
|
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
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
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 =
|
|
6634
|
-
const cloneDir =
|
|
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 (
|
|
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 ?
|
|
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 (!
|
|
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 ?
|
|
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 (!
|
|
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("/") ?
|
|
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 =
|
|
7423
|
-
if (!
|
|
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 =
|
|
7454
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7490
|
-
const packageJson = JSON.parse(
|
|
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 =
|
|
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
|
};
|