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.
Files changed (88) hide show
  1. package/.eslintrc.js +22 -22
  2. package/README.md +260 -151
  3. package/dist/agent-cli.js +25 -25
  4. package/dist/agent.js +4 -4
  5. package/dist/blast-radius.d.ts +80 -0
  6. package/dist/blast-radius.d.ts.map +1 -0
  7. package/dist/blast-radius.js +545 -0
  8. package/dist/blast-radius.js.map +1 -0
  9. package/dist/cli.js +55 -17
  10. package/dist/cli.js.map +1 -1
  11. package/dist/mcp-server.d.ts.map +1 -1
  12. package/dist/mcp-server.js +98 -24
  13. package/dist/mcp-server.js.map +1 -1
  14. package/dist/telemetry.js +12 -12
  15. package/gemini-extension/GEMINI.md +26 -26
  16. package/gemini-extension/gemini-extension.json +12 -12
  17. package/gemini-extension/package.json +14 -14
  18. package/gemini-extension/src/server.ts +69 -69
  19. package/gemini-extension/tsconfig.json +14 -14
  20. package/jest.config.js +4 -4
  21. package/package.json +54 -53
  22. package/src/agent-cli.ts +397 -397
  23. package/src/agent.ts +344 -344
  24. package/src/benchmark.ts +389 -389
  25. package/src/blast-radius.ts +661 -0
  26. package/src/cache.ts +317 -317
  27. package/src/cli.ts +461 -415
  28. package/src/config.ts +22 -22
  29. package/src/formatter.ts +105 -105
  30. package/src/index.ts +223 -223
  31. package/src/languages/GenericAdapter.ts +44 -44
  32. package/src/languages/LanguageAdapter.ts +21 -21
  33. package/src/languages/PythonAdapter.ts +285 -285
  34. package/src/languages/TypeScriptAdapter.ts +453 -453
  35. package/src/languages/registry.ts +49 -49
  36. package/src/mapper.ts +455 -455
  37. package/src/mcp-cli.ts +8 -8
  38. package/src/mcp-server.ts +151 -103
  39. package/src/ranker-enhanced.ts +514 -514
  40. package/src/ranker.ts +110 -110
  41. package/src/scanner.ts +226 -226
  42. package/src/skeletonizer.ts +22 -22
  43. package/src/telemetry.ts +146 -146
  44. package/src/tokenizer.ts +20 -20
  45. package/src/types.ts +92 -92
  46. package/src/watcher.ts +119 -119
  47. package/src/why.ts +88 -88
  48. package/tests/agent.tools.test.ts +81 -81
  49. package/tests/benchmark.test.ts +30 -30
  50. package/tests/blast-radius.test.ts +290 -0
  51. package/tests/fixtures/sample.py +17 -17
  52. package/tests/fixtures/sample.ts +13 -13
  53. package/tests/fixtures/src/api/routes.ts +1 -1
  54. package/tests/fixtures/src/auth/permission.ts +3 -3
  55. package/tests/python-adapter.test.ts +31 -31
  56. package/tests/ranker-enhanced.test.ts +70 -70
  57. package/tests/ranker.test.ts +79 -79
  58. package/tests/scanner.scope.test.ts +66 -66
  59. package/tests/setup-fixtures.js +28 -28
  60. package/tests/skeletonizer.test.ts +149 -149
  61. package/tsconfig.json +21 -21
  62. package/uca-landing/index.html +17 -17
  63. package/uca-landing/package.json +23 -23
  64. package/uca-landing/postcss.config.js +6 -6
  65. package/uca-landing/src/App.jsx +43 -43
  66. package/uca-landing/src/components/AgentMode.jsx +45 -45
  67. package/uca-landing/src/components/CliReference.jsx +58 -58
  68. package/uca-landing/src/components/Features.jsx +82 -82
  69. package/uca-landing/src/components/Footer.jsx +35 -35
  70. package/uca-landing/src/components/Hero.jsx +125 -125
  71. package/uca-landing/src/components/HowItWorks.jsx +60 -60
  72. package/uca-landing/src/components/Install.jsx +103 -103
  73. package/uca-landing/src/components/LanguageSupport.jsx +63 -63
  74. package/uca-landing/src/components/Navbar.jsx +86 -86
  75. package/uca-landing/src/components/Problem.jsx +51 -51
  76. package/uca-landing/src/components/Reveal.jsx +40 -40
  77. package/uca-landing/src/components/WorksWith.jsx +59 -59
  78. package/uca-landing/src/hooks/useScrollNav.js +13 -13
  79. package/uca-landing/src/hooks/useTypewriter.js +41 -41
  80. package/uca-landing/src/index.css +13 -13
  81. package/uca-landing/src/main.jsx +10 -10
  82. package/uca-landing/tailwind.config.js +68 -68
  83. package/uca-landing/vercel.json +3 -3
  84. package/uca-landing/vite.config.js +6 -6
  85. package/dist/rocket-chat-scope.d.ts +0 -7
  86. package/dist/rocket-chat-scope.d.ts.map +0 -1
  87. package/dist/rocket-chat-scope.js +0 -95
  88. 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
- program.addHelpText('after', `
152
- Examples:
153
- skannr --question "how does auth work?" --root .
154
- skannr --question "database setup" --root /path/to/repo --limit 5
155
- skannr --question "class structure" --root . --lang python
156
- skannr --question "changed files" --root . --diff HEAD~1
157
- skannr --report --root . # health report
158
- skannr --question "..." --root . --format markdown
159
- skannr --question "..." --root . --format json
160
- skannr --question "..." --root . --watch # re-run on file changes
161
- skannr-agent --root . # interactive mode
162
-
163
- Monorepo tip:
164
- skannr --question "..." --root packages/my-package
165
-
166
- MCP (stdio): npx -y skannr --mcp (or: skannr-mcp)
167
- Cursor: { "mcpServers": { "skannr": { "command": "npx", "args": ["-y", "skannr", "--mcp"] } } }
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();