skannr 0.1.2 → 0.1.4
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/.eslintrc.js +22 -22
- package/README.md +260 -151
- package/dist/agent-cli.js +25 -25
- package/dist/agent.js +4 -4
- package/dist/blast-radius.d.ts +80 -0
- package/dist/blast-radius.d.ts.map +1 -0
- package/dist/blast-radius.js +545 -0
- package/dist/blast-radius.js.map +1 -0
- package/dist/cli.js +55 -17
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +98 -24
- package/dist/mcp-server.js.map +1 -1
- package/dist/telemetry.js +12 -12
- package/gemini-extension/GEMINI.md +26 -26
- package/gemini-extension/gemini-extension.json +12 -12
- package/gemini-extension/package.json +14 -14
- package/gemini-extension/src/server.ts +69 -69
- package/gemini-extension/tsconfig.json +14 -14
- package/jest.config.js +4 -4
- package/package.json +54 -53
- package/src/agent-cli.ts +397 -397
- package/src/agent.ts +344 -344
- package/src/benchmark.ts +389 -389
- package/src/blast-radius.ts +661 -0
- package/src/cache.ts +317 -317
- package/src/cli.ts +461 -415
- package/src/config.ts +22 -22
- package/src/formatter.ts +105 -105
- package/src/index.ts +223 -223
- package/src/languages/GenericAdapter.ts +44 -44
- package/src/languages/LanguageAdapter.ts +21 -21
- package/src/languages/PythonAdapter.ts +285 -285
- package/src/languages/TypeScriptAdapter.ts +453 -453
- package/src/languages/registry.ts +49 -49
- package/src/mapper.ts +455 -455
- package/src/mcp-cli.ts +8 -8
- package/src/mcp-server.ts +151 -103
- package/src/ranker-enhanced.ts +514 -514
- package/src/ranker.ts +110 -110
- package/src/scanner.ts +226 -226
- package/src/skeletonizer.ts +22 -22
- package/src/telemetry.ts +146 -146
- package/src/tokenizer.ts +20 -20
- package/src/types.ts +92 -92
- package/src/watcher.ts +119 -119
- package/src/why.ts +88 -88
- package/tests/agent.tools.test.ts +81 -81
- package/tests/benchmark.test.ts +30 -30
- package/tests/blast-radius.test.ts +290 -0
- package/tests/fixtures/sample.py +17 -17
- package/tests/fixtures/sample.ts +13 -13
- package/tests/fixtures/src/api/routes.ts +1 -1
- package/tests/fixtures/src/auth/permission.ts +3 -3
- package/tests/python-adapter.test.ts +31 -31
- package/tests/ranker-enhanced.test.ts +70 -70
- package/tests/ranker.test.ts +79 -79
- package/tests/scanner.scope.test.ts +66 -66
- package/tests/setup-fixtures.js +28 -28
- package/tests/skeletonizer.test.ts +149 -149
- package/tsconfig.json +21 -21
- package/uca-landing/index.html +17 -17
- package/uca-landing/package.json +23 -23
- package/uca-landing/postcss.config.js +6 -6
- package/uca-landing/src/App.jsx +43 -43
- package/uca-landing/src/components/AgentMode.jsx +45 -45
- package/uca-landing/src/components/CliReference.jsx +58 -58
- package/uca-landing/src/components/Features.jsx +82 -82
- package/uca-landing/src/components/Footer.jsx +35 -35
- package/uca-landing/src/components/Hero.jsx +125 -125
- package/uca-landing/src/components/HowItWorks.jsx +60 -60
- package/uca-landing/src/components/Install.jsx +103 -103
- package/uca-landing/src/components/LanguageSupport.jsx +63 -63
- package/uca-landing/src/components/Navbar.jsx +86 -86
- package/uca-landing/src/components/Problem.jsx +51 -51
- package/uca-landing/src/components/Reveal.jsx +40 -40
- package/uca-landing/src/components/WorksWith.jsx +59 -59
- package/uca-landing/src/hooks/useScrollNav.js +13 -13
- package/uca-landing/src/hooks/useTypewriter.js +41 -41
- package/uca-landing/src/index.css +13 -13
- package/uca-landing/src/main.jsx +10 -10
- package/uca-landing/tailwind.config.js +68 -68
- package/uca-landing/vercel.json +3 -3
- package/uca-landing/vite.config.js +6 -6
- package/dist/rocket-chat-scope.d.ts +0 -7
- package/dist/rocket-chat-scope.d.ts.map +0 -1
- package/dist/rocket-chat-scope.js +0 -95
- package/dist/rocket-chat-scope.js.map +0 -1
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Blast Radius: determines downstream impact and risk of a git diff.
|
|
4
|
+
*
|
|
5
|
+
* Pipeline: parse diff → identify changed files → traverse reverse dependency
|
|
6
|
+
* graph N hops → score affected nodes by centrality → flag untested nodes →
|
|
7
|
+
* compute aggregate risk score.
|
|
8
|
+
*
|
|
9
|
+
* Limitation (v1): graph traversal is file-level, not symbol-level.
|
|
10
|
+
* A changed file is treated as a unit; individual function-call edges are not
|
|
11
|
+
* tracked. Symbol extraction from diffs is used for reporting only.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
47
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.computeBlastRadius = computeBlastRadius;
|
|
51
|
+
exports.buildReverseGraph = buildReverseGraph;
|
|
52
|
+
exports.traverseAffected = traverseAffected;
|
|
53
|
+
exports.formatBlastRadiusText = formatBlastRadiusText;
|
|
54
|
+
exports.formatBlastRadiusJson = formatBlastRadiusJson;
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const child_process_1 = require("child_process");
|
|
58
|
+
const parse_diff_1 = __importDefault(require("parse-diff"));
|
|
59
|
+
const scanner_1 = require("./scanner");
|
|
60
|
+
const ranker_enhanced_1 = require("./ranker-enhanced");
|
|
61
|
+
const registry_1 = require("./languages/registry");
|
|
62
|
+
const config_1 = require("./config");
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Risk formula weights (sum = 10). Shift toward WEIGHT_UNTESTED because
|
|
65
|
+
// untested affected code is the strongest signal that a merge is risky.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
const WEIGHT_AFFECTED_COUNT = 2.5;
|
|
68
|
+
const WEIGHT_CENTRALITY = 2.5;
|
|
69
|
+
const WEIGHT_UNTESTED = 3.5;
|
|
70
|
+
const WEIGHT_HOP_SPREAD = 1.5;
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Public entry point
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/** Compute the blast radius for a diff against a project. */
|
|
75
|
+
function computeBlastRadius(options) {
|
|
76
|
+
const { root, hops = 2 } = options;
|
|
77
|
+
const diff = resolveDiff(options);
|
|
78
|
+
const parsedFiles = (0, parse_diff_1.default)(diff);
|
|
79
|
+
// 1. Extract changed file paths (relative to root).
|
|
80
|
+
const changedFiles = extractChangedFiles(parsedFiles, root);
|
|
81
|
+
if (changedFiles.length === 0) {
|
|
82
|
+
return emptyResult(hops);
|
|
83
|
+
}
|
|
84
|
+
// 2. Extract changed symbols for reporting.
|
|
85
|
+
const changedSymbols = extractChangedSymbolsFromDiff(parsedFiles, root);
|
|
86
|
+
// 3. Build reverse dependency graph for the project.
|
|
87
|
+
const allFiles = scanAllProjectFiles(root);
|
|
88
|
+
const forwardGraph = (0, ranker_enhanced_1.analyzeDependencyGraph)(allFiles, root);
|
|
89
|
+
const reverseGraph = buildReverseGraph(forwardGraph);
|
|
90
|
+
// 4. BFS N hops outward on reverse graph from changed files.
|
|
91
|
+
const affectedMap = traverseAffected(changedFiles, reverseGraph, hops);
|
|
92
|
+
// 5. Compute centrality for each affected node.
|
|
93
|
+
const centralityMap = computeCentralityMap(reverseGraph, allFiles, root);
|
|
94
|
+
// 6. Detect test files.
|
|
95
|
+
const testFileSet = detectTestFiles(allFiles, root);
|
|
96
|
+
// 7. Assemble AffectedNode list.
|
|
97
|
+
const affectedNodes = buildAffectedNodes(affectedMap, centralityMap, testFileSet);
|
|
98
|
+
// 8. Compute risk score.
|
|
99
|
+
const formulaInputs = computeFormulaInputs(affectedNodes, allFiles.length, hops);
|
|
100
|
+
const riskScore = computeRisk(formulaInputs);
|
|
101
|
+
const summary = buildSummary(riskScore, changedFiles, affectedNodes);
|
|
102
|
+
return {
|
|
103
|
+
riskScore,
|
|
104
|
+
summary,
|
|
105
|
+
changedFiles,
|
|
106
|
+
changedSymbols,
|
|
107
|
+
affectedNodes,
|
|
108
|
+
formulaInputs,
|
|
109
|
+
hops,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Diff resolution
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
/** Resolve the diff content from options: file path > explicit string > git working tree. */
|
|
116
|
+
function resolveDiff(options) {
|
|
117
|
+
if (options.diffPath) {
|
|
118
|
+
const absPath = path.resolve(options.root, options.diffPath);
|
|
119
|
+
if (!fs.existsSync(absPath)) {
|
|
120
|
+
throw new Error(`Diff file not found: ${absPath}`);
|
|
121
|
+
}
|
|
122
|
+
return fs.readFileSync(absPath, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
if (options.diffContent !== undefined) {
|
|
125
|
+
return options.diffContent;
|
|
126
|
+
}
|
|
127
|
+
// Default: working tree diff against HEAD.
|
|
128
|
+
try {
|
|
129
|
+
return (0, child_process_1.execSync)('git diff HEAD', {
|
|
130
|
+
cwd: options.root,
|
|
131
|
+
encoding: 'utf-8',
|
|
132
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
throw new Error(`Failed to run "git diff HEAD" in ${options.root}: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Changed file extraction
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/** Map diff file entries to relative paths within the project root. */
|
|
143
|
+
function extractChangedFiles(parsedFiles, root) {
|
|
144
|
+
const files = [];
|
|
145
|
+
for (const file of parsedFiles) {
|
|
146
|
+
// parse-diff gives paths like "a/src/foo.ts" or "b/src/foo.ts"
|
|
147
|
+
const raw = file.to ?? file.from;
|
|
148
|
+
if (!raw || raw === '/dev/null')
|
|
149
|
+
continue;
|
|
150
|
+
// Strip the leading a/ or b/ prefix that git uses.
|
|
151
|
+
const rel = raw.replace(/^[ab]\//, '');
|
|
152
|
+
// Only include if the file exists in the project.
|
|
153
|
+
if (fs.existsSync(path.join(root, rel))) {
|
|
154
|
+
files.push(rel);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(files)];
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Changed symbol extraction (reporting only — traversal is file-level)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/** Identify which symbols fall within changed line ranges in the diff. */
|
|
163
|
+
function extractChangedSymbolsFromDiff(parsedFiles, root) {
|
|
164
|
+
const symbols = [];
|
|
165
|
+
for (const file of parsedFiles) {
|
|
166
|
+
const raw = file.to ?? file.from;
|
|
167
|
+
if (!raw || raw === '/dev/null')
|
|
168
|
+
continue;
|
|
169
|
+
const rel = raw.replace(/^[ab]\//, '');
|
|
170
|
+
const absPath = path.join(root, rel);
|
|
171
|
+
if (!fs.existsSync(absPath))
|
|
172
|
+
continue;
|
|
173
|
+
const content = (0, scanner_1.readFileContent)(absPath);
|
|
174
|
+
if (!content)
|
|
175
|
+
continue;
|
|
176
|
+
const adapter = (0, registry_1.getAdapter)(absPath);
|
|
177
|
+
const fileSymbols = adapter.extractSymbols(content);
|
|
178
|
+
// Collect changed line numbers from the diff.
|
|
179
|
+
const changedLines = new Set();
|
|
180
|
+
for (const chunk of file.chunks) {
|
|
181
|
+
for (const change of chunk.changes) {
|
|
182
|
+
if (change.type === 'add' || change.type === 'del') {
|
|
183
|
+
// 'ln' for add, 'ln' for del (line number in respective file)
|
|
184
|
+
const lineNum = 'ln' in change ? change.ln : undefined;
|
|
185
|
+
if (typeof lineNum === 'number') {
|
|
186
|
+
changedLines.add(lineNum);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Match symbols whose line overlaps a changed line.
|
|
192
|
+
for (const sym of fileSymbols) {
|
|
193
|
+
if (sym.kind === 'export')
|
|
194
|
+
continue; // skip synthetic export markers
|
|
195
|
+
if (changedLines.has(sym.line)) {
|
|
196
|
+
symbols.push({ name: sym.name, kind: sym.kind, file: rel });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return symbols;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Reverse dependency graph
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* Invert the forward graph (file → imports) into a reverse graph
|
|
207
|
+
* (file → files that import it).
|
|
208
|
+
*/
|
|
209
|
+
function buildReverseGraph(forwardGraph) {
|
|
210
|
+
const reverse = new Map();
|
|
211
|
+
for (const [file, imports] of forwardGraph.entries()) {
|
|
212
|
+
for (const imp of imports) {
|
|
213
|
+
// The forward graph stores resolved relative paths (potentially without ext).
|
|
214
|
+
// Match by checking if the import target's basename matches any known file.
|
|
215
|
+
const importers = reverse.get(imp) ?? [];
|
|
216
|
+
importers.push(file);
|
|
217
|
+
reverse.set(imp, importers);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return reverse;
|
|
221
|
+
}
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// BFS traversal
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
/**
|
|
226
|
+
* BFS from changed files outward on the reverse graph, up to maxHops.
|
|
227
|
+
* Returns a map of affected file → hop distance (excluding the changed files
|
|
228
|
+
* themselves).
|
|
229
|
+
*/
|
|
230
|
+
function traverseAffected(changedFiles, reverseGraph, maxHops) {
|
|
231
|
+
const visited = new Map();
|
|
232
|
+
const queue = [];
|
|
233
|
+
// Seed with changed files at hop 0 (they won't appear in output).
|
|
234
|
+
for (const file of changedFiles) {
|
|
235
|
+
visited.set(file, 0);
|
|
236
|
+
queue.push({ file, hop: 0 });
|
|
237
|
+
}
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const { file, hop } = queue.shift();
|
|
240
|
+
if (hop >= maxHops)
|
|
241
|
+
continue;
|
|
242
|
+
const importers = findImporters(file, reverseGraph);
|
|
243
|
+
for (const importer of importers) {
|
|
244
|
+
if (!visited.has(importer)) {
|
|
245
|
+
const nextHop = hop + 1;
|
|
246
|
+
visited.set(importer, nextHop);
|
|
247
|
+
queue.push({ file: importer, hop: nextHop });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Remove the seed changed files from the result.
|
|
252
|
+
for (const file of changedFiles) {
|
|
253
|
+
visited.delete(file);
|
|
254
|
+
}
|
|
255
|
+
return visited;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Find files that import the target, handling partial path matching.
|
|
259
|
+
* The reverse graph keys may be partial (e.g. "src/utils" without extension)
|
|
260
|
+
* and use OS-specific separators, so we normalize and match flexibly.
|
|
261
|
+
*/
|
|
262
|
+
function findImporters(target, reverseGraph) {
|
|
263
|
+
const normalize = (p) => p.split(path.sep).join('/');
|
|
264
|
+
const normalizedTarget = normalize(target);
|
|
265
|
+
const targetNoExt = normalizedTarget.replace(/\.[^.]+$/, '');
|
|
266
|
+
const targetBase = path.basename(target, path.extname(target));
|
|
267
|
+
// Direct lookup (normalized).
|
|
268
|
+
for (const [key, files] of reverseGraph.entries()) {
|
|
269
|
+
const normalizedKey = normalize(key);
|
|
270
|
+
if (normalizedKey === normalizedTarget || normalizedKey === targetNoExt) {
|
|
271
|
+
return files;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Fuzzy match: target is "src/utils.ts", graph key might be "src/utils".
|
|
275
|
+
const importers = [];
|
|
276
|
+
for (const [key, files] of reverseGraph.entries()) {
|
|
277
|
+
const normalizedKey = normalize(key);
|
|
278
|
+
if (normalizedKey.endsWith('/' + targetBase) || normalizedKey === targetBase) {
|
|
279
|
+
importers.push(...files);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return importers;
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Centrality computation
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
/**
|
|
288
|
+
* Compute normalized in-degree centrality for all project files.
|
|
289
|
+
* Centrality = (number of files that import this file) / max(in-degrees).
|
|
290
|
+
* This reuses the reverse graph already built from analyzeDependencyGraph output.
|
|
291
|
+
*/
|
|
292
|
+
function computeCentralityMap(reverseGraph, allFiles, root) {
|
|
293
|
+
// Count in-degree for each file (by relative path).
|
|
294
|
+
const inDegree = new Map();
|
|
295
|
+
for (const file of allFiles) {
|
|
296
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
297
|
+
inDegree.set(rel, 0);
|
|
298
|
+
}
|
|
299
|
+
for (const [_, importers] of reverseGraph.entries()) {
|
|
300
|
+
// Each entry's importers list tells us who imports the key.
|
|
301
|
+
// The key itself gains in-degree equal to importers.length — but that's
|
|
302
|
+
// backwards. The importers are the files that import the key.
|
|
303
|
+
// So the key's in-degree (how many depend on it) = importers.length.
|
|
304
|
+
// But we want: for each affected file, how many files depend on IT.
|
|
305
|
+
// So we need reverse-of-reverse = forward. Let's just count directly.
|
|
306
|
+
}
|
|
307
|
+
// Simpler approach: count how many times each file appears as an import target.
|
|
308
|
+
// That's exactly what the reverse graph encodes: key = import target, value = importers.
|
|
309
|
+
for (const [target, importers] of reverseGraph.entries()) {
|
|
310
|
+
const existing = inDegree.get(target);
|
|
311
|
+
if (existing !== undefined) {
|
|
312
|
+
inDegree.set(target, importers.length);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// target may be a partial path — try to match to an allFiles entry
|
|
316
|
+
for (const file of allFiles) {
|
|
317
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
318
|
+
const relNoExt = rel.replace(/\.[^.]+$/, '');
|
|
319
|
+
if (rel === target || relNoExt === target || relNoExt.endsWith('/' + target)) {
|
|
320
|
+
inDegree.set(rel, (inDegree.get(rel) ?? 0) + importers.length);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Normalize to [0,1].
|
|
326
|
+
const maxDegree = Math.max(1, ...inDegree.values());
|
|
327
|
+
const centralityMap = new Map();
|
|
328
|
+
for (const [file, degree] of inDegree.entries()) {
|
|
329
|
+
centralityMap.set(file, degree / maxDegree);
|
|
330
|
+
}
|
|
331
|
+
return centralityMap;
|
|
332
|
+
}
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Test file detection
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
/**
|
|
337
|
+
* Detect test files using the conventions present in this repo:
|
|
338
|
+
* - Files in tests/ directory with .test.ts suffix
|
|
339
|
+
* - Files matching *.test.ts, *.spec.ts anywhere
|
|
340
|
+
* - Files inside __tests__/ directories
|
|
341
|
+
*
|
|
342
|
+
* Returns a set of source-file relative paths that have at least one test file.
|
|
343
|
+
*/
|
|
344
|
+
function detectTestFiles(allFiles, root) {
|
|
345
|
+
const testPatterns = /\.(test|spec)\.(ts|tsx|js|jsx)$|[/\\]__tests__[/\\]/;
|
|
346
|
+
const testedSources = new Set();
|
|
347
|
+
// Collect all test files.
|
|
348
|
+
const testFiles = [];
|
|
349
|
+
for (const file of allFiles) {
|
|
350
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
351
|
+
if (testPatterns.test(rel)) {
|
|
352
|
+
testFiles.push(rel);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Also scan the tests/ directory specifically (may not be in allFiles if not
|
|
356
|
+
// under a scanned module).
|
|
357
|
+
const testsDir = path.join(root, 'tests');
|
|
358
|
+
if (fs.existsSync(testsDir)) {
|
|
359
|
+
const entries = fs.readdirSync(testsDir, { recursive: true });
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
const rel = 'tests/' + String(entry).split(path.sep).join('/');
|
|
362
|
+
if (testPatterns.test(rel)) {
|
|
363
|
+
testFiles.push(rel);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// For each test file, infer which source file it covers.
|
|
368
|
+
// Convention: tests/ranker.test.ts covers src/ranker.ts
|
|
369
|
+
// src/__tests__/foo.test.ts covers src/foo.ts
|
|
370
|
+
for (const testFile of testFiles) {
|
|
371
|
+
const base = path.basename(testFile)
|
|
372
|
+
.replace(/\.(test|spec)\.(ts|tsx|js|jsx)$/, '');
|
|
373
|
+
// Match against all known source-relative paths.
|
|
374
|
+
for (const file of allFiles) {
|
|
375
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
376
|
+
if (testPatterns.test(rel))
|
|
377
|
+
continue; // skip other test files
|
|
378
|
+
const srcBase = path.basename(rel, path.extname(rel));
|
|
379
|
+
if (srcBase === base) {
|
|
380
|
+
testedSources.add(rel);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return testedSources;
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Assemble affected nodes
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
function buildAffectedNodes(affectedMap, centralityMap, testFileSet) {
|
|
390
|
+
const nodes = [];
|
|
391
|
+
for (const [file, hopDistance] of affectedMap.entries()) {
|
|
392
|
+
nodes.push({
|
|
393
|
+
file,
|
|
394
|
+
hopDistance,
|
|
395
|
+
centrality: centralityMap.get(file) ?? 0,
|
|
396
|
+
hasTest: testFileSet.has(file),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
// Sort: higher centrality first within each hop group.
|
|
400
|
+
nodes.sort((a, b) => a.hopDistance - b.hopDistance || b.centrality - a.centrality);
|
|
401
|
+
return nodes;
|
|
402
|
+
}
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Risk score computation
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
/**
|
|
407
|
+
* Risk score formula (0–10):
|
|
408
|
+
*
|
|
409
|
+
* risk = WEIGHT_AFFECTED_COUNT * normalizedAffectedCount
|
|
410
|
+
* + WEIGHT_CENTRALITY * avgCentrality
|
|
411
|
+
* + WEIGHT_UNTESTED * untestedRatio
|
|
412
|
+
* + WEIGHT_HOP_SPREAD * normalizedMaxHopSpread
|
|
413
|
+
*
|
|
414
|
+
* Where:
|
|
415
|
+
* normalizedAffectedCount = min(affectedCount / totalProjectFiles, 1)
|
|
416
|
+
* avgCentrality = mean centrality of affected nodes [0,1]
|
|
417
|
+
* untestedRatio = fraction of affected nodes with no test [0,1]
|
|
418
|
+
* normalizedMaxHopSpread = maxHopReached / maxHopsConfigured [0,1]
|
|
419
|
+
*
|
|
420
|
+
* Weights: 2.5 + 2.5 + 3.5 + 1.5 = 10
|
|
421
|
+
*/
|
|
422
|
+
function computeFormulaInputs(affectedNodes, totalFiles, maxHops) {
|
|
423
|
+
if (affectedNodes.length === 0) {
|
|
424
|
+
return {
|
|
425
|
+
normalizedAffectedCount: 0,
|
|
426
|
+
avgCentrality: 0,
|
|
427
|
+
untestedRatio: 0,
|
|
428
|
+
normalizedMaxHopSpread: 0,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const normalizedAffectedCount = Math.min(affectedNodes.length / Math.max(totalFiles, 1), 1);
|
|
432
|
+
const avgCentrality = affectedNodes.reduce((sum, n) => sum + n.centrality, 0) / affectedNodes.length;
|
|
433
|
+
const untestedCount = affectedNodes.filter((n) => !n.hasTest).length;
|
|
434
|
+
const untestedRatio = untestedCount / affectedNodes.length;
|
|
435
|
+
const maxHopReached = Math.max(...affectedNodes.map((n) => n.hopDistance));
|
|
436
|
+
const normalizedMaxHopSpread = maxHopReached / Math.max(maxHops, 1);
|
|
437
|
+
return { normalizedAffectedCount, avgCentrality, untestedRatio, normalizedMaxHopSpread };
|
|
438
|
+
}
|
|
439
|
+
function computeRisk(inputs) {
|
|
440
|
+
const raw = WEIGHT_AFFECTED_COUNT * inputs.normalizedAffectedCount +
|
|
441
|
+
WEIGHT_CENTRALITY * inputs.avgCentrality +
|
|
442
|
+
WEIGHT_UNTESTED * inputs.untestedRatio +
|
|
443
|
+
WEIGHT_HOP_SPREAD * inputs.normalizedMaxHopSpread;
|
|
444
|
+
return Math.round(raw * 10) / 10; // one decimal place
|
|
445
|
+
}
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Summary
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
function buildSummary(riskScore, changedFiles, affectedNodes) {
|
|
450
|
+
const untested = affectedNodes.filter((n) => !n.hasTest).length;
|
|
451
|
+
const riskLabel = riskScore <= 2 ? 'low' : riskScore <= 5 ? 'moderate' : riskScore <= 7.5 ? 'high' : 'critical';
|
|
452
|
+
return (`Risk ${riskScore}/10 (${riskLabel}): ` +
|
|
453
|
+
`${changedFiles.length} file(s) changed, ` +
|
|
454
|
+
`${affectedNodes.length} downstream affected, ` +
|
|
455
|
+
`${untested} untested.`);
|
|
456
|
+
}
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Output formatters
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
/** Plain-text report for terminal output. */
|
|
461
|
+
function formatBlastRadiusText(result) {
|
|
462
|
+
const lines = [];
|
|
463
|
+
lines.push('');
|
|
464
|
+
lines.push(` Blast Radius Analysis (${result.hops}-hop traversal)`);
|
|
465
|
+
lines.push(' ' + '─'.repeat(50));
|
|
466
|
+
lines.push('');
|
|
467
|
+
lines.push(` Risk Score: ${result.riskScore}/10`);
|
|
468
|
+
lines.push(` ${result.summary}`);
|
|
469
|
+
lines.push('');
|
|
470
|
+
// Changed files.
|
|
471
|
+
lines.push(' Changed files:');
|
|
472
|
+
for (const f of result.changedFiles) {
|
|
473
|
+
lines.push(` ${f}`);
|
|
474
|
+
}
|
|
475
|
+
lines.push('');
|
|
476
|
+
// Changed symbols.
|
|
477
|
+
if (result.changedSymbols.length > 0) {
|
|
478
|
+
lines.push(' Changed symbols:');
|
|
479
|
+
for (const s of result.changedSymbols) {
|
|
480
|
+
lines.push(` ${s.kind} ${s.name} (${s.file})`);
|
|
481
|
+
}
|
|
482
|
+
lines.push('');
|
|
483
|
+
}
|
|
484
|
+
// Affected nodes grouped by hop.
|
|
485
|
+
if (result.affectedNodes.length > 0) {
|
|
486
|
+
const maxHop = Math.max(...result.affectedNodes.map((n) => n.hopDistance));
|
|
487
|
+
for (let hop = 1; hop <= maxHop; hop++) {
|
|
488
|
+
const atHop = result.affectedNodes.filter((n) => n.hopDistance === hop);
|
|
489
|
+
if (atHop.length === 0)
|
|
490
|
+
continue;
|
|
491
|
+
lines.push(` Hop ${hop} (${atHop.length} file${atHop.length > 1 ? 's' : ''}):`);
|
|
492
|
+
for (const node of atHop) {
|
|
493
|
+
const testTag = node.hasTest ? '' : ' [NO TEST]';
|
|
494
|
+
lines.push(` ${node.file} centrality=${node.centrality.toFixed(2)}${testTag}`);
|
|
495
|
+
}
|
|
496
|
+
lines.push('');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
lines.push(' No downstream files affected.');
|
|
501
|
+
lines.push('');
|
|
502
|
+
}
|
|
503
|
+
// Formula transparency.
|
|
504
|
+
const fi = result.formulaInputs;
|
|
505
|
+
lines.push(' Formula inputs:');
|
|
506
|
+
lines.push(` affected_count (normalized): ${fi.normalizedAffectedCount.toFixed(3)}`);
|
|
507
|
+
lines.push(` avg_centrality: ${fi.avgCentrality.toFixed(3)}`);
|
|
508
|
+
lines.push(` untested_ratio: ${fi.untestedRatio.toFixed(3)}`);
|
|
509
|
+
lines.push(` hop_spread (normalized): ${fi.normalizedMaxHopSpread.toFixed(3)}`);
|
|
510
|
+
lines.push('');
|
|
511
|
+
return lines.join('\n');
|
|
512
|
+
}
|
|
513
|
+
/** JSON output (for --json flag and MCP). */
|
|
514
|
+
function formatBlastRadiusJson(result) {
|
|
515
|
+
return JSON.stringify(result, null, 2);
|
|
516
|
+
}
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Helpers
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
/** Scan all project source files using existing scanner infrastructure. */
|
|
521
|
+
function scanAllProjectFiles(root) {
|
|
522
|
+
const config = (0, config_1.loadConfig)(root);
|
|
523
|
+
return (0, scanner_1.scanFiles)(root, {
|
|
524
|
+
extensions: config.extensions,
|
|
525
|
+
exclude: config.exclude,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/** Return an empty result when no files were changed. */
|
|
529
|
+
function emptyResult(hops) {
|
|
530
|
+
return {
|
|
531
|
+
riskScore: 0,
|
|
532
|
+
summary: 'No changed files detected in diff.',
|
|
533
|
+
changedFiles: [],
|
|
534
|
+
changedSymbols: [],
|
|
535
|
+
affectedNodes: [],
|
|
536
|
+
formulaInputs: {
|
|
537
|
+
normalizedAffectedCount: 0,
|
|
538
|
+
avgCentrality: 0,
|
|
539
|
+
untestedRatio: 0,
|
|
540
|
+
normalizedMaxHopSpread: 0,
|
|
541
|
+
},
|
|
542
|
+
hops,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
//# sourceMappingURL=blast-radius.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blast-radius.js","sourceRoot":"","sources":["../src/blast-radius.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -148,23 +148,61 @@ program
|
|
|
148
148
|
.option('--telemetry-on', 'enable anonymous usage telemetry (flags only)')
|
|
149
149
|
.option('--telemetry-off', 'disable anonymous usage telemetry')
|
|
150
150
|
.option('--mcp', 'run as Model Context Protocol stdio server (same as skannr-mcp)');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Subcommand: blast-radius
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
program
|
|
155
|
+
.command('blast-radius')
|
|
156
|
+
.description('Analyze downstream impact and risk of a git diff')
|
|
157
|
+
.option('--root <path>', 'project root directory', process.cwd())
|
|
158
|
+
.option('--diff <path>', 'path to a diff file (default: working tree vs HEAD)')
|
|
159
|
+
.option('--hops <n>', 'max traversal hops (default: 2)', (v) => {
|
|
160
|
+
const n = parseInt(v, 10);
|
|
161
|
+
if (Number.isNaN(n) || n < 1) {
|
|
162
|
+
throw new commander_1.InvalidArgumentError('must be a positive integer');
|
|
163
|
+
}
|
|
164
|
+
return n;
|
|
165
|
+
})
|
|
166
|
+
.option('--json', 'output as JSON instead of plain text')
|
|
167
|
+
.action(async (cmdOpts) => {
|
|
168
|
+
try {
|
|
169
|
+
const { computeBlastRadius, formatBlastRadiusText, formatBlastRadiusJson } = await Promise.resolve().then(() => __importStar(require('./blast-radius')));
|
|
170
|
+
const absoluteRoot = path.resolve(cmdOpts.root);
|
|
171
|
+
assertRootExists(absoluteRoot);
|
|
172
|
+
const result = computeBlastRadius({
|
|
173
|
+
root: absoluteRoot,
|
|
174
|
+
diffPath: cmdOpts.diff,
|
|
175
|
+
hops: cmdOpts.hops ?? 2,
|
|
176
|
+
});
|
|
177
|
+
const output = cmdOpts.json
|
|
178
|
+
? formatBlastRadiusJson(result)
|
|
179
|
+
: formatBlastRadiusText(result);
|
|
180
|
+
process.stdout.write(output + (output.endsWith('\n') ? '' : '\n'));
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error('Error computing blast radius:', error instanceof Error ? error.message : error);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
program.addHelpText('after', `
|
|
188
|
+
Examples:
|
|
189
|
+
skannr --question "how does auth work?" --root .
|
|
190
|
+
skannr --question "database setup" --root /path/to/repo --limit 5
|
|
191
|
+
skannr --question "class structure" --root . --lang python
|
|
192
|
+
skannr --question "changed files" --root . --diff HEAD~1
|
|
193
|
+
skannr --report --root . # health report
|
|
194
|
+
skannr --question "..." --root . --format markdown
|
|
195
|
+
skannr --question "..." --root . --format json
|
|
196
|
+
skannr --question "..." --root . --watch # re-run on file changes
|
|
197
|
+
skannr-agent --root . # interactive mode
|
|
198
|
+
skannr blast-radius --root . --hops 3 # blast radius analysis
|
|
199
|
+
skannr blast-radius --diff changes.patch --json
|
|
200
|
+
|
|
201
|
+
Monorepo tip:
|
|
202
|
+
skannr --question "..." --root packages/my-package
|
|
203
|
+
|
|
204
|
+
MCP (stdio): npx -y skannr --mcp (or: skannr-mcp)
|
|
205
|
+
Cursor: { "mcpServers": { "skannr": { "command": "npx", "args": ["-y", "skannr", "--mcp"] } } }
|
|
168
206
|
`);
|
|
169
207
|
program.parse(process.argv);
|
|
170
208
|
const opts = program.opts();
|