sigmap 2.3.0 → 2.5.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/CHANGELOG.md +55 -0
- package/README.md +91 -16
- package/gen-context.js +248 -3
- package/package.json +7 -1
- package/packages/cli/index.js +63 -0
- package/packages/cli/package.json +26 -0
- package/packages/core/README.md +141 -0
- package/packages/core/index.js +215 -0
- package/packages/core/package.json +28 -0
- package/src/config/defaults.js +8 -0
- package/src/graph/builder.js +259 -0
- package/src/graph/impact.js +235 -0
- package/src/mcp/handlers.js +21 -1
- package/src/mcp/server.js +3 -2
- package/src/mcp/tools.js +24 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Impact radius calculator (v2.5).
|
|
5
|
+
*
|
|
6
|
+
* Given a changed file and a dependency graph, finds every file that
|
|
7
|
+
* transitively imports it using BFS. Handles circular dependencies safely.
|
|
8
|
+
*
|
|
9
|
+
* @module src/graph/impact
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { buildFromCwd } = require('./builder');
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Core BFS traversal
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Walk the reverse graph from `startFile` using BFS up to `maxDepth` levels.
|
|
21
|
+
* Returns separate sets for direct and transitive dependents.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} startFile - absolute path of file that changed
|
|
24
|
+
* @param {Map<string,string[]>} reverseGraph
|
|
25
|
+
* @param {number} maxDepth - 0 = unlimited
|
|
26
|
+
* @returns {{ direct: Set<string>, transitive: Set<string> }}
|
|
27
|
+
*/
|
|
28
|
+
function bfs(startFile, reverseGraph, maxDepth) {
|
|
29
|
+
const direct = new Set();
|
|
30
|
+
const transitive = new Set();
|
|
31
|
+
const visited = new Set([startFile]);
|
|
32
|
+
|
|
33
|
+
// Level 1 — direct importers
|
|
34
|
+
const firstLevel = reverseGraph.get(startFile) || [];
|
|
35
|
+
for (const f of firstLevel) {
|
|
36
|
+
if (!visited.has(f)) {
|
|
37
|
+
direct.add(f);
|
|
38
|
+
visited.add(f);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (maxDepth === 1) return { direct, transitive };
|
|
43
|
+
|
|
44
|
+
// BFS for deeper levels
|
|
45
|
+
let frontier = [...direct];
|
|
46
|
+
let depth = 1;
|
|
47
|
+
|
|
48
|
+
while (frontier.length > 0 && (maxDepth === 0 || depth < maxDepth)) {
|
|
49
|
+
const nextFrontier = [];
|
|
50
|
+
for (const node of frontier) {
|
|
51
|
+
const importers = reverseGraph.get(node) || [];
|
|
52
|
+
for (const imp of importers) {
|
|
53
|
+
if (!visited.has(imp)) {
|
|
54
|
+
transitive.add(imp);
|
|
55
|
+
visited.add(imp);
|
|
56
|
+
nextFrontier.push(imp);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
frontier = nextFrontier;
|
|
61
|
+
depth++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { direct, transitive };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Helper: classify impacted files into tests / routes / other
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const TEST_PATTERNS = [
|
|
72
|
+
/[./\\](test|tests|spec|__tests__)[./\\]/,
|
|
73
|
+
/\.(test|spec)\.[jt]sx?$/,
|
|
74
|
+
/_test\.[jt]sx?$/,
|
|
75
|
+
/_test\.py$/,
|
|
76
|
+
/test_[^/\\]+\.py$/,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const ROUTE_PATTERNS = [
|
|
80
|
+
/router?\.[jt]sx?$/i,
|
|
81
|
+
/routes?\.[jt]sx?$/i,
|
|
82
|
+
/controller\.[jt]sx?$/i,
|
|
83
|
+
/views?\.[jt]sx?$/i,
|
|
84
|
+
/handlers?\.[jt]sx?$/i,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function isTestFile(f) { return TEST_PATTERNS.some((re) => re.test(f.replace(/\\/g, '/'))); }
|
|
88
|
+
function isRouteFile(f) { return ROUTE_PATTERNS.some((re) => re.test(f.replace(/\\/g, '/'))); }
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Public API
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compute the impact of changing `changedFile`.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} changedFile - absolute or cwd-relative path
|
|
98
|
+
* @param {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }} graph
|
|
99
|
+
* @param {object} [opts]
|
|
100
|
+
* @param {number} [opts.depth=0] - BFS depth limit (0 = unlimited)
|
|
101
|
+
* @param {string} [opts.cwd] - project root for relative path display
|
|
102
|
+
* @returns {{
|
|
103
|
+
* changed: string,
|
|
104
|
+
* direct: string[],
|
|
105
|
+
* transitive: string[],
|
|
106
|
+
* tests: string[],
|
|
107
|
+
* routes: string[],
|
|
108
|
+
* totalImpact: number
|
|
109
|
+
* }}
|
|
110
|
+
*/
|
|
111
|
+
function getImpact(changedFile, graph, opts) {
|
|
112
|
+
const { depth = 0, cwd = process.cwd() } = opts || {};
|
|
113
|
+
|
|
114
|
+
const absChanged = path.resolve(cwd, changedFile);
|
|
115
|
+
|
|
116
|
+
// Bail gracefully if file not in graph
|
|
117
|
+
if (!graph || !graph.reverse) {
|
|
118
|
+
return { changed: changedFile, direct: [], transitive: [], tests: [], routes: [], totalImpact: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { direct, transitive } = bfs(absChanged, graph.reverse, depth);
|
|
122
|
+
|
|
123
|
+
const allImpacted = [...direct, ...transitive];
|
|
124
|
+
const tests = allImpacted.filter(isTestFile);
|
|
125
|
+
const routes = allImpacted.filter(isRouteFile);
|
|
126
|
+
|
|
127
|
+
const toRel = (f) => path.relative(cwd, f).replace(/\\/g, '/');
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
changed: toRel(absChanged),
|
|
131
|
+
direct: [...direct].map(toRel),
|
|
132
|
+
transitive: [...transitive].map(toRel),
|
|
133
|
+
tests: tests.map(toRel),
|
|
134
|
+
routes: routes.map(toRel),
|
|
135
|
+
totalImpact: direct.size + transitive.size,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Analyse the impact of one or more changed files, building the graph from cwd.
|
|
141
|
+
* This is the high-level convenience function used by the CLI and MCP tool.
|
|
142
|
+
*
|
|
143
|
+
* @param {string|string[]} changedFiles
|
|
144
|
+
* @param {string} cwd
|
|
145
|
+
* @param {object} [opts]
|
|
146
|
+
* @param {number} [opts.depth=3]
|
|
147
|
+
* @param {string[]} [opts.srcDirs]
|
|
148
|
+
* @param {string[]} [opts.exclude]
|
|
149
|
+
* @returns {{ file: string, impact: object }[]}
|
|
150
|
+
*/
|
|
151
|
+
function analyzeImpact(changedFiles, cwd, opts) {
|
|
152
|
+
const { depth = 3 } = opts || {};
|
|
153
|
+
const files = Array.isArray(changedFiles) ? changedFiles : [changedFiles];
|
|
154
|
+
|
|
155
|
+
let graph;
|
|
156
|
+
try {
|
|
157
|
+
graph = buildFromCwd(cwd, opts);
|
|
158
|
+
} catch (_) {
|
|
159
|
+
graph = { forward: new Map(), reverse: new Map() };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return files.map((f) => ({
|
|
163
|
+
file: f,
|
|
164
|
+
impact: getImpact(f, graph, { depth, cwd }),
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Formatting helpers
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format an impact result as a readable markdown string.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} result - return value of getImpact()
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
function formatImpact(result) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
lines.push(`## Impact: \`${result.changed}\``);
|
|
181
|
+
lines.push('');
|
|
182
|
+
|
|
183
|
+
if (result.direct.length === 0 && result.transitive.length === 0) {
|
|
184
|
+
lines.push('_No files import this file — zero blast radius._');
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push(`**Total impacted files:** ${result.totalImpact}`);
|
|
189
|
+
lines.push('');
|
|
190
|
+
|
|
191
|
+
if (result.direct.length > 0) {
|
|
192
|
+
lines.push('### Direct importers');
|
|
193
|
+
for (const f of result.direct) lines.push(`- \`${f}\``);
|
|
194
|
+
lines.push('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (result.transitive.length > 0) {
|
|
198
|
+
lines.push('### Transitive importers');
|
|
199
|
+
for (const f of result.transitive) lines.push(`- \`${f}\``);
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (result.tests.length > 0) {
|
|
204
|
+
lines.push('### Affected tests');
|
|
205
|
+
for (const f of result.tests) lines.push(`- \`${f}\``);
|
|
206
|
+
lines.push('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (result.routes.length > 0) {
|
|
210
|
+
lines.push('### Affected routes / controllers');
|
|
211
|
+
for (const f of result.routes) lines.push(`- \`${f}\``);
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Format an impact result as a JSON-serialisable object.
|
|
220
|
+
*
|
|
221
|
+
* @param {object} result - return value of getImpact()
|
|
222
|
+
* @returns {object}
|
|
223
|
+
*/
|
|
224
|
+
function formatImpactJSON(result) {
|
|
225
|
+
return {
|
|
226
|
+
changed: result.changed,
|
|
227
|
+
direct: result.direct,
|
|
228
|
+
transitive: result.transitive,
|
|
229
|
+
tests: result.tests,
|
|
230
|
+
routes: result.routes,
|
|
231
|
+
totalImpact: result.totalImpact,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { getImpact, analyzeImpact, formatImpact, formatImpactJSON };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -457,4 +457,24 @@ function queryContext(args, cwd) {
|
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
|
|
460
|
+
/**
|
|
461
|
+
* get_impact({ file, depth? }) → string
|
|
462
|
+
*
|
|
463
|
+
* Returns a formatted markdown impact report for the given file:
|
|
464
|
+
* direct importers, transitive importers, affected tests, affected routes.
|
|
465
|
+
*/
|
|
466
|
+
function getImpact(args, cwd) {
|
|
467
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const { analyzeImpact, formatImpact } = require('../graph/impact');
|
|
471
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 3);
|
|
472
|
+
const results = analyzeImpact(args.file, cwd, { depth });
|
|
473
|
+
if (results.length === 0) return `No impact data for: ${args.file}`;
|
|
474
|
+
return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return `_get_impact failed: ${err.message}_`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
|
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 } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '2.
|
|
21
|
+
version: '2.5.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_impact') text = getImpact(args, cwd);
|
|
77
78
|
else {
|
|
78
79
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
79
80
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -144,6 +144,30 @@ const TOOLS = [
|
|
|
144
144
|
required: ['query'],
|
|
145
145
|
},
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
name: 'get_impact',
|
|
149
|
+
description:
|
|
150
|
+
'Show every file that is impacted when a given file changes — direct importers, ' +
|
|
151
|
+
'transitive importers, affected tests, and affected routes/controllers. ' +
|
|
152
|
+
'Gives agents instant blast-radius awareness before making a change. ' +
|
|
153
|
+
'Handles circular dependencies safely (no infinite loops).',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
file: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description:
|
|
160
|
+
'Relative path from the project root of the file that changed ' +
|
|
161
|
+
'(e.g. "src/extractors/python.js"). Use forward slashes.',
|
|
162
|
+
},
|
|
163
|
+
depth: {
|
|
164
|
+
type: 'number',
|
|
165
|
+
description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ['file'],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
147
171
|
];
|
|
148
172
|
|
|
149
173
|
module.exports = { TOOLS };
|