@kage-core/kage-graph-mcp 1.1.29 → 1.1.31
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 +5 -2
- package/dist/index.js +1 -1
- package/dist/kernel.js +119 -8
- package/package.json +1 -1
- package/viewer/app.js +10 -4
package/README.md
CHANGED
|
@@ -36,8 +36,11 @@ Restart your agent once after setup so MCP tools reload.
|
|
|
36
36
|
|
|
37
37
|
- repo-local memory for decisions, runbooks, bug fixes, gotchas, conventions,
|
|
38
38
|
and code explanations
|
|
39
|
-
- a code graph for files, symbols, imports, calls, routes,
|
|
40
|
-
including generic call/test signals and mixed-language
|
|
39
|
+
- a code graph for files, symbols, imports, confidence-scored calls, routes,
|
|
40
|
+
tests, and packages, including generic call/test signals and mixed-language
|
|
41
|
+
framework routes
|
|
42
|
+
- conservative cleanup candidates for unreferenced files, unused exports, and
|
|
43
|
+
internal-looking unused symbols
|
|
41
44
|
- memory-code links so project knowledge points at the code it affects
|
|
42
45
|
- decision intelligence for why-memory coverage, stale/weak packets, and
|
|
43
46
|
important files that still lack linked repo knowledge
|
package/dist/index.js
CHANGED
|
@@ -221,7 +221,7 @@ function listTools() {
|
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
223
|
name: "kage_cleanup_candidates",
|
|
224
|
-
description: "Find conservative cleanup candidates from Kage's code graph. Reports unreferenced source files with confidence and reasons; never auto-deletes.",
|
|
224
|
+
description: "Find conservative cleanup candidates from Kage's code graph. Reports unreferenced source files, unused exports, and internal-looking unused symbols with confidence and reasons; never auto-deletes.",
|
|
225
225
|
inputSchema: {
|
|
226
226
|
type: "object",
|
|
227
227
|
properties: {
|
package/dist/kernel.js
CHANGED
|
@@ -1656,7 +1656,9 @@ function hydrateCodeGraphArtifact(projectDir, artifact, structural) {
|
|
|
1656
1656
|
...index.imports,
|
|
1657
1657
|
...(artifact.extra_imports ?? []),
|
|
1658
1658
|
].sort((a, b) => a.from_path.localeCompare(b.from_path) || a.line - b.line || a.specifier.localeCompare(b.specifier)),
|
|
1659
|
-
calls: artifact.calls ?? []
|
|
1659
|
+
calls: (artifact.calls ?? [])
|
|
1660
|
+
.map((call) => normalizeCallEdge(call, { confidence: 0.7, resolution: "generic_static_name" }))
|
|
1661
|
+
.filter((call) => Boolean(call)),
|
|
1660
1662
|
routes: artifact.routes ?? [],
|
|
1661
1663
|
tests: artifact.tests ?? [],
|
|
1662
1664
|
packages: artifact.packages ?? [],
|
|
@@ -2755,6 +2757,27 @@ function symbolAtLine(symbols, path, line) {
|
|
|
2755
2757
|
.filter((symbol) => symbol.path === path && symbol.line <= line && (symbol.end_line ?? symbol.line) >= line)
|
|
2756
2758
|
.sort((a, b) => (b.line - a.line) || ((a.end_line ?? a.line) - (b.end_line ?? b.line)))[0] ?? null;
|
|
2757
2759
|
}
|
|
2760
|
+
function normalizeCallConfidence(value, fallback) {
|
|
2761
|
+
const numeric = Number(value);
|
|
2762
|
+
if (!Number.isFinite(numeric))
|
|
2763
|
+
return fallback;
|
|
2764
|
+
return Number(Math.max(0, Math.min(1, numeric)).toFixed(2));
|
|
2765
|
+
}
|
|
2766
|
+
function normalizeCallResolution(value, fallback) {
|
|
2767
|
+
return value === "typescript_ast_name" || value === "generic_static_name" || value === "external_index" ? value : fallback;
|
|
2768
|
+
}
|
|
2769
|
+
function normalizeCallEdge(call, fallback) {
|
|
2770
|
+
if (!isRecord(call) || typeof call.to_symbol !== "string")
|
|
2771
|
+
return null;
|
|
2772
|
+
return {
|
|
2773
|
+
from_symbol: typeof call.from_symbol === "string" ? call.from_symbol : null,
|
|
2774
|
+
to_symbol: call.to_symbol,
|
|
2775
|
+
path: String(call.path ?? ""),
|
|
2776
|
+
line: Math.max(1, Number(call.line ?? 1)),
|
|
2777
|
+
confidence: normalizeCallConfidence(call.confidence, fallback.confidence),
|
|
2778
|
+
resolution: normalizeCallResolution(call.resolution, fallback.resolution),
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2758
2781
|
function extractCalls(path, text, symbols, symbolByName) {
|
|
2759
2782
|
const sourceFile = sourceFileFor(path, text);
|
|
2760
2783
|
const calls = [];
|
|
@@ -2782,7 +2805,14 @@ function extractCalls(path, text, symbols, symbolByName) {
|
|
|
2782
2805
|
break;
|
|
2783
2806
|
if (target.path === path && target.line === line)
|
|
2784
2807
|
continue;
|
|
2785
|
-
calls.push({
|
|
2808
|
+
calls.push({
|
|
2809
|
+
from_symbol: caller?.id ?? null,
|
|
2810
|
+
to_symbol: target.id,
|
|
2811
|
+
path,
|
|
2812
|
+
line,
|
|
2813
|
+
confidence: target.path === path ? 0.9 : 0.75,
|
|
2814
|
+
resolution: "typescript_ast_name",
|
|
2815
|
+
});
|
|
2786
2816
|
}
|
|
2787
2817
|
ts.forEachChild(node, visit);
|
|
2788
2818
|
};
|
|
@@ -2824,7 +2854,14 @@ function extractGenericCalls(path, text, symbols, symbolByName) {
|
|
|
2824
2854
|
for (const target of targets.slice(0, 3)) {
|
|
2825
2855
|
if (calls.length >= MAX_CODE_GRAPH_CALLS_PER_FILE)
|
|
2826
2856
|
break;
|
|
2827
|
-
calls.push({
|
|
2857
|
+
calls.push({
|
|
2858
|
+
from_symbol: caller?.id ?? null,
|
|
2859
|
+
to_symbol: target.id,
|
|
2860
|
+
path,
|
|
2861
|
+
line,
|
|
2862
|
+
confidence: target.path === path ? 0.7 : 0.55,
|
|
2863
|
+
resolution: "generic_static_name",
|
|
2864
|
+
});
|
|
2828
2865
|
}
|
|
2829
2866
|
}
|
|
2830
2867
|
}
|
|
@@ -3173,9 +3210,8 @@ function parseKageExternalIndex(projectDir, parser, path) {
|
|
|
3173
3210
|
: [];
|
|
3174
3211
|
const calls = Array.isArray(raw.calls)
|
|
3175
3212
|
? raw.calls.flatMap((item) => {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
return [{ from_symbol: typeof item.from_symbol === "string" ? item.from_symbol : null, to_symbol: item.to_symbol, path: String(item.path ?? ""), line: Math.max(1, Number(item.line ?? 1)) }];
|
|
3213
|
+
const call = normalizeCallEdge(item, { confidence: 0.85, resolution: "external_index" });
|
|
3214
|
+
return call ? [call] : [];
|
|
3179
3215
|
})
|
|
3180
3216
|
: [];
|
|
3181
3217
|
return { symbols, imports, calls };
|
|
@@ -3230,7 +3266,7 @@ function parseScipJsonObject(projectDir, raw) {
|
|
|
3230
3266
|
symbols.push(symbol);
|
|
3231
3267
|
}
|
|
3232
3268
|
else {
|
|
3233
|
-
calls.push({ from_symbol: null, to_symbol: name, path: rel, line });
|
|
3269
|
+
calls.push({ from_symbol: null, to_symbol: name, path: rel, line, confidence: 0.85, resolution: "external_index" });
|
|
3234
3270
|
}
|
|
3235
3271
|
}
|
|
3236
3272
|
}
|
|
@@ -4995,7 +5031,7 @@ function queryCodeGraph(projectDir, query, limit = 10, graph) {
|
|
|
4995
5031
|
...imports.map(({ item }, index) => `${index + 1}. ${item.from_path}:${item.line} ${item.kind} ${item.specifier}${item.to_path ? ` -> ${item.to_path}` : ""}`),
|
|
4996
5032
|
calls.length ? "" : "",
|
|
4997
5033
|
calls.length ? "## Calls" : "",
|
|
4998
|
-
...calls.map((call, index) => `${index + 1}. ${call.from_symbol ? symbolNameById.get(call.from_symbol) ?? call.from_symbol : call.path} calls ${symbolNameById.get(call.to_symbol) ?? call.to_symbol} at ${call.path}:${call.line}`),
|
|
5034
|
+
...calls.map((call, index) => `${index + 1}. ${call.from_symbol ? symbolNameById.get(call.from_symbol) ?? call.from_symbol : call.path} calls ${symbolNameById.get(call.to_symbol) ?? call.to_symbol} at ${call.path}:${call.line} (${call.resolution}, confidence ${call.confidence.toFixed(2)})`),
|
|
4999
5035
|
];
|
|
5000
5036
|
return {
|
|
5001
5037
|
query,
|
|
@@ -5501,6 +5537,26 @@ function hasRuntimePathReference(projectDir, graph, target) {
|
|
|
5501
5537
|
}
|
|
5502
5538
|
return false;
|
|
5503
5539
|
}
|
|
5540
|
+
function cleanupSymbolKind(symbol) {
|
|
5541
|
+
return ["function", "method", "class", "constant"].includes(symbol.kind);
|
|
5542
|
+
}
|
|
5543
|
+
function symbolCleanupCandidate(symbol, kind, reasons, score, coveredByTests, git) {
|
|
5544
|
+
return {
|
|
5545
|
+
path: symbol.path,
|
|
5546
|
+
kind,
|
|
5547
|
+
symbol_id: symbol.id,
|
|
5548
|
+
symbol_name: symbol.name,
|
|
5549
|
+
line: symbol.line,
|
|
5550
|
+
confidence: cleanupConfidence(score),
|
|
5551
|
+
score,
|
|
5552
|
+
reasons,
|
|
5553
|
+
inbound_imports: 0,
|
|
5554
|
+
source_inbound_imports: 0,
|
|
5555
|
+
outbound_imports: 0,
|
|
5556
|
+
covered_by_tests: coveredByTests,
|
|
5557
|
+
last_commit_at: git?.last_commit_at ?? null,
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5504
5560
|
function kageCleanupCandidates(projectDir) {
|
|
5505
5561
|
const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
5506
5562
|
const fileByPath = new Map(graph.files.map((file) => [file.path, file]));
|
|
@@ -5525,6 +5581,7 @@ function kageCleanupCandidates(projectDir) {
|
|
|
5525
5581
|
warnings.push("Git history is unavailable, so cleanup confidence does not use recency.");
|
|
5526
5582
|
const candidates = [];
|
|
5527
5583
|
const skippedRuntimeReferences = [];
|
|
5584
|
+
const wholeFileCandidates = new Set();
|
|
5528
5585
|
for (const file of graph.files) {
|
|
5529
5586
|
if (file.kind !== "source")
|
|
5530
5587
|
continue;
|
|
@@ -5577,6 +5634,60 @@ function kageCleanupCandidates(projectDir) {
|
|
|
5577
5634
|
covered_by_tests: coveredByTests,
|
|
5578
5635
|
last_commit_at: git?.last_commit_at ?? null,
|
|
5579
5636
|
});
|
|
5637
|
+
wholeFileCandidates.add(file.path);
|
|
5638
|
+
}
|
|
5639
|
+
const calledSymbols = new Set(graph.calls.map((call) => call.to_symbol));
|
|
5640
|
+
const routeHandlers = new Set(graph.routes.map((route) => route.handler_symbol).filter((value) => Boolean(value)));
|
|
5641
|
+
const coveredSymbolNames = new Set(graph.tests.map((test) => test.covers_symbol?.toLowerCase()).filter((value) => Boolean(value)));
|
|
5642
|
+
const symbolsByPath = new Map();
|
|
5643
|
+
for (const symbol of graph.symbols.filter(cleanupSymbolKind)) {
|
|
5644
|
+
const list = symbolsByPath.get(symbol.path) ?? [];
|
|
5645
|
+
list.push(symbol);
|
|
5646
|
+
symbolsByPath.set(symbol.path, list);
|
|
5647
|
+
}
|
|
5648
|
+
const importedNamesByPath = new Map();
|
|
5649
|
+
for (const edge of graph.imports) {
|
|
5650
|
+
if (!edge.to_path || !edge.imported.length)
|
|
5651
|
+
continue;
|
|
5652
|
+
const names = importedNamesByPath.get(edge.to_path) ?? new Set();
|
|
5653
|
+
for (const name of edge.imported)
|
|
5654
|
+
names.add(name);
|
|
5655
|
+
importedNamesByPath.set(edge.to_path, names);
|
|
5656
|
+
}
|
|
5657
|
+
for (const file of graph.files) {
|
|
5658
|
+
if (file.kind !== "source" || wholeFileCandidates.has(file.path))
|
|
5659
|
+
continue;
|
|
5660
|
+
if (isEntrypointLike(file.path) || routeFiles.has(file.path))
|
|
5661
|
+
continue;
|
|
5662
|
+
const fileSymbols = symbolsByPath.get(file.path) ?? [];
|
|
5663
|
+
if (!fileSymbols.length)
|
|
5664
|
+
continue;
|
|
5665
|
+
const git = hasGit ? gitFileSignal(projectDir, file.path, graphPaths) : null;
|
|
5666
|
+
const coveredByTests = hasTestCoverage(file.path, graph);
|
|
5667
|
+
const importedNames = importedNamesByPath.get(file.path) ?? new Set();
|
|
5668
|
+
const exportedSymbols = fileSymbols.filter((symbol) => symbol.export);
|
|
5669
|
+
const hasMatchedNamedExport = exportedSymbols.some((symbol) => importedNames.has(symbol.name));
|
|
5670
|
+
for (const symbol of fileSymbols) {
|
|
5671
|
+
const symbolReferenced = calledSymbols.has(symbol.id) || routeHandlers.has(symbol.id) || coveredSymbolNames.has(symbol.name.toLowerCase());
|
|
5672
|
+
if (symbolReferenced)
|
|
5673
|
+
continue;
|
|
5674
|
+
if (symbol.export) {
|
|
5675
|
+
if (!hasMatchedNamedExport || importedNames.has(symbol.name))
|
|
5676
|
+
continue;
|
|
5677
|
+
candidates.push(symbolCleanupCandidate(symbol, "unused_export", [
|
|
5678
|
+
`export "${symbol.name}" is not imported by current named import edges`,
|
|
5679
|
+
"symbol is not a known call target, route handler, or covered test target",
|
|
5680
|
+
"file has at least one other exported symbol imported by name",
|
|
5681
|
+
], 0.62, coveredByTests, git));
|
|
5682
|
+
}
|
|
5683
|
+
else if (/^_[A-Za-z0-9_]+/.test(symbol.name)) {
|
|
5684
|
+
candidates.push(symbolCleanupCandidate(symbol, "unused_internal_symbol", [
|
|
5685
|
+
`internal-looking symbol "${symbol.name}" has no known call edge`,
|
|
5686
|
+
"symbol is not a route handler or covered test target",
|
|
5687
|
+
"candidate is review input only; dynamic references may exist",
|
|
5688
|
+
], 0.5, coveredByTests, git));
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5580
5691
|
}
|
|
5581
5692
|
candidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
5582
5693
|
return {
|
package/package.json
CHANGED
package/viewer/app.js
CHANGED
|
@@ -698,19 +698,21 @@
|
|
|
698
698
|
seen.add(entity.id);
|
|
699
699
|
entities.push(entity);
|
|
700
700
|
};
|
|
701
|
-
var addEdge = function (from, to, relation, fact, source) {
|
|
701
|
+
var addEdge = function (from, to, relation, fact, source, options) {
|
|
702
702
|
if (!from || !to) return;
|
|
703
|
+
options = options || {};
|
|
703
704
|
edges.push({
|
|
704
705
|
id: relation + ":" + from + ":" + to + ":" + edges.length,
|
|
705
706
|
from: from,
|
|
706
707
|
to: to,
|
|
707
708
|
relation: relation,
|
|
708
709
|
fact: fact,
|
|
709
|
-
confidence: 1,
|
|
710
|
+
confidence: options.confidence == null ? 1 : Number(options.confidence),
|
|
710
711
|
evidence: [],
|
|
711
712
|
commit: graph.repo_state && graph.repo_state.head,
|
|
712
713
|
source: source || "code_graph",
|
|
713
|
-
graph_kind: "code"
|
|
714
|
+
graph_kind: "code",
|
|
715
|
+
resolution: options.resolution
|
|
714
716
|
});
|
|
715
717
|
};
|
|
716
718
|
|
|
@@ -764,7 +766,11 @@
|
|
|
764
766
|
});
|
|
765
767
|
|
|
766
768
|
(graph.calls || []).forEach(function (call) {
|
|
767
|
-
|
|
769
|
+
var confidence = call.confidence == null ? 0.7 : Number(call.confidence);
|
|
770
|
+
addEdge(call.from_symbol || "file:" + call.path, call.to_symbol, "calls", call.path + ":" + call.line + " calls target symbol.", "calls", {
|
|
771
|
+
confidence: confidence,
|
|
772
|
+
resolution: call.resolution
|
|
773
|
+
});
|
|
768
774
|
});
|
|
769
775
|
|
|
770
776
|
(graph.routes || []).forEach(function (route) {
|