sigmap 8.11.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/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.11.0 | Benchmark: sigmap-v8.11-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.11-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
  ---
@@ -114,6 +114,7 @@ sigmap verify-plan <plan.md|-> Check a plan vs the live index — file
114
114
  sigmap review-pr Audit a diff — scope drift, god-node edits, missing tests, security files (--staged, --base, --json, --markdown)
115
115
  sigmap review-pr --markdown PR Evidence Report — branded Markdown (signatures + blast radius + tests) to post as a PR comment
116
116
  sigmap create "<task>" Grounded-creation pipeline: scaffold → verify-plan → verify-ai-output → review-pr (--staged)
117
+ sigmap wiki Deterministic architecture wiki from signatures + graph — no LLM (--json, --out <path>)
117
118
  sigmap squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
118
119
  sigmap squeeze --response <file|-> Minimize an agent/tool response (same engine; also exposed as the squeeze_output MCP tool)
119
120
  sigmap ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
@@ -135,7 +136,7 @@ sigmap --version Show version
135
136
 
136
137
  ---
137
138
 
138
- ## MCP server — 19 tools
139
+ ## MCP server — 20 tools
139
140
 
140
141
  Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
141
142
 
@@ -207,6 +208,14 @@ Rank and return the most relevant files for a specific task or question. Uses ke
207
208
  Input: { query: string, topK?: number }
208
209
  ```
209
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
+
210
219
  ### get_impact
211
220
 
212
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.11.0 | Benchmark: sigmap-v8.11-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.11-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.11.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.11.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.11.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.11.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,