@vohongtho.infotech/code-intel 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 +3 -2
- package/dist/cli/main.js +893 -47
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +891 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/main.js
CHANGED
|
@@ -3058,7 +3058,8 @@ var init_schema = __esm({
|
|
|
3058
3058
|
constant: "const_nodes",
|
|
3059
3059
|
route: "route_nodes",
|
|
3060
3060
|
cluster: "cluster_nodes",
|
|
3061
|
-
flow: "flow_nodes"
|
|
3061
|
+
flow: "flow_nodes",
|
|
3062
|
+
vulnerability: "vuln_nodes"
|
|
3062
3063
|
};
|
|
3063
3064
|
ALL_NODE_TABLES = [...new Set(Object.values(NODE_TABLE_MAP))];
|
|
3064
3065
|
}
|
|
@@ -5516,9 +5517,9 @@ var init_orphan_files = __esm({
|
|
|
5516
5517
|
// src/health/health-score.ts
|
|
5517
5518
|
var health_score_exports = {};
|
|
5518
5519
|
__export(health_score_exports, {
|
|
5519
|
-
computeHealthReport: () =>
|
|
5520
|
+
computeHealthReport: () => computeHealthReport2
|
|
5520
5521
|
});
|
|
5521
|
-
function
|
|
5522
|
+
function computeHealthReport2(graph, godNodeConfig) {
|
|
5522
5523
|
const deadCode = detectDeadCode(graph);
|
|
5523
5524
|
const cycles = detectCircularDeps(graph);
|
|
5524
5525
|
const godNodes = detectGodNodes(graph, godNodeConfig);
|
|
@@ -9590,6 +9591,624 @@ init_metadata();
|
|
|
9590
9591
|
init_group_registry();
|
|
9591
9592
|
init_tracing();
|
|
9592
9593
|
init_metrics();
|
|
9594
|
+
|
|
9595
|
+
// src/query/explain-relationship.ts
|
|
9596
|
+
function explainRelationship(graph, from, to) {
|
|
9597
|
+
const allNodes = [...graph.allNodes()];
|
|
9598
|
+
const fromNode = allNodes.find((n) => n.name === from);
|
|
9599
|
+
if (!fromNode) {
|
|
9600
|
+
const firstChar = from[0]?.toLowerCase() ?? "";
|
|
9601
|
+
const fromLower = from.toLowerCase();
|
|
9602
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(fromLower)).slice(0, 5).map((n) => n.name);
|
|
9603
|
+
return { error: `Symbol not found: ${from}`, suggestions };
|
|
9604
|
+
}
|
|
9605
|
+
const toNode = allNodes.find((n) => n.name === to);
|
|
9606
|
+
if (!toNode) {
|
|
9607
|
+
const firstChar = to[0]?.toLowerCase() ?? "";
|
|
9608
|
+
const toLower = to.toLowerCase();
|
|
9609
|
+
const suggestions = allNodes.filter((n) => n.name.toLowerCase().startsWith(firstChar) || n.name.toLowerCase().includes(toLower)).slice(0, 5).map((n) => n.name);
|
|
9610
|
+
return { error: `Symbol not found: ${to}`, suggestions };
|
|
9611
|
+
}
|
|
9612
|
+
const paths = [];
|
|
9613
|
+
const queue = [{
|
|
9614
|
+
id: fromNode.id,
|
|
9615
|
+
nodeNames: [fromNode.name],
|
|
9616
|
+
lastEdgeKind: "",
|
|
9617
|
+
visited: /* @__PURE__ */ new Set([fromNode.id])
|
|
9618
|
+
}];
|
|
9619
|
+
while (queue.length > 0 && paths.length < 10) {
|
|
9620
|
+
const entry = queue.shift();
|
|
9621
|
+
const { id, nodeNames, visited } = entry;
|
|
9622
|
+
if (nodeNames.length > 6) continue;
|
|
9623
|
+
for (const edge of graph.findEdgesFrom(id)) {
|
|
9624
|
+
const targetNode = graph.getNode(edge.target);
|
|
9625
|
+
if (!targetNode) continue;
|
|
9626
|
+
if (visited.has(edge.target)) continue;
|
|
9627
|
+
const newNames = [...nodeNames, targetNode.name];
|
|
9628
|
+
if (edge.target === toNode.id) {
|
|
9629
|
+
paths.push({ hops: newNames.length - 1, nodes: newNames, edgeKind: edge.kind });
|
|
9630
|
+
if (paths.length >= 10) break;
|
|
9631
|
+
continue;
|
|
9632
|
+
}
|
|
9633
|
+
if (newNames.length < 6) {
|
|
9634
|
+
const newVisited = new Set(visited);
|
|
9635
|
+
newVisited.add(edge.target);
|
|
9636
|
+
queue.push({ id: edge.target, nodeNames: newNames, lastEdgeKind: edge.kind, visited: newVisited });
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
}
|
|
9640
|
+
const fromImports = /* @__PURE__ */ new Set();
|
|
9641
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
9642
|
+
if (edge.kind === "imports") fromImports.add(edge.target);
|
|
9643
|
+
}
|
|
9644
|
+
const sharedImportIds = [];
|
|
9645
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
9646
|
+
if (edge.kind === "imports" && fromImports.has(edge.target)) {
|
|
9647
|
+
sharedImportIds.push(edge.target);
|
|
9648
|
+
}
|
|
9649
|
+
}
|
|
9650
|
+
const sharedImports = sharedImportIds.map((id) => graph.getNode(id)?.name ?? id);
|
|
9651
|
+
let heritage = null;
|
|
9652
|
+
for (const edge of graph.findEdgesFrom(fromNode.id)) {
|
|
9653
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === toNode.id) {
|
|
9654
|
+
heritage = `${from} ${edge.kind} ${to}`;
|
|
9655
|
+
break;
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
if (!heritage) {
|
|
9659
|
+
for (const edge of graph.findEdgesFrom(toNode.id)) {
|
|
9660
|
+
if ((edge.kind === "extends" || edge.kind === "implements") && edge.target === fromNode.id) {
|
|
9661
|
+
heritage = `${to} ${edge.kind} ${from}`;
|
|
9662
|
+
break;
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
const sharedStr = sharedImports.length > 0 ? sharedImports.join(", ") : "none";
|
|
9667
|
+
const heritageStr = heritage ?? "none";
|
|
9668
|
+
const connectionStr = paths.length === 0 ? "No connection found." : `${from} \u2192 ${to} via ${paths.length} path(s).`;
|
|
9669
|
+
const summary = `${connectionStr} Shared imports: [${sharedStr}]. Heritage: ${heritageStr}.`;
|
|
9670
|
+
return { paths, sharedImports, heritage, summary };
|
|
9671
|
+
}
|
|
9672
|
+
|
|
9673
|
+
// src/query/pr-impact.ts
|
|
9674
|
+
function parseDiffFiles(diff) {
|
|
9675
|
+
const files = [];
|
|
9676
|
+
for (const line of diff.split("\n")) {
|
|
9677
|
+
const match = line.match(/^\+\+\+ b\/(.+)/);
|
|
9678
|
+
if (match) {
|
|
9679
|
+
files.push(match[1]);
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
return files;
|
|
9683
|
+
}
|
|
9684
|
+
function computePRImpact(graph, changedFiles, maxHops) {
|
|
9685
|
+
const changedSymbolIds = /* @__PURE__ */ new Set();
|
|
9686
|
+
for (const node of graph.allNodes()) {
|
|
9687
|
+
if (!node.filePath) continue;
|
|
9688
|
+
for (const changedFile of changedFiles) {
|
|
9689
|
+
if (node.filePath === changedFile || node.filePath.endsWith(changedFile) || changedFile.endsWith(node.filePath)) {
|
|
9690
|
+
changedSymbolIds.add(node.id);
|
|
9691
|
+
break;
|
|
9692
|
+
}
|
|
9693
|
+
}
|
|
9694
|
+
}
|
|
9695
|
+
const allBlastRadiusNodes = /* @__PURE__ */ new Set();
|
|
9696
|
+
const changedSymbols = [];
|
|
9697
|
+
for (const symbolId of changedSymbolIds) {
|
|
9698
|
+
const symbolNode = graph.getNode(symbolId);
|
|
9699
|
+
if (!symbolNode) continue;
|
|
9700
|
+
const blastRadius = /* @__PURE__ */ new Set();
|
|
9701
|
+
const queue = [{ id: symbolId, depth: 0 }];
|
|
9702
|
+
const visited = /* @__PURE__ */ new Set();
|
|
9703
|
+
while (queue.length > 0) {
|
|
9704
|
+
const { id, depth } = queue.shift();
|
|
9705
|
+
if (visited.has(id) || depth > maxHops) continue;
|
|
9706
|
+
visited.add(id);
|
|
9707
|
+
if (id !== symbolId) blastRadius.add(id);
|
|
9708
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
9709
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
9710
|
+
queue.push({ id: edge.source, depth: depth + 1 });
|
|
9711
|
+
}
|
|
9712
|
+
}
|
|
9713
|
+
}
|
|
9714
|
+
for (const id of blastRadius) allBlastRadiusNodes.add(id);
|
|
9715
|
+
const blastCount = blastRadius.size;
|
|
9716
|
+
let risk;
|
|
9717
|
+
if (blastCount > 50) {
|
|
9718
|
+
risk = "HIGH";
|
|
9719
|
+
} else if (blastCount >= 10) {
|
|
9720
|
+
risk = "MEDIUM";
|
|
9721
|
+
} else {
|
|
9722
|
+
risk = "LOW";
|
|
9723
|
+
}
|
|
9724
|
+
let callerCount = 0;
|
|
9725
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
9726
|
+
if (edge.kind === "calls") callerCount++;
|
|
9727
|
+
}
|
|
9728
|
+
let testCoverage = false;
|
|
9729
|
+
for (const edge of graph.findEdgesTo(symbolId)) {
|
|
9730
|
+
if (edge.kind === "imports") {
|
|
9731
|
+
const callerNode = graph.getNode(edge.source);
|
|
9732
|
+
if (callerNode?.filePath && (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec."))) {
|
|
9733
|
+
testCoverage = true;
|
|
9734
|
+
break;
|
|
9735
|
+
}
|
|
9736
|
+
}
|
|
9737
|
+
}
|
|
9738
|
+
changedSymbols.push({ name: symbolNode.name, risk, callerCount, testCoverage });
|
|
9739
|
+
}
|
|
9740
|
+
const impactedSymbols = [];
|
|
9741
|
+
for (const id of allBlastRadiusNodes) {
|
|
9742
|
+
if (changedSymbolIds.has(id)) continue;
|
|
9743
|
+
const node = graph.getNode(id);
|
|
9744
|
+
if (node) {
|
|
9745
|
+
impactedSymbols.push({ name: node.name, filePath: node.filePath });
|
|
9746
|
+
}
|
|
9747
|
+
}
|
|
9748
|
+
const riskSummary = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
|
9749
|
+
for (const s of changedSymbols) {
|
|
9750
|
+
riskSummary[s.risk]++;
|
|
9751
|
+
}
|
|
9752
|
+
const coverageGaps = [];
|
|
9753
|
+
for (const s of changedSymbols) {
|
|
9754
|
+
if ((s.risk === "HIGH" || s.risk === "MEDIUM") && !s.testCoverage) {
|
|
9755
|
+
coverageGaps.push(`${s.name} has no test coverage`);
|
|
9756
|
+
}
|
|
9757
|
+
}
|
|
9758
|
+
const fileImpactCount = /* @__PURE__ */ new Map();
|
|
9759
|
+
for (const sym of impactedSymbols) {
|
|
9760
|
+
if (sym.filePath) {
|
|
9761
|
+
fileImpactCount.set(sym.filePath, (fileImpactCount.get(sym.filePath) ?? 0) + 1);
|
|
9762
|
+
}
|
|
9763
|
+
}
|
|
9764
|
+
const filesToReview = [...fileImpactCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([fp]) => fp);
|
|
9765
|
+
return {
|
|
9766
|
+
changedSymbols,
|
|
9767
|
+
impactedSymbols,
|
|
9768
|
+
riskSummary,
|
|
9769
|
+
coverageGaps,
|
|
9770
|
+
filesToReview,
|
|
9771
|
+
crossRepoImpact: null
|
|
9772
|
+
};
|
|
9773
|
+
}
|
|
9774
|
+
|
|
9775
|
+
// src/query/similar-symbols.ts
|
|
9776
|
+
function levenshtein(a, b) {
|
|
9777
|
+
const m = a.length;
|
|
9778
|
+
const n = b.length;
|
|
9779
|
+
const dp = Array.from(
|
|
9780
|
+
{ length: m + 1 },
|
|
9781
|
+
(_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
9782
|
+
);
|
|
9783
|
+
for (let i = 1; i <= m; i++) {
|
|
9784
|
+
for (let j = 1; j <= n; j++) {
|
|
9785
|
+
if (a[i - 1] === b[j - 1]) {
|
|
9786
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
9787
|
+
} else {
|
|
9788
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
9789
|
+
}
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
return dp[m][n];
|
|
9793
|
+
}
|
|
9794
|
+
function findSimilarSymbols(graph, symbolName, limit) {
|
|
9795
|
+
const clampedLimit = Math.min(Math.max(1, limit), 50);
|
|
9796
|
+
const allNodes = [...graph.allNodes()];
|
|
9797
|
+
const targetNode = allNodes.find((n) => n.name === symbolName);
|
|
9798
|
+
if (!targetNode) {
|
|
9799
|
+
return { similar: [] };
|
|
9800
|
+
}
|
|
9801
|
+
let targetCluster = null;
|
|
9802
|
+
for (const edge of graph.findEdgesFrom(targetNode.id)) {
|
|
9803
|
+
if (edge.kind === "belongs_to") {
|
|
9804
|
+
const clusterNode = graph.getNode(edge.target);
|
|
9805
|
+
if (clusterNode) {
|
|
9806
|
+
targetCluster = clusterNode.name;
|
|
9807
|
+
break;
|
|
9808
|
+
}
|
|
9809
|
+
}
|
|
9810
|
+
}
|
|
9811
|
+
if (!targetCluster) {
|
|
9812
|
+
for (const edge of graph.findEdgesTo(targetNode.id)) {
|
|
9813
|
+
if (edge.kind === "belongs_to") {
|
|
9814
|
+
const clusterNode = graph.getNode(edge.source);
|
|
9815
|
+
if (clusterNode) {
|
|
9816
|
+
targetCluster = clusterNode.name;
|
|
9817
|
+
break;
|
|
9818
|
+
}
|
|
9819
|
+
}
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
const results = [];
|
|
9823
|
+
for (const node of allNodes) {
|
|
9824
|
+
if (node.id === targetNode.id) continue;
|
|
9825
|
+
const maxLen = Math.max(symbolName.length, node.name.length);
|
|
9826
|
+
const nameSim = maxLen === 0 ? 1 : 1 - levenshtein(symbolName, node.name) / maxLen;
|
|
9827
|
+
const structuralSim = node.kind === targetNode.kind ? 0.5 : 0;
|
|
9828
|
+
const combined = 0.5 * nameSim + 0.5 * structuralSim;
|
|
9829
|
+
const reasons = [];
|
|
9830
|
+
if (nameSim >= 0.6) reasons.push("similar name");
|
|
9831
|
+
if (node.kind === targetNode.kind) reasons.push("same kind");
|
|
9832
|
+
if (targetCluster !== null) {
|
|
9833
|
+
let nodeCluster = null;
|
|
9834
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
9835
|
+
if (edge.kind === "belongs_to") {
|
|
9836
|
+
const clusterNode = graph.getNode(edge.target);
|
|
9837
|
+
if (clusterNode) {
|
|
9838
|
+
nodeCluster = clusterNode.name;
|
|
9839
|
+
break;
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
}
|
|
9843
|
+
if (!nodeCluster) {
|
|
9844
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
9845
|
+
if (edge.kind === "belongs_to") {
|
|
9846
|
+
const clusterNode = graph.getNode(edge.source);
|
|
9847
|
+
if (clusterNode) {
|
|
9848
|
+
nodeCluster = clusterNode.name;
|
|
9849
|
+
break;
|
|
9850
|
+
}
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9853
|
+
}
|
|
9854
|
+
if (nodeCluster !== null && nodeCluster === targetCluster) {
|
|
9855
|
+
reasons.push("same module");
|
|
9856
|
+
}
|
|
9857
|
+
}
|
|
9858
|
+
if (targetNode.metadata?.["cluster"] !== void 0 && node.metadata?.["cluster"] !== void 0 && node.metadata["cluster"] === targetNode.metadata["cluster"]) {
|
|
9859
|
+
if (!reasons.includes("same module")) reasons.push("same module");
|
|
9860
|
+
}
|
|
9861
|
+
results.push({ name: node.name, similarity: combined, reasons });
|
|
9862
|
+
}
|
|
9863
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
9864
|
+
return { similar: results.slice(0, clampedLimit) };
|
|
9865
|
+
}
|
|
9866
|
+
|
|
9867
|
+
// src/query/health-report.ts
|
|
9868
|
+
function computeHealthReport(graph, scope) {
|
|
9869
|
+
const wholeRepo = scope === ".";
|
|
9870
|
+
function inScope(filePath) {
|
|
9871
|
+
if (wholeRepo) return true;
|
|
9872
|
+
return filePath.startsWith(scope) || filePath.includes(scope);
|
|
9873
|
+
}
|
|
9874
|
+
const scopedNodes = [...graph.allNodes()].filter((n) => inScope(n.filePath));
|
|
9875
|
+
const deadCodeKinds = /* @__PURE__ */ new Set(["function", "method", "class"]);
|
|
9876
|
+
const deadCode = [];
|
|
9877
|
+
for (const node of scopedNodes) {
|
|
9878
|
+
if (!deadCodeKinds.has(node.kind)) continue;
|
|
9879
|
+
if (node.exported === true) continue;
|
|
9880
|
+
let hasIncoming = false;
|
|
9881
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
9882
|
+
hasIncoming = true;
|
|
9883
|
+
break;
|
|
9884
|
+
}
|
|
9885
|
+
if (!hasIncoming) {
|
|
9886
|
+
deadCode.push({ name: node.name, filePath: node.filePath, kind: node.kind });
|
|
9887
|
+
if (deadCode.length >= 20) break;
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
const cycles = [];
|
|
9891
|
+
const scopedNodeIds = new Set(scopedNodes.map((n) => n.id));
|
|
9892
|
+
const importAdj = /* @__PURE__ */ new Map();
|
|
9893
|
+
for (const node of scopedNodes) {
|
|
9894
|
+
importAdj.set(node.id, []);
|
|
9895
|
+
}
|
|
9896
|
+
for (const edge of graph.findEdgesByKind("imports")) {
|
|
9897
|
+
if (scopedNodeIds.has(edge.source) && scopedNodeIds.has(edge.target)) {
|
|
9898
|
+
importAdj.get(edge.source).push(edge.target);
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
const visited = /* @__PURE__ */ new Set();
|
|
9902
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
9903
|
+
const stackPath = [];
|
|
9904
|
+
function dfs(nodeId) {
|
|
9905
|
+
if (cycles.length >= 5) return;
|
|
9906
|
+
visited.add(nodeId);
|
|
9907
|
+
inStack.add(nodeId);
|
|
9908
|
+
stackPath.push(nodeId);
|
|
9909
|
+
for (const neighborId of importAdj.get(nodeId) ?? []) {
|
|
9910
|
+
if (cycles.length >= 5) break;
|
|
9911
|
+
if (inStack.has(neighborId)) {
|
|
9912
|
+
const cycleStart = stackPath.indexOf(neighborId);
|
|
9913
|
+
const cyclePath = stackPath.slice(cycleStart).map((id) => {
|
|
9914
|
+
const node = graph.getNode(id);
|
|
9915
|
+
return node ? node.name : id;
|
|
9916
|
+
});
|
|
9917
|
+
cycles.push(cyclePath);
|
|
9918
|
+
} else if (!visited.has(neighborId)) {
|
|
9919
|
+
dfs(neighborId);
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
stackPath.pop();
|
|
9923
|
+
inStack.delete(nodeId);
|
|
9924
|
+
}
|
|
9925
|
+
for (const node of scopedNodes) {
|
|
9926
|
+
if (cycles.length >= 5) break;
|
|
9927
|
+
if (!visited.has(node.id)) {
|
|
9928
|
+
dfs(node.id);
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
const godNodes = [];
|
|
9932
|
+
for (const node of scopedNodes) {
|
|
9933
|
+
let edgeCount = 0;
|
|
9934
|
+
for (const _edge of graph.findEdgesFrom(node.id)) {
|
|
9935
|
+
edgeCount++;
|
|
9936
|
+
}
|
|
9937
|
+
if (edgeCount > 10) {
|
|
9938
|
+
godNodes.push({ name: node.name, edgeCount, filePath: node.filePath });
|
|
9939
|
+
}
|
|
9940
|
+
}
|
|
9941
|
+
godNodes.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
9942
|
+
godNodes.splice(10);
|
|
9943
|
+
const filePathToNodes = /* @__PURE__ */ new Map();
|
|
9944
|
+
for (const node of scopedNodes) {
|
|
9945
|
+
if (!node.filePath) continue;
|
|
9946
|
+
let arr = filePathToNodes.get(node.filePath);
|
|
9947
|
+
if (!arr) {
|
|
9948
|
+
arr = [];
|
|
9949
|
+
filePathToNodes.set(node.filePath, arr);
|
|
9950
|
+
}
|
|
9951
|
+
arr.push(node.id);
|
|
9952
|
+
}
|
|
9953
|
+
const orphanFiles = [];
|
|
9954
|
+
for (const [filePath, nodeIds] of filePathToNodes) {
|
|
9955
|
+
if (orphanFiles.length >= 10) break;
|
|
9956
|
+
let hasAnyEdge = false;
|
|
9957
|
+
for (const nodeId of nodeIds) {
|
|
9958
|
+
let hasOut = false;
|
|
9959
|
+
for (const _edge of graph.findEdgesFrom(nodeId)) {
|
|
9960
|
+
hasOut = true;
|
|
9961
|
+
break;
|
|
9962
|
+
}
|
|
9963
|
+
let hasIn = false;
|
|
9964
|
+
for (const _edge of graph.findEdgesTo(nodeId)) {
|
|
9965
|
+
hasIn = true;
|
|
9966
|
+
break;
|
|
9967
|
+
}
|
|
9968
|
+
if (hasOut || hasIn) {
|
|
9969
|
+
hasAnyEdge = true;
|
|
9970
|
+
break;
|
|
9971
|
+
}
|
|
9972
|
+
}
|
|
9973
|
+
if (!hasAnyEdge) {
|
|
9974
|
+
orphanFiles.push(filePath);
|
|
9975
|
+
}
|
|
9976
|
+
}
|
|
9977
|
+
const hotspotCandidates = [];
|
|
9978
|
+
for (const node of scopedNodes) {
|
|
9979
|
+
const visitedBfs = /* @__PURE__ */ new Set();
|
|
9980
|
+
const queue = [{ id: node.id, depth: 0 }];
|
|
9981
|
+
while (queue.length > 0) {
|
|
9982
|
+
const item = queue.shift();
|
|
9983
|
+
if (item.depth > 5 || visitedBfs.has(item.id)) continue;
|
|
9984
|
+
visitedBfs.add(item.id);
|
|
9985
|
+
for (const edge of graph.findEdgesTo(item.id)) {
|
|
9986
|
+
if (edge.kind === "calls" || edge.kind === "imports") {
|
|
9987
|
+
if (!visitedBfs.has(edge.source)) {
|
|
9988
|
+
queue.push({ id: edge.source, depth: item.depth + 1 });
|
|
9989
|
+
}
|
|
9990
|
+
}
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
const blastRadius = visitedBfs.size - 1;
|
|
9994
|
+
hotspotCandidates.push({ name: node.name, blastRadius, filePath: node.filePath });
|
|
9995
|
+
}
|
|
9996
|
+
hotspotCandidates.sort((a, b) => b.blastRadius - a.blastRadius);
|
|
9997
|
+
const complexityHotspots = hotspotCandidates.slice(0, 5);
|
|
9998
|
+
const healthScore = Math.max(
|
|
9999
|
+
0,
|
|
10000
|
+
Math.min(100, 100 - deadCode.length * 2 - cycles.length * 5 - godNodes.length * 3)
|
|
10001
|
+
);
|
|
10002
|
+
return {
|
|
10003
|
+
healthScore,
|
|
10004
|
+
deadCode,
|
|
10005
|
+
cycles,
|
|
10006
|
+
godNodes,
|
|
10007
|
+
orphanFiles,
|
|
10008
|
+
complexityHotspots
|
|
10009
|
+
};
|
|
10010
|
+
}
|
|
10011
|
+
|
|
10012
|
+
// src/query/suggest-tests.ts
|
|
10013
|
+
function getSuggestedCases(symbolName) {
|
|
10014
|
+
const lower = symbolName.toLowerCase();
|
|
10015
|
+
if (/parse|validate|check|verify/.test(lower)) {
|
|
10016
|
+
return [
|
|
10017
|
+
"Valid input \u2192 success",
|
|
10018
|
+
"Invalid input \u2192 throws error",
|
|
10019
|
+
"Edge case: empty/null input \u2192 handled gracefully"
|
|
10020
|
+
];
|
|
10021
|
+
}
|
|
10022
|
+
if (/create|add|insert|save/.test(lower)) {
|
|
10023
|
+
return [
|
|
10024
|
+
"Success: valid data \u2192 created",
|
|
10025
|
+
"Duplicate: existing item \u2192 error or no-op",
|
|
10026
|
+
"Missing required fields \u2192 validation error"
|
|
10027
|
+
];
|
|
10028
|
+
}
|
|
10029
|
+
if (/delete|remove|destroy/.test(lower)) {
|
|
10030
|
+
return [
|
|
10031
|
+
"Existing item \u2192 deleted successfully",
|
|
10032
|
+
"Non-existent item \u2192 no error or 404",
|
|
10033
|
+
"Unauthorized access \u2192 rejected"
|
|
10034
|
+
];
|
|
10035
|
+
}
|
|
10036
|
+
if (/get|find|fetch|load/.test(lower)) {
|
|
10037
|
+
return [
|
|
10038
|
+
"Found: returns correct data",
|
|
10039
|
+
"Not found: returns null or throws",
|
|
10040
|
+
"Empty collection: returns []"
|
|
10041
|
+
];
|
|
10042
|
+
}
|
|
10043
|
+
return [
|
|
10044
|
+
"Happy path: valid input \u2192 expected output",
|
|
10045
|
+
"Error case: invalid input \u2192 error handled",
|
|
10046
|
+
"Edge case: boundary values \u2192 correct behavior"
|
|
10047
|
+
];
|
|
10048
|
+
}
|
|
10049
|
+
function suggestTests(graph, symbolName) {
|
|
10050
|
+
let targetNode = void 0;
|
|
10051
|
+
for (const node of graph.allNodes()) {
|
|
10052
|
+
if (node.name === symbolName) {
|
|
10053
|
+
targetNode = node;
|
|
10054
|
+
break;
|
|
10055
|
+
}
|
|
10056
|
+
}
|
|
10057
|
+
if (!targetNode) {
|
|
10058
|
+
return { error: `Symbol not found: ${symbolName}` };
|
|
10059
|
+
}
|
|
10060
|
+
const targetId = targetNode.id;
|
|
10061
|
+
const callPaths = [];
|
|
10062
|
+
const pathQueue = [{ id: targetId, path: [symbolName], depth: 0 }];
|
|
10063
|
+
while (pathQueue.length > 0 && callPaths.length < 5) {
|
|
10064
|
+
const { id, path: path31, depth } = pathQueue.shift();
|
|
10065
|
+
let hasCallers = false;
|
|
10066
|
+
for (const edge of graph.findEdgesTo(id)) {
|
|
10067
|
+
if (edge.kind !== "calls") continue;
|
|
10068
|
+
const callerNode = graph.getNode(edge.source);
|
|
10069
|
+
if (!callerNode) continue;
|
|
10070
|
+
hasCallers = true;
|
|
10071
|
+
const newPath = [callerNode.name, ...path31];
|
|
10072
|
+
if (depth + 1 >= 3 || callPaths.length >= 5) {
|
|
10073
|
+
if (callPaths.length < 5) callPaths.push(newPath);
|
|
10074
|
+
continue;
|
|
10075
|
+
}
|
|
10076
|
+
pathQueue.push({ id: edge.source, path: newPath, depth: depth + 1 });
|
|
10077
|
+
}
|
|
10078
|
+
if (!hasCallers && path31.length > 1) {
|
|
10079
|
+
callPaths.push(path31);
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
if (callPaths.length === 0) {
|
|
10083
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
10084
|
+
if (edge.kind !== "calls") continue;
|
|
10085
|
+
const callerNode = graph.getNode(edge.source);
|
|
10086
|
+
if (!callerNode) continue;
|
|
10087
|
+
callPaths.push([callerNode.name, symbolName]);
|
|
10088
|
+
if (callPaths.length >= 5) break;
|
|
10089
|
+
}
|
|
10090
|
+
}
|
|
10091
|
+
const existingTestFiles = /* @__PURE__ */ new Set();
|
|
10092
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
10093
|
+
if (edge.kind !== "imports") continue;
|
|
10094
|
+
const importerNode = graph.getNode(edge.source);
|
|
10095
|
+
if (!importerNode) continue;
|
|
10096
|
+
if (importerNode.filePath.includes(".test.") || importerNode.filePath.includes(".spec.")) {
|
|
10097
|
+
existingTestFiles.add(importerNode.filePath);
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
const existingTests = [...existingTestFiles];
|
|
10101
|
+
const untestedCallers = [];
|
|
10102
|
+
for (const edge of graph.findEdgesTo(targetId)) {
|
|
10103
|
+
if (edge.kind !== "calls") continue;
|
|
10104
|
+
const callerNode = graph.getNode(edge.source);
|
|
10105
|
+
if (!callerNode) continue;
|
|
10106
|
+
if (callerNode.filePath.includes(".test.") || callerNode.filePath.includes(".spec.")) {
|
|
10107
|
+
continue;
|
|
10108
|
+
}
|
|
10109
|
+
let callerHasTest = false;
|
|
10110
|
+
for (const callerImportEdge of graph.findEdgesTo(callerNode.id)) {
|
|
10111
|
+
if (callerImportEdge.kind !== "imports") continue;
|
|
10112
|
+
const importerOfCaller = graph.getNode(callerImportEdge.source);
|
|
10113
|
+
if (!importerOfCaller) continue;
|
|
10114
|
+
if (importerOfCaller.filePath.includes(".test.") || importerOfCaller.filePath.includes(".spec.")) {
|
|
10115
|
+
callerHasTest = true;
|
|
10116
|
+
break;
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
10119
|
+
if (!callerHasTest) {
|
|
10120
|
+
untestedCallers.push(callerNode.name);
|
|
10121
|
+
}
|
|
10122
|
+
}
|
|
10123
|
+
const suggestedCases = getSuggestedCases(symbolName);
|
|
10124
|
+
return {
|
|
10125
|
+
callPaths,
|
|
10126
|
+
suggestedCases,
|
|
10127
|
+
existingTests,
|
|
10128
|
+
untestedCallers
|
|
10129
|
+
};
|
|
10130
|
+
}
|
|
10131
|
+
|
|
10132
|
+
// src/query/cluster-summary.ts
|
|
10133
|
+
function getPathPrefix(filePath) {
|
|
10134
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
10135
|
+
return parts.slice(0, 2).join("/");
|
|
10136
|
+
}
|
|
10137
|
+
function summarizeCluster(graph, cluster) {
|
|
10138
|
+
const clusterNodes = [...graph.allNodes()].filter(
|
|
10139
|
+
(n) => n.filePath.startsWith(cluster) || n.metadata?.["cluster"] === cluster
|
|
10140
|
+
);
|
|
10141
|
+
if (clusterNodes.length === 0) {
|
|
10142
|
+
return { error: `Cluster not found: ${cluster}` };
|
|
10143
|
+
}
|
|
10144
|
+
const clusterNodeIds = new Set(clusterNodes.map((n) => n.id));
|
|
10145
|
+
const callerCountMap = /* @__PURE__ */ new Map();
|
|
10146
|
+
for (const node of clusterNodes) {
|
|
10147
|
+
let count = 0;
|
|
10148
|
+
for (const _edge of graph.findEdgesTo(node.id)) {
|
|
10149
|
+
count++;
|
|
10150
|
+
}
|
|
10151
|
+
callerCountMap.set(node.id, count);
|
|
10152
|
+
}
|
|
10153
|
+
const sortedByCallers = [...clusterNodes].sort(
|
|
10154
|
+
(a, b) => (callerCountMap.get(b.id) ?? 0) - (callerCountMap.get(a.id) ?? 0)
|
|
10155
|
+
);
|
|
10156
|
+
const keySymbols = sortedByCallers.slice(0, 5).map((n) => ({
|
|
10157
|
+
name: n.name,
|
|
10158
|
+
callerCount: callerCountMap.get(n.id) ?? 0
|
|
10159
|
+
}));
|
|
10160
|
+
const depsSet = /* @__PURE__ */ new Set();
|
|
10161
|
+
for (const node of clusterNodes) {
|
|
10162
|
+
for (const edge of graph.findEdgesFrom(node.id)) {
|
|
10163
|
+
if (edge.kind !== "imports") continue;
|
|
10164
|
+
const targetNode = graph.getNode(edge.target);
|
|
10165
|
+
if (!targetNode) continue;
|
|
10166
|
+
if (!clusterNodeIds.has(targetNode.id)) {
|
|
10167
|
+
const prefix = getPathPrefix(targetNode.filePath);
|
|
10168
|
+
depsSet.add(prefix);
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
}
|
|
10172
|
+
const dependencies = [...depsSet];
|
|
10173
|
+
const dependentsSet = /* @__PURE__ */ new Set();
|
|
10174
|
+
for (const node of clusterNodes) {
|
|
10175
|
+
for (const edge of graph.findEdgesTo(node.id)) {
|
|
10176
|
+
if (edge.kind !== "imports") continue;
|
|
10177
|
+
const sourceNode = graph.getNode(edge.source);
|
|
10178
|
+
if (!sourceNode) continue;
|
|
10179
|
+
if (!clusterNodeIds.has(sourceNode.id)) {
|
|
10180
|
+
const prefix = getPathPrefix(sourceNode.filePath);
|
|
10181
|
+
dependentsSet.add(prefix);
|
|
10182
|
+
}
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
const dependents = [...dependentsSet];
|
|
10186
|
+
const healthResult = computeHealthReport(graph, cluster);
|
|
10187
|
+
const health = { score: healthResult.healthScore };
|
|
10188
|
+
const symbolCount = {};
|
|
10189
|
+
for (const node of clusterNodes) {
|
|
10190
|
+
symbolCount[node.kind] = (symbolCount[node.kind] ?? 0) + 1;
|
|
10191
|
+
}
|
|
10192
|
+
let purpose;
|
|
10193
|
+
const topNode = sortedByCallers[0];
|
|
10194
|
+
if (topNode?.metadata?.["summary"] && typeof topNode.metadata["summary"] === "string") {
|
|
10195
|
+
purpose = topNode.metadata["summary"];
|
|
10196
|
+
} else {
|
|
10197
|
+
const clusterName = cluster.split("/").pop() ?? cluster;
|
|
10198
|
+
purpose = `Handles ${clusterName.replace(/[-_/]/g, " ")} functionality`;
|
|
10199
|
+
}
|
|
10200
|
+
return {
|
|
10201
|
+
cluster,
|
|
10202
|
+
purpose,
|
|
10203
|
+
keySymbols,
|
|
10204
|
+
dependencies,
|
|
10205
|
+
dependents,
|
|
10206
|
+
health,
|
|
10207
|
+
symbolCount
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
|
|
10211
|
+
// src/mcp-server/server.ts
|
|
9593
10212
|
function createMcpServer(graph, repoName, workspaceRoot) {
|
|
9594
10213
|
const server = new Server(
|
|
9595
10214
|
{ name: "code-intel", version: "0.1.0" },
|
|
@@ -9619,7 +10238,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9619
10238
|
type: "object",
|
|
9620
10239
|
properties: {
|
|
9621
10240
|
query: { type: "string", description: "Search query (symbol name, keyword, or partial match)" },
|
|
9622
|
-
|
|
10241
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
10242
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
9623
10243
|
..._tokenProp
|
|
9624
10244
|
},
|
|
9625
10245
|
required: ["query"]
|
|
@@ -9662,6 +10282,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9662
10282
|
type: "object",
|
|
9663
10283
|
properties: {
|
|
9664
10284
|
file_path: { type: "string", description: 'File path (partial match is supported, e.g. "auth/login.ts")' },
|
|
10285
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
10286
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
9665
10287
|
..._tokenProp
|
|
9666
10288
|
},
|
|
9667
10289
|
required: ["file_path"]
|
|
@@ -9691,7 +10313,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9691
10313
|
type: "string",
|
|
9692
10314
|
description: "Filter by node kind: function | class | interface | method | type_alias | constant | enum (optional)"
|
|
9693
10315
|
},
|
|
9694
|
-
|
|
10316
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
10317
|
+
limit: { type: "number", description: "Max results per page (default: 50, max: 500)" },
|
|
9695
10318
|
..._tokenProp
|
|
9696
10319
|
}
|
|
9697
10320
|
}
|
|
@@ -9708,7 +10331,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9708
10331
|
inputSchema: {
|
|
9709
10332
|
type: "object",
|
|
9710
10333
|
properties: {
|
|
9711
|
-
|
|
10334
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
10335
|
+
limit: { type: "number", description: "Max clusters per page (default: 50, max: 500)" },
|
|
9712
10336
|
..._tokenProp
|
|
9713
10337
|
}
|
|
9714
10338
|
}
|
|
@@ -9719,7 +10343,8 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9719
10343
|
inputSchema: {
|
|
9720
10344
|
type: "object",
|
|
9721
10345
|
properties: {
|
|
9722
|
-
|
|
10346
|
+
offset: { type: "number", description: "Number of results to skip for pagination (default: 0)" },
|
|
10347
|
+
limit: { type: "number", description: "Max flows per page (default: 50, max: 500)" },
|
|
9723
10348
|
..._tokenProp
|
|
9724
10349
|
}
|
|
9725
10350
|
}
|
|
@@ -9841,6 +10466,91 @@ function createMcpServer(graph, repoName, workspaceRoot) {
|
|
|
9841
10466
|
},
|
|
9842
10467
|
required: ["name"]
|
|
9843
10468
|
}
|
|
10469
|
+
},
|
|
10470
|
+
// ── Reasoning / analysis tools ────────────────────────────────────────
|
|
10471
|
+
{
|
|
10472
|
+
name: "explain_relationship",
|
|
10473
|
+
description: "Explain how two symbols are connected: directed paths, shared imports, and heritage (extends/implements). Returns up to 10 paths with at most 5 hops each.",
|
|
10474
|
+
inputSchema: {
|
|
10475
|
+
type: "object",
|
|
10476
|
+
properties: {
|
|
10477
|
+
from: { type: "string", description: "Source symbol name" },
|
|
10478
|
+
to: { type: "string", description: "Target symbol name" },
|
|
10479
|
+
..._tokenProp
|
|
10480
|
+
},
|
|
10481
|
+
required: ["from", "to"]
|
|
10482
|
+
}
|
|
10483
|
+
},
|
|
10484
|
+
{
|
|
10485
|
+
name: "pr_impact",
|
|
10486
|
+
description: "Given changed files or a unified diff, compute full blast radius with risk scores (HIGH/MEDIUM/LOW), test coverage gaps, and top files to review.",
|
|
10487
|
+
inputSchema: {
|
|
10488
|
+
type: "object",
|
|
10489
|
+
properties: {
|
|
10490
|
+
changedFiles: {
|
|
10491
|
+
type: "array",
|
|
10492
|
+
items: { type: "string" },
|
|
10493
|
+
description: "List of changed file paths (relative or absolute)"
|
|
10494
|
+
},
|
|
10495
|
+
diff: {
|
|
10496
|
+
type: "string",
|
|
10497
|
+
description: "Raw unified diff text. Changed files are extracted automatically."
|
|
10498
|
+
},
|
|
10499
|
+
maxHops: {
|
|
10500
|
+
type: "number",
|
|
10501
|
+
description: "Maximum BFS depth for blast radius (default: 5)"
|
|
10502
|
+
},
|
|
10503
|
+
..._tokenProp
|
|
10504
|
+
}
|
|
10505
|
+
}
|
|
10506
|
+
},
|
|
10507
|
+
{
|
|
10508
|
+
name: "similar_symbols",
|
|
10509
|
+
description: "Find symbols with similar names or structure using Levenshtein distance and kind matching. Useful for finding related functions, classes, or interfaces.",
|
|
10510
|
+
inputSchema: {
|
|
10511
|
+
type: "object",
|
|
10512
|
+
properties: {
|
|
10513
|
+
symbol: { type: "string", description: "Symbol name to find similar symbols for" },
|
|
10514
|
+
limit: { type: "number", description: "Maximum number of results (default: 10, max: 50)" },
|
|
10515
|
+
..._tokenProp
|
|
10516
|
+
},
|
|
10517
|
+
required: ["symbol"]
|
|
10518
|
+
}
|
|
10519
|
+
},
|
|
10520
|
+
{
|
|
10521
|
+
name: "health_report",
|
|
10522
|
+
description: "Code health signals for a scope: dead code, cycles, god nodes, orphan files, complexity hotspots",
|
|
10523
|
+
inputSchema: {
|
|
10524
|
+
type: "object",
|
|
10525
|
+
properties: {
|
|
10526
|
+
scope: { type: "string", description: "Directory scope, e.g. 'src/api/' or '.' for whole repo" },
|
|
10527
|
+
..._tokenProp
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
},
|
|
10531
|
+
{
|
|
10532
|
+
name: "suggest_tests",
|
|
10533
|
+
description: "Suggest test cases for a symbol: call paths, suggested cases, existing tests, untested callers",
|
|
10534
|
+
inputSchema: {
|
|
10535
|
+
type: "object",
|
|
10536
|
+
properties: {
|
|
10537
|
+
symbol: { type: "string", description: "Symbol name to generate test suggestions for" },
|
|
10538
|
+
..._tokenProp
|
|
10539
|
+
},
|
|
10540
|
+
required: ["symbol"]
|
|
10541
|
+
}
|
|
10542
|
+
},
|
|
10543
|
+
{
|
|
10544
|
+
name: "cluster_summary",
|
|
10545
|
+
description: "Rich summary of a module/cluster: purpose, key symbols, dependencies, health",
|
|
10546
|
+
inputSchema: {
|
|
10547
|
+
type: "object",
|
|
10548
|
+
properties: {
|
|
10549
|
+
cluster: { type: "string", description: "Cluster path e.g. 'src/auth'" },
|
|
10550
|
+
..._tokenProp
|
|
10551
|
+
},
|
|
10552
|
+
required: ["cluster"]
|
|
10553
|
+
}
|
|
9844
10554
|
}
|
|
9845
10555
|
]
|
|
9846
10556
|
}));
|
|
@@ -9911,8 +10621,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
9911
10621
|
for (const edge of graph.allEdges()) {
|
|
9912
10622
|
edgeCounts[edge.kind] = (edgeCounts[edge.kind] ?? 0) + 1;
|
|
9913
10623
|
}
|
|
9914
|
-
const { computeHealthReport:
|
|
9915
|
-
const healthReport =
|
|
10624
|
+
const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
|
|
10625
|
+
const healthReport = computeHealthReport3(graph);
|
|
9916
10626
|
const health = {
|
|
9917
10627
|
score: Math.round(healthReport.score),
|
|
9918
10628
|
grade: healthReport.grade,
|
|
@@ -9937,10 +10647,37 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
9937
10647
|
// ── search ─────────────────────────────────────────────────────────────
|
|
9938
10648
|
case "search": {
|
|
9939
10649
|
const query = a.query;
|
|
9940
|
-
const
|
|
10650
|
+
const offset = a.offset ?? 0;
|
|
10651
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
9941
10652
|
const vdbPath = workspaceRoot ? getVectorDbPath(workspaceRoot) : void 0;
|
|
9942
|
-
const
|
|
9943
|
-
|
|
10653
|
+
const fetchLimit = Math.min(offset + effectiveLimit, 500);
|
|
10654
|
+
const { results: allResults, searchMode } = await hybridSearch(graph, query, fetchLimit, { vectorDbPath: vdbPath });
|
|
10655
|
+
const total = allResults.length;
|
|
10656
|
+
const results = allResults.slice(offset, offset + effectiveLimit);
|
|
10657
|
+
const hasMore = offset + effectiveLimit < total;
|
|
10658
|
+
const suggestNextTools = [];
|
|
10659
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
10660
|
+
if (suggestEnabled && results.length > 0) {
|
|
10661
|
+
const topName = results[0].name;
|
|
10662
|
+
suggestNextTools.push(
|
|
10663
|
+
{ tool: "inspect", reason: "Inspect the top result in detail", input: { symbol: topName } },
|
|
10664
|
+
{ tool: "similar_symbols", reason: "Find symbols similar to the top result", input: { symbol: topName } }
|
|
10665
|
+
);
|
|
10666
|
+
}
|
|
10667
|
+
return {
|
|
10668
|
+
content: [{
|
|
10669
|
+
type: "text",
|
|
10670
|
+
text: JSON.stringify({
|
|
10671
|
+
results,
|
|
10672
|
+
searchMode,
|
|
10673
|
+
total,
|
|
10674
|
+
offset,
|
|
10675
|
+
limit: effectiveLimit,
|
|
10676
|
+
hasMore,
|
|
10677
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
10678
|
+
}, null, 2)
|
|
10679
|
+
}]
|
|
10680
|
+
};
|
|
9944
10681
|
}
|
|
9945
10682
|
// ── inspect ────────────────────────────────────────────────────────────
|
|
9946
10683
|
case "inspect": {
|
|
@@ -9949,6 +10686,26 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
9949
10686
|
if (!node) return { content: [{ type: "text", text: `Symbol "${symbolName}" not found. Try search first.` }] };
|
|
9950
10687
|
const incoming = [...graph.findEdgesTo(node.id)];
|
|
9951
10688
|
const outgoing = [...graph.findEdgesFrom(node.id)];
|
|
10689
|
+
const callers = incoming.filter((e) => e.kind === "calls").map((e) => ({
|
|
10690
|
+
id: e.source,
|
|
10691
|
+
name: graph.getNode(e.source)?.name,
|
|
10692
|
+
file: graph.getNode(e.source)?.filePath
|
|
10693
|
+
}));
|
|
10694
|
+
const callees = outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
10695
|
+
id: e.target,
|
|
10696
|
+
name: graph.getNode(e.target)?.name,
|
|
10697
|
+
file: graph.getNode(e.target)?.filePath
|
|
10698
|
+
}));
|
|
10699
|
+
const cluster = incoming.filter((e) => e.kind === "belongs_to").map((e) => graph.getNode(e.target)?.name)[0];
|
|
10700
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
10701
|
+
const suggestNextTools = [];
|
|
10702
|
+
if (suggestEnabled) {
|
|
10703
|
+
const topCallerName = callers[0]?.name;
|
|
10704
|
+
suggestNextTools.push(
|
|
10705
|
+
...topCallerName ? [{ tool: "explain_relationship", reason: "Explain connection to a related symbol", input: { from: node.name, to: topCallerName } }] : [],
|
|
10706
|
+
...cluster ? [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster } }] : [{ tool: "cluster_summary", reason: "Summarize the module this symbol belongs to", input: { cluster: node.filePath } }]
|
|
10707
|
+
);
|
|
10708
|
+
}
|
|
9952
10709
|
return {
|
|
9953
10710
|
content: [{
|
|
9954
10711
|
type: "text",
|
|
@@ -9962,16 +10719,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
9962
10719
|
endLine: node.endLine,
|
|
9963
10720
|
exported: node.exported
|
|
9964
10721
|
},
|
|
9965
|
-
callers
|
|
9966
|
-
|
|
9967
|
-
name: graph.getNode(e.source)?.name,
|
|
9968
|
-
file: graph.getNode(e.source)?.filePath
|
|
9969
|
-
})),
|
|
9970
|
-
callees: outgoing.filter((e) => e.kind === "calls").map((e) => ({
|
|
9971
|
-
id: e.target,
|
|
9972
|
-
name: graph.getNode(e.target)?.name,
|
|
9973
|
-
file: graph.getNode(e.target)?.filePath
|
|
9974
|
-
})),
|
|
10722
|
+
callers,
|
|
10723
|
+
callees,
|
|
9975
10724
|
imports: incoming.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.source)?.name),
|
|
9976
10725
|
importedBy: outgoing.filter((e) => e.kind === "imports").map((e) => graph.getNode(e.target)?.name),
|
|
9977
10726
|
extends: outgoing.filter((e) => e.kind === "extends").map((e) => graph.getNode(e.target)?.name),
|
|
@@ -9980,8 +10729,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
9980
10729
|
name: graph.getNode(e.target)?.name,
|
|
9981
10730
|
kind: graph.getNode(e.target)?.kind
|
|
9982
10731
|
})),
|
|
9983
|
-
cluster
|
|
9984
|
-
content: node.content?.slice(0, 500)
|
|
10732
|
+
cluster,
|
|
10733
|
+
content: node.content?.slice(0, 500),
|
|
10734
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
9985
10735
|
}, null, 2)
|
|
9986
10736
|
}]
|
|
9987
10737
|
};
|
|
@@ -10017,6 +10767,16 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10017
10767
|
return n ? { id, name: n.name, kind: n.kind, filePath: n.filePath } : { id };
|
|
10018
10768
|
});
|
|
10019
10769
|
const risk = affected.size > 10 ? "HIGH" : affected.size > 5 ? "MEDIUM" : "LOW";
|
|
10770
|
+
const suggestEnabled = process.env["CODE_INTEL_SUGGEST_NEXT_TOOLS"] !== "false";
|
|
10771
|
+
const suggestNextTools = [];
|
|
10772
|
+
if (suggestEnabled) {
|
|
10773
|
+
const highestRiskSymbol = node.name;
|
|
10774
|
+
const firstFilePath = affectedDetails[0]?.filePath ?? "";
|
|
10775
|
+
suggestNextTools.push(
|
|
10776
|
+
{ tool: "suggest_tests", reason: "Generate tests for the highest-risk symbol", input: { symbol: highestRiskSymbol } },
|
|
10777
|
+
{ tool: "pr_impact", reason: "Compute full PR impact for changed files", input: { changedFiles: [firstFilePath] } }
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10020
10780
|
return {
|
|
10021
10781
|
content: [{
|
|
10022
10782
|
type: "text",
|
|
@@ -10024,7 +10784,8 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10024
10784
|
target: node.name,
|
|
10025
10785
|
affectedCount: affected.size,
|
|
10026
10786
|
riskLevel: risk,
|
|
10027
|
-
affected: affectedDetails
|
|
10787
|
+
affected: affectedDetails,
|
|
10788
|
+
...suggestEnabled ? { suggested_next_tools: suggestNextTools } : {}
|
|
10028
10789
|
}, null, 2)
|
|
10029
10790
|
}]
|
|
10030
10791
|
};
|
|
@@ -10032,17 +10793,27 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10032
10793
|
// ── file_symbols ───────────────────────────────────────────────────────
|
|
10033
10794
|
case "file_symbols": {
|
|
10034
10795
|
const filePath = a.file_path;
|
|
10035
|
-
const
|
|
10796
|
+
const offset = a.offset ?? 0;
|
|
10797
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
10798
|
+
const allMatches = [];
|
|
10036
10799
|
for (const node of graph.allNodes()) {
|
|
10037
10800
|
if (node.filePath && node.filePath.includes(filePath)) {
|
|
10038
|
-
|
|
10801
|
+
allMatches.push({ kind: node.kind, name: node.name, startLine: node.startLine, exported: node.exported });
|
|
10039
10802
|
}
|
|
10040
10803
|
}
|
|
10041
|
-
if (
|
|
10804
|
+
if (allMatches.length === 0) {
|
|
10042
10805
|
return { content: [{ type: "text", text: `No symbols found for file path matching "${filePath}".` }] };
|
|
10043
10806
|
}
|
|
10044
|
-
|
|
10045
|
-
|
|
10807
|
+
allMatches.sort((a2, b) => (a2.startLine ?? 0) - (b.startLine ?? 0));
|
|
10808
|
+
const total = allMatches.length;
|
|
10809
|
+
const matches = allMatches.slice(offset, offset + effectiveLimit);
|
|
10810
|
+
const hasMore = offset + effectiveLimit < total;
|
|
10811
|
+
return {
|
|
10812
|
+
content: [{
|
|
10813
|
+
type: "text",
|
|
10814
|
+
text: JSON.stringify({ symbols: matches, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
10815
|
+
}]
|
|
10816
|
+
};
|
|
10046
10817
|
}
|
|
10047
10818
|
// ── find_path ──────────────────────────────────────────────────────────
|
|
10048
10819
|
case "find_path": {
|
|
@@ -10088,15 +10859,23 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10088
10859
|
// ── list_exports ───────────────────────────────────────────────────────
|
|
10089
10860
|
case "list_exports": {
|
|
10090
10861
|
const kindFilter = a.kind;
|
|
10091
|
-
const
|
|
10092
|
-
const
|
|
10862
|
+
const offset = a.offset ?? 0;
|
|
10863
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
10864
|
+
const allExports = [];
|
|
10093
10865
|
for (const node of graph.allNodes()) {
|
|
10094
10866
|
if (!node.exported) continue;
|
|
10095
10867
|
if (kindFilter && node.kind !== kindFilter) continue;
|
|
10096
|
-
|
|
10097
|
-
if (exports$1.length >= limit) break;
|
|
10868
|
+
allExports.push({ kind: node.kind, name: node.name, filePath: node.filePath, startLine: node.startLine });
|
|
10098
10869
|
}
|
|
10099
|
-
|
|
10870
|
+
const total = allExports.length;
|
|
10871
|
+
const exports$1 = allExports.slice(offset, offset + effectiveLimit);
|
|
10872
|
+
const hasMore = offset + effectiveLimit < total;
|
|
10873
|
+
return {
|
|
10874
|
+
content: [{
|
|
10875
|
+
type: "text",
|
|
10876
|
+
text: JSON.stringify({ exports: exports$1, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
10877
|
+
}]
|
|
10878
|
+
};
|
|
10100
10879
|
}
|
|
10101
10880
|
// ── routes ─────────────────────────────────────────────────────────────
|
|
10102
10881
|
case "routes": {
|
|
@@ -10110,8 +10889,9 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10110
10889
|
}
|
|
10111
10890
|
// ── clusters ───────────────────────────────────────────────────────────
|
|
10112
10891
|
case "clusters": {
|
|
10113
|
-
const
|
|
10114
|
-
const
|
|
10892
|
+
const offset = a.offset ?? 0;
|
|
10893
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
10894
|
+
const allClusters = [];
|
|
10115
10895
|
for (const node of graph.allNodes()) {
|
|
10116
10896
|
if (node.kind === "cluster") {
|
|
10117
10897
|
const members = [];
|
|
@@ -10123,35 +10903,50 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10123
10903
|
}
|
|
10124
10904
|
}
|
|
10125
10905
|
}
|
|
10126
|
-
|
|
10906
|
+
allClusters.push({
|
|
10127
10907
|
id: node.id,
|
|
10128
10908
|
name: node.name,
|
|
10129
10909
|
memberCount: node.metadata?.memberCount ?? members.length,
|
|
10130
10910
|
topSymbols: members.slice(0, 10)
|
|
10131
10911
|
});
|
|
10132
|
-
if (clusters.length >= limit) break;
|
|
10133
10912
|
}
|
|
10134
10913
|
}
|
|
10135
|
-
|
|
10914
|
+
const total = allClusters.length;
|
|
10915
|
+
const clusters = allClusters.slice(offset, offset + effectiveLimit);
|
|
10916
|
+
const hasMore = offset + effectiveLimit < total;
|
|
10917
|
+
return {
|
|
10918
|
+
content: [{
|
|
10919
|
+
type: "text",
|
|
10920
|
+
text: JSON.stringify({ clusters, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
10921
|
+
}]
|
|
10922
|
+
};
|
|
10136
10923
|
}
|
|
10137
10924
|
// ── flows ──────────────────────────────────────────────────────────────
|
|
10138
10925
|
case "flows": {
|
|
10139
|
-
const
|
|
10140
|
-
const
|
|
10926
|
+
const offset = a.offset ?? 0;
|
|
10927
|
+
const effectiveLimit = Math.min(a.limit ?? 50, 500);
|
|
10928
|
+
const allFlows = [];
|
|
10141
10929
|
for (const node of graph.allNodes()) {
|
|
10142
10930
|
if (node.kind === "flow") {
|
|
10143
10931
|
const steps = node.metadata?.steps;
|
|
10144
|
-
|
|
10932
|
+
allFlows.push({
|
|
10145
10933
|
id: node.id,
|
|
10146
10934
|
name: node.name,
|
|
10147
10935
|
entryPoint: node.metadata?.entryPoint,
|
|
10148
10936
|
steps: steps ?? [],
|
|
10149
10937
|
stepCount: Array.isArray(steps) ? steps.length : 0
|
|
10150
10938
|
});
|
|
10151
|
-
if (flows.length >= limit) break;
|
|
10152
10939
|
}
|
|
10153
10940
|
}
|
|
10154
|
-
|
|
10941
|
+
const total = allFlows.length;
|
|
10942
|
+
const flows = allFlows.slice(offset, offset + effectiveLimit);
|
|
10943
|
+
const hasMore = offset + effectiveLimit < total;
|
|
10944
|
+
return {
|
|
10945
|
+
content: [{
|
|
10946
|
+
type: "text",
|
|
10947
|
+
text: JSON.stringify({ flows, total, offset, limit: effectiveLimit, hasMore }, null, 2)
|
|
10948
|
+
}]
|
|
10949
|
+
};
|
|
10155
10950
|
}
|
|
10156
10951
|
// ── detect_changes ─────────────────────────────────────────────────────
|
|
10157
10952
|
case "detect_changes": {
|
|
@@ -10401,6 +11196,57 @@ async function dispatchTool(name, a, graph, repoName, workspaceRoot) {
|
|
|
10401
11196
|
}]
|
|
10402
11197
|
};
|
|
10403
11198
|
}
|
|
11199
|
+
// ── explain_relationship ───────────────────────────────────────────────
|
|
11200
|
+
case "explain_relationship": {
|
|
11201
|
+
const fromName = a.from;
|
|
11202
|
+
const toName = a.to;
|
|
11203
|
+
const result = explainRelationship(graph, fromName, toName);
|
|
11204
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11205
|
+
}
|
|
11206
|
+
// ── pr_impact ──────────────────────────────────────────────────────────
|
|
11207
|
+
case "pr_impact": {
|
|
11208
|
+
const maxHops = a.maxHops ?? 5;
|
|
11209
|
+
let changedFiles = a.changedFiles ?? [];
|
|
11210
|
+
if (a.diff && typeof a.diff === "string") {
|
|
11211
|
+
const diffFiles = parseDiffFiles(a.diff);
|
|
11212
|
+
changedFiles = [.../* @__PURE__ */ new Set([...changedFiles, ...diffFiles])];
|
|
11213
|
+
}
|
|
11214
|
+
if (changedFiles.length === 0) {
|
|
11215
|
+
return {
|
|
11216
|
+
content: [{
|
|
11217
|
+
type: "text",
|
|
11218
|
+
text: JSON.stringify({ error: 'No changed files provided. Supply "changedFiles" or "diff".' })
|
|
11219
|
+
}]
|
|
11220
|
+
};
|
|
11221
|
+
}
|
|
11222
|
+
const result = computePRImpact(graph, changedFiles, maxHops);
|
|
11223
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11224
|
+
}
|
|
11225
|
+
// ── similar_symbols ────────────────────────────────────────────────────
|
|
11226
|
+
case "similar_symbols": {
|
|
11227
|
+
const symbolName = a.symbol;
|
|
11228
|
+
const limit = a.limit ?? 10;
|
|
11229
|
+
const result = findSimilarSymbols(graph, symbolName, limit);
|
|
11230
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11231
|
+
}
|
|
11232
|
+
// ── health_report ──────────────────────────────────────────────────────
|
|
11233
|
+
case "health_report": {
|
|
11234
|
+
const scope = a.scope ?? ".";
|
|
11235
|
+
const result = computeHealthReport(graph, scope);
|
|
11236
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11237
|
+
}
|
|
11238
|
+
// ── suggest_tests ──────────────────────────────────────────────────────
|
|
11239
|
+
case "suggest_tests": {
|
|
11240
|
+
const sym = a.symbol;
|
|
11241
|
+
const result = suggestTests(graph, sym);
|
|
11242
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11243
|
+
}
|
|
11244
|
+
// ── cluster_summary ────────────────────────────────────────────────────
|
|
11245
|
+
case "cluster_summary": {
|
|
11246
|
+
const cluster = a.cluster;
|
|
11247
|
+
const result = summarizeCluster(graph, cluster);
|
|
11248
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11249
|
+
}
|
|
10404
11250
|
default:
|
|
10405
11251
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
10406
11252
|
}
|
|
@@ -12990,13 +13836,13 @@ program.command("health").description("Run code health checks: dead code, circul
|
|
|
12990
13836
|
console.error(" Run `code-intel analyze` first to build the index.\n");
|
|
12991
13837
|
process.exit(1);
|
|
12992
13838
|
}
|
|
12993
|
-
const { computeHealthReport:
|
|
13839
|
+
const { computeHealthReport: computeHealthReport3 } = await Promise.resolve().then(() => (init_health_score(), health_score_exports));
|
|
12994
13840
|
const graph = createKnowledgeGraph();
|
|
12995
13841
|
const db = new DbManager(dbPath);
|
|
12996
13842
|
await db.init();
|
|
12997
13843
|
await loadGraphFromDB(graph, db);
|
|
12998
13844
|
db.close();
|
|
12999
|
-
const report =
|
|
13845
|
+
const report = computeHealthReport3(graph);
|
|
13000
13846
|
if (opts.json) {
|
|
13001
13847
|
console.log(JSON.stringify({
|
|
13002
13848
|
score: report.score,
|