sigmap 8.29.0 → 8.31.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/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.29.0 | Benchmark: sigmap-v8.29-main (2026-09-01)
14
+ # Version: 8.31.0 | Benchmark: sigmap-v8.31-main (2026-09-08)
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.29-main, 2026-09-01)
20
+ ## Core metrics (benchmark: sigmap-v8.31-main, 2026-09-08)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
@@ -132,6 +132,8 @@ sigmap tune Recommend config from repo detection
132
132
  sigmap tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
133
133
  sigmap skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
134
134
  sigmap skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
135
+ sigmap lines <file> <start>-<end> Print an exact line range — CLI twin of get_lines (secrets redacted)
136
+ sigmap lines <file> :<line> --context <n> Window around one signature anchor (default ±10)
135
137
  sigmap note "<text>" Append a note to the cross-session decision log
136
138
  sigmap note List recent notes (also: note --list <N>)
137
139
  sigmap status Show repo state — branch, dirty files, index freshness, notes
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.29.0 | Benchmark: sigmap-v8.29-main (2026-09-01)
14
+ # Version: 8.31.0 | Benchmark: sigmap-v8.31-main (2026-09-08)
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.29-main, 2026-09-01)
26
+ ## Core metrics (benchmark: sigmap-v8.31-main, 2026-09-08)
27
27
 
28
28
  - hit@5 retrieval: 81.1% vs 44.0% single-shot grep baseline (1.73× lift)
29
29
  - Token reduction: 96.8% average across benchmark repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.29.0",
3
+ "version": "8.31.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": {
@@ -31,6 +31,8 @@
31
31
  "benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
32
32
  "benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
33
33
  "benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
34
+ "benchmark:callgraph-jvm": "node scripts/run-callgraph-jvm-benchmark.mjs",
35
+ "validate:callgraph-jvm": "node scripts/run-callgraph-jvm-benchmark.mjs --gate",
34
36
  "benchmark:centrality-blend": "node scripts/run-centrality-blend-benchmark.mjs --save",
35
37
  "benchmark:surface-enrichment": "node scripts/run-surface-enrichment-benchmark.mjs --save",
36
38
  "validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.29.0",
3
+ "version": "8.31.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.29.0",
3
+ "version": "8.31.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,6 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
+ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+
6
+ // Class bodies are scanned to this many characters. Generated JVM sources
7
+ // (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
8
+ // against pathological input rather than trimming ordinary classes.
9
+ const MAX_CLASS_BODY_CHARS = 200000;
10
+
11
+ // Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
12
+ // caller's configured budget governs the output rather than this file.
13
+ const MAX_MEMBERS_PER_CLASS = 120;
14
+
15
+ // Per-file signature ceiling, likewise above the configured default.
16
+ const MAX_SIGS_PER_FILE = 200;
4
17
 
5
18
  /**
6
19
  * Extract signatures from Java source code.
@@ -28,17 +41,20 @@ function extract(src) {
28
41
  const block = extractBlock(stripped, bodyStart);
29
42
  sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
30
43
  for (const meth of extractMembers(block)) {
31
- sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
44
+ // The disclosure marker carries no offsets; anchor it at the class body.
45
+ const declIdx = meth.declIdx || 0;
46
+ const endIdx = meth.endIdx || 0;
47
+ sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
32
48
  }
33
49
  }
34
50
 
35
- return sigs.slice(0, 25);
51
+ return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
36
52
  }
37
53
 
38
54
  function extractBlock(src, startIndex) {
39
55
  let depth = 1;
40
56
  let i = startIndex;
41
- const end = Math.min(src.length, startIndex + 5000);
57
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
42
58
  while (i < end && depth > 0) {
43
59
  if (src[i] === '{') depth++;
44
60
  else if (src[i] === '}') depth--;
@@ -60,7 +76,7 @@ function extractMembers(block) {
60
76
  endIdx: m.index + m[0].length,
61
77
  });
62
78
  }
63
- return members.slice(0, 8);
79
+ return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
64
80
  }
65
81
 
66
82
  function normalizeParams(params) {
@@ -434,24 +434,60 @@ function build(files, cwd, ctx) {
434
434
  return { forward, reverse };
435
435
  }
436
436
 
437
+ // Directory names assumed when neither the caller nor the project config says
438
+ // otherwise. A Maven/Gradle module (`mall-portal/`, `service-api/`) matches none
439
+ // of them, which is why the config is consulted first.
440
+ const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'R', 'inst'];
441
+
442
+ // Walk depth measured from EACH srcDir root, not from cwd — so this is not the
443
+ // same quantity as the extractor's cwd-relative `maxDepth` and must not be read
444
+ // from it. A standard Maven tree reaches `src/main/java/<group>/<artifact>/…`
445
+ // nine directories below its module root, so the previous ceiling of 8 silently
446
+ // dropped the deepest packages (on macrozheng/mall: every `service/impl/` class).
447
+ const DEFAULT_WALK_DEPTH = 12;
448
+
449
+ /**
450
+ * Source directories declared in the project's own config, or null when there
451
+ * is no readable config. Read directly rather than through `loadConfig`, which
452
+ * can fetch `extends` over the network and spawn a child process — neither is
453
+ * acceptable inside a graph build.
454
+ */
455
+ function _configuredSrcDirs(cwd) {
456
+ try {
457
+ const raw = fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8');
458
+ const cfg = JSON.parse(raw);
459
+ if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
460
+ } catch (_) { /* absent or unparsable — fall back to the defaults */ }
461
+ return null;
462
+ }
463
+
437
464
  /**
438
465
  * Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
439
466
  * files under srcDirs. Useful for the MCP tool handler.
440
467
  *
468
+ * srcDirs resolution order: explicit `opts.srcDirs` → `gen-context.config.json`
469
+ * → DEFAULT_SRC_DIRS. Without the config step the graph is empty on any repo
470
+ * whose sources do not sit under a conventionally-named directory.
471
+ *
441
472
  * @param {string} cwd
442
473
  * @param {object} [opts]
443
474
  * @param {string[]} [opts.srcDirs]
444
475
  * @param {string[]} [opts.exclude]
476
+ * @param {number} [opts.maxDepth] - walk depth from each srcDir root
445
477
  * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
446
478
  */
447
479
  function buildFromCwd(cwd, opts) {
448
480
  // R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
449
481
  // The existence check below makes these no-ops in non-R projects.
450
- const { srcDirs = ['src', 'app', 'lib', 'R', 'inst'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
482
+ const {
483
+ srcDirs = _configuredSrcDirs(cwd) || DEFAULT_SRC_DIRS,
484
+ exclude = ['node_modules', '.git', 'dist', 'build'],
485
+ maxDepth = DEFAULT_WALK_DEPTH,
486
+ } = opts || {};
451
487
  const excludeSet = new Set(exclude);
452
488
 
453
489
  function walkDir(dir, depth) {
454
- if (depth > 8) return [];
490
+ if (depth > maxDepth) return [];
455
491
  let entries;
456
492
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
457
493
  const out = [];
@@ -500,4 +536,4 @@ function buildFromCwd(cwd, opts) {
500
536
  return build(files, cwd, ctx);
501
537
  }
502
538
 
503
- module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
539
+ module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias, _configuredSrcDirs, DEFAULT_SRC_DIRS, DEFAULT_WALK_DEPTH };
@@ -248,6 +248,44 @@ function goDefs(masked) {
248
248
  // Java: methods + constructors with braced bodies. Statement-shaped matches
249
249
  // (calls, control flow) are rejected because their `)` is followed by `;`,
250
250
  // and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
251
+ // Words that can precede `name(` in a STATEMENT, so their presence means the
252
+ // line is not a method declaration.
253
+ const STMT_KEYWORDS = new Set([
254
+ 'return', 'throw', 'else', 'do', 'try', 'case', 'yield', 'assert', 'new',
255
+ 'if', 'while', 'for', 'switch', 'catch', 'synchronized', 'instanceof', 'await',
256
+ ]);
257
+
258
+ // Types a Java class declares it implements or extends, with generic arguments
259
+ // stripped and any package qualifier dropped: `implements Foo<Bar, Baz>` yields
260
+ // ['Foo'], never 'Baz>'. Also records whether the class is a Spring bean and
261
+ // whether it is @Primary, which is what disambiguates several implementations.
262
+ function javaTypeDecl(masked) {
263
+ const m = /(?:^|\n)[^\n]*?\bclass\s+([A-Za-z_$][\w$]*)([^{]*)\{/.exec(masked);
264
+ if (!m) return null;
265
+ const [, className, tail] = m;
266
+ const supers = [];
267
+ for (const kw of ['implements', 'extends']) {
268
+ const k = new RegExp('\\b' + kw + '\\s+([^{]*?)(?=\\b(?:implements|extends)\\b|$)').exec(tail);
269
+ if (!k) continue;
270
+ let depth = 0;
271
+ let cur = '';
272
+ for (const ch of k[1]) {
273
+ if (ch === '<') { depth++; continue; }
274
+ if (ch === '>') { depth--; continue; }
275
+ if (ch === ',' && depth === 0) { if (cur.trim()) supers.push(cur.trim()); cur = ''; continue; }
276
+ if (depth === 0) cur += ch;
277
+ }
278
+ if (cur.trim()) supers.push(cur.trim());
279
+ }
280
+ const head = masked.slice(0, m.index + m[0].length);
281
+ return {
282
+ className,
283
+ supers: supers.map((t) => t.split('.').pop().trim()).filter(Boolean),
284
+ isBean: /@(Service|Component|Repository|Controller|RestController)\b/.test(head),
285
+ isPrimary: /@Primary\b/.test(head),
286
+ };
287
+ }
288
+
251
289
  function javaDefs(masked) {
252
290
  const defs = [];
253
291
  const seen = new Set();
@@ -264,11 +302,24 @@ function javaDefs(masked) {
264
302
  // skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
265
303
  let k = close + 1;
266
304
  while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
267
- if (masked[k] !== '{') continue;
305
+ // A `;` here is an interface or abstract method DECLARATION. It owns no body,
306
+ // so it emits no calls — but in Spring the declared interface is what callers
307
+ // name, so without it as a node every controller→service edge has no target.
308
+ // Recorded with an empty body range: can receive edges, never produces them.
309
+ // `before` is everything between line start and the method name. A real
310
+ // declaration has modifiers or a return type there (`void chargeCard(`);
311
+ // a call statement has only whitespace (`identity(1);`) or a statement
312
+ // keyword (`return helper(a);`) — neither may be read as a declaration,
313
+ // or the call resolves to a phantom local def instead of the real target.
314
+ const headWord = (before.match(/([A-Za-z_$][\w$]*)\s*$/) || [])[1];
315
+ const isDecl = masked[k] === ';' && /\S/.test(before) && !STMT_KEYWORDS.has(headWord);
316
+ if (masked[k] !== '{' && !isDecl) continue;
268
317
  const key = name + ':' + k;
269
318
  if (seen.has(key)) continue;
270
319
  seen.add(key);
271
- defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
320
+ defs.push(isDecl
321
+ ? { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: k }
322
+ : { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
272
323
  }
273
324
  return defs;
274
325
  }
@@ -322,7 +373,7 @@ function callsInRange(masked, start, end) {
322
373
  const re = /([A-Za-z_$][\w$]*)\s*\(/g;
323
374
  let m;
324
375
  while ((m = re.exec(slice)) !== null) {
325
- // skip a `.name(` method access (can't resolve the receiver deterministically)
376
+ // skip a `.name(` method access resolved separately via receiverCallsInRange
326
377
  const before = slice[m.index - 1];
327
378
  if (before === '.') continue;
328
379
  if (!NON_CALL.has(m[1])) names.add(m[1]);
@@ -330,16 +381,75 @@ function callsInRange(masked, start, end) {
330
381
  return names;
331
382
  }
332
383
 
384
+ // Collect `receiver.method(` pairs within [start,end). A chained or computed
385
+ // receiver (`a.b().c(`, `arr[0].c(`) is skipped: only a plain identifier can be
386
+ // looked up in the declaration map, and guessing is worse than no edge.
387
+ function receiverCallsInRange(masked, start, end) {
388
+ const slice = masked.slice(start, end);
389
+ const out = [];
390
+ const re = /([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g;
391
+ let m;
392
+ while ((m = re.exec(slice)) !== null) {
393
+ const before = slice[m.index - 1];
394
+ if (before === '.' || before === ')' || before === ']') continue;
395
+ if (NON_CALL.has(m[2])) continue;
396
+ out.push({ receiver: m[1], method: m[2] });
397
+ }
398
+ return out;
399
+ }
400
+
401
+ // `private UserService userService;` / `UserService svc = new UserService();`
402
+ // / `for (OmsOrderItem item : list)` → { userService: 'UserService', … }.
403
+ // Declarations only: a bare assignment carries no type and is not inferred.
404
+ const DECL_RE = /(?:^|[;{}(,\n])\s*(?:(?:public|private|protected|static|final|volatile|transient)\s+)*([A-Z][\w$]*)(?:\s*<[^>;=(){}]*>)?(?:\s*\[\s*\])?\s+([a-z_$][\w$]*)\s*(?=[;=:)])/g;
405
+
406
+ function buildTypeMap(masked) {
407
+ const map = new Map();
408
+ let m;
409
+ DECL_RE.lastIndex = 0;
410
+ while ((m = DECL_RE.exec(masked)) !== null) {
411
+ const [, type, name] = m;
412
+ if (JVM_KEYWORDS.has(type) || JVM_KEYWORDS.has(name)) continue;
413
+ if (!map.has(name)) map.set(name, type); // first declaration wins — deterministic
414
+ }
415
+ return map;
416
+ }
417
+
418
+ // Type names that are never a user class, so never a resolvable receiver type.
419
+ const JVM_KEYWORDS = new Set([
420
+ 'return', 'new', 'if', 'else', 'for', 'while', 'switch', 'case', 'throw', 'catch',
421
+ 'String', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Object', 'List', 'Map',
422
+ 'Set', 'Collection', 'Optional', 'Override', 'Autowired', 'Resource', 'Deprecated',
423
+ ]);
424
+
333
425
  // ── Public API ───────────────────────────────────────────────────────────────
334
426
 
335
- function _walk(dir, excludeSet, out, depth) {
336
- if (depth > 8) return;
427
+ // Walk depth from each srcDir root (not from cwd). A Maven module reaches
428
+ // `src/main/java/<group>/<artifact>/service/impl` nine directories down, so the
429
+ // previous ceiling of 8 never saw the classes that own the method bodies.
430
+ const DEFAULT_WALK_DEPTH = 12;
431
+
432
+ /**
433
+ * Source directories declared in the project's own config, or null. Read
434
+ * directly rather than through `loadConfig`, which can fetch `extends` over the
435
+ * network and spawn a child process — neither belongs inside a graph build.
436
+ */
437
+ function _configuredSrcDirs(cwd) {
438
+ try {
439
+ const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
440
+ if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
441
+ } catch (_) { /* absent or unparsable — fall back to the defaults */ }
442
+ return null;
443
+ }
444
+
445
+ function _walk(dir, excludeSet, out, depth, maxDepth) {
446
+ if (depth > (maxDepth === undefined ? DEFAULT_WALK_DEPTH : maxDepth)) return;
337
447
  let entries;
338
448
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
339
449
  for (const e of entries) {
340
450
  if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
341
451
  const full = path.join(dir, e.name);
342
- if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
452
+ if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
343
453
  else if (e.isFile()) {
344
454
  const ext = path.extname(e.name).toLowerCase();
345
455
  if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
@@ -365,9 +475,13 @@ function buildCallGraph(cwd, opts = {}) {
365
475
  const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
366
476
  let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
367
477
  if (!opts.files) {
368
- for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
478
+ // Same resolution order as the dependency graph (#560): explicit opts →
479
+ // the project's own config → the historical defaults. Without the config
480
+ // step this is empty on any repo whose sources are not under src/app/lib.
481
+ const srcDirs = opts.srcDirs || _configuredSrcDirs(cwd) || ['src', 'app', 'lib'];
482
+ for (const sd of srcDirs) {
369
483
  const abs = path.resolve(cwd, sd);
370
- if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
484
+ if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0, opts.maxDepth);
371
485
  }
372
486
  }
373
487
 
@@ -381,6 +495,44 @@ function buildCallGraph(cwd, opts = {}) {
381
495
  const normToAbs = new Map(); // normalized abs → abs
382
496
  const defs = new Map(); // symbolId → {file,name,line}
383
497
 
498
+ // JVM convention: a public type lives in a file of the same name. This is the
499
+ // deterministic type→file mapping receiver resolution needs, with no AST.
500
+ const fileByTypeName = new Map(); // 'UserService' → [absFile]
501
+ for (const f of files) {
502
+ const ext = path.extname(f).toLowerCase();
503
+ if (JAVA_EXTS.has(ext)) {
504
+ const base = path.basename(f, path.extname(f));
505
+ if (!fileByTypeName.has(base)) fileByTypeName.set(base, []);
506
+ fileByTypeName.get(base).push(f);
507
+ }
508
+ }
509
+
510
+ // interface/superclass name → implementing files, for the Spring hop below.
511
+ const implsByType = new Map(); // 'PaymentService' → [{ file, isBean, isPrimary }]
512
+ for (const f of files) {
513
+ if (!JAVA_EXTS.has(path.extname(f).toLowerCase())) continue;
514
+ let decl;
515
+ try { decl = javaTypeDecl(maskJs(fs.readFileSync(f, 'utf8'))); } catch (_) { continue; }
516
+ if (!decl) continue;
517
+ for (const sup of decl.supers) {
518
+ if (!implsByType.has(sup)) implsByType.set(sup, []);
519
+ implsByType.get(sup).push({ file: f, isBean: decl.isBean, isPrimary: decl.isPrimary });
520
+ }
521
+ }
522
+
523
+ /**
524
+ * The single implementing file for a type, or null when it is ambiguous.
525
+ * One implementation resolves outright; several resolve only via @Primary.
526
+ * Anything still ambiguous yields no edge — polymorphism is not guessed.
527
+ */
528
+ const soleImpl = (typeName) => {
529
+ const cands = implsByType.get(typeName) || [];
530
+ if (cands.length === 1) return cands[0].file;
531
+ const primary = cands.filter((c) => c.isPrimary);
532
+ if (primary.length === 1) return primary[0].file;
533
+ return null;
534
+ };
535
+
384
536
  for (const f of files) {
385
537
  normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
386
538
  let src;
@@ -400,12 +552,20 @@ function buildCallGraph(cwd, opts = {}) {
400
552
 
401
553
  const forward = new Map();
402
554
  const reverse = new Map();
403
- const addEdge = (from, to) => {
555
+ // Additive: `forward`/`reverse` keep their existing shape, so every current
556
+ // consumer is unaffected. Confidence is looked up by "from\u0000to".
557
+ const edgeConfidence = new Map();
558
+ const addEdge = (from, to, confidence) => {
404
559
  if (from === to) return;
405
560
  if (!forward.has(from)) forward.set(from, new Set());
406
561
  forward.get(from).add(to);
407
562
  if (!reverse.has(to)) reverse.set(to, new Set());
408
563
  reverse.get(to).add(from);
564
+ if (confidence) {
565
+ const k = from + '\u0000' + to;
566
+ // A 'high' resolution never loses to a later 'medium' one.
567
+ if (edgeConfidence.get(k) !== 'high') edgeConfidence.set(k, confidence);
568
+ }
409
569
  };
410
570
 
411
571
  for (const [f, fileDefs] of perFileDefs.entries()) {
@@ -423,16 +583,59 @@ function buildCallGraph(cwd, opts = {}) {
423
583
  .sort();
424
584
  importedAbs.push(...siblings);
425
585
  }
586
+ // Receiver types come from declarations anywhere in the file: fields are
587
+ // declared outside any method body, locals inside one.
588
+ const typeMap = JAVA_EXTS.has(ext) ? buildTypeMap(masked) : null;
589
+ // Types reachable from this file, by name — imports first, then same-package
590
+ // siblings, so an import always wins over a coincidental sibling name.
591
+ const scopeByTypeName = new Map();
592
+ if (typeMap) {
593
+ for (const imp of importedAbs) {
594
+ const base = path.basename(imp, path.extname(imp));
595
+ if (!scopeByTypeName.has(base)) scopeByTypeName.set(base, imp);
596
+ }
597
+ }
598
+
426
599
  for (const d of fileDefs) {
427
600
  const callerId = symId(cwd, f, d.name);
428
601
  if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
429
602
  const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
430
603
  for (const nm of callees) {
431
604
  const local = (defsByName.get(f) || new Map()).get(nm);
432
- if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
605
+ if (local && local.length) { for (const id of local) addEdge(callerId, id, 'high'); continue; }
433
606
  for (const imp of importedAbs) {
434
607
  const ids = (defsByName.get(imp) || new Map()).get(nm);
435
- if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
608
+ if (ids && ids.length) { for (const id of ids) addEdge(callerId, id, 'high'); break; }
609
+ }
610
+ }
611
+
612
+ // `receiver.method(` — resolve the receiver's declared type to a file.
613
+ if (!typeMap) continue;
614
+ for (const { receiver, method } of receiverCallsInRange(masked, d.bodyStart, d.bodyEnd)) {
615
+ // A receiver that is itself a type name is a static call: `Foo.bar()`.
616
+ const typeName = typeMap.get(receiver)
617
+ || (fileByTypeName.has(receiver) ? receiver : null);
618
+ if (!typeName) continue; // unknown receiver → no edge, never a guess
619
+
620
+ let target = scopeByTypeName.get(typeName);
621
+ let confidence = 'high'; // typed receiver, resolved in scope
622
+ if (!target) {
623
+ const candidates = fileByTypeName.get(typeName) || [];
624
+ if (candidates.length !== 1) continue; // ambiguous or absent → no edge
625
+ target = candidates[0];
626
+ confidence = 'medium'; // type known, but not in this file's scope
627
+ }
628
+ const ids = (defsByName.get(target) || new Map()).get(method);
629
+ if (ids && ids.length) for (const id of ids) addEdge(callerId, id, confidence);
630
+
631
+ // Spring: the call names the interface, but the code that runs — and
632
+ // that a reviewer changes — lives in the implementation. Both edges are
633
+ // true, so both are recorded; without the second, blast radius on an
634
+ // implementation is empty.
635
+ const implFile = soleImpl(typeName);
636
+ if (implFile && implFile !== target) {
637
+ const implIds = (defsByName.get(implFile) || new Map()).get(method);
638
+ if (implIds && implIds.length) for (const id of implIds) addEdge(callerId, id, confidence);
436
639
  }
437
640
  }
438
641
  }
@@ -443,7 +646,7 @@ function buildCallGraph(cwd, opts = {}) {
443
646
  for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
444
647
  return out;
445
648
  };
446
- return { forward: toArr(forward), reverse: toArr(reverse), defs };
649
+ return { forward: toArr(forward), reverse: toArr(reverse), defs, edgeConfidence };
447
650
  }
448
651
 
449
652
  /**
@@ -565,7 +768,7 @@ function formatCallGraphJSON(result, kind) {
565
768
  }
566
769
 
567
770
  module.exports = {
568
- buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
771
+ buildCallGraph, buildTypeMap, receiverCallsInRange, javaTypeDecl, DEFAULT_WALK_DEPTH, buildCallFileGraph, methodImpact, methodCallees,
569
772
  formatCallGraph, formatCallGraphJSON,
570
773
  extractDefs, maskJs, maskPy, maskRust,
571
774
  };
@@ -17,6 +17,7 @@ const os = require('os');
17
17
 
18
18
  // Config shapes the supported clients use.
19
19
  // - 'json' → { mcpServers: { sigmap: { command, args } } }
20
+ // - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
20
21
  // - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
21
22
  // - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
22
23
  const CLIENTS = {
@@ -25,7 +26,7 @@ const CLIENTS = {
25
26
  windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
26
27
  project: ['.windsurf', 'mcp.json'],
27
28
  global: ['.codeium', 'windsurf', 'mcp_config.json'] },
28
- vscode: { label: 'VS Code', format: 'json', scope: 'project', project: ['.vscode', 'mcp.json'] },
29
+ vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
29
30
  opencode: { label: 'OpenCode', format: 'json', scope: 'both',
30
31
  project: ['opencode.json'],
31
32
  global: ['.config', 'opencode', 'config.json'] },
@@ -80,6 +81,31 @@ function _installJson(filePath, scriptPath) {
80
81
  return 'installed';
81
82
  }
82
83
 
84
+ /**
85
+ * Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
86
+ * (not `mcpServers`) and expects an explicit transport `type`. A config written
87
+ * by an older SigMap under `mcpServers` is migrated rather than left in place,
88
+ * so re-running repairs it instead of leaving two entries VS Code cannot read.
89
+ */
90
+ function _installVscode(filePath, scriptPath) {
91
+ let settings = {};
92
+ if (fs.existsSync(filePath)) {
93
+ try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
94
+ catch (_) { settings = {}; }
95
+ }
96
+ const stale = settings.mcpServers && settings.mcpServers.sigmap;
97
+ if (stale) {
98
+ delete settings.mcpServers.sigmap;
99
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
100
+ }
101
+ if (!settings.servers) settings.servers = {};
102
+ if (settings.servers.sigmap && !stale) return 'already';
103
+ settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
104
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
105
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
106
+ return stale ? 'updated' : 'installed';
107
+ }
108
+
83
109
  /** Install into Zed's `context_servers` config (create file/dir if absent). */
84
110
  function _installZed(filePath, scriptPath) {
85
111
  let settings = {};
@@ -132,7 +158,8 @@ function installClient(client, opts = {}) {
132
158
  const filePath = resolveTarget(spec, cwd, home, opts.global);
133
159
 
134
160
  let status;
135
- if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
161
+ if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
162
+ else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
136
163
  else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
137
164
  else status = _installJson(filePath, scriptPath);
138
165
 
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.29.0',
21
+ version: '8.31.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24