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