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 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 — 1 second ago)
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.13-main (21 repositories, including R language)
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 Python. Deterministic, zero-dependency, regex + brace/indent matching.
10949
- * Call sites are resolved with high precision: a call resolves to a definition
10950
- * of that name in the *same file* first, then in a *directly-imported* file
10951
- * (via the existing file-level import graph). Names that resolve to no repo
10952
- * definition produce no edge — over-approximation noise is avoided.
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 = JS_EXTS.has(path.extname(f).toLowerCase()) ? maskJs(fs.readFileSync(f, 'utf8')) : maskPy(fs.readFileSync(f, 'utf8'));
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.13.0',
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 + Python call-graph; deterministic, no LLM.',
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.13.0';
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 + Python)
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.13.0 | Benchmark: sigmap-v8.13-main (2026-07-11)
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.13-main, 2026-07-11)
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 + Python)
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 + Python call-graph; deterministic, no LLM.
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.13.0 | Benchmark: sigmap-v8.13-main (2026-07-11)
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.13-main, 2026-07-11)
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.13.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.13.0",
3
+ "version": "8.14.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.13.0",
3
+ "version": "8.14.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 Python. Deterministic, zero-dependency, regex + brace/indent matching.
8
- * Call sites are resolved with high precision: a call resolves to a definition
9
- * of that name in the *same file* first, then in a *directly-imported* file
10
- * (via the existing file-level import graph). Names that resolve to no repo
11
- * definition produce no edge — over-approximation noise is avoided.
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 = JS_EXTS.has(path.extname(f).toLowerCase()) ? maskJs(fs.readFileSync(f, 'utf8')) : maskPy(fs.readFileSync(f, 'utf8'));
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
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.13.0',
21
+ version: '8.14.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
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 + Python call-graph; deterministic, no LLM.',
157
+ 'JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.',
158
158
  inputSchema: {
159
159
  type: 'object',
160
160
  properties: {