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 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 — 0 seconds ago)
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 } :140-140
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-107
204
- function start(cwd) :112-138
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.12-main (21 repositories, including R language)
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** — 19 on-demand tools for Claude Code and Cursor:
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` | 19 MCP tools (`search_signatures`, `get_lines`, `get_diff_context`, `squeeze_output`…) |
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.12.0',
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 (19 tools).
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, get_impact, get_lines, read_memory,
14176
- * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
14177
- * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview,
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.12.0';
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.12.0 | Benchmark: sigmap-v8.12-main (2026-07-11)
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.12-main, 2026-07-11)
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 | — | 19 |
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 — 19 tools
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.12.0 | Benchmark: sigmap-v8.12-main (2026-07-11)
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.12-main, 2026-07-11)
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: 19
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.12.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 };
@@ -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.12.0',
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 (19 tools).
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, get_impact, get_lines, read_memory,
7
- * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
8
- * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview,
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) {
@@ -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,