sigmap 8.12.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -63
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/gen-context.js +237 -10
- package/llms-full.txt +12 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/graph/blast-radius.js +132 -0
- package/src/mcp/handlers.js +23 -1
- package/src/mcp/server.js +3 -2
- package/src/mcp/tools.js +34 -5
- package/src/review/pr-evidence.js +17 -0
- package/src/review/review-pr.js +23 -1
package/AGENTS.md
CHANGED
|
@@ -33,22 +33,16 @@ Always run `sigmap ask` (or `sigmap --query`) before searching for files relevan
|
|
|
33
33
|
src/extractors/python_ast.py ← ast
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
## changes (last 5 commits —
|
|
36
|
+
## changes (last 5 commits — 1 second ago)
|
|
37
37
|
```
|
|
38
38
|
src/format/terse.js +splitAnchor +encodeTerseSig +encodeTerseSigs +_tokens
|
|
39
|
+
src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
|
|
40
|
+
src/mcp/handlers.js +that +getMethodImpact ~queryContext ~squeezeOutput
|
|
41
|
+
src/mcp/server.js ~dispatch
|
|
42
|
+
src/mcp/tools.js +it
|
|
43
|
+
src/review/pr-evidence.js ~buildPrEvidence ~formatPrEvidenceMarkdown
|
|
44
|
+
src/review/review-pr.js ~isSource ~reviewPr
|
|
39
45
|
src/wiki/generate.js +_rel +_pct +_identity +_modules
|
|
40
|
-
src/conventions/extract.js ~classifyNaming ~scoreConvention
|
|
41
|
-
src/conventions/fix.js ~buildFixList
|
|
42
|
-
src/extractors/javascript.js ~extract ~extractClassMembers ~extractBlock ~buildReturnHints
|
|
43
|
-
src/extractors/typescript.js ~extract ~extractInterfaceMembers ~extractClassMembers ~extractBlock
|
|
44
|
-
src/graph/builder.js +probeJs +resolveJsPath +stripJsonc +loadAliasMap
|
|
45
|
-
src/graph/impact.js ~formatImpact
|
|
46
|
-
src/health/scorer.js +gradeFor +composeHealth +score ~score
|
|
47
|
-
src/judge/judge-engine.js +extracts +claimGrounding ~groundedness ~judge
|
|
48
|
-
src/plan/planner.js +createPlan ~createPlan
|
|
49
|
-
src/review/pr-evidence.js ~formatPrEvidenceMarkdown
|
|
50
|
-
src/review/review-pr.js ~reviewPr
|
|
51
|
-
src/util/truncate.js +members +capWithNotice +block +capMembersWithNotice
|
|
52
46
|
```
|
|
53
47
|
|
|
54
48
|
## packages
|
|
@@ -195,13 +189,70 @@ function _tokens(sigs) :62-64
|
|
|
195
189
|
function measureTerse(sigsList) → { beforeTokens: number, a :72-84
|
|
196
190
|
```
|
|
197
191
|
|
|
192
|
+
### src/graph/blast-radius.js
|
|
193
|
+
```
|
|
194
|
+
module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
|
|
195
|
+
function tierFor(score) :25-31
|
|
196
|
+
function _normRel(p) :33-35
|
|
197
|
+
function _bfs(seedIds, reverse, maxDepth) :38-60
|
|
198
|
+
function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### src/mcp/handlers.js
|
|
202
|
+
```
|
|
203
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :966-966
|
|
204
|
+
function _readContextFiles(cwd) :10-17
|
|
205
|
+
function readContext(args, cwd) :36-66
|
|
206
|
+
function searchSignatures(args, cwd) :74-100
|
|
207
|
+
function getMap(args, cwd) :108-131
|
|
208
|
+
function createCheckpoint(args, cwd) :143-215
|
|
209
|
+
function getRouting(args, cwd) :224-261
|
|
210
|
+
function explainFile(args, cwd) :269-356
|
|
211
|
+
function listModules(args, cwd) :364-403
|
|
212
|
+
function queryContext(args, cwd) :411-429
|
|
213
|
+
function getMethodImpact(args, cwd) :437-451
|
|
214
|
+
function getImpact(args, cwd) :459-471
|
|
215
|
+
function getLines(args, cwd) :480-528
|
|
216
|
+
function readMemory(args, cwd) :536-571
|
|
217
|
+
function getCalleeSignatures(args, cwd) :580-625
|
|
218
|
+
function _pkgVersion(cwd) :632-635
|
|
219
|
+
function notifyFileCreated(args, cwd) :639-661
|
|
220
|
+
function notifySymbolAdded(args, cwd) :664-684
|
|
221
|
+
function notifyFileDeleted(args, cwd) :687-701
|
|
222
|
+
function _changedFiles(cwd, args) :707-719
|
|
223
|
+
function getDiffContext(args, cwd) :728-799
|
|
224
|
+
function getArchitectureOverview(args, cwd) :808-876
|
|
225
|
+
function verifySuggestion(args, cwd) :886-921
|
|
226
|
+
function squeezeOutput(args, cwd) :931-964
|
|
227
|
+
```
|
|
228
|
+
|
|
198
229
|
### src/mcp/server.js
|
|
199
230
|
```
|
|
200
|
-
module.exports = { start } :
|
|
231
|
+
module.exports = { start } :141-141
|
|
201
232
|
function respond(id, result) :28-30
|
|
202
233
|
function respondError(id, code, message) :32-36
|
|
203
|
-
function dispatch(msg, cwd) :41-
|
|
204
|
-
function start(cwd) :
|
|
234
|
+
function dispatch(msg, cwd) :41-108
|
|
235
|
+
function start(cwd) :113-139
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### src/mcp/tools.js
|
|
239
|
+
```
|
|
240
|
+
module.exports = { TOOLS } :392-392
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### src/review/pr-evidence.js
|
|
244
|
+
```
|
|
245
|
+
module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :157-157
|
|
246
|
+
function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-84
|
|
247
|
+
function formatPrEvidenceMarkdown(evidence, opts = {}) :89-155
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### src/review/review-pr.js
|
|
251
|
+
```
|
|
252
|
+
module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :150-150
|
|
253
|
+
function isTestFile(p) :32-34
|
|
254
|
+
function isSource(p) :35-37
|
|
255
|
+
function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
|
|
205
256
|
```
|
|
206
257
|
|
|
207
258
|
### src/wiki/generate.js
|
|
@@ -1099,33 +1150,6 @@ function shouldSkipFile(rel) :18-21
|
|
|
1099
1150
|
function analyze(files, cwd) :23-125
|
|
1100
1151
|
```
|
|
1101
1152
|
|
|
1102
|
-
### src/mcp/handlers.js
|
|
1103
|
-
```
|
|
1104
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :944-944
|
|
1105
|
-
function _readContextFiles(cwd) :10-17
|
|
1106
|
-
function readContext(args, cwd) :36-66
|
|
1107
|
-
function searchSignatures(args, cwd) :74-100
|
|
1108
|
-
function getMap(args, cwd) :108-131
|
|
1109
|
-
function createCheckpoint(args, cwd) :143-215
|
|
1110
|
-
function getRouting(args, cwd) :224-261
|
|
1111
|
-
function explainFile(args, cwd) :269-356
|
|
1112
|
-
function listModules(args, cwd) :364-403
|
|
1113
|
-
function queryContext(args, cwd) :411-429
|
|
1114
|
-
function getImpact(args, cwd) :437-449
|
|
1115
|
-
function getLines(args, cwd) :458-506
|
|
1116
|
-
function readMemory(args, cwd) :514-549
|
|
1117
|
-
function getCalleeSignatures(args, cwd) :558-603
|
|
1118
|
-
function _pkgVersion(cwd) :610-613
|
|
1119
|
-
function notifyFileCreated(args, cwd) :617-639
|
|
1120
|
-
function notifySymbolAdded(args, cwd) :642-662
|
|
1121
|
-
function notifyFileDeleted(args, cwd) :665-679
|
|
1122
|
-
function _changedFiles(cwd, args) :685-697
|
|
1123
|
-
function getDiffContext(args, cwd) :706-777
|
|
1124
|
-
function getArchitectureOverview(args, cwd) :786-854
|
|
1125
|
-
function verifySuggestion(args, cwd) :864-899
|
|
1126
|
-
function squeezeOutput(args, cwd) :909-942
|
|
1127
|
-
```
|
|
1128
|
-
|
|
1129
1153
|
### src/mcp/install.js
|
|
1130
1154
|
```
|
|
1131
1155
|
module.exports = { CLIENTS, listClients, installClient, resolveTarget } :142-142
|
|
@@ -1138,11 +1162,6 @@ function _installYaml(filePath, scriptPath) :99-117
|
|
|
1138
1162
|
function installClient(client, opts = {}) → client, label, path, stat :124-140
|
|
1139
1163
|
```
|
|
1140
1164
|
|
|
1141
|
-
### src/mcp/tools.js
|
|
1142
|
-
```
|
|
1143
|
-
module.exports = { TOOLS } :363-363
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
1165
|
### src/nudge.js
|
|
1147
1166
|
```
|
|
1148
1167
|
module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD } :92-92
|
|
@@ -1200,21 +1219,6 @@ module.exports = { tokenize, STOP_WORDS } :54-54
|
|
|
1200
1219
|
function tokenize(text, opts) → string[] :31-52
|
|
1201
1220
|
```
|
|
1202
1221
|
|
|
1203
|
-
### src/review/pr-evidence.js
|
|
1204
|
-
```
|
|
1205
|
-
module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :140-140
|
|
1206
|
-
function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-77
|
|
1207
|
-
function formatPrEvidenceMarkdown(evidence, opts = {}) :82-138
|
|
1208
|
-
```
|
|
1209
|
-
|
|
1210
|
-
### src/review/review-pr.js
|
|
1211
|
-
```
|
|
1212
|
-
module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :128-128
|
|
1213
|
-
function isTestFile(p) :32-34
|
|
1214
|
-
function isSource(p) :35-37
|
|
1215
|
-
function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-126
|
|
1216
|
-
```
|
|
1217
|
-
|
|
1218
1222
|
### src/routing/classifier.js
|
|
1219
1223
|
```
|
|
1220
1224
|
module.exports = { classify, classifyAll } :102-102
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.13.0] — 2026-07-11
|
|
14
|
+
|
|
15
|
+
Minor release — **method-level blast-radius scoring** (GR2, the biggest Phase-2 lever in the master plan §7.4). The D4 call-graph finally has consumers: review findings, the PR Evidence report, and a 20th MCP tool now answer *which functions break*, not just which files.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Method-level blast-radius scoring (#468, PR #469):** new `src/graph/blast-radius.js` — for each changed file, reverse-BFS the method call-graph and score the change with a documented deterministic formula (`min(100, direct×4 + transitive×1)`; tiers none/low/medium/high/critical). Wired into three consumers: `review-pr` gains a `methodBlast` result field and a `method-blast` finding on high/critical tiers; the **PR Evidence** report gains a per-file `Method blast radius: N function(s) impacted (score X/100, tier)` line with top caller ids; and the new **`get_method_impact`** MCP tool (`symbol`, `direction: callers|callees`, `depth`) exposes per-symbol blast radius to agents — **19 → 20 MCP tools**. Graceful degradation when no call graph resolves; byte-stable output for a fixed tree.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## [8.12.0] — 2026-07-11
|
|
14
23
|
|
|
15
24
|
Minor release — **`sigmap wiki`** (D9, the final unstarted in-boundary item from the master plan §3.5 backlog). With this, every in-boundary initiative D1–D9 has shipped.
|
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.13-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)
|
|
@@ -222,7 +222,7 @@ Use SigMap with open-source tools and fully self-hosted setups:
|
|
|
222
222
|
| **JetBrains** | [Marketplace](https://plugins.jetbrains.com/plugin/31109-sigmap--ai-context-engine/) | [github.com/manojmallick/sigmap-jetbrains](https://github.com/manojmallick/sigmap-jetbrains) | IntelliJ IDEA, WebStorm, PyCharm, GoLand — tool window + actions |
|
|
223
223
|
| **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
|
|
224
224
|
|
|
225
|
-
**MCP server** —
|
|
225
|
+
**MCP server** — 20 on-demand tools for Claude Code and Cursor:
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
228
|
sigmap --mcp
|
|
@@ -267,7 +267,7 @@ SigMap treats coding agents as **consumers, not competitors**: it hands them a d
|
|
|
267
267
|
|
|
268
268
|
| Agent | One-time setup | How it consumes SigMap |
|
|
269
269
|
|---|---|---|
|
|
270
|
-
| **Claude Code** | `sigmap mcp install claude` |
|
|
270
|
+
| **Claude Code** | `sigmap mcp install claude` | 20 MCP tools (`search_signatures`, `get_lines`, `get_diff_context`, `squeeze_output`…) |
|
|
271
271
|
| **Cursor** | `sigmap mcp install cursor` | MCP tools, plus the `cursor` adapter writes `.cursorrules` |
|
|
272
272
|
| **Cline** | `sigmap mcp install cursor` | Reads `.cursorrules`; same MCP server |
|
|
273
273
|
| **Continue** | `sigmap mcp install vscode` | MCP tools inside the Continue extension |
|
package/gen-context.js
CHANGED
|
@@ -10304,6 +10304,142 @@ __factories["./src/format/verify-report"] = function(module, exports) {
|
|
|
10304
10304
|
|
|
10305
10305
|
};
|
|
10306
10306
|
|
|
10307
|
+
// ── ./src/graph/blast-radius ──
|
|
10308
|
+
__factories["./src/graph/blast-radius"] = function(module, exports) {
|
|
10309
|
+
|
|
10310
|
+
/**
|
|
10311
|
+
* Method-level blast-radius scoring (GR2).
|
|
10312
|
+
*
|
|
10313
|
+
* Consumes the D4 call-graph (src/graph/call-graph.js): for each changed file,
|
|
10314
|
+
* resolve the functions it defines, BFS the reverse call edges, and score how
|
|
10315
|
+
* much of the codebase transitively calls into the change. The score is a
|
|
10316
|
+
* documented deterministic formula — no heuristics that vary run to run — so
|
|
10317
|
+
* review-pr findings and PR Evidence lines are byte-stable for a fixed tree.
|
|
10318
|
+
*
|
|
10319
|
+
* Score: min(100, direct×4 + transitive×1). Tiers:
|
|
10320
|
+
* 0 → none · 1–9 → low · 10–29 → medium · 30–59 → high · 60+ → critical
|
|
10321
|
+
*/
|
|
10322
|
+
|
|
10323
|
+
const path = require('path');
|
|
10324
|
+
const { buildCallGraph } = __require('./src/graph/call-graph');
|
|
10325
|
+
|
|
10326
|
+
const DIRECT_WEIGHT = 4;
|
|
10327
|
+
const TRANSITIVE_WEIGHT = 1;
|
|
10328
|
+
const IMPACTED_FUNCTIONS_CAP = 12;
|
|
10329
|
+
|
|
10330
|
+
const TEST_FILE_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//;
|
|
10331
|
+
|
|
10332
|
+
function tierFor(score) {
|
|
10333
|
+
if (score >= 60) return 'critical';
|
|
10334
|
+
if (score >= 30) return 'high';
|
|
10335
|
+
if (score >= 10) return 'medium';
|
|
10336
|
+
if (score >= 1) return 'low';
|
|
10337
|
+
return 'none';
|
|
10338
|
+
}
|
|
10339
|
+
|
|
10340
|
+
function _normRel(p) {
|
|
10341
|
+
return String(p).replace(/\\/g, '/');
|
|
10342
|
+
}
|
|
10343
|
+
|
|
10344
|
+
// BFS the reverse edges from a set of symbol ids; returns direct/transitive id sets.
|
|
10345
|
+
function _bfs(seedIds, reverse, maxDepth) {
|
|
10346
|
+
const direct = new Set();
|
|
10347
|
+
const transitive = new Set();
|
|
10348
|
+
const visited = new Set(seedIds);
|
|
10349
|
+
let frontier = [];
|
|
10350
|
+
for (const s of seedIds) {
|
|
10351
|
+
for (const nb of (reverse.get(s) || [])) {
|
|
10352
|
+
if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
|
|
10353
|
+
}
|
|
10354
|
+
}
|
|
10355
|
+
let depth = 1;
|
|
10356
|
+
while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
|
|
10357
|
+
const next = [];
|
|
10358
|
+
for (const node of frontier) {
|
|
10359
|
+
for (const nb of (reverse.get(node) || [])) {
|
|
10360
|
+
if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
frontier = next;
|
|
10364
|
+
depth++;
|
|
10365
|
+
}
|
|
10366
|
+
return { direct, transitive };
|
|
10367
|
+
}
|
|
10368
|
+
|
|
10369
|
+
/**
|
|
10370
|
+
* Score the method-level blast radius of a changed-file list.
|
|
10371
|
+
*
|
|
10372
|
+
* @param {string[]} changedFiles repo-relative paths
|
|
10373
|
+
* @param {string} cwd
|
|
10374
|
+
* @param {object} [opts]
|
|
10375
|
+
* @param {object} [opts.graph] injected call graph (tests); else built from cwd
|
|
10376
|
+
* @param {number} [opts.depth=0] BFS depth limit (0 = unlimited)
|
|
10377
|
+
* @returns {{
|
|
10378
|
+
* available: boolean,
|
|
10379
|
+
* files: Array<{ file:string, symbols:number, directCallers:number,
|
|
10380
|
+
* transitiveCallers:number, testCallers:number,
|
|
10381
|
+
* impactedFunctions:string[], score:number, tier:string }>,
|
|
10382
|
+
* aggregate: { score:number, tier:string, impactedFunctions:number }
|
|
10383
|
+
* }}
|
|
10384
|
+
*/
|
|
10385
|
+
function methodBlastRadius(changedFiles, cwd, opts = {}) {
|
|
10386
|
+
const empty = { available: false, files: [], aggregate: { score: 0, tier: 'none', impactedFunctions: 0 } };
|
|
10387
|
+
let graph;
|
|
10388
|
+
try {
|
|
10389
|
+
graph = opts.graph || buildCallGraph(cwd, opts);
|
|
10390
|
+
} catch (_) {
|
|
10391
|
+
return empty;
|
|
10392
|
+
}
|
|
10393
|
+
if (!graph || !graph.defs || graph.defs.size === 0) return empty;
|
|
10394
|
+
|
|
10395
|
+
// Group defined symbol ids by their (normalized) defining file.
|
|
10396
|
+
const idsByFile = new Map();
|
|
10397
|
+
for (const [id, def] of graph.defs.entries()) {
|
|
10398
|
+
const rel = _normRel(def.file);
|
|
10399
|
+
if (!idsByFile.has(rel)) idsByFile.set(rel, []);
|
|
10400
|
+
idsByFile.get(rel).push(id);
|
|
10401
|
+
}
|
|
10402
|
+
|
|
10403
|
+
const depth = Number.isFinite(opts.depth) ? opts.depth : 0;
|
|
10404
|
+
const files = [];
|
|
10405
|
+
const allImpacted = new Set();
|
|
10406
|
+
|
|
10407
|
+
for (const changed of (changedFiles || []).map(_normRel).sort()) {
|
|
10408
|
+
const ids = idsByFile.get(changed);
|
|
10409
|
+
if (!ids || !ids.length) continue;
|
|
10410
|
+
const { direct, transitive } = _bfs(ids, graph.reverse, depth);
|
|
10411
|
+
const impacted = [...direct, ...transitive].sort();
|
|
10412
|
+
for (const id of impacted) allImpacted.add(id);
|
|
10413
|
+
const testCallers = impacted.filter((id) => {
|
|
10414
|
+
const def = graph.defs.get(id);
|
|
10415
|
+
return def && TEST_FILE_RE.test(_normRel(def.file));
|
|
10416
|
+
}).length;
|
|
10417
|
+
const score = Math.min(100, direct.size * DIRECT_WEIGHT + transitive.size * TRANSITIVE_WEIGHT);
|
|
10418
|
+
files.push({
|
|
10419
|
+
file: changed,
|
|
10420
|
+
symbols: ids.length,
|
|
10421
|
+
directCallers: direct.size,
|
|
10422
|
+
transitiveCallers: transitive.size,
|
|
10423
|
+
testCallers,
|
|
10424
|
+
impactedFunctions: impacted.slice(0, IMPACTED_FUNCTIONS_CAP),
|
|
10425
|
+
score,
|
|
10426
|
+
tier: tierFor(score),
|
|
10427
|
+
});
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
if (!files.length) return empty;
|
|
10431
|
+
const maxScore = files.reduce((m, f) => Math.max(m, f.score), 0);
|
|
10432
|
+
return {
|
|
10433
|
+
available: true,
|
|
10434
|
+
files,
|
|
10435
|
+
aggregate: { score: maxScore, tier: tierFor(maxScore), impactedFunctions: allImpacted.size },
|
|
10436
|
+
};
|
|
10437
|
+
}
|
|
10438
|
+
|
|
10439
|
+
module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT };
|
|
10440
|
+
|
|
10441
|
+
};
|
|
10442
|
+
|
|
10307
10443
|
// ── ./src/graph/builder ──
|
|
10308
10444
|
__factories["./src/graph/builder"] = function(module, exports) {
|
|
10309
10445
|
|
|
@@ -13359,6 +13495,28 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13359
13495
|
}
|
|
13360
13496
|
}
|
|
13361
13497
|
|
|
13498
|
+
/**
|
|
13499
|
+
* get_method_impact({ symbol, direction?, depth? }) → string
|
|
13500
|
+
*
|
|
13501
|
+
* Method-level blast radius (GR2): every function that transitively calls
|
|
13502
|
+
* `symbol` (direction 'callers', default), or everything it calls ('callees').
|
|
13503
|
+
*/
|
|
13504
|
+
function getMethodImpact(args, cwd) {
|
|
13505
|
+
if (!args || !args.symbol) return 'Missing required argument: symbol';
|
|
13506
|
+
|
|
13507
|
+
try {
|
|
13508
|
+
const { methodImpact, methodCallees, formatCallGraph } = __require('./src/graph/call-graph');
|
|
13509
|
+
const kind = args.direction === 'callees' ? 'callees' : 'callers';
|
|
13510
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 0);
|
|
13511
|
+
const result = kind === 'callees'
|
|
13512
|
+
? methodCallees(args.symbol, cwd, { depth })
|
|
13513
|
+
: methodImpact(args.symbol, cwd, { depth });
|
|
13514
|
+
return formatCallGraph(result, kind);
|
|
13515
|
+
} catch (err) {
|
|
13516
|
+
return `_get_method_impact failed: ${err.message}_`;
|
|
13517
|
+
}
|
|
13518
|
+
}
|
|
13519
|
+
|
|
13362
13520
|
/**
|
|
13363
13521
|
* get_impact({ file, depth? }) → string
|
|
13364
13522
|
*
|
|
@@ -13872,7 +14030,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
13872
14030
|
return header + sq.squeezed;
|
|
13873
14031
|
}
|
|
13874
14032
|
|
|
13875
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
14033
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
13876
14034
|
|
|
13877
14035
|
};
|
|
13878
14036
|
|
|
@@ -14039,11 +14197,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14039
14197
|
|
|
14040
14198
|
const readline = require('readline');
|
|
14041
14199
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
14042
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
|
|
14200
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
|
|
14043
14201
|
|
|
14044
14202
|
const SERVER_INFO = {
|
|
14045
14203
|
name: 'sigmap',
|
|
14046
|
-
version: '8.
|
|
14204
|
+
version: '8.13.0',
|
|
14047
14205
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14048
14206
|
};
|
|
14049
14207
|
|
|
@@ -14099,6 +14257,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14099
14257
|
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
14100
14258
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
14101
14259
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
14260
|
+
else if (name === 'get_method_impact') text = getMethodImpact(args, cwd);
|
|
14102
14261
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
14103
14262
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
14104
14263
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
@@ -14170,12 +14329,12 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14170
14329
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
14171
14330
|
|
|
14172
14331
|
/**
|
|
14173
|
-
* MCP tool definitions for SigMap (
|
|
14332
|
+
* MCP tool definitions for SigMap (20 tools).
|
|
14174
14333
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
14175
|
-
* explain_file, list_modules, query_context,
|
|
14176
|
-
* get_callee_signatures, sigmap_notify_file_created,
|
|
14177
|
-
* sigmap_notify_file_deleted, get_diff_context,
|
|
14178
|
-
* verify_suggestion, squeeze_output.
|
|
14334
|
+
* explain_file, list_modules, query_context, get_method_impact, get_impact,
|
|
14335
|
+
* get_lines, read_memory, get_callee_signatures, sigmap_notify_file_created,
|
|
14336
|
+
* sigmap_notify_symbol_added, sigmap_notify_file_deleted, get_diff_context,
|
|
14337
|
+
* get_architecture_overview, verify_suggestion, squeeze_output.
|
|
14179
14338
|
*/
|
|
14180
14339
|
|
|
14181
14340
|
const TOOLS = [
|
|
@@ -14317,6 +14476,35 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
14317
14476
|
required: ['query'],
|
|
14318
14477
|
},
|
|
14319
14478
|
},
|
|
14479
|
+
{
|
|
14480
|
+
name: 'get_method_impact',
|
|
14481
|
+
description:
|
|
14482
|
+
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
14483
|
+
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
14484
|
+
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
14485
|
+
'JS/TS + Python call-graph; deterministic, no LLM.',
|
|
14486
|
+
inputSchema: {
|
|
14487
|
+
type: 'object',
|
|
14488
|
+
properties: {
|
|
14489
|
+
symbol: {
|
|
14490
|
+
type: 'string',
|
|
14491
|
+
description:
|
|
14492
|
+
'Function/method name (e.g. "validateToken") or a full "file#name" id ' +
|
|
14493
|
+
'(e.g. "src/auth/session.js#validateToken") to disambiguate.',
|
|
14494
|
+
},
|
|
14495
|
+
direction: {
|
|
14496
|
+
type: 'string',
|
|
14497
|
+
enum: ['callers', 'callees'],
|
|
14498
|
+
description: '"callers" (default) = blast radius; "callees" = what the symbol calls.',
|
|
14499
|
+
},
|
|
14500
|
+
depth: {
|
|
14501
|
+
type: 'number',
|
|
14502
|
+
description: 'BFS depth limit (default 0 = unlimited).',
|
|
14503
|
+
},
|
|
14504
|
+
},
|
|
14505
|
+
required: ['symbol'],
|
|
14506
|
+
},
|
|
14507
|
+
},
|
|
14320
14508
|
{
|
|
14321
14509
|
name: 'get_impact',
|
|
14322
14510
|
description:
|
|
@@ -15723,6 +15911,12 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15723
15911
|
impactByFile = new Map(analyzeImpact(srcPaths, cwd, { depth }).map((r) => [r.file, r.impact]));
|
|
15724
15912
|
} catch (_) { /* graph optional */ }
|
|
15725
15913
|
|
|
15914
|
+
// GR2: method-level blast radius per changed file (reviewPr already computed
|
|
15915
|
+
// it when the call graph resolved — reuse, don't rebuild the graph).
|
|
15916
|
+
const methodBlastByFile = new Map(
|
|
15917
|
+
(review.methodBlast && review.methodBlast.files || []).map((m) => [m.file, m])
|
|
15918
|
+
);
|
|
15919
|
+
|
|
15726
15920
|
const fileReports = files.map((f) => {
|
|
15727
15921
|
const deleted = f.status === 'D';
|
|
15728
15922
|
let signatures = [];
|
|
@@ -15731,6 +15925,7 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15731
15925
|
}
|
|
15732
15926
|
const impact = impactByFile.get(f.path) || null;
|
|
15733
15927
|
return {
|
|
15928
|
+
methodBlast: methodBlastByFile.get(f.path.replace(/\\/g, '/')) || null,
|
|
15734
15929
|
path: f.path,
|
|
15735
15930
|
status: f.status,
|
|
15736
15931
|
riskLabel: riskLabelFor(f.path),
|
|
@@ -15773,6 +15968,7 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15773
15968
|
else if (f.type === 'security-file') L.push(`- ⚠️ **sensitive path touched** (path heuristic, not a content scan) — \`${f.file}\``);
|
|
15774
15969
|
else if (f.type === 'secret-detected') L.push(`- 🔑 **secret detected** (${f.secret}) — \`${f.file}\``);
|
|
15775
15970
|
else if (f.type === 'god-node') L.push(`- ⚠️ **god node** — \`${f.file}\` → ${f.count} dependents (high blast radius)`);
|
|
15971
|
+
else if (f.type === 'method-blast') L.push(`- ⚠️ **method blast radius ${f.tier}** — \`${f.file}\` → ${f.functions} function(s) transitively call into this change (score ${f.score}/100)`);
|
|
15776
15972
|
else if (f.type === 'scope-drift') L.push(`- ⚠️ **scope drift** — ${f.count} top-level dirs touched (${f.dirs.join(', ')})`);
|
|
15777
15973
|
}
|
|
15778
15974
|
L.push('');
|
|
@@ -15794,6 +15990,15 @@ __factories["./src/review/pr-evidence"] = function(module, exports) {
|
|
|
15794
15990
|
} else {
|
|
15795
15991
|
L.push('**Blast radius:** _(not in dependency graph — new or leaf file)_');
|
|
15796
15992
|
}
|
|
15993
|
+
if (f.methodBlast && (f.methodBlast.directCallers + f.methodBlast.transitiveCallers) > 0) {
|
|
15994
|
+
const mb = f.methodBlast;
|
|
15995
|
+
const total = mb.directCallers + mb.transitiveCallers;
|
|
15996
|
+
L.push(
|
|
15997
|
+
`**Method blast radius:** ${total} function(s) impacted (score ${mb.score}/100, ${mb.tier}) — ` +
|
|
15998
|
+
mb.impactedFunctions.slice(0, 6).map((id) => '`' + id + '`').join(', ') +
|
|
15999
|
+
(total > 6 ? ` +${total - 6} more` : '')
|
|
16000
|
+
);
|
|
16001
|
+
}
|
|
15797
16002
|
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.slice(0, 8).map((t) => '`' + t + '`').join(', ')}`);
|
|
15798
16003
|
|
|
15799
16004
|
if (f.signatures.length) {
|
|
@@ -15860,7 +16065,7 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15860
16065
|
* @param {object} [opts]
|
|
15861
16066
|
* @param {number} [opts.godNodeThreshold=15]
|
|
15862
16067
|
* @param {number} [opts.scopeThreshold=5]
|
|
15863
|
-
* @returns {{ findings: object[], blast: object[], summary: object }}
|
|
16068
|
+
* @returns {{ findings: object[], blast: object[], methodBlast: object|null, summary: object }}
|
|
15864
16069
|
*/
|
|
15865
16070
|
function reviewPr(changedFiles, cwd, opts = {}) {
|
|
15866
16071
|
const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
|
|
@@ -15921,6 +16126,27 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15921
16126
|
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
15922
16127
|
}
|
|
15923
16128
|
|
|
16129
|
+
// 3b. Method-level blast radius (GR2) — how many FUNCTIONS transitively call
|
|
16130
|
+
// into the change, scored deterministically. Graph optional, like 3.
|
|
16131
|
+
let methodBlast = null;
|
|
16132
|
+
if (srcChanged.length) {
|
|
16133
|
+
try {
|
|
16134
|
+
const { methodBlastRadius } = __require('./src/graph/blast-radius');
|
|
16135
|
+
const mb = methodBlastRadius(srcChanged, cwd, opts.methodBlastOpts || {});
|
|
16136
|
+
if (mb.available) {
|
|
16137
|
+
methodBlast = mb;
|
|
16138
|
+
for (const f of mb.files) {
|
|
16139
|
+
if (f.tier === 'high' || f.tier === 'critical') {
|
|
16140
|
+
findings.push({
|
|
16141
|
+
type: 'method-blast', file: f.file, severity: 'warn',
|
|
16142
|
+
functions: f.directCallers + f.transitiveCallers, score: f.score, tier: f.tier,
|
|
16143
|
+
});
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
} catch (_) { /* call graph optional */ }
|
|
16148
|
+
}
|
|
16149
|
+
|
|
15924
16150
|
// 4. Scope drift: distinct top-level directories touched.
|
|
15925
16151
|
const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
|
|
15926
16152
|
if (dirs.length > scopeThreshold) {
|
|
@@ -15931,6 +16157,7 @@ __factories["./src/review/review-pr"] = function(module, exports) {
|
|
|
15931
16157
|
return {
|
|
15932
16158
|
findings,
|
|
15933
16159
|
blast,
|
|
16160
|
+
methodBlast,
|
|
15934
16161
|
summary: {
|
|
15935
16162
|
filesChanged: files.length,
|
|
15936
16163
|
sourceChanged: srcChanged.length,
|
|
@@ -18987,7 +19214,7 @@ function __tryGit(args, opts = {}) {
|
|
|
18987
19214
|
catch (_) { return ''; }
|
|
18988
19215
|
}
|
|
18989
19216
|
|
|
18990
|
-
const VERSION = '8.
|
|
19217
|
+
const VERSION = '8.13.0';
|
|
18991
19218
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
18992
19219
|
|
|
18993
19220
|
function requireSourceOrBundled(key) {
|
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.13.0 | Benchmark: sigmap-v8.13-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.13-main, 2026-07-11)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -26,7 +26,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
26
26
|
| Task success proxy | 10% | 67.8% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.44 (49.2% fewer) |
|
|
28
28
|
| Supported languages | — | 33 |
|
|
29
|
-
| MCP tools | — |
|
|
29
|
+
| MCP tools | — | 20 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
31
31
|
|
|
32
32
|
---
|
|
@@ -136,7 +136,7 @@ sigmap --version Show version
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
-
## MCP server —
|
|
139
|
+
## MCP server — 20 tools
|
|
140
140
|
|
|
141
141
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
142
142
|
|
|
@@ -208,6 +208,14 @@ Rank and return the most relevant files for a specific task or question. Uses ke
|
|
|
208
208
|
Input: { query: string, topK?: number }
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
+
### get_method_impact
|
|
212
|
+
|
|
213
|
+
Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — or, with direction "callees", every repo function it calls. Finer-grained than the file-level get_impact: tells an agent which functions break, not just which files. JS/TS + Python call-graph; deterministic, no LLM.
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
Input: { symbol: string, direction?: string, depth?: number }
|
|
217
|
+
```
|
|
218
|
+
|
|
211
219
|
### get_impact
|
|
212
220
|
|
|
213
221
|
Show every file that is impacted when a given file changes — direct importers, transitive importers, affected tests, and affected routes/controllers. Gives agents instant blast-radius awareness before making a change. Handles circular dependencies safely (no infinite loops).
|
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.13.0 | Benchmark: sigmap-v8.13-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,13 +23,13 @@ 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.13-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
|
|
30
30
|
- Task success: 67.8% vs 10% without SigMap
|
|
31
31
|
- Prompts per task: 1.44 vs 2.84 baseline (49.2% fewer)
|
|
32
|
-
- Languages: 33 supported · MCP tools:
|
|
32
|
+
- Languages: 33 supported · MCP tools: 20
|
|
33
33
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
34
34
|
|
|
35
35
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Method-level blast-radius scoring (GR2).
|
|
5
|
+
*
|
|
6
|
+
* Consumes the D4 call-graph (src/graph/call-graph.js): for each changed file,
|
|
7
|
+
* resolve the functions it defines, BFS the reverse call edges, and score how
|
|
8
|
+
* much of the codebase transitively calls into the change. The score is a
|
|
9
|
+
* documented deterministic formula — no heuristics that vary run to run — so
|
|
10
|
+
* review-pr findings and PR Evidence lines are byte-stable for a fixed tree.
|
|
11
|
+
*
|
|
12
|
+
* Score: min(100, direct×4 + transitive×1). Tiers:
|
|
13
|
+
* 0 → none · 1–9 → low · 10–29 → medium · 30–59 → high · 60+ → critical
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { buildCallGraph } = require('./call-graph');
|
|
18
|
+
|
|
19
|
+
const DIRECT_WEIGHT = 4;
|
|
20
|
+
const TRANSITIVE_WEIGHT = 1;
|
|
21
|
+
const IMPACTED_FUNCTIONS_CAP = 12;
|
|
22
|
+
|
|
23
|
+
const TEST_FILE_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//;
|
|
24
|
+
|
|
25
|
+
function tierFor(score) {
|
|
26
|
+
if (score >= 60) return 'critical';
|
|
27
|
+
if (score >= 30) return 'high';
|
|
28
|
+
if (score >= 10) return 'medium';
|
|
29
|
+
if (score >= 1) return 'low';
|
|
30
|
+
return 'none';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _normRel(p) {
|
|
34
|
+
return String(p).replace(/\\/g, '/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// BFS the reverse edges from a set of symbol ids; returns direct/transitive id sets.
|
|
38
|
+
function _bfs(seedIds, reverse, maxDepth) {
|
|
39
|
+
const direct = new Set();
|
|
40
|
+
const transitive = new Set();
|
|
41
|
+
const visited = new Set(seedIds);
|
|
42
|
+
let frontier = [];
|
|
43
|
+
for (const s of seedIds) {
|
|
44
|
+
for (const nb of (reverse.get(s) || [])) {
|
|
45
|
+
if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let depth = 1;
|
|
49
|
+
while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
|
|
50
|
+
const next = [];
|
|
51
|
+
for (const node of frontier) {
|
|
52
|
+
for (const nb of (reverse.get(node) || [])) {
|
|
53
|
+
if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
frontier = next;
|
|
57
|
+
depth++;
|
|
58
|
+
}
|
|
59
|
+
return { direct, transitive };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Score the method-level blast radius of a changed-file list.
|
|
64
|
+
*
|
|
65
|
+
* @param {string[]} changedFiles repo-relative paths
|
|
66
|
+
* @param {string} cwd
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {object} [opts.graph] injected call graph (tests); else built from cwd
|
|
69
|
+
* @param {number} [opts.depth=0] BFS depth limit (0 = unlimited)
|
|
70
|
+
* @returns {{
|
|
71
|
+
* available: boolean,
|
|
72
|
+
* files: Array<{ file:string, symbols:number, directCallers:number,
|
|
73
|
+
* transitiveCallers:number, testCallers:number,
|
|
74
|
+
* impactedFunctions:string[], score:number, tier:string }>,
|
|
75
|
+
* aggregate: { score:number, tier:string, impactedFunctions:number }
|
|
76
|
+
* }}
|
|
77
|
+
*/
|
|
78
|
+
function methodBlastRadius(changedFiles, cwd, opts = {}) {
|
|
79
|
+
const empty = { available: false, files: [], aggregate: { score: 0, tier: 'none', impactedFunctions: 0 } };
|
|
80
|
+
let graph;
|
|
81
|
+
try {
|
|
82
|
+
graph = opts.graph || buildCallGraph(cwd, opts);
|
|
83
|
+
} catch (_) {
|
|
84
|
+
return empty;
|
|
85
|
+
}
|
|
86
|
+
if (!graph || !graph.defs || graph.defs.size === 0) return empty;
|
|
87
|
+
|
|
88
|
+
// Group defined symbol ids by their (normalized) defining file.
|
|
89
|
+
const idsByFile = new Map();
|
|
90
|
+
for (const [id, def] of graph.defs.entries()) {
|
|
91
|
+
const rel = _normRel(def.file);
|
|
92
|
+
if (!idsByFile.has(rel)) idsByFile.set(rel, []);
|
|
93
|
+
idsByFile.get(rel).push(id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const depth = Number.isFinite(opts.depth) ? opts.depth : 0;
|
|
97
|
+
const files = [];
|
|
98
|
+
const allImpacted = new Set();
|
|
99
|
+
|
|
100
|
+
for (const changed of (changedFiles || []).map(_normRel).sort()) {
|
|
101
|
+
const ids = idsByFile.get(changed);
|
|
102
|
+
if (!ids || !ids.length) continue;
|
|
103
|
+
const { direct, transitive } = _bfs(ids, graph.reverse, depth);
|
|
104
|
+
const impacted = [...direct, ...transitive].sort();
|
|
105
|
+
for (const id of impacted) allImpacted.add(id);
|
|
106
|
+
const testCallers = impacted.filter((id) => {
|
|
107
|
+
const def = graph.defs.get(id);
|
|
108
|
+
return def && TEST_FILE_RE.test(_normRel(def.file));
|
|
109
|
+
}).length;
|
|
110
|
+
const score = Math.min(100, direct.size * DIRECT_WEIGHT + transitive.size * TRANSITIVE_WEIGHT);
|
|
111
|
+
files.push({
|
|
112
|
+
file: changed,
|
|
113
|
+
symbols: ids.length,
|
|
114
|
+
directCallers: direct.size,
|
|
115
|
+
transitiveCallers: transitive.size,
|
|
116
|
+
testCallers,
|
|
117
|
+
impactedFunctions: impacted.slice(0, IMPACTED_FUNCTIONS_CAP),
|
|
118
|
+
score,
|
|
119
|
+
tier: tierFor(score),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!files.length) return empty;
|
|
124
|
+
const maxScore = files.reduce((m, f) => Math.max(m, f.score), 0);
|
|
125
|
+
return {
|
|
126
|
+
available: true,
|
|
127
|
+
files,
|
|
128
|
+
aggregate: { score: maxScore, tier: tierFor(maxScore), impactedFunctions: allImpacted.size },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -428,6 +428,28 @@ function queryContext(args, cwd) {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* get_method_impact({ symbol, direction?, depth? }) → string
|
|
433
|
+
*
|
|
434
|
+
* Method-level blast radius (GR2): every function that transitively calls
|
|
435
|
+
* `symbol` (direction 'callers', default), or everything it calls ('callees').
|
|
436
|
+
*/
|
|
437
|
+
function getMethodImpact(args, cwd) {
|
|
438
|
+
if (!args || !args.symbol) return 'Missing required argument: symbol';
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const { methodImpact, methodCallees, formatCallGraph } = require('../graph/call-graph');
|
|
442
|
+
const kind = args.direction === 'callees' ? 'callees' : 'callers';
|
|
443
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 0);
|
|
444
|
+
const result = kind === 'callees'
|
|
445
|
+
? methodCallees(args.symbol, cwd, { depth })
|
|
446
|
+
: methodImpact(args.symbol, cwd, { depth });
|
|
447
|
+
return formatCallGraph(result, kind);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
return `_get_method_impact failed: ${err.message}_`;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
431
453
|
/**
|
|
432
454
|
* get_impact({ file, depth? }) → string
|
|
433
455
|
*
|
|
@@ -941,4 +963,4 @@ function squeezeOutput(args, cwd) {
|
|
|
941
963
|
return header + sq.squeezed;
|
|
942
964
|
}
|
|
943
965
|
|
|
944
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
|
966
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
|
package/src/mcp/server.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { TOOLS } = require('./tools');
|
|
17
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '8.
|
|
21
|
+
version: '8.13.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -74,6 +74,7 @@ function dispatch(msg, cwd) {
|
|
|
74
74
|
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
75
75
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
76
76
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
77
|
+
else if (name === 'get_method_impact') text = getMethodImpact(args, cwd);
|
|
77
78
|
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
78
79
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
79
80
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* MCP tool definitions for SigMap (
|
|
4
|
+
* MCP tool definitions for SigMap (20 tools).
|
|
5
5
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6
|
-
* explain_file, list_modules, query_context,
|
|
7
|
-
* get_callee_signatures, sigmap_notify_file_created,
|
|
8
|
-
* sigmap_notify_file_deleted, get_diff_context,
|
|
9
|
-
* verify_suggestion, squeeze_output.
|
|
6
|
+
* explain_file, list_modules, query_context, get_method_impact, get_impact,
|
|
7
|
+
* get_lines, read_memory, get_callee_signatures, sigmap_notify_file_created,
|
|
8
|
+
* sigmap_notify_symbol_added, sigmap_notify_file_deleted, get_diff_context,
|
|
9
|
+
* get_architecture_overview, verify_suggestion, squeeze_output.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const TOOLS = [
|
|
@@ -148,6 +148,35 @@ const TOOLS = [
|
|
|
148
148
|
required: ['query'],
|
|
149
149
|
},
|
|
150
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: 'get_method_impact',
|
|
153
|
+
description:
|
|
154
|
+
'Method-level blast radius for a symbol: every FUNCTION that (transitively) calls it — ' +
|
|
155
|
+
'or, with direction "callees", every repo function it calls. Finer-grained than the ' +
|
|
156
|
+
'file-level get_impact: tells an agent which functions break, not just which files. ' +
|
|
157
|
+
'JS/TS + Python call-graph; deterministic, no LLM.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
symbol: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description:
|
|
164
|
+
'Function/method name (e.g. "validateToken") or a full "file#name" id ' +
|
|
165
|
+
'(e.g. "src/auth/session.js#validateToken") to disambiguate.',
|
|
166
|
+
},
|
|
167
|
+
direction: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
enum: ['callers', 'callees'],
|
|
170
|
+
description: '"callers" (default) = blast radius; "callees" = what the symbol calls.',
|
|
171
|
+
},
|
|
172
|
+
depth: {
|
|
173
|
+
type: 'number',
|
|
174
|
+
description: 'BFS depth limit (default 0 = unlimited).',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['symbol'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
151
180
|
{
|
|
152
181
|
name: 'get_impact',
|
|
153
182
|
description:
|
|
@@ -50,6 +50,12 @@ function buildPrEvidence(changedFiles, cwd, opts = {}) {
|
|
|
50
50
|
impactByFile = new Map(analyzeImpact(srcPaths, cwd, { depth }).map((r) => [r.file, r.impact]));
|
|
51
51
|
} catch (_) { /* graph optional */ }
|
|
52
52
|
|
|
53
|
+
// GR2: method-level blast radius per changed file (reviewPr already computed
|
|
54
|
+
// it when the call graph resolved — reuse, don't rebuild the graph).
|
|
55
|
+
const methodBlastByFile = new Map(
|
|
56
|
+
(review.methodBlast && review.methodBlast.files || []).map((m) => [m.file, m])
|
|
57
|
+
);
|
|
58
|
+
|
|
53
59
|
const fileReports = files.map((f) => {
|
|
54
60
|
const deleted = f.status === 'D';
|
|
55
61
|
let signatures = [];
|
|
@@ -58,6 +64,7 @@ function buildPrEvidence(changedFiles, cwd, opts = {}) {
|
|
|
58
64
|
}
|
|
59
65
|
const impact = impactByFile.get(f.path) || null;
|
|
60
66
|
return {
|
|
67
|
+
methodBlast: methodBlastByFile.get(f.path.replace(/\\/g, '/')) || null,
|
|
61
68
|
path: f.path,
|
|
62
69
|
status: f.status,
|
|
63
70
|
riskLabel: riskLabelFor(f.path),
|
|
@@ -100,6 +107,7 @@ function formatPrEvidenceMarkdown(evidence, opts = {}) {
|
|
|
100
107
|
else if (f.type === 'security-file') L.push(`- ⚠️ **sensitive path touched** (path heuristic, not a content scan) — \`${f.file}\``);
|
|
101
108
|
else if (f.type === 'secret-detected') L.push(`- 🔑 **secret detected** (${f.secret}) — \`${f.file}\``);
|
|
102
109
|
else if (f.type === 'god-node') L.push(`- ⚠️ **god node** — \`${f.file}\` → ${f.count} dependents (high blast radius)`);
|
|
110
|
+
else if (f.type === 'method-blast') L.push(`- ⚠️ **method blast radius ${f.tier}** — \`${f.file}\` → ${f.functions} function(s) transitively call into this change (score ${f.score}/100)`);
|
|
103
111
|
else if (f.type === 'scope-drift') L.push(`- ⚠️ **scope drift** — ${f.count} top-level dirs touched (${f.dirs.join(', ')})`);
|
|
104
112
|
}
|
|
105
113
|
L.push('');
|
|
@@ -121,6 +129,15 @@ function formatPrEvidenceMarkdown(evidence, opts = {}) {
|
|
|
121
129
|
} else {
|
|
122
130
|
L.push('**Blast radius:** _(not in dependency graph — new or leaf file)_');
|
|
123
131
|
}
|
|
132
|
+
if (f.methodBlast && (f.methodBlast.directCallers + f.methodBlast.transitiveCallers) > 0) {
|
|
133
|
+
const mb = f.methodBlast;
|
|
134
|
+
const total = mb.directCallers + mb.transitiveCallers;
|
|
135
|
+
L.push(
|
|
136
|
+
`**Method blast radius:** ${total} function(s) impacted (score ${mb.score}/100, ${mb.tier}) — ` +
|
|
137
|
+
mb.impactedFunctions.slice(0, 6).map((id) => '`' + id + '`').join(', ') +
|
|
138
|
+
(total > 6 ? ` +${total - 6} more` : '')
|
|
139
|
+
);
|
|
140
|
+
}
|
|
124
141
|
if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.slice(0, 8).map((t) => '`' + t + '`').join(', ')}`);
|
|
125
142
|
|
|
126
143
|
if (f.signatures.length) {
|
package/src/review/review-pr.js
CHANGED
|
@@ -43,7 +43,7 @@ function isSource(p) {
|
|
|
43
43
|
* @param {object} [opts]
|
|
44
44
|
* @param {number} [opts.godNodeThreshold=15]
|
|
45
45
|
* @param {number} [opts.scopeThreshold=5]
|
|
46
|
-
* @returns {{ findings: object[], blast: object[], summary: object }}
|
|
46
|
+
* @returns {{ findings: object[], blast: object[], methodBlast: object|null, summary: object }}
|
|
47
47
|
*/
|
|
48
48
|
function reviewPr(changedFiles, cwd, opts = {}) {
|
|
49
49
|
const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
|
|
@@ -104,6 +104,27 @@ function reviewPr(changedFiles, cwd, opts = {}) {
|
|
|
104
104
|
blast.sort((a, b) => b.totalImpact - a.totalImpact);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// 3b. Method-level blast radius (GR2) — how many FUNCTIONS transitively call
|
|
108
|
+
// into the change, scored deterministically. Graph optional, like 3.
|
|
109
|
+
let methodBlast = null;
|
|
110
|
+
if (srcChanged.length) {
|
|
111
|
+
try {
|
|
112
|
+
const { methodBlastRadius } = require('../graph/blast-radius');
|
|
113
|
+
const mb = methodBlastRadius(srcChanged, cwd, opts.methodBlastOpts || {});
|
|
114
|
+
if (mb.available) {
|
|
115
|
+
methodBlast = mb;
|
|
116
|
+
for (const f of mb.files) {
|
|
117
|
+
if (f.tier === 'high' || f.tier === 'critical') {
|
|
118
|
+
findings.push({
|
|
119
|
+
type: 'method-blast', file: f.file, severity: 'warn',
|
|
120
|
+
functions: f.directCallers + f.transitiveCallers, score: f.score, tier: f.tier,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (_) { /* call graph optional */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
// 4. Scope drift: distinct top-level directories touched.
|
|
108
129
|
const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
|
|
109
130
|
if (dirs.length > scopeThreshold) {
|
|
@@ -114,6 +135,7 @@ function reviewPr(changedFiles, cwd, opts = {}) {
|
|
|
114
135
|
return {
|
|
115
136
|
findings,
|
|
116
137
|
blast,
|
|
138
|
+
methodBlast,
|
|
117
139
|
summary: {
|
|
118
140
|
filesChanged: files.length,
|
|
119
141
|
sourceChanged: srcChanged.length,
|