sigmap 8.14.0 → 8.16.1

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
@@ -35,15 +35,15 @@ src/extractors/python_ast.py ← ast
35
35
 
36
36
  ## changes (last 5 commits — 0 seconds ago)
37
37
  ```
38
- src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
38
+ src/evidence/pack.js +riskFactorsFor ~parseAnchor ~buildEvidencePack ~formatMarkdown
39
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
+ src/retrieval/ranker.js ~scoreFile ~rank
44
+ src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
43
45
  src/review/pr-evidence.js ~buildPrEvidence ~formatPrEvidenceMarkdown
44
46
  src/review/review-pr.js ~isSource ~reviewPr
45
- src/format/terse.js +splitAnchor +encodeTerseSig +encodeTerseSigs +_tokens
46
- src/wiki/generate.js +_rel +_pct +_identity +_modules
47
47
  ```
48
48
 
49
49
  ## packages
@@ -175,18 +175,33 @@ code-fence ---
175
175
 
176
176
  ## src
177
177
 
178
- ### src/graph/blast-radius.js
178
+ ### src/config/defaults.js
179
179
  ```
180
- module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
181
- function tierFor(score) :25-31
182
- function _normRel(p) :33-35
183
- function _bfs(seedIds, reverse, maxDepth) :38-60
184
- function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
180
+ module.exports = { DEFAULTS } :163-163
181
+ ```
182
+
183
+ ### src/evidence/pack.js
184
+ ```
185
+ module.exports = { buildEvidencePack, formatJSON, formatMarkdown, parseAnchor, riskLabelFor, riskFactorsFor, findRelatedTests, SCHEMA_VERSION, SCHEMA_URL, TEST_DISCOVERY } :326-337
186
+ function parseAnchor(sig) → { symbol: string, start: :64-72
187
+ function riskLabelFor(relPath) → 'generated'|'test'|'migra :84-86
188
+ function riskFactorsFor(relPath) → string[] :96-108
189
+ function stemOf(relPath) :111-114
190
+ function testTargetStem(relPath) → string :126-132
191
+ function findRelatedTests(relPath, allFiles) → string[] :143-154
192
+ function reasonFor(signals) :157-168
193
+ function sigTokens(sigs) :171-173
194
+ function canonicalize(value) → string :180-182
195
+ function sortKeys(value) :184-192
196
+ function buildEvidencePack(query, cwd, opts = {}) → object :205-281
197
+ function ranked0Empty(query) :284-286
198
+ function formatJSON(pack) :289-291
199
+ function formatMarkdown(pack) :294-324
185
200
  ```
186
201
 
187
202
  ### src/graph/call-graph.js
188
203
  ```
189
- module.exports = { buildCallGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy, maskRust } :527-531
204
+ module.exports = { buildCallGraph, buildCallFileGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy, maskRust } :563-567
190
205
  function normalizePath(p) :41-41
191
206
  function toRel(cwd, f) :42-42
192
207
  function symId(cwd, absFile, name) :43-43
@@ -205,17 +220,18 @@ function extractDefs(filePath, src) :307-315
205
220
  function callsInRange(masked, start, end) :318-330
206
221
  function _walk(dir, excludeSet, out, depth) :334-347
207
222
  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
223
+ function buildCallFileGraph(cwd, opts = {}) → { forward: Map<string,str :459-482
224
+ function _resolveSymbol(symbol, defs) :485-490
225
+ function _bfs(seedIds, graph, maxDepth) :493-506
226
+ function methodImpact(symbol, cwd, opts = {}) → { symbol:string, resolved :516-522
227
+ function methodCallees(symbol, cwd, opts = {}) → { symbol:string, resolved :528-534
228
+ function formatCallGraph(result, kind) :537-549
229
+ … +1 more signatures
214
230
  ```
215
231
 
216
232
  ### src/mcp/handlers.js
217
233
  ```
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
234
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :975-975
219
235
  function _readContextFiles(cwd) :10-17
220
236
  function readContext(args, cwd) :36-66
221
237
  function searchSignatures(args, cwd) :74-100
@@ -224,21 +240,21 @@ function createCheckpoint(args, cwd) :143-215
224
240
  function getRouting(args, cwd) :224-261
225
241
  function explainFile(args, cwd) :269-356
226
242
  function listModules(args, cwd) :364-403
227
- function queryContext(args, cwd) :411-429
228
- function getMethodImpact(args, cwd) :437-451
229
- function getImpact(args, cwd) :459-471
230
- function getLines(args, cwd) :480-528
231
- function readMemory(args, cwd) :536-571
232
- function getCalleeSignatures(args, cwd) :580-625
233
- function _pkgVersion(cwd) :632-635
234
- function notifyFileCreated(args, cwd) :639-661
235
- function notifySymbolAdded(args, cwd) :664-684
236
- function notifyFileDeleted(args, cwd) :687-701
237
- function _changedFiles(cwd, args) :707-719
238
- function getDiffContext(args, cwd) :728-799
239
- function getArchitectureOverview(args, cwd) :808-876
240
- function verifySuggestion(args, cwd) :886-921
241
- function squeezeOutput(args, cwd) :931-964
243
+ function queryContext(args, cwd) :411-438
244
+ function getMethodImpact(args, cwd) :446-460
245
+ function getImpact(args, cwd) :468-480
246
+ function getLines(args, cwd) :489-537
247
+ function readMemory(args, cwd) :545-580
248
+ function getCalleeSignatures(args, cwd) :589-634
249
+ function _pkgVersion(cwd) :641-644
250
+ function notifyFileCreated(args, cwd) :648-670
251
+ function notifySymbolAdded(args, cwd) :673-693
252
+ function notifyFileDeleted(args, cwd) :696-710
253
+ function _changedFiles(cwd, args) :716-728
254
+ function getDiffContext(args, cwd) :737-808
255
+ function getArchitectureOverview(args, cwd) :817-885
256
+ function verifySuggestion(args, cwd) :895-930
257
+ function squeezeOutput(args, cwd) :940-973
242
258
  ```
243
259
 
244
260
  ### src/mcp/server.js
@@ -255,19 +271,22 @@ function start(cwd) :113-139
255
271
  module.exports = { TOOLS } :392-392
256
272
  ```
257
273
 
258
- ### src/review/pr-evidence.js
259
- ```
260
- module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :157-157
261
- function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-84
262
- function formatPrEvidenceMarkdown(evidence, opts = {}) :89-155
274
+ ### src/retrieval/ranker.js
263
275
  ```
264
-
265
- ### src/review/review-pr.js
266
- ```
267
- module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :150-150
268
- function isTestFile(p) :32-34
269
- function isSource(p) :35-37
270
- function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
276
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent } :598-598
277
+ function _computePenalty(filePath) :63-70
278
+ function _computeHubs(graph) :73-84
279
+ function _isHub(filePath) :87-91
280
+ function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals: :102-159
281
+ function rank(query, sigIndex, opts) → { file: string, score: nu :177-302
282
+ function _parseContextFile(contextPath) → Map<string, string[]> :372-404
283
+ function _mergeSigIndex(target, source) :407-415
284
+ function _buildSigIndexFromCache(cwd) → Map<string, string[]> :422-442
285
+ function _enrichSigIndexFromStrategy(cwd, index) → Map<string, string[]> :450-456
286
+ function buildSigIndex(cwd, opts) → Map<string, string[]> :473-506
287
+ function formatRankTable(results, query) → string :515-551
288
+ function formatRankJSON(results, query) → object :560-575
289
+ function detectIntent(query) :590-596
271
290
  ```
272
291
 
273
292
  ### src/analysis/coverage-score.js
@@ -307,11 +326,6 @@ function getChangedFiles(files, cache) → { changed: string[], unch :71-88
307
326
  function updateCacheEntries(cache, extracted) :96-103
308
327
  ```
309
328
 
310
- ### src/config/defaults.js
311
- ```
312
- module.exports = { DEFAULTS } :161-161
313
- ```
314
-
315
329
  ### src/config/loader.js
316
330
  ```
317
331
  module.exports = { loadConfig, loadBaseConfig } :313-313
@@ -525,24 +539,6 @@ function scoreUsefulness(taskResult, rankingScore) :11-38
525
539
  function computeUsefulnessStats(taskResults) :40-66
526
540
  ```
527
541
 
528
- ### src/evidence/pack.js
529
- ```
530
- module.exports = { buildEvidencePack, formatJSON, formatMarkdown, parseAnchor, riskLabelFor, findRelatedTests, SCHEMA_VERSION } :293-301
531
- function parseAnchor(sig) → { symbol: string, start: :50-58
532
- function riskLabelFor(relPath) → 'generated'|'test'|'migra :70-81
533
- function stemOf(relPath) :84-87
534
- function testTargetStem(relPath) → string :99-105
535
- function findRelatedTests(relPath, allFiles) → string[] :116-127
536
- function reasonFor(signals) :130-141
537
- function sigTokens(sigs) :144-146
538
- function canonicalize(value) → string :153-155
539
- function sortKeys(value) :157-165
540
- function buildEvidencePack(query, cwd, opts = {}) → object :178-249
541
- function ranked0Empty(query) :252-254
542
- function formatJSON(pack) :257-259
543
- function formatMarkdown(pack) :262-291
544
- ```
545
-
546
542
  ### src/extractors/coverage.js
547
543
  ```
548
544
  module.exports = { buildTestIndex, isTested } :79-79
@@ -1012,6 +1008,15 @@ function renderReportHtml(result, opts = {}) → string :48-115
1012
1008
  function renderReportMarkdown(result) :144-162
1013
1009
  ```
1014
1010
 
1011
+ ### src/graph/blast-radius.js
1012
+ ```
1013
+ module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
1014
+ function tierFor(score) :25-31
1015
+ function _normRel(p) :33-35
1016
+ function _bfs(seedIds, reverse, maxDepth) :38-60
1017
+ function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
1018
+ ```
1019
+
1015
1020
  ### src/graph/builder.js
1016
1021
  ```
1017
1022
  module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias } :494-494
@@ -1187,30 +1192,27 @@ function expandQuery(qToks) → Map<string, number> :132-141
1187
1192
  function bm25rank(query, candidates) → Array<object & { score: n :154-193
1188
1193
  ```
1189
1194
 
1190
- ### src/retrieval/ranker.js
1191
- ```
1192
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent } :570-570
1193
- function _computePenalty(filePath) :62-69
1194
- function _computeHubs(graph) :72-83
1195
- function _isHub(filePath) :86-90
1196
- function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals: :101-158
1197
- function rank(query, sigIndex, opts) → { file: string, score: nu :174-298
1198
- function _parseContextFile(contextPath) → Map<string, string[]> :344-376
1199
- function _mergeSigIndex(target, source) :379-387
1200
- function _buildSigIndexFromCache(cwd) → Map<string, string[]> :394-414
1201
- function _enrichSigIndexFromStrategy(cwd, index) → Map<string, string[]> :422-428
1202
- function buildSigIndex(cwd, opts) → Map<string, string[]> :445-478
1203
- function formatRankTable(results, query) → string :487-523
1204
- function formatRankJSON(results, query) → object :532-547
1205
- function detectIntent(query) :562-568
1206
- ```
1207
-
1208
1195
  ### src/retrieval/tokenizer.js
1209
1196
  ```
1210
1197
  module.exports = { tokenize, STOP_WORDS } :54-54
1211
1198
  function tokenize(text, opts) → string[] :31-52
1212
1199
  ```
1213
1200
 
1201
+ ### src/review/pr-evidence.js
1202
+ ```
1203
+ module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :157-157
1204
+ function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-84
1205
+ function formatPrEvidenceMarkdown(evidence, opts = {}) :89-155
1206
+ ```
1207
+
1208
+ ### src/review/review-pr.js
1209
+ ```
1210
+ module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :150-150
1211
+ function isTestFile(p) :32-34
1212
+ function isSource(p) :35-37
1213
+ function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
1214
+ ```
1215
+
1214
1216
  ### src/routing/classifier.js
1215
1217
  ```
1216
1218
  module.exports = { classify, classifyAll } :102-102
package/CHANGELOG.md CHANGED
@@ -10,6 +10,35 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.16.1] — 2026-07-12
14
+
15
+ Patch release — **benchmark hygiene**; the published package is unchanged (scripts + tests only).
16
+
17
+ ### Fixed
18
+ - **Hermetic grounding benchmark (#480, PR #481):** the grounding suite regenerated the shared cached benchmark repos with a plain default config, silently overwriting the retrieval harness's per-repo-scoped contexts — later context-reading suites saw degraded indexes (the callgraph A/B measured 32.2% hit@5 instead of ~87.8%). `measureGroundingHermetic` now snapshots every context artifact before its regen and restores them byte-exactly after measuring (leftovers removed). Verified: A/B 87.8% → grounding → 87.8%; first retrieval run post-grounding 87.8% with no transient; grounding's own aggregate unchanged. The callgraph A/B also discloses it reads contexts as-is. 3 regression tests.
19
+
20
+ ---
21
+
22
+ ## [8.16.0] — 2026-07-12
23
+
24
+ Minor release — **Evidence Pack schema v2** (the last Machine-lever item from the master plan §7.4). Also the first published release carrying v8.15.0, which was prepared but never tagged (see its note below).
25
+
26
+ ### Added
27
+ - **Evidence Pack schema v2 (#477, PR #478):** additive over v1 — `schemaVersion: '2.0'` + `schemaUrl` pointing at a **published draft-07 JSON Schema** (`sigmap.io/schemas/evidence-pack-2.json`) that CI/agents can validate packs against; **`files[].riskFactors`** exposes *every* matched risk category in precedence order (a migration touching payments carries `['migration','payment']`) while `riskLabel` stays the first factor for v1 consumers; a **`testDiscovery` provenance block** carries the measured accuracy of the stem-affix related-tests method (**F1 0.98, precision 0.971, recall 0.988** over 3,701 pairs / 28 repos) — constants sourced from the committed benchmark report and guard-tested against it; and `generator: { name, version }` records what built the pack. Byte-stable; `contextHash` determinism preserved.
28
+
29
+ ---
30
+
31
+ ## [8.15.0] — 2026-07-11
32
+
33
+ > **Not published as a standalone release** — prepared and merged to main, but never tagged; first shipped as part of v8.16.0.
34
+
35
+ Minor release — **call-graph ranking boost, opt-in and measure-gated**. The milestone item "call-graph edges into ranking" is done — and shipped **dark**, because the measure gate said so.
36
+
37
+ ### Added
38
+ - **Call-graph ranking boost (#474, PR #475):** `buildCallFileGraph` collapses symbol edges to deterministic file-level bidirectional edges; `rank()` gains `opts.callGraph` — a single-hop, hub-suppressed neighbor boost (`callHop: 0.30`) with a `callGraphBoost` explain signal. Config-gated (non-fatal) into `ask`, `--query`, and the `query_context` MCP tool via **`retrieval.callGraphBoost` (default `false`)**. **Measured** by the new `npm run benchmark:callgraph-boost` A/B (90 tasks / 18 cached repos through the full ranker): import-graph arm 80% hit@5, +call-graph arm 80% — **delta +0, no repo moved** — so per the gate the default stays off; the boost exists for call-topology-heavy repos (Go/Java same-package) where import edges structurally can't see the relationship. The headline BM25 harness is untouched and reproduces 87.8%. Report: `benchmarks/reports/callgraph-boost.json`.
39
+
40
+ ---
41
+
13
42
  ## [8.14.0] — 2026-07-11
14
43
 
15
44
  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.
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.14-main (21 repositories, including R language)
125
+ Benchmark : sigmap-v8.16-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
@@ -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
  };
@@ -11386,6 +11424,42 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11386
11424
  return { forward: toArr(forward), reverse: toArr(reverse), defs };
11387
11425
  }
11388
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
+
11389
11463
  // Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
11390
11464
  function _resolveSymbol(symbol, defs) {
11391
11465
  if (defs.has(symbol)) return [symbol];
@@ -11466,7 +11540,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
11466
11540
  }
11467
11541
 
11468
11542
  module.exports = {
11469
- buildCallGraph, methodImpact, methodCallees,
11543
+ buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
11470
11544
  formatCallGraph, formatCallGraphJSON,
11471
11545
  extractDefs, maskJs, maskPy, maskRust,
11472
11546
  };
@@ -13618,7 +13692,16 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
13618
13692
  // Build dependency graph for neighbor boost — non-fatal if it fails
13619
13693
  let graph = null;
13620
13694
  try { graph = buildFromCwd(cwd); } catch (_) {}
13621
- 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 });
13622
13705
  return formatRankTable(results, args.query);
13623
13706
  } catch (err) {
13624
13707
  return `_query_context failed: ${err.message}_`;
@@ -14331,7 +14414,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
14331
14414
 
14332
14415
  const SERVER_INFO = {
14333
14416
  name: 'sigmap',
14334
- version: '8.14.0',
14417
+ version: '8.16.1',
14335
14418
  description: 'SigMap MCP server — code signatures on demand',
14336
14419
  };
14337
14420
 
@@ -15396,6 +15479,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15396
15479
  const GRAPH_BOOST_AMOUNTS = {
15397
15480
  hop1: 0.40, // direct import neighbor of a file with score > 0
15398
15481
  hop2: 0.15, // 2 hops away (transitive), with decay
15482
+ callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
15399
15483
  };
15400
15484
 
15401
15485
  // Intent-specific weight adjustments
@@ -15528,6 +15612,8 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15528
15612
  * @param {object} [opts.weights] - override scoring weights
15529
15613
  * @param {string} [opts.cwd] - project root for learned ranking weights
15530
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
15531
15617
  * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
15532
15618
  */
15533
15619
  function rank(query, sigIndex, opts) {
@@ -15651,6 +15737,31 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
15651
15737
  }
15652
15738
  }
15653
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
+
15654
15765
  // Compute confidence levels based on score distribution
15655
15766
  if (scored.length > 0) {
15656
15767
  const scores = scored.map(s => s.score);
@@ -19344,7 +19455,7 @@ function __tryGit(args, opts = {}) {
19344
19455
  catch (_) { return ''; }
19345
19456
  }
19346
19457
 
19347
- const VERSION = '8.14.0';
19458
+ const VERSION = '8.16.1';
19348
19459
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
19349
19460
 
19350
19461
  function requireSourceOrBundled(key) {
@@ -21667,7 +21778,13 @@ function main() {
21667
21778
  } catch (_) { /* squeeze is best-effort — never break ask */ }
21668
21779
  }
21669
21780
 
21670
- 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 });
21671
21788
 
21672
21789
  // v6.10: Workspace scoping — infer package from query and apply boost
21673
21790
  const workspaces = detectWorkspaces(cwd);
@@ -21824,7 +21941,7 @@ function main() {
21824
21941
 
21825
21942
  const { buildEvidencePack, formatJSON, formatMarkdown } = requireSourceOrBundled('./src/evidence/pack');
21826
21943
 
21827
- const opts = {};
21944
+ const opts = { version: VERSION };
21828
21945
  const topIdx = args.indexOf('--top');
21829
21946
  if (topIdx !== -1 && args[topIdx + 1]) opts.top = parseInt(args[topIdx + 1], 10);
21830
21947
  const budgetIdx = args.indexOf('--budget');
@@ -23839,7 +23956,12 @@ function main() {
23839
23956
  const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
23840
23957
  : ((config && config.retrieval && config.retrieval.topK) || 10);
23841
23958
  const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
23842
- 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 });
23843
23965
  if (args.includes('--context')) {
23844
23966
  const miniCtx = buildMiniContext(results, cwd);
23845
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.14.0 | Benchmark: sigmap-v8.14-main (2026-07-11)
14
+ # Version: 8.16.1 | 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.14-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
  |--------|----------------|-------------|
@@ -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.14.0 | Benchmark: sigmap-v8.14-main (2026-07-11)
14
+ # Version: 8.16.1 | 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.14-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.14.0",
3
+ "version": "8.16.1",
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.14.0",
3
+ "version": "8.16.1",
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.14.0",
3
+ "version": "8.16.1",
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)
@@ -1,12 +1,15 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Evidence Pack v1 (v8.0 E1).
4
+ * Evidence Pack v2 (v8.0 E1; schema v2 adds a published schema URL, per-file
5
+ * multi-factor risk labels, measured test-discovery provenance, and generator
6
+ * identity — all additive over v1).
5
7
  *
6
8
  * A deterministic, machine-consumable signature-and-evidence map. Replaces the
7
9
  * "paste this into your prompt" workflow with a byte-stable JSON artifact that
8
10
  * an agent or CI can ingest directly — every entry anchored to a real file,
9
- * symbol, and line range.
11
+ * symbol, and line range. The published schema lives at
12
+ * docs-vp/public/schemas/evidence-pack-2.json (sigmap.io/schemas/).
10
13
  *
11
14
  * Composed entirely from shipped zero-dep modules:
12
15
  * - retrieval/ranker → ranked files, scores, signals
@@ -26,10 +29,21 @@ const crypto = require('crypto');
26
29
  const { buildSigIndex, rank, detectIntent } = require('../retrieval/ranker');
27
30
  const { scan } = require('../security/scanner');
28
31
 
29
- const SCHEMA_VERSION = '1.0';
32
+ const SCHEMA_VERSION = '2.0';
33
+ const SCHEMA_URL = 'https://sigmap.io/schemas/evidence-pack-2.json';
30
34
  const DEFAULT_BUDGET = 6000;
31
35
  const DEFAULT_TOP = 12;
32
36
 
37
+ // Measured accuracy of the stem-affix test-discovery method (C2). Constants
38
+ // are sourced from the committed benchmarks/reports/test-discovery.json and
39
+ // guarded by a test that fails on drift — re-run `npm run
40
+ // benchmark:test-discovery` and update together, never hand-invent.
41
+ const TEST_DISCOVERY = {
42
+ method: 'stem-affix-match',
43
+ measured: { f1: 0.98, precision: 0.971, recall: 0.988, pairs: 3701, repos: 28 },
44
+ benchmark: 'npm run benchmark:test-discovery',
45
+ };
46
+
33
47
  const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
34
48
  const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
35
49
  const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
@@ -68,16 +82,29 @@ function parseAnchor(sig) {
68
82
  * @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
69
83
  */
70
84
  function riskLabelFor(relPath) {
85
+ return riskFactorsFor(relPath)[0];
86
+ }
87
+
88
+ /**
89
+ * Every risk category a file matches, in the same strict precedence order
90
+ * riskLabelFor uses (schema v2). Where v1 collapsed a migration touching
91
+ * payments to `migration`, the factors list carries both. Always non-empty:
92
+ * a file matching nothing is `['source']`.
93
+ * @param {string} relPath
94
+ * @returns {string[]}
95
+ */
96
+ function riskFactorsFor(relPath) {
71
97
  const p = relPath.replace(/\\/g, '/');
72
- if (GENERATED_RE.test(p)) return 'generated';
73
- if (TEST_RE.test(p)) return 'test';
74
- if (MIGRATION_RE.test(p)) return 'migration';
75
- if (PAYMENT_RE.test(p)) return 'payment';
76
- if (AUTH_RE.test(p)) return 'auth';
77
- if (SECURITY_RE.test(p)) return 'security';
78
- if (CONFIG_RE.test(p)) return 'config';
79
- if (PUBLIC_API_RE.test(p)) return 'public-api';
80
- return 'source';
98
+ const factors = [];
99
+ if (GENERATED_RE.test(p)) factors.push('generated');
100
+ if (TEST_RE.test(p)) factors.push('test');
101
+ if (MIGRATION_RE.test(p)) factors.push('migration');
102
+ if (PAYMENT_RE.test(p)) factors.push('payment');
103
+ if (AUTH_RE.test(p)) factors.push('auth');
104
+ if (SECURITY_RE.test(p)) factors.push('security');
105
+ if (CONFIG_RE.test(p)) factors.push('config');
106
+ if (PUBLIC_API_RE.test(p)) factors.push('public-api');
107
+ return factors.length ? factors : ['source'];
81
108
  }
82
109
 
83
110
  /** Filename stem (basename minus the first extension chain). */
@@ -209,6 +236,7 @@ function buildEvidencePack(query, cwd, opts = {}) {
209
236
  if (start !== null) sourceLines.push({ symbol, start, end });
210
237
  }
211
238
 
239
+ const riskFactors = riskFactorsFor(r.file);
212
240
  files.push({
213
241
  path: r.file,
214
242
  symbols,
@@ -216,7 +244,8 @@ function buildEvidencePack(query, cwd, opts = {}) {
216
244
  confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
217
245
  sourceLines,
218
246
  relatedTests: findRelatedTests(r.file, allFiles),
219
- riskLabel: riskLabelFor(r.file),
247
+ riskLabel: riskFactors[0],
248
+ riskFactors,
220
249
  });
221
250
  }
222
251
 
@@ -225,11 +254,14 @@ function buildEvidencePack(query, cwd, opts = {}) {
225
254
 
226
255
  const pack = {
227
256
  schemaVersion: SCHEMA_VERSION,
257
+ schemaUrl: SCHEMA_URL,
258
+ generator: { name: 'sigmap', version: typeof opts.version === 'string' ? opts.version : null },
228
259
  query,
229
260
  intent,
230
261
  files,
231
262
  tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
232
263
  droppedFiles,
264
+ testDiscovery: TEST_DISCOVERY,
233
265
  grounding: {
234
266
  symbolCount,
235
267
  anchoredSymbols,
@@ -271,7 +303,8 @@ function formatMarkdown(pack) {
271
303
  L.push('');
272
304
 
273
305
  for (const f of pack.files) {
274
- L.push(`## \`${f.path}\` _(${f.riskLabel}, confidence ${f.confidence})_`);
306
+ const risk = (f.riskFactors && f.riskFactors.length > 1) ? f.riskFactors.join(' + ') : f.riskLabel;
307
+ L.push(`## \`${f.path}\` _(${risk}, confidence ${f.confidence})_`);
275
308
  L.push(`_${f.reason}_`);
276
309
  if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
277
310
  L.push('');
@@ -296,6 +329,9 @@ module.exports = {
296
329
  formatMarkdown,
297
330
  parseAnchor,
298
331
  riskLabelFor,
332
+ riskFactorsFor,
299
333
  findRelatedTests,
300
334
  SCHEMA_VERSION,
335
+ SCHEMA_URL,
336
+ TEST_DISCOVERY,
301
337
  };
@@ -445,6 +445,42 @@ function buildCallGraph(cwd, opts = {}) {
445
445
  return { forward: toArr(forward), reverse: toArr(reverse), defs };
446
446
  }
447
447
 
448
+ /**
449
+ * Collapse the symbol-level call-graph to FILE-level bidirectional edges for
450
+ * the ranker's neighbor boost (opt-in via `retrieval.callGraphBoost`). A file
451
+ * whose functions call into — or are called by — another file gets an edge in
452
+ * both directions. Keys are `path.resolve`-form absolute paths (matching the
453
+ * ranker's lookups); entries and neighbor lists are sorted for determinism.
454
+ *
455
+ * @param {string} cwd
456
+ * @param {object} [opts] { graph } to inject a prebuilt call graph (tests)
457
+ * @returns {{ forward: Map<string,string[]> }}
458
+ */
459
+ function buildCallFileGraph(cwd, opts = {}) {
460
+ const graph = opts.graph || buildCallGraph(cwd, opts);
461
+ const edges = new Map(); // absFile → Set<absFile>
462
+ const add = (a, b) => {
463
+ if (a === b) return;
464
+ if (!edges.has(a)) edges.set(a, new Set());
465
+ edges.get(a).add(b);
466
+ };
467
+ for (const [callerId, calleeIds] of graph.forward.entries()) {
468
+ const callerDef = graph.defs.get(callerId);
469
+ if (!callerDef) continue;
470
+ for (const calleeId of calleeIds) {
471
+ const calleeDef = graph.defs.get(calleeId);
472
+ if (!calleeDef || calleeDef.file === callerDef.file) continue;
473
+ const a = path.resolve(cwd, callerDef.file);
474
+ const b = path.resolve(cwd, calleeDef.file);
475
+ add(a, b);
476
+ add(b, a);
477
+ }
478
+ }
479
+ const forward = new Map();
480
+ for (const k of [...edges.keys()].sort()) forward.set(k, [...edges.get(k)].sort());
481
+ return { forward };
482
+ }
483
+
448
484
  // Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
449
485
  function _resolveSymbol(symbol, defs) {
450
486
  if (defs.has(symbol)) return [symbol];
@@ -525,7 +561,7 @@ function formatCallGraphJSON(result, kind) {
525
561
  }
526
562
 
527
563
  module.exports = {
528
- buildCallGraph, methodImpact, methodCallees,
564
+ buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
529
565
  formatCallGraph, formatCallGraphJSON,
530
566
  extractDefs, maskJs, maskPy, maskRust,
531
567
  };
@@ -421,7 +421,16 @@ function queryContext(args, cwd) {
421
421
  // Build dependency graph for neighbor boost — non-fatal if it fails
422
422
  let graph = null;
423
423
  try { graph = buildFromCwd(cwd); } catch (_) {}
424
- const results = rank(args.query, index, { topK, cwd, graph });
424
+ // Opt-in call-graph neighbor boost (retrieval.callGraphBoost) non-fatal
425
+ let callGraph = null;
426
+ try {
427
+ const { loadConfig } = require('../config/loader');
428
+ const retrieval = loadConfig(cwd).retrieval;
429
+ if (retrieval && retrieval.callGraphBoost) {
430
+ callGraph = require('../graph/call-graph').buildCallFileGraph(cwd);
431
+ }
432
+ } catch (_) {}
433
+ const results = rank(args.query, index, { topK, cwd, graph, callGraph });
425
434
  return formatRankTable(results, args.query);
426
435
  } catch (err) {
427
436
  return `_query_context failed: ${err.message}_`;
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.14.0',
21
+ version: '8.16.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -37,6 +37,7 @@ const DEFAULT_WEIGHTS = {
37
37
  const GRAPH_BOOST_AMOUNTS = {
38
38
  hop1: 0.40, // direct import neighbor of a file with score > 0
39
39
  hop2: 0.15, // 2 hops away (transitive), with decay
40
+ callHop: 0.30, // call-graph file neighbor (opt-in retrieval.callGraphBoost)
40
41
  };
41
42
 
42
43
  // Intent-specific weight adjustments
@@ -169,6 +170,8 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
169
170
  * @param {object} [opts.weights] - override scoring weights
170
171
  * @param {string} [opts.cwd] - project root for learned ranking weights
171
172
  * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
173
+ * @param {{ forward: Map<string,string[]> }} [opts.callGraph] - file-level call-graph edges
174
+ * (from buildCallFileGraph) for the opt-in call-neighbor boost
172
175
  * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
173
176
  */
174
177
  function rank(query, sigIndex, opts) {
@@ -292,6 +295,31 @@ function rank(query, sigIndex, opts) {
292
295
  }
293
296
  }
294
297
 
298
+ // Call-graph neighbor boost (opt-in via retrieval.callGraphBoost): a file
299
+ // whose functions call into — or are called by — a positively-scored file is
300
+ // relevant even when no import edge exists (Go/Java same-package, dynamic
301
+ // dispatch). Single hop; seeds snapshotted first so boosts never cascade.
302
+ const callGraph = (opts && opts.callGraph && opts.callGraph.forward instanceof Map) ? opts.callGraph : null;
303
+ if (callGraph && cwd) {
304
+ const path = require('path');
305
+ const relToIdx = new Map();
306
+ for (let i = 0; i < scored.length; i++) relToIdx.set(scored[i].file, i);
307
+ const hubs = _computeHubs(callGraph);
308
+ const seeds = scored.filter((e) => e.score > 0).map((e) => e.file);
309
+ for (const file of seeds) {
310
+ const abs = path.resolve(cwd, file);
311
+ for (const neighborAbs of (callGraph.forward.get(abs) || [])) {
312
+ if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
313
+ const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
314
+ const idx = relToIdx.get(neighborRel);
315
+ if (idx !== undefined && scored[idx].file !== file) {
316
+ scored[idx].score += GRAPH_BOOST_AMOUNTS.callHop;
317
+ scored[idx].signals.callGraphBoost = (scored[idx].signals.callGraphBoost || 0) + GRAPH_BOOST_AMOUNTS.callHop;
318
+ }
319
+ }
320
+ }
321
+ }
322
+
295
323
  // Compute confidence levels based on score distribution
296
324
  if (scored.length > 0) {
297
325
  const scores = scored.map(s => s.score);