sigmap 8.13.0 → 8.14.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/AGENTS.md +61 -55
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/gen-context.js +144 -14
- package/llms-full.txt +4 -4
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/graph/call-graph.js +140 -10
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +1 -1
package/AGENTS.md
CHANGED
|
@@ -33,15 +33,16 @@ Always run `sigmap ask` (or `sigmap --query`) before searching for files relevan
|
|
|
33
33
|
src/extractors/python_ast.py ← ast
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
## changes (last 5 commits —
|
|
36
|
+
## changes (last 5 commits — 0 seconds ago)
|
|
37
37
|
```
|
|
38
|
-
src/format/terse.js +splitAnchor +encodeTerseSig +encodeTerseSigs +_tokens
|
|
39
38
|
src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
|
|
39
|
+
src/graph/call-graph.js +calls +maskRust +name +goDefs
|
|
40
40
|
src/mcp/handlers.js +that +getMethodImpact ~queryContext ~squeezeOutput
|
|
41
41
|
src/mcp/server.js ~dispatch
|
|
42
42
|
src/mcp/tools.js +it
|
|
43
43
|
src/review/pr-evidence.js ~buildPrEvidence ~formatPrEvidenceMarkdown
|
|
44
44
|
src/review/review-pr.js ~isSource ~reviewPr
|
|
45
|
+
src/format/terse.js +splitAnchor +encodeTerseSig +encodeTerseSigs +_tokens
|
|
45
46
|
src/wiki/generate.js +_rel +_pct +_identity +_modules
|
|
46
47
|
```
|
|
47
48
|
|
|
@@ -174,21 +175,6 @@ code-fence ---
|
|
|
174
175
|
|
|
175
176
|
## src
|
|
176
177
|
|
|
177
|
-
### src/config/defaults.js
|
|
178
|
-
```
|
|
179
|
-
module.exports = { DEFAULTS } :161-161
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### src/format/terse.js
|
|
183
|
-
```
|
|
184
|
-
module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor } :86-86
|
|
185
|
-
function splitAnchor(sig) → { text: string, suffix: s :25-30
|
|
186
|
-
function encodeTerseSig(sig) → string :37-50
|
|
187
|
-
function encodeTerseSigs(sigs) → string[] :57-59
|
|
188
|
-
function _tokens(sigs) :62-64
|
|
189
|
-
function measureTerse(sigsList) → { beforeTokens: number, a :72-84
|
|
190
|
-
```
|
|
191
|
-
|
|
192
178
|
### src/graph/blast-radius.js
|
|
193
179
|
```
|
|
194
180
|
module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
|
|
@@ -198,6 +184,35 @@ function _bfs(seedIds, reverse, maxDepth) :38-60
|
|
|
198
184
|
function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
|
|
199
185
|
```
|
|
200
186
|
|
|
187
|
+
### src/graph/call-graph.js
|
|
188
|
+
```
|
|
189
|
+
module.exports = { buildCallGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy, maskRust } :527-531
|
|
190
|
+
function normalizePath(p) :41-41
|
|
191
|
+
function toRel(cwd, f) :42-42
|
|
192
|
+
function symId(cwd, absFile, name) :43-43
|
|
193
|
+
function maskJs(src) :49-65
|
|
194
|
+
function maskRust(src) :70-91
|
|
195
|
+
function maskPy(src) :93-110
|
|
196
|
+
function matchDelim(masked, openIdx, open, close) :113-120
|
|
197
|
+
function lineAt(src, idx) :122-127
|
|
198
|
+
function jsDefs(masked) :132-237
|
|
199
|
+
function pyDefs(masked) :200-220
|
|
200
|
+
function goDefs(masked) :224-344
|
|
201
|
+
function javaDefs(masked) :250-367
|
|
202
|
+
function rustDefs(masked) :278-401
|
|
203
|
+
function maskFor(filePath, src) :300-305
|
|
204
|
+
function extractDefs(filePath, src) :307-315
|
|
205
|
+
function callsInRange(masked, start, end) :318-330
|
|
206
|
+
function _walk(dir, excludeSet, out, depth) :334-347
|
|
207
|
+
function buildCallGraph(cwd, opts = {}) → { * forward: Map<string,s :363-446
|
|
208
|
+
function _resolveSymbol(symbol, defs) :449-454
|
|
209
|
+
function _bfs(seedIds, graph, maxDepth) :457-470
|
|
210
|
+
function methodImpact(symbol, cwd, opts = {}) → { symbol:string, resolved :480-486
|
|
211
|
+
function methodCallees(symbol, cwd, opts = {}) → { symbol:string, resolved :492-498
|
|
212
|
+
function formatCallGraph(result, kind) :501-513
|
|
213
|
+
function formatCallGraphJSON(result, kind) :515-525
|
|
214
|
+
```
|
|
215
|
+
|
|
201
216
|
### src/mcp/handlers.js
|
|
202
217
|
```
|
|
203
218
|
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :966-966
|
|
@@ -255,20 +270,6 @@ function isSource(p) :35-37
|
|
|
255
270
|
function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
|
|
256
271
|
```
|
|
257
272
|
|
|
258
|
-
### src/wiki/generate.js
|
|
259
|
-
```
|
|
260
|
-
module.exports = { buildWiki, renderWikiMarkdown } :250-250
|
|
261
|
-
function _rel(cwd, f) :22-24
|
|
262
|
-
function _pct(fraction) :26-28
|
|
263
|
-
function _identity(cwd) :31-37
|
|
264
|
-
function _modules(index) :40-67
|
|
265
|
-
function _flow(cwd) :70-97
|
|
266
|
-
function _conventions(cwd, index) :100-117
|
|
267
|
-
function _health(cwd) :119-127
|
|
268
|
-
function buildWiki(cwd, opts = {}) → { data: object, markdown: :137-162
|
|
269
|
-
function renderWikiMarkdown(data, sigmapVersion) → string :171-248
|
|
270
|
-
```
|
|
271
|
-
|
|
272
273
|
### src/analysis/coverage-score.js
|
|
273
274
|
```
|
|
274
275
|
module.exports = { coverageScore, CODE_EXTS } :105-105
|
|
@@ -306,6 +307,11 @@ function getChangedFiles(files, cache) → { changed: string[], unch :71-88
|
|
|
306
307
|
function updateCacheEntries(cache, extracted) :96-103
|
|
307
308
|
```
|
|
308
309
|
|
|
310
|
+
### src/config/defaults.js
|
|
311
|
+
```
|
|
312
|
+
module.exports = { DEFAULTS } :161-161
|
|
313
|
+
```
|
|
314
|
+
|
|
309
315
|
### src/config/loader.js
|
|
310
316
|
```
|
|
311
317
|
module.exports = { loadConfig, loadBaseConfig } :313-313
|
|
@@ -980,6 +986,16 @@ function detectVersion(cwd) :13-19
|
|
|
980
986
|
function format(context, cwd, writtenFiles, sigmapVersion) :21-69
|
|
981
987
|
```
|
|
982
988
|
|
|
989
|
+
### src/format/terse.js
|
|
990
|
+
```
|
|
991
|
+
module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor } :86-86
|
|
992
|
+
function splitAnchor(sig) → { text: string, suffix: s :25-30
|
|
993
|
+
function encodeTerseSig(sig) → string :37-50
|
|
994
|
+
function encodeTerseSigs(sigs) → string[] :57-59
|
|
995
|
+
function _tokens(sigs) :62-64
|
|
996
|
+
function measureTerse(sigsList) → { beforeTokens: number, a :72-84
|
|
997
|
+
```
|
|
998
|
+
|
|
983
999
|
### src/format/usage-guidance.js
|
|
984
1000
|
```
|
|
985
1001
|
module.exports = { usageBlock } :28-28
|
|
@@ -1012,30 +1028,6 @@ function build(files, cwd, ctx) → { forward: Map<string,str :387-426
|
|
|
1012
1028
|
function buildFromCwd(cwd, opts) → { forward: Map<string,str :438-492
|
|
1013
1029
|
```
|
|
1014
1030
|
|
|
1015
|
-
### src/graph/call-graph.js
|
|
1016
|
-
```
|
|
1017
|
-
module.exports = { buildCallGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy } :397-401
|
|
1018
|
-
function normalizePath(p) :35-35
|
|
1019
|
-
function toRel(cwd, f) :36-36
|
|
1020
|
-
function symId(cwd, absFile, name) :37-37
|
|
1021
|
-
function maskJs(src) :43-59
|
|
1022
|
-
function maskPy(src) :61-78
|
|
1023
|
-
function matchDelim(masked, openIdx, open, close) :81-88
|
|
1024
|
-
function lineAt(src, idx) :90-95
|
|
1025
|
-
function jsDefs(masked) :100-206
|
|
1026
|
-
function pyDefs(masked) :168-188
|
|
1027
|
-
function extractDefs(filePath, src) :190-195
|
|
1028
|
-
function callsInRange(masked, start, end) :198-210
|
|
1029
|
-
function _walk(dir, excludeSet, out, depth) :214-227
|
|
1030
|
-
function buildCallGraph(cwd, opts = {}) → { * forward: Map<string,s :243-316
|
|
1031
|
-
function _resolveSymbol(symbol, defs) :319-324
|
|
1032
|
-
function _bfs(seedIds, graph, maxDepth) :327-340
|
|
1033
|
-
function methodImpact(symbol, cwd, opts = {}) → { symbol:string, resolved :350-356
|
|
1034
|
-
function methodCallees(symbol, cwd, opts = {}) → { symbol:string, resolved :362-368
|
|
1035
|
-
function formatCallGraph(result, kind) :371-383
|
|
1036
|
-
function formatCallGraphJSON(result, kind) :385-395
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
1031
|
### src/graph/impact.js
|
|
1040
1032
|
```
|
|
1041
1033
|
module.exports = { getImpact, analyzeImpact, formatImpact, formatImpactJSON } :240-240
|
|
@@ -1416,6 +1408,20 @@ function extractNpmScripts(text) → { name: string, line: num :168-184
|
|
|
1416
1408
|
function extractSymbols(text) → { name: string, line: num :192-209
|
|
1417
1409
|
```
|
|
1418
1410
|
|
|
1411
|
+
### src/wiki/generate.js
|
|
1412
|
+
```
|
|
1413
|
+
module.exports = { buildWiki, renderWikiMarkdown } :250-250
|
|
1414
|
+
function _rel(cwd, f) :22-24
|
|
1415
|
+
function _pct(fraction) :26-28
|
|
1416
|
+
function _identity(cwd) :31-37
|
|
1417
|
+
function _modules(index) :40-67
|
|
1418
|
+
function _flow(cwd) :70-97
|
|
1419
|
+
function _conventions(cwd, index) :100-117
|
|
1420
|
+
function _health(cwd) :119-127
|
|
1421
|
+
function buildWiki(cwd, opts = {}) → { data: object, markdown: :137-162
|
|
1422
|
+
function renderWikiMarkdown(data, sigmapVersion) → string :171-248
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1419
1425
|
### src/workspace/detector.js
|
|
1420
1426
|
```
|
|
1421
1427
|
module.exports = { detectWorkspaces, inferPackage, scopeToPackage } :4-4
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.14.0] — 2026-07-11
|
|
14
|
+
|
|
15
|
+
Minor release — **call-graph support for Java, Go, and Rust** (GR1 language expansion). The method-level call-graph goes from 2 to 5 languages, and every consumer — `--callers`/`--callees`, blast-radius scoring, `review-pr` method-blast findings, `get_method_impact` — inherits them with no further changes.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Java/Go/Rust call-graph (#471, PR #472):** new def extractors in `src/graph/call-graph.js` — Go `func` + receiver methods (parenthesized return lists handled), Java methods + constructors (generics/`throws` tolerated; control-flow and `new Foo(){}` anonymous classes rejected), Rust `fn` incl. generics + `where` clauses and `impl` methods (bodiless trait declarations skipped). New **lifetime-safe `maskRust`** masker: `'a` lifetimes pass through untouched while char literals and strings are masked, preserving offsets. **Same-package resolution scope:** Go/Java call same-package functions across files with no import statement, so same-directory same-language siblings join the resolution scope (sorted, deterministic). Per North-Star #1, unparseable constructs are skipped — never a parser dep. Surface claims updated to "JS/TS, Python, Java, Go, Rust" in `--help`, the `get_method_impact` description, and the MCP guide.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## [8.13.0] — 2026-07-11
|
|
14
23
|
|
|
15
24
|
Minor release — **method-level blast-radius scoring** (GR2, the biggest Phase-2 lever in the master plan §7.4). The D4 call-graph finally has consumers: review findings, the PR Evidence report, and a 20th MCP tool now answer *which functions break*, not just which files.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
125
|
+
Benchmark : sigmap-v8.14-main (21 repositories, including R language)
|
|
126
126
|
Date : 2026-07-11
|
|
127
127
|
|
|
128
128
|
Hit@5 : 87.8% (baseline 13.6% — 6.5× lift)
|
package/gen-context.js
CHANGED
|
@@ -10942,14 +10942,16 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
10942
10942
|
__factories["./src/graph/call-graph"] = function(module, exports) {
|
|
10943
10943
|
|
|
10944
10944
|
/**
|
|
10945
|
-
* Method/caller-level call-graph (D4 v1).
|
|
10945
|
+
* Method/caller-level call-graph (D4 v1, languages expanded in GR1).
|
|
10946
10946
|
*
|
|
10947
|
-
* Builds symbol-level edges — which function calls which function — for JS/TS
|
|
10948
|
-
* and
|
|
10949
|
-
* Call sites are resolved with high precision: a call
|
|
10950
|
-
* of that name in the *same file* first, then in a
|
|
10951
|
-
* (via the existing file-level import graph). Names
|
|
10952
|
-
* definition produce no edge — over-approximation
|
|
10947
|
+
* Builds symbol-level edges — which function calls which function — for JS/TS,
|
|
10948
|
+
* Python, Java, Go, and Rust. Deterministic, zero-dependency, regex +
|
|
10949
|
+
* brace/indent matching. Call sites are resolved with high precision: a call
|
|
10950
|
+
* resolves to a definition of that name in the *same file* first, then in a
|
|
10951
|
+
* *directly-imported* file (via the existing file-level import graph). Names
|
|
10952
|
+
* that resolve to no repo definition produce no edge — over-approximation
|
|
10953
|
+
* noise is avoided. Constructs that can't be parsed dependency-free are
|
|
10954
|
+
* skipped (less fidelity, never a parser dep).
|
|
10953
10955
|
*
|
|
10954
10956
|
* Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
|
|
10955
10957
|
*
|
|
@@ -10962,6 +10964,9 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10962
10964
|
|
|
10963
10965
|
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
10964
10966
|
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
10967
|
+
const JAVA_EXTS = new Set(['.java']);
|
|
10968
|
+
const GO_EXTS = new Set(['.go']);
|
|
10969
|
+
const RS_EXTS = new Set(['.rs']);
|
|
10965
10970
|
|
|
10966
10971
|
// Tokens that look like `name(` calls or definition headers but are language
|
|
10967
10972
|
// keywords, not user symbols — never treated as a call or a definition.
|
|
@@ -10971,6 +10976,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10971
10976
|
'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
|
|
10972
10977
|
'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
|
|
10973
10978
|
'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
|
|
10979
|
+
'synchronized',
|
|
10974
10980
|
]);
|
|
10975
10981
|
|
|
10976
10982
|
function normalizePath(p) { return path.normalize(p).toLowerCase(); }
|
|
@@ -10999,6 +11005,32 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
10999
11005
|
return out.join('');
|
|
11000
11006
|
}
|
|
11001
11007
|
|
|
11008
|
+
// Rust: `//`, `/* */`, and `"..."` mask like JS, but a bare `'` is usually a
|
|
11009
|
+
// lifetime (`'a`), not a string — masking to the "closing" quote would corrupt
|
|
11010
|
+
// offsets. Only char literals (`'x'`, `'\n'`) are masked; lifetimes pass through.
|
|
11011
|
+
function maskRust(src) {
|
|
11012
|
+
const out = src.split('');
|
|
11013
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
11014
|
+
let i = 0; const n = src.length;
|
|
11015
|
+
while (i < n) {
|
|
11016
|
+
const c = src[i], d = src[i + 1];
|
|
11017
|
+
if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
11018
|
+
if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
|
|
11019
|
+
if (c === '"') {
|
|
11020
|
+
let j = i + 1;
|
|
11021
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === '"') break; j++; }
|
|
11022
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
11023
|
+
}
|
|
11024
|
+
if (c === "'") {
|
|
11025
|
+
if (d === '\\' && src[i + 3] === "'") { blank(i, i + 4); i += 4; continue; } // '\n'
|
|
11026
|
+
if (d !== undefined && src[i + 2] === "'") { blank(i, i + 3); i += 3; continue; } // 'x'
|
|
11027
|
+
i++; continue; // lifetime `'a` — leave untouched
|
|
11028
|
+
}
|
|
11029
|
+
i++;
|
|
11030
|
+
}
|
|
11031
|
+
return out.join('');
|
|
11032
|
+
}
|
|
11033
|
+
|
|
11002
11034
|
function maskPy(src) {
|
|
11003
11035
|
const out = src.split('');
|
|
11004
11036
|
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
@@ -11128,10 +11160,98 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11128
11160
|
return defs;
|
|
11129
11161
|
}
|
|
11130
11162
|
|
|
11163
|
+
// Go: func name(...) { } | func (r Recv) name(...) (T, error) { }
|
|
11164
|
+
// The return list may itself be parenthesized, so scan past it to the body `{`.
|
|
11165
|
+
function goDefs(masked) {
|
|
11166
|
+
const defs = [];
|
|
11167
|
+
const re = /(?:^|\n)func\s+(?:\([^)\n]*\)\s*)?([A-Za-z_]\w*)\s*\(/g;
|
|
11168
|
+
let m;
|
|
11169
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11170
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
11171
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
11172
|
+
let k = close + 1;
|
|
11173
|
+
let depth = 0;
|
|
11174
|
+
while (k < masked.length) {
|
|
11175
|
+
const ch = masked[k];
|
|
11176
|
+
if (ch === '(') depth++;
|
|
11177
|
+
else if (ch === ')') depth--;
|
|
11178
|
+
else if (ch === '{' && depth === 0) break;
|
|
11179
|
+
else if (ch === '\n' && depth === 0) { k = -1; break; } // no body on this header
|
|
11180
|
+
k++;
|
|
11181
|
+
}
|
|
11182
|
+
if (k === -1 || k >= masked.length) continue;
|
|
11183
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
11184
|
+
}
|
|
11185
|
+
return defs;
|
|
11186
|
+
}
|
|
11187
|
+
|
|
11188
|
+
// Java: methods + constructors with braced bodies. Statement-shaped matches
|
|
11189
|
+
// (calls, control flow) are rejected because their `)` is followed by `;`,
|
|
11190
|
+
// and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
|
|
11191
|
+
function javaDefs(masked) {
|
|
11192
|
+
const defs = [];
|
|
11193
|
+
const seen = new Set();
|
|
11194
|
+
const re = /(?:^|\n)[ \t]*((?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\s+)*)(?:<[^>\n]{0,80}>\s*)?(?:[\w$][\w$.<>\[\],?\s]*?\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
11195
|
+
let m;
|
|
11196
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11197
|
+
const name = m[2];
|
|
11198
|
+
if (NON_CALL.has(name)) continue;
|
|
11199
|
+
// `new Foo() { … }` anonymous classes are uses, not definitions.
|
|
11200
|
+
const before = masked.slice(Math.max(0, m.index), m.index + m[0].length - name.length - 1);
|
|
11201
|
+
if (/\bnew\s*$/.test(before)) continue;
|
|
11202
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
11203
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
11204
|
+
// skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
|
|
11205
|
+
let k = close + 1;
|
|
11206
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
|
|
11207
|
+
if (masked[k] !== '{') continue;
|
|
11208
|
+
const key = name + ':' + k;
|
|
11209
|
+
if (seen.has(key)) continue;
|
|
11210
|
+
seen.add(key);
|
|
11211
|
+
defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
11212
|
+
}
|
|
11213
|
+
return defs;
|
|
11214
|
+
}
|
|
11215
|
+
|
|
11216
|
+
// Rust: fn name(...) { } | fn name<T>(...) -> T where … { } — inside or
|
|
11217
|
+
// outside impl/trait blocks. A `;` before the body brace (trait declaration)
|
|
11218
|
+
// means no body: skipped.
|
|
11219
|
+
function rustDefs(masked) {
|
|
11220
|
+
const defs = [];
|
|
11221
|
+
const re = /\bfn\s+([A-Za-z_]\w*)/g;
|
|
11222
|
+
let m;
|
|
11223
|
+
while ((m = re.exec(masked)) !== null) {
|
|
11224
|
+
let k = m.index + m[0].length;
|
|
11225
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
11226
|
+
if (masked[k] === '<') k = matchDelim(masked, k, '<', '>') + 1;
|
|
11227
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
11228
|
+
if (masked[k] !== '(') continue;
|
|
11229
|
+
const close = matchDelim(masked, k, '(', ')');
|
|
11230
|
+
// return type / where clause may span lines; stop at body `{` or decl `;`
|
|
11231
|
+
let b = close + 1;
|
|
11232
|
+
while (b < masked.length && masked[b] !== '{' && masked[b] !== ';') b++;
|
|
11233
|
+
if (masked[b] !== '{') continue;
|
|
11234
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index), bodyStart: b, bodyEnd: matchDelim(masked, b, '{', '}') });
|
|
11235
|
+
}
|
|
11236
|
+
return defs;
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
// Pick the masker whose comment/string syntax matches the language.
|
|
11240
|
+
// Java and Go share JS syntax (Go raw strings mask like template literals).
|
|
11241
|
+
function maskFor(filePath, src) {
|
|
11242
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
11243
|
+
if (PY_EXTS.has(ext)) return maskPy(src);
|
|
11244
|
+
if (RS_EXTS.has(ext)) return maskRust(src);
|
|
11245
|
+
return maskJs(src);
|
|
11246
|
+
}
|
|
11247
|
+
|
|
11131
11248
|
function extractDefs(filePath, src) {
|
|
11132
11249
|
const ext = path.extname(filePath).toLowerCase();
|
|
11133
11250
|
if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
|
|
11134
11251
|
if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
|
|
11252
|
+
if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
|
|
11253
|
+
if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
|
|
11254
|
+
if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
|
|
11135
11255
|
return null; // unsupported language
|
|
11136
11256
|
}
|
|
11137
11257
|
|
|
@@ -11162,7 +11282,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11162
11282
|
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
11163
11283
|
else if (e.isFile()) {
|
|
11164
11284
|
const ext = path.extname(e.name).toLowerCase();
|
|
11165
|
-
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
|
|
11285
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
|
|
11166
11286
|
}
|
|
11167
11287
|
}
|
|
11168
11288
|
}
|
|
@@ -11229,10 +11349,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11229
11349
|
};
|
|
11230
11350
|
|
|
11231
11351
|
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
11232
|
-
const masked =
|
|
11352
|
+
const masked = maskFor(f, fs.readFileSync(f, 'utf8'));
|
|
11233
11353
|
// resolution scope: this file's defs, then directly-imported files' defs
|
|
11234
11354
|
const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
|
|
11235
11355
|
.map((nf) => normToAbs.get(nf)).filter(Boolean);
|
|
11356
|
+
// Go/Java: same-package symbols are visible with no import statement, and
|
|
11357
|
+
// a package is (in practice) a directory — extend the scope to same-dir
|
|
11358
|
+
// same-language siblings. Sorted for deterministic resolution order.
|
|
11359
|
+
const ext = path.extname(f).toLowerCase();
|
|
11360
|
+
if (GO_EXTS.has(ext) || JAVA_EXTS.has(ext)) {
|
|
11361
|
+
const siblings = [...perFileDefs.keys()]
|
|
11362
|
+
.filter((o) => o !== f && path.dirname(o) === path.dirname(f) && path.extname(o).toLowerCase() === ext)
|
|
11363
|
+
.sort();
|
|
11364
|
+
importedAbs.push(...siblings);
|
|
11365
|
+
}
|
|
11236
11366
|
for (const d of fileDefs) {
|
|
11237
11367
|
const callerId = symId(cwd, f, d.name);
|
|
11238
11368
|
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
@@ -11338,7 +11468,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
11338
11468
|
module.exports = {
|
|
11339
11469
|
buildCallGraph, methodImpact, methodCallees,
|
|
11340
11470
|
formatCallGraph, formatCallGraphJSON,
|
|
11341
|
-
extractDefs, maskJs, maskPy,
|
|
11471
|
+
extractDefs, maskJs, maskPy, maskRust,
|
|
11342
11472
|
};
|
|
11343
11473
|
|
|
11344
11474
|
};
|
|
@@ -14201,7 +14331,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14201
14331
|
|
|
14202
14332
|
const SERVER_INFO = {
|
|
14203
14333
|
name: 'sigmap',
|
|
14204
|
-
version: '8.
|
|
14334
|
+
version: '8.14.0',
|
|
14205
14335
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14206
14336
|
};
|
|
14207
14337
|
|
|
@@ -14482,7 +14612,7 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
14482
14612
|
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
14483
14613
|
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
14484
14614
|
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
14485
|
-
'JS/TS
|
|
14615
|
+
'JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.',
|
|
14486
14616
|
inputSchema: {
|
|
14487
14617
|
type: 'object',
|
|
14488
14618
|
properties: {
|
|
@@ -19214,7 +19344,7 @@ function __tryGit(args, opts = {}) {
|
|
|
19214
19344
|
catch (_) { return ''; }
|
|
19215
19345
|
}
|
|
19216
19346
|
|
|
19217
|
-
const VERSION = '8.
|
|
19347
|
+
const VERSION = '8.14.0';
|
|
19218
19348
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
19219
19349
|
|
|
19220
19350
|
function requireSourceOrBundled(key) {
|
|
@@ -21075,7 +21205,7 @@ Usage:
|
|
|
21075
21205
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
21076
21206
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
21077
21207
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
21078
|
-
${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS
|
|
21208
|
+
${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS, Python, Java, Go, Rust)
|
|
21079
21209
|
${cmd} --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
21080
21210
|
${cmd} --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
21081
21211
|
${cmd} verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.14.0 | Benchmark: sigmap-v8.14-main (2026-07-11)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.14-main, 2026-07-11)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -101,7 +101,7 @@ sigmap weights --json Learned weights as JSON
|
|
|
101
101
|
sigmap --impact <file> Show every file impacted by changing <file>
|
|
102
102
|
sigmap --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
103
103
|
sigmap --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
104
|
-
sigmap --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS
|
|
104
|
+
sigmap --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS, Python, Java, Go, Rust)
|
|
105
105
|
sigmap --callees <symbol> Every repo function that <symbol> (transitively) calls
|
|
106
106
|
sigmap --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
|
|
107
107
|
sigmap verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
|
|
@@ -210,7 +210,7 @@ Input: { query: string, topK?: number }
|
|
|
210
210
|
|
|
211
211
|
### get_method_impact
|
|
212
212
|
|
|
213
|
-
Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — or, with direction "callees", every repo function it calls. Finer-grained than the file-level get_impact: tells an agent which functions break, not just which files. JS/TS
|
|
213
|
+
Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — or, with direction "callees", every repo function it calls. Finer-grained than the file-level get_impact: tells an agent which functions break, not just which files. JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.
|
|
214
214
|
|
|
215
215
|
```
|
|
216
216
|
Input: { symbol: string, direction?: string, depth?: number }
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.14.0 | Benchmark: sigmap-v8.14-main (2026-07-11)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.14-main, 2026-07-11)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
package/src/graph/call-graph.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Method/caller-level call-graph (D4 v1).
|
|
4
|
+
* Method/caller-level call-graph (D4 v1, languages expanded in GR1).
|
|
5
5
|
*
|
|
6
|
-
* Builds symbol-level edges — which function calls which function — for JS/TS
|
|
7
|
-
* and
|
|
8
|
-
* Call sites are resolved with high precision: a call
|
|
9
|
-
* of that name in the *same file* first, then in a
|
|
10
|
-
* (via the existing file-level import graph). Names
|
|
11
|
-
* definition produce no edge — over-approximation
|
|
6
|
+
* Builds symbol-level edges — which function calls which function — for JS/TS,
|
|
7
|
+
* Python, Java, Go, and Rust. Deterministic, zero-dependency, regex +
|
|
8
|
+
* brace/indent matching. Call sites are resolved with high precision: a call
|
|
9
|
+
* resolves to a definition of that name in the *same file* first, then in a
|
|
10
|
+
* *directly-imported* file (via the existing file-level import graph). Names
|
|
11
|
+
* that resolve to no repo definition produce no edge — over-approximation
|
|
12
|
+
* noise is avoided. Constructs that can't be parsed dependency-free are
|
|
13
|
+
* skipped (less fidelity, never a parser dep).
|
|
12
14
|
*
|
|
13
15
|
* Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
|
|
14
16
|
*
|
|
@@ -21,6 +23,9 @@ const { build } = require('./builder');
|
|
|
21
23
|
|
|
22
24
|
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
23
25
|
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
26
|
+
const JAVA_EXTS = new Set(['.java']);
|
|
27
|
+
const GO_EXTS = new Set(['.go']);
|
|
28
|
+
const RS_EXTS = new Set(['.rs']);
|
|
24
29
|
|
|
25
30
|
// Tokens that look like `name(` calls or definition headers but are language
|
|
26
31
|
// keywords, not user symbols — never treated as a call or a definition.
|
|
@@ -30,6 +35,7 @@ const NON_CALL = new Set([
|
|
|
30
35
|
'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
|
|
31
36
|
'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
|
|
32
37
|
'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
|
|
38
|
+
'synchronized',
|
|
33
39
|
]);
|
|
34
40
|
|
|
35
41
|
function normalizePath(p) { return path.normalize(p).toLowerCase(); }
|
|
@@ -58,6 +64,32 @@ function maskJs(src) {
|
|
|
58
64
|
return out.join('');
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
// Rust: `//`, `/* */`, and `"..."` mask like JS, but a bare `'` is usually a
|
|
68
|
+
// lifetime (`'a`), not a string — masking to the "closing" quote would corrupt
|
|
69
|
+
// offsets. Only char literals (`'x'`, `'\n'`) are masked; lifetimes pass through.
|
|
70
|
+
function maskRust(src) {
|
|
71
|
+
const out = src.split('');
|
|
72
|
+
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
73
|
+
let i = 0; const n = src.length;
|
|
74
|
+
while (i < n) {
|
|
75
|
+
const c = src[i], d = src[i + 1];
|
|
76
|
+
if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
|
|
77
|
+
if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
|
|
78
|
+
if (c === '"') {
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === '"') break; j++; }
|
|
81
|
+
j = Math.min(n, j + 1); blank(i, j); i = j; continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "'") {
|
|
84
|
+
if (d === '\\' && src[i + 3] === "'") { blank(i, i + 4); i += 4; continue; } // '\n'
|
|
85
|
+
if (d !== undefined && src[i + 2] === "'") { blank(i, i + 3); i += 3; continue; } // 'x'
|
|
86
|
+
i++; continue; // lifetime `'a` — leave untouched
|
|
87
|
+
}
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
return out.join('');
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
function maskPy(src) {
|
|
62
94
|
const out = src.split('');
|
|
63
95
|
const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
@@ -187,10 +219,98 @@ function pyDefs(masked) {
|
|
|
187
219
|
return defs;
|
|
188
220
|
}
|
|
189
221
|
|
|
222
|
+
// Go: func name(...) { } | func (r Recv) name(...) (T, error) { }
|
|
223
|
+
// The return list may itself be parenthesized, so scan past it to the body `{`.
|
|
224
|
+
function goDefs(masked) {
|
|
225
|
+
const defs = [];
|
|
226
|
+
const re = /(?:^|\n)func\s+(?:\([^)\n]*\)\s*)?([A-Za-z_]\w*)\s*\(/g;
|
|
227
|
+
let m;
|
|
228
|
+
while ((m = re.exec(masked)) !== null) {
|
|
229
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
230
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
231
|
+
let k = close + 1;
|
|
232
|
+
let depth = 0;
|
|
233
|
+
while (k < masked.length) {
|
|
234
|
+
const ch = masked[k];
|
|
235
|
+
if (ch === '(') depth++;
|
|
236
|
+
else if (ch === ')') depth--;
|
|
237
|
+
else if (ch === '{' && depth === 0) break;
|
|
238
|
+
else if (ch === '\n' && depth === 0) { k = -1; break; } // no body on this header
|
|
239
|
+
k++;
|
|
240
|
+
}
|
|
241
|
+
if (k === -1 || k >= masked.length) continue;
|
|
242
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
243
|
+
}
|
|
244
|
+
return defs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Java: methods + constructors with braced bodies. Statement-shaped matches
|
|
248
|
+
// (calls, control flow) are rejected because their `)` is followed by `;`,
|
|
249
|
+
// and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
|
|
250
|
+
function javaDefs(masked) {
|
|
251
|
+
const defs = [];
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
const re = /(?:^|\n)[ \t]*((?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\s+)*)(?:<[^>\n]{0,80}>\s*)?(?:[\w$][\w$.<>\[\],?\s]*?\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
254
|
+
let m;
|
|
255
|
+
while ((m = re.exec(masked)) !== null) {
|
|
256
|
+
const name = m[2];
|
|
257
|
+
if (NON_CALL.has(name)) continue;
|
|
258
|
+
// `new Foo() { … }` anonymous classes are uses, not definitions.
|
|
259
|
+
const before = masked.slice(Math.max(0, m.index), m.index + m[0].length - name.length - 1);
|
|
260
|
+
if (/\bnew\s*$/.test(before)) continue;
|
|
261
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
262
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
263
|
+
// skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
|
|
264
|
+
let k = close + 1;
|
|
265
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
|
|
266
|
+
if (masked[k] !== '{') continue;
|
|
267
|
+
const key = name + ':' + k;
|
|
268
|
+
if (seen.has(key)) continue;
|
|
269
|
+
seen.add(key);
|
|
270
|
+
defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
271
|
+
}
|
|
272
|
+
return defs;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Rust: fn name(...) { } | fn name<T>(...) -> T where … { } — inside or
|
|
276
|
+
// outside impl/trait blocks. A `;` before the body brace (trait declaration)
|
|
277
|
+
// means no body: skipped.
|
|
278
|
+
function rustDefs(masked) {
|
|
279
|
+
const defs = [];
|
|
280
|
+
const re = /\bfn\s+([A-Za-z_]\w*)/g;
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = re.exec(masked)) !== null) {
|
|
283
|
+
let k = m.index + m[0].length;
|
|
284
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
285
|
+
if (masked[k] === '<') k = matchDelim(masked, k, '<', '>') + 1;
|
|
286
|
+
while (k < masked.length && /\s/.test(masked[k])) k++;
|
|
287
|
+
if (masked[k] !== '(') continue;
|
|
288
|
+
const close = matchDelim(masked, k, '(', ')');
|
|
289
|
+
// return type / where clause may span lines; stop at body `{` or decl `;`
|
|
290
|
+
let b = close + 1;
|
|
291
|
+
while (b < masked.length && masked[b] !== '{' && masked[b] !== ';') b++;
|
|
292
|
+
if (masked[b] !== '{') continue;
|
|
293
|
+
defs.push({ name: m[1], line: lineAt(masked, m.index), bodyStart: b, bodyEnd: matchDelim(masked, b, '{', '}') });
|
|
294
|
+
}
|
|
295
|
+
return defs;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Pick the masker whose comment/string syntax matches the language.
|
|
299
|
+
// Java and Go share JS syntax (Go raw strings mask like template literals).
|
|
300
|
+
function maskFor(filePath, src) {
|
|
301
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
302
|
+
if (PY_EXTS.has(ext)) return maskPy(src);
|
|
303
|
+
if (RS_EXTS.has(ext)) return maskRust(src);
|
|
304
|
+
return maskJs(src);
|
|
305
|
+
}
|
|
306
|
+
|
|
190
307
|
function extractDefs(filePath, src) {
|
|
191
308
|
const ext = path.extname(filePath).toLowerCase();
|
|
192
309
|
if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
|
|
193
310
|
if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
|
|
311
|
+
if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
|
|
312
|
+
if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
|
|
313
|
+
if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
|
|
194
314
|
return null; // unsupported language
|
|
195
315
|
}
|
|
196
316
|
|
|
@@ -221,7 +341,7 @@ function _walk(dir, excludeSet, out, depth) {
|
|
|
221
341
|
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
222
342
|
else if (e.isFile()) {
|
|
223
343
|
const ext = path.extname(e.name).toLowerCase();
|
|
224
|
-
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
|
|
344
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
|
|
225
345
|
}
|
|
226
346
|
}
|
|
227
347
|
}
|
|
@@ -288,10 +408,20 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
288
408
|
};
|
|
289
409
|
|
|
290
410
|
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
291
|
-
const masked =
|
|
411
|
+
const masked = maskFor(f, fs.readFileSync(f, 'utf8'));
|
|
292
412
|
// resolution scope: this file's defs, then directly-imported files' defs
|
|
293
413
|
const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
|
|
294
414
|
.map((nf) => normToAbs.get(nf)).filter(Boolean);
|
|
415
|
+
// Go/Java: same-package symbols are visible with no import statement, and
|
|
416
|
+
// a package is (in practice) a directory — extend the scope to same-dir
|
|
417
|
+
// same-language siblings. Sorted for deterministic resolution order.
|
|
418
|
+
const ext = path.extname(f).toLowerCase();
|
|
419
|
+
if (GO_EXTS.has(ext) || JAVA_EXTS.has(ext)) {
|
|
420
|
+
const siblings = [...perFileDefs.keys()]
|
|
421
|
+
.filter((o) => o !== f && path.dirname(o) === path.dirname(f) && path.extname(o).toLowerCase() === ext)
|
|
422
|
+
.sort();
|
|
423
|
+
importedAbs.push(...siblings);
|
|
424
|
+
}
|
|
295
425
|
for (const d of fileDefs) {
|
|
296
426
|
const callerId = symId(cwd, f, d.name);
|
|
297
427
|
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
@@ -397,5 +527,5 @@ function formatCallGraphJSON(result, kind) {
|
|
|
397
527
|
module.exports = {
|
|
398
528
|
buildCallGraph, methodImpact, methodCallees,
|
|
399
529
|
formatCallGraph, formatCallGraphJSON,
|
|
400
|
-
extractDefs, maskJs, maskPy,
|
|
530
|
+
extractDefs, maskJs, maskPy, maskRust,
|
|
401
531
|
};
|
package/src/mcp/server.js
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -154,7 +154,7 @@ const TOOLS = [
|
|
|
154
154
|
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
155
155
|
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
156
156
|
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
157
|
-
'JS/TS
|
|
157
|
+
'JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.',
|
|
158
158
|
inputSchema: {
|
|
159
159
|
type: 'object',
|
|
160
160
|
properties: {
|