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 +87 -85
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/gen-context.js +143 -21
- package/llms-full.txt +3 -3
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +2 -0
- package/src/evidence/pack.js +50 -14
- package/src/graph/call-graph.js +37 -1
- package/src/mcp/handlers.js +10 -1
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +28 -0
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/
|
|
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/
|
|
178
|
+
### src/config/defaults.js
|
|
179
179
|
```
|
|
180
|
-
module.exports = {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 } :
|
|
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
|
|
209
|
-
function
|
|
210
|
-
function
|
|
211
|
-
function
|
|
212
|
-
function
|
|
213
|
-
function
|
|
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 } :
|
|
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-
|
|
228
|
-
function getMethodImpact(args, cwd) :
|
|
229
|
-
function getImpact(args, cwd) :
|
|
230
|
-
function getLines(args, cwd) :
|
|
231
|
-
function readMemory(args, cwd) :
|
|
232
|
-
function getCalleeSignatures(args, cwd) :
|
|
233
|
-
function _pkgVersion(cwd) :
|
|
234
|
-
function notifyFileCreated(args, cwd) :
|
|
235
|
-
function notifySymbolAdded(args, cwd) :
|
|
236
|
-
function notifyFileDeleted(args, cwd) :
|
|
237
|
-
function _changedFiles(cwd, args) :
|
|
238
|
-
function getDiffContext(args, cwd) :
|
|
239
|
-
function getArchitectureOverview(args, cwd) :
|
|
240
|
-
function verifySuggestion(args, cwd) :
|
|
241
|
-
function squeezeOutput(args, cwd) :
|
|
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/
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
function
|
|
269
|
-
function
|
|
270
|
-
function
|
|
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.
|
|
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
|
|
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 = '
|
|
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
|
-
|
|
4803
|
-
if (
|
|
4804
|
-
if (
|
|
4805
|
-
if (
|
|
4806
|
-
if (
|
|
4807
|
-
if (
|
|
4808
|
-
if (
|
|
4809
|
-
if (
|
|
4810
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
# 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.
|
|
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
|
+
# 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.
|
|
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.
|
|
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",
|
package/src/config/defaults.js
CHANGED
|
@@ -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)
|
package/src/evidence/pack.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Evidence Pack
|
|
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 = '
|
|
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
|
-
|
|
73
|
-
if (
|
|
74
|
-
if (
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
77
|
-
if (
|
|
78
|
-
if (
|
|
79
|
-
if (
|
|
80
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
};
|
package/src/graph/call-graph.js
CHANGED
|
@@ -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
|
};
|
package/src/mcp/handlers.js
CHANGED
|
@@ -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
|
-
|
|
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
package/src/retrieval/ranker.js
CHANGED
|
@@ -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);
|