sigmap 8.13.0 → 8.16.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/gen-context.js CHANGED
@@ -1531,6 +1531,8 @@ __factories["./src/config/defaults"] = function(module, exports) {
1531
1531
  topK: 10,
1532
1532
  // Multiplier applied to recently-changed files (>1 boosts them up)
1533
1533
  recencyBoost: 1.5,
1534
+ // Boost files call-graph-connected to query matches (opt-in, measure-gated)
1535
+ callGraphBoost: false,
1534
1536
  },
1535
1537
 
1536
1538
  // Impact layer settings (v2.5)
@@ -4731,12 +4733,15 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
4731
4733
  __factories["./src/evidence/pack"] = function(module, exports) {
4732
4734
 
4733
4735
  /**
4734
- * Evidence Pack v1 (v8.0 E1).
4736
+ * Evidence Pack v2 (v8.0 E1; schema v2 adds a published schema URL, per-file
4737
+ * multi-factor risk labels, measured test-discovery provenance, and generator
4738
+ * identity — all additive over v1).
4735
4739
  *
4736
4740
  * A deterministic, machine-consumable signature-and-evidence map. Replaces the
4737
4741
  * "paste this into your prompt" workflow with a byte-stable JSON artifact that
4738
4742
  * an agent or CI can ingest directly — every entry anchored to a real file,
4739
- * symbol, and line range.
4743
+ * symbol, and line range. The published schema lives at
4744
+ * docs-vp/public/schemas/evidence-pack-2.json (sigmap.io/schemas/).
4740
4745
  *
4741
4746
  * Composed entirely from shipped zero-dep modules:
4742
4747
  * - retrieval/ranker → ranked files, scores, signals
@@ -4756,10 +4761,21 @@ __factories["./src/evidence/pack"] = function(module, exports) {
4756
4761
  const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
4757
4762
  const { scan } = __require('./src/security/scanner');
4758
4763
 
4759
- const SCHEMA_VERSION = '1.0';
4764
+ const SCHEMA_VERSION = '2.0';
4765
+ const SCHEMA_URL = 'https://sigmap.io/schemas/evidence-pack-2.json';
4760
4766
  const DEFAULT_BUDGET = 6000;
4761
4767
  const DEFAULT_TOP = 12;
4762
4768
 
4769
+ // Measured accuracy of the stem-affix test-discovery method (C2). Constants
4770
+ // are sourced from the committed benchmarks/reports/test-discovery.json and
4771
+ // guarded by a test that fails on drift — re-run `npm run
4772
+ // benchmark:test-discovery` and update together, never hand-invent.
4773
+ const TEST_DISCOVERY = {
4774
+ method: 'stem-affix-match',
4775
+ measured: { f1: 0.98, precision: 0.971, recall: 0.988, pairs: 3701, repos: 28 },
4776
+ benchmark: 'npm run benchmark:test-discovery',
4777
+ };
4778
+
4763
4779
  const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
4764
4780
  const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
4765
4781
  const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
@@ -4798,16 +4814,29 @@ __factories["./src/evidence/pack"] = function(module, exports) {
4798
4814
  * @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
4799
4815
  */
4800
4816
  function riskLabelFor(relPath) {
4817
+ return riskFactorsFor(relPath)[0];
4818
+ }
4819
+
4820
+ /**
4821
+ * Every risk category a file matches, in the same strict precedence order
4822
+ * riskLabelFor uses (schema v2). Where v1 collapsed a migration touching
4823
+ * payments to `migration`, the factors list carries both. Always non-empty:
4824
+ * a file matching nothing is `['source']`.
4825
+ * @param {string} relPath
4826
+ * @returns {string[]}
4827
+ */
4828
+ function riskFactorsFor(relPath) {
4801
4829
  const p = relPath.replace(/\\/g, '/');
4802
- if (GENERATED_RE.test(p)) return 'generated';
4803
- if (TEST_RE.test(p)) return 'test';
4804
- if (MIGRATION_RE.test(p)) return 'migration';
4805
- if (PAYMENT_RE.test(p)) return 'payment';
4806
- if (AUTH_RE.test(p)) return 'auth';
4807
- if (SECURITY_RE.test(p)) return 'security';
4808
- if (CONFIG_RE.test(p)) return 'config';
4809
- if (PUBLIC_API_RE.test(p)) return 'public-api';
4810
- return 'source';
4830
+ const factors = [];
4831
+ if (GENERATED_RE.test(p)) factors.push('generated');
4832
+ if (TEST_RE.test(p)) factors.push('test');
4833
+ if (MIGRATION_RE.test(p)) factors.push('migration');
4834
+ if (PAYMENT_RE.test(p)) factors.push('payment');
4835
+ if (AUTH_RE.test(p)) factors.push('auth');
4836
+ if (SECURITY_RE.test(p)) factors.push('security');
4837
+ if (CONFIG_RE.test(p)) factors.push('config');
4838
+ if (PUBLIC_API_RE.test(p)) factors.push('public-api');
4839
+ return factors.length ? factors : ['source'];
4811
4840
  }
4812
4841
 
4813
4842
  /** Filename stem (basename minus the first extension chain). */
@@ -4939,6 +4968,7 @@ __factories["./src/evidence/pack"] = function(module, exports) {
4939
4968
  if (start !== null) sourceLines.push({ symbol, start, end });
4940
4969
  }
4941
4970
 
4971
+ const riskFactors = riskFactorsFor(r.file);
4942
4972
  files.push({
4943
4973
  path: r.file,
4944
4974
  symbols,
@@ -4946,7 +4976,8 @@ __factories["./src/evidence/pack"] = function(module, exports) {
4946
4976
  confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
4947
4977
  sourceLines,
4948
4978
  relatedTests: findRelatedTests(r.file, allFiles),
4949
- riskLabel: riskLabelFor(r.file),
4979
+ riskLabel: riskFactors[0],
4980
+ riskFactors,
4950
4981
  });
4951
4982
  }
4952
4983
 
@@ -4955,11 +4986,14 @@ __factories["./src/evidence/pack"] = function(module, exports) {
4955
4986
 
4956
4987
  const pack = {
4957
4988
  schemaVersion: SCHEMA_VERSION,
4989
+ schemaUrl: SCHEMA_URL,
4990
+ generator: { name: 'sigmap', version: typeof opts.version === 'string' ? opts.version : null },
4958
4991
  query,
4959
4992
  intent,
4960
4993
  files,
4961
4994
  tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
4962
4995
  droppedFiles,
4996
+ testDiscovery: TEST_DISCOVERY,
4963
4997
  grounding: {
4964
4998
  symbolCount,
4965
4999
  anchoredSymbols,
@@ -5001,7 +5035,8 @@ __factories["./src/evidence/pack"] = function(module, exports) {
5001
5035
  L.push('');
5002
5036
 
5003
5037
  for (const f of pack.files) {
5004
- L.push(`## \`${f.path}\` _(${f.riskLabel}, confidence ${f.confidence})_`);
5038
+ const risk = (f.riskFactors && f.riskFactors.length > 1) ? f.riskFactors.join(' + ') : f.riskLabel;
5039
+ L.push(`## \`${f.path}\` _(${risk}, confidence ${f.confidence})_`);
5005
5040
  L.push(`_${f.reason}_`);
5006
5041
  if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
5007
5042
  L.push('');
@@ -5026,8 +5061,11 @@ __factories["./src/evidence/pack"] = function(module, exports) {
5026
5061
  formatMarkdown,
5027
5062
  parseAnchor,
5028
5063
  riskLabelFor,
5064
+ riskFactorsFor,
5029
5065
  findRelatedTests,
5030
5066
  SCHEMA_VERSION,
5067
+ SCHEMA_URL,
5068
+ TEST_DISCOVERY,
5031
5069
  };
5032
5070
 
5033
5071
  };
@@ -10942,14 +10980,16 @@ __factories["./src/graph/builder"] = function(module, exports) {
10942
10980
  __factories["./src/graph/call-graph"] = function(module, exports) {
10943
10981
 
10944
10982
  /**
10945
- * Method/caller-level call-graph (D4 v1).
10983
+ * Method/caller-level call-graph (D4 v1, languages expanded in GR1).
10946
10984
  *
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.
10985
+ * Builds symbol-level edges — which function calls which function — for JS/TS,
10986
+ * Python, Java, Go, and Rust. Deterministic, zero-dependency, regex +
10987
+ * brace/indent matching. Call sites are resolved with high precision: a call
10988
+ * resolves to a definition of that name in the *same file* first, then in a
10989
+ * *directly-imported* file (via the existing file-level import graph). Names
10990
+ * that resolve to no repo definition produce no edge — over-approximation
10991
+ * noise is avoided. Constructs that can't be parsed dependency-free are
10992
+ * skipped (less fidelity, never a parser dep).
10953
10993
  *
10954
10994
  * Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
10955
10995
  *
@@ -10962,6 +11002,9 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
10962
11002
 
10963
11003
  const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
10964
11004
  const PY_EXTS = new Set(['.py', '.pyw']);
11005
+ const JAVA_EXTS = new Set(['.java']);
11006
+ const GO_EXTS = new Set(['.go']);
11007
+ const RS_EXTS = new Set(['.rs']);
10965
11008
 
10966
11009
  // Tokens that look like `name(` calls or definition headers but are language
10967
11010
  // keywords, not user symbols — never treated as a call or a definition.
@@ -10971,6 +11014,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
10971
11014
  'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
10972
11015
  'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
10973
11016
  'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
11017
+ 'synchronized',
10974
11018
  ]);
10975
11019
 
10976
11020
  function normalizePath(p) { return path.normalize(p).toLowerCase(); }
@@ -10999,6 +11043,32 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
10999
11043
  return out.join('');
11000
11044
  }
11001
11045
 
11046
+ // Rust: `//`, `/* */`, and `"..."` mask like JS, but a bare `'` is usually a
11047
+ // lifetime (`'a`), not a string — masking to the "closing" quote would corrupt
11048
+ // offsets. Only char literals (`'x'`, `'\n'`) are masked; lifetimes pass through.
11049
+ function maskRust(src) {
11050
+ const out = src.split('');
11051
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
11052
+ let i = 0; const n = src.length;
11053
+ while (i < n) {
11054
+ const c = src[i], d = src[i + 1];
11055
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
11056
+ 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; }
11057
+ if (c === '"') {
11058
+ let j = i + 1;
11059
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === '"') break; j++; }
11060
+ j = Math.min(n, j + 1); blank(i, j); i = j; continue;
11061
+ }
11062
+ if (c === "'") {
11063
+ if (d === '\\' && src[i + 3] === "'") { blank(i, i + 4); i += 4; continue; } // '\n'
11064
+ if (d !== undefined && src[i + 2] === "'") { blank(i, i + 3); i += 3; continue; } // 'x'
11065
+ i++; continue; // lifetime `'a` — leave untouched
11066
+ }
11067
+ i++;
11068
+ }
11069
+ return out.join('');
11070
+ }
11071
+
11002
11072
  function maskPy(src) {
11003
11073
  const out = src.split('');
11004
11074
  const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
@@ -11128,10 +11198,98 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11128
11198
  return defs;
11129
11199
  }
11130
11200
 
11201
+ // Go: func name(...) { } | func (r Recv) name(...) (T, error) { }
11202
+ // The return list may itself be parenthesized, so scan past it to the body `{`.
11203
+ function goDefs(masked) {
11204
+ const defs = [];
11205
+ const re = /(?:^|\n)func\s+(?:\([^)\n]*\)\s*)?([A-Za-z_]\w*)\s*\(/g;
11206
+ let m;
11207
+ while ((m = re.exec(masked)) !== null) {
11208
+ const paren = masked.indexOf('(', m.index + m[0].length - 1);
11209
+ const close = matchDelim(masked, paren, '(', ')');
11210
+ let k = close + 1;
11211
+ let depth = 0;
11212
+ while (k < masked.length) {
11213
+ const ch = masked[k];
11214
+ if (ch === '(') depth++;
11215
+ else if (ch === ')') depth--;
11216
+ else if (ch === '{' && depth === 0) break;
11217
+ else if (ch === '\n' && depth === 0) { k = -1; break; } // no body on this header
11218
+ k++;
11219
+ }
11220
+ if (k === -1 || k >= masked.length) continue;
11221
+ defs.push({ name: m[1], line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
11222
+ }
11223
+ return defs;
11224
+ }
11225
+
11226
+ // Java: methods + constructors with braced bodies. Statement-shaped matches
11227
+ // (calls, control flow) are rejected because their `)` is followed by `;`,
11228
+ // and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
11229
+ function javaDefs(masked) {
11230
+ const defs = [];
11231
+ const seen = new Set();
11232
+ 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;
11233
+ let m;
11234
+ while ((m = re.exec(masked)) !== null) {
11235
+ const name = m[2];
11236
+ if (NON_CALL.has(name)) continue;
11237
+ // `new Foo() { … }` anonymous classes are uses, not definitions.
11238
+ const before = masked.slice(Math.max(0, m.index), m.index + m[0].length - name.length - 1);
11239
+ if (/\bnew\s*$/.test(before)) continue;
11240
+ const paren = masked.indexOf('(', m.index + m[0].length - 1);
11241
+ const close = matchDelim(masked, paren, '(', ')');
11242
+ // skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
11243
+ let k = close + 1;
11244
+ while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
11245
+ if (masked[k] !== '{') continue;
11246
+ const key = name + ':' + k;
11247
+ if (seen.has(key)) continue;
11248
+ seen.add(key);
11249
+ defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
11250
+ }
11251
+ return defs;
11252
+ }
11253
+
11254
+ // Rust: fn name(...) { } | fn name<T>(...) -> T where … { } — inside or
11255
+ // outside impl/trait blocks. A `;` before the body brace (trait declaration)
11256
+ // means no body: skipped.
11257
+ function rustDefs(masked) {
11258
+ const defs = [];
11259
+ const re = /\bfn\s+([A-Za-z_]\w*)/g;
11260
+ let m;
11261
+ while ((m = re.exec(masked)) !== null) {
11262
+ let k = m.index + m[0].length;
11263
+ while (k < masked.length && /\s/.test(masked[k])) k++;
11264
+ if (masked[k] === '<') k = matchDelim(masked, k, '<', '>') + 1;
11265
+ while (k < masked.length && /\s/.test(masked[k])) k++;
11266
+ if (masked[k] !== '(') continue;
11267
+ const close = matchDelim(masked, k, '(', ')');
11268
+ // return type / where clause may span lines; stop at body `{` or decl `;`
11269
+ let b = close + 1;
11270
+ while (b < masked.length && masked[b] !== '{' && masked[b] !== ';') b++;
11271
+ if (masked[b] !== '{') continue;
11272
+ defs.push({ name: m[1], line: lineAt(masked, m.index), bodyStart: b, bodyEnd: matchDelim(masked, b, '{', '}') });
11273
+ }
11274
+ return defs;
11275
+ }
11276
+
11277
+ // Pick the masker whose comment/string syntax matches the language.
11278
+ // Java and Go share JS syntax (Go raw strings mask like template literals).
11279
+ function maskFor(filePath, src) {
11280
+ const ext = path.extname(filePath).toLowerCase();
11281
+ if (PY_EXTS.has(ext)) return maskPy(src);
11282
+ if (RS_EXTS.has(ext)) return maskRust(src);
11283
+ return maskJs(src);
11284
+ }
11285
+
11131
11286
  function extractDefs(filePath, src) {
11132
11287
  const ext = path.extname(filePath).toLowerCase();
11133
11288
  if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
11134
11289
  if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
11290
+ if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
11291
+ if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
11292
+ if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
11135
11293
  return null; // unsupported language
11136
11294
  }
11137
11295
 
@@ -11162,7 +11320,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11162
11320
  if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
11163
11321
  else if (e.isFile()) {
11164
11322
  const ext = path.extname(e.name).toLowerCase();
11165
- if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
11323
+ 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
11324
  }
11167
11325
  }
11168
11326
  }
@@ -11229,10 +11387,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11229
11387
  };
11230
11388
 
11231
11389
  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'));
11390
+ const masked = maskFor(f, fs.readFileSync(f, 'utf8'));
11233
11391
  // resolution scope: this file's defs, then directly-imported files' defs
11234
11392
  const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
11235
11393
  .map((nf) => normToAbs.get(nf)).filter(Boolean);
11394
+ // Go/Java: same-package symbols are visible with no import statement, and
11395
+ // a package is (in practice) a directory — extend the scope to same-dir
11396
+ // same-language siblings. Sorted for deterministic resolution order.
11397
+ const ext = path.extname(f).toLowerCase();
11398
+ if (GO_EXTS.has(ext) || JAVA_EXTS.has(ext)) {
11399
+ const siblings = [...perFileDefs.keys()]
11400
+ .filter((o) => o !== f && path.dirname(o) === path.dirname(f) && path.extname(o).toLowerCase() === ext)
11401
+ .sort();
11402
+ importedAbs.push(...siblings);
11403
+ }
11236
11404
  for (const d of fileDefs) {
11237
11405
  const callerId = symId(cwd, f, d.name);
11238
11406
  if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
@@ -11256,6 +11424,42 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11256
11424
  return { forward: toArr(forward), reverse: toArr(reverse), defs };
11257
11425
  }
11258
11426
 
11427
+ /**
11428
+ * Collapse the symbol-level call-graph to FILE-level bidirectional edges for
11429
+ * the ranker's neighbor boost (opt-in via `retrieval.callGraphBoost`). A file
11430
+ * whose functions call into — or are called by — another file gets an edge in
11431
+ * both directions. Keys are `path.resolve`-form absolute paths (matching the
11432
+ * ranker's lookups); entries and neighbor lists are sorted for determinism.
11433
+ *
11434
+ * @param {string} cwd
11435
+ * @param {object} [opts] { graph } to inject a prebuilt call graph (tests)
11436
+ * @returns {{ forward: Map<string,string[]> }}
11437
+ */
11438
+ function buildCallFileGraph(cwd, opts = {}) {
11439
+ const graph = opts.graph || buildCallGraph(cwd, opts);
11440
+ const edges = new Map(); // absFile → Set<absFile>
11441
+ const add = (a, b) => {
11442
+ if (a === b) return;
11443
+ if (!edges.has(a)) edges.set(a, new Set());
11444
+ edges.get(a).add(b);
11445
+ };
11446
+ for (const [callerId, calleeIds] of graph.forward.entries()) {
11447
+ const callerDef = graph.defs.get(callerId);
11448
+ if (!callerDef) continue;
11449
+ for (const calleeId of calleeIds) {
11450
+ const calleeDef = graph.defs.get(calleeId);
11451
+ if (!calleeDef || calleeDef.file === callerDef.file) continue;
11452
+ const a = path.resolve(cwd, callerDef.file);
11453
+ const b = path.resolve(cwd, calleeDef.file);
11454
+ add(a, b);
11455
+ add(b, a);
11456
+ }
11457
+ }
11458
+ const forward = new Map();
11459
+ for (const k of [...edges.keys()].sort()) forward.set(k, [...edges.get(k)].sort());
11460
+ return { forward };
11461
+ }
11462
+
11259
11463
  // Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
11260
11464
  function _resolveSymbol(symbol, defs) {
11261
11465
  if (defs.has(symbol)) return [symbol];
@@ -11336,9 +11540,9 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11336
11540
  }
11337
11541
 
11338
11542
  module.exports = {
11339
- buildCallGraph, methodImpact, methodCallees,
11543
+ buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
11340
11544
  formatCallGraph, formatCallGraphJSON,
11341
- extractDefs, maskJs, maskPy,
11545
+ extractDefs, maskJs, maskPy, maskRust,
11342
11546
  };
11343
11547
 
11344
11548
  };
@@ -13488,7 +13692,16 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
13488
13692
  // Build dependency graph for neighbor boost — non-fatal if it fails
13489
13693
  let graph = null;
13490
13694
  try { graph = buildFromCwd(cwd); } catch (_) {}
13491
- const results = rank(args.query, index, { topK, cwd, graph });
13695
+ // Opt-in call-graph neighbor boost (retrieval.callGraphBoost) non-fatal
13696
+ let callGraph = null;
13697
+ try {
13698
+ const { loadConfig } = __require('./src/config/loader');
13699
+ const retrieval = loadConfig(cwd).retrieval;
13700
+ if (retrieval && retrieval.callGraphBoost) {
13701
+ callGraph = __require('./src/graph/call-graph').buildCallFileGraph(cwd);
13702
+ }
13703
+ } catch (_) {}
13704
+ const results = rank(args.query, index, { topK, cwd, graph, callGraph });
13492
13705
  return formatRankTable(results, args.query);
13493
13706
  } catch (err) {
13494
13707
  return `_query_context failed: ${err.message}_`;
@@ -14201,7 +14414,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
14201
14414
 
14202
14415
  const SERVER_INFO = {
14203
14416
  name: 'sigmap',
14204
- version: '8.13.0',
14417
+ version: '8.16.0',
14205
14418
  description: 'SigMap MCP server — code signatures on demand',
14206
14419
  };
14207
14420
 
@@ -14482,7 +14695,7 @@ __factories["./src/mcp/tools"] = function(module, exports) {
14482
14695
  'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
14483
14696
  'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
14484
14697
  'file-level get_impact: tells an agent which functions break, not just which files. ' +
14485
- 'JS/TS + Python call-graph; deterministic, no LLM.',
14698
+ 'JS/TS, Python, Java, Go, and Rust call-graph; deterministic, no LLM.',
14486
14699
  inputSchema: {
14487
14700
  type: 'object',
14488
14701
  properties: {
@@ -15266,6 +15479,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15266
15479
  const GRAPH_BOOST_AMOUNTS = {
15267
15480
  hop1: 0.40, // direct import neighbor of a file with score > 0
15268
15481
  hop2: 0.15, // 2 hops away (transitive), with decay
15482
+ callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
15269
15483
  };
15270
15484
 
15271
15485
  // Intent-specific weight adjustments
@@ -15398,6 +15612,8 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15398
15612
  * @param {object} [opts.weights] - override scoring weights
15399
15613
  * @param {string} [opts.cwd] - project root for learned ranking weights
15400
15614
  * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
15615
+ * @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
15616
+ * (from buildCallFileGraph) for the opt-in call-neighbor boost
15401
15617
  * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
15402
15618
  */
15403
15619
  function rank(query, sigIndex, opts) {
@@ -15521,6 +15737,31 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15521
15737
  }
15522
15738
  }
15523
15739
 
15740
+ // Call-graph neighbor boost (opt-in via retrieval.callGraphBoost): a file
15741
+ // whose functions call into — or are called by — a positively-scored file is
15742
+ // relevant even when no import edge exists (Go/Java same-package, dynamic
15743
+ // dispatch). Single hop; seeds snapshotted first so boosts never cascade.
15744
+ const callGraph = (opts && opts.callGraph && opts.callGraph.forward instanceof Map) ? opts.callGraph : null;
15745
+ if (callGraph && cwd) {
15746
+ const path = require('path');
15747
+ const relToIdx = new Map();
15748
+ for (let i = 0; i < scored.length; i++) relToIdx.set(scored[i].file, i);
15749
+ const hubs = _computeHubs(callGraph);
15750
+ const seeds = scored.filter((e) => e.score > 0).map((e) => e.file);
15751
+ for (const file of seeds) {
15752
+ const abs = path.resolve(cwd, file);
15753
+ for (const neighborAbs of (callGraph.forward.get(abs) || [])) {
15754
+ if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
15755
+ const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
15756
+ const idx = relToIdx.get(neighborRel);
15757
+ if (idx !== undefined && scored[idx].file !== file) {
15758
+ scored[idx].score += GRAPH_BOOST_AMOUNTS.callHop;
15759
+ scored[idx].signals.callGraphBoost = (scored[idx].signals.callGraphBoost || 0) + GRAPH_BOOST_AMOUNTS.callHop;
15760
+ }
15761
+ }
15762
+ }
15763
+ }
15764
+
15524
15765
  // Compute confidence levels based on score distribution
15525
15766
  if (scored.length > 0) {
15526
15767
  const scores = scored.map(s => s.score);
@@ -19214,7 +19455,7 @@ function __tryGit(args, opts = {}) {
19214
19455
  catch (_) { return ''; }
19215
19456
  }
19216
19457
 
19217
- const VERSION = '8.13.0';
19458
+ const VERSION = '8.16.0';
19218
19459
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
19219
19460
 
19220
19461
  function requireSourceOrBundled(key) {
@@ -21075,7 +21316,7 @@ Usage:
21075
21316
  ${cmd} --impact <file> Show every file impacted by changing <file>
21076
21317
  ${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
21077
21318
  ${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)
21319
+ ${cmd} --callers <symbol> Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS, Python, Java, Go, Rust)
21079
21320
  ${cmd} --callees <symbol> Every repo function that <symbol> (transitively) calls
21080
21321
  ${cmd} --callers <symbol> --json --depth <n> Call-graph edges as JSON (depth 0 = unlimited)
21081
21322
  ${cmd} verify <answer.md> Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
@@ -21537,7 +21778,13 @@ function main() {
21537
21778
  } catch (_) { /* squeeze is best-effort — never break ask */ }
21538
21779
  }
21539
21780
 
21540
- let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
21781
+ // Opt-in call-graph neighbor boost (retrieval.callGraphBoost) non-fatal
21782
+ let askCallGraph = null;
21783
+ if (config && config.retrieval && config.retrieval.callGraphBoost) {
21784
+ try { askCallGraph = requireSourceOrBundled('./src/graph/call-graph').buildCallFileGraph(cwd); } catch (_) {}
21785
+ }
21786
+
21787
+ let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd, callGraph: askCallGraph });
21541
21788
 
21542
21789
  // v6.10: Workspace scoping — infer package from query and apply boost
21543
21790
  const workspaces = detectWorkspaces(cwd);
@@ -21694,7 +21941,7 @@ function main() {
21694
21941
 
21695
21942
  const { buildEvidencePack, formatJSON, formatMarkdown } = requireSourceOrBundled('./src/evidence/pack');
21696
21943
 
21697
- const opts = {};
21944
+ const opts = { version: VERSION };
21698
21945
  const topIdx = args.indexOf('--top');
21699
21946
  if (topIdx !== -1 && args[topIdx + 1]) opts.top = parseInt(args[topIdx + 1], 10);
21700
21947
  const budgetIdx = args.indexOf('--budget');
@@ -23709,7 +23956,12 @@ function main() {
23709
23956
  const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
23710
23957
  : ((config && config.retrieval && config.retrieval.topK) || 10);
23711
23958
  const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
23712
- const results = rank(query, index, { topK, recencyBoost, cwd });
23959
+ // Opt-in call-graph neighbor boost (retrieval.callGraphBoost) non-fatal
23960
+ let queryCallGraph = null;
23961
+ if (config && config.retrieval && config.retrieval.callGraphBoost) {
23962
+ try { queryCallGraph = requireSourceOrBundled('./src/graph/call-graph').buildCallFileGraph(cwd); } catch (_) {}
23963
+ }
23964
+ const results = rank(query, index, { topK, recencyBoost, cwd, callGraph: queryCallGraph });
23713
23965
  if (args.includes('--context')) {
23714
23966
  const miniCtx = buildMiniContext(results, cwd);
23715
23967
  const ctxOut = path.join(cwd, '.context', 'query-context.md');
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.16.0 | Benchmark: sigmap-v8.16-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.16-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 }
@@ -343,7 +343,7 @@ testCoverage = false
343
343
  testDirs = ["tests","test","__tests__","spec"]
344
344
  sigCache = false
345
345
  impactRadius = false
346
- retrieval = {"topK":10,"recencyBoost":1.5}
346
+ retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false}
347
347
  impact = {"depth":3,"includeSigs":true}
348
348
  ```
349
349
 
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.16.0 | Benchmark: sigmap-v8.16-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.16-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.16.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": {
@@ -30,6 +30,7 @@
30
30
  "benchmark:squeeze": "node scripts/run-squeeze-benchmark.mjs --save",
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
+ "benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
33
34
  "validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
34
35
  "health": "node gen-context.js --health",
35
36
  "map": "node gen-project-map.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.13.0",
3
+ "version": "8.16.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.16.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -147,6 +147,8 @@ const DEFAULTS = {
147
147
  topK: 10,
148
148
  // Multiplier applied to recently-changed files (>1 boosts them up)
149
149
  recencyBoost: 1.5,
150
+ // Boost files call-graph-connected to query matches (opt-in, measure-gated)
151
+ callGraphBoost: false,
150
152
  },
151
153
 
152
154
  // Impact layer settings (v2.5)