Swadhin Biswas Feature/ai code reviewer (#73) · 8/2/2026
105a287
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* Blast Radius Calculator
* Estimates the total system impact of a PR.
*/
import type { DiffChunk } from "./diff-chunker";
import type { DependencyGraph } from "./dep-graph";
import type { ArchImpact, ArchLayer } from "./arch-impact";
export interface BlastRadius {
directChanges: number;
transitiveChanges: number;
affectedRoutes: number;
affectedComponents: number;
affectedLayers: ArchLayer[];
riskScore: number;
riskLevel: "low" | "medium" | "high" | "critical";
summary: string;
breakdown: {
filesChanged: number;
routesAffected: number;
componentsAffected: number;
servicesAffected: number;
databasesAffected: number;
};
}
export function calculateBlastRadius(
chunks: DiffChunk[],
graph: DependencyGraph,
archImpact: ArchImpact,
): BlastRadius {
const directChanges = chunks.length;
// Count transitive changes (files that import changed files)
const changedSet = new Set(chunks.map(c => c.filePath));
const transitiveSet = new Set<string>();
for (const edge of graph.edges) {
if (changedSet.has(edge.to) && !changedSet.has(edge.from)) {
transitiveSet.add(edge.from);
}
}
const transitiveChanges = transitiveSet.size;
const affectedRoutes = graph.affectedRoutes.length;
const affectedComponents = graph.affectedComponents.length;
const affectedLayers = archImpact.affectedLayers;
// Calculate risk score
let riskScore = 0;
// Direct changes (0-25)
riskScore += Math.min(directChanges * 3, 25);
// Transitive changes (0-20)
riskScore += Math.min(transitiveChanges * 4, 20);
// Routes affected (0-15)
riskScore += Math.min(affectedRoutes * 5, 15);
// Components affected (0-10)
riskScore += Math.min(affectedComponents * 2, 10);
// Layer count penalty (0-15)
riskScore += Math.min(affectedLayers.length * 3, 15);
// Security/Database impact (0-15)
if (archImpact.securityImpact) riskScore += 10;
if (archImpact.databaseImpact) riskScore += 8;
// Cross-layer penalty (0-10)
if (archImpact.crossLayerChanges) riskScore += 8;
// Circular dependencies penalty (0-10)
if (graph.circularDependencies.length > 0) riskScore += 10;
riskScore = Math.min(Math.round(riskScore), 100);
let riskLevel: BlastRadius["riskLevel"] = "low";
if (riskScore >= 75) riskLevel = "critical";
else if (riskScore >= 50) riskLevel = "high";
else if (riskScore >= 25) riskLevel = "medium";
// Count by category
let servicesAffected = 0;
let databasesAffected = 0;
for (const layer of archImpact.layers) {
if (layer.layer === "backend") servicesAffected = layer.changeCount;
if (layer.layer === "database") databasesAffected = layer.changeCount;
}
const summaryParts: string[] = [];
summaryParts.push(`${directChanges} file(s) changed`);
if (transitiveChanges > 0) summaryParts.push(`${transitiveChanges} file(s) transitively affected`);
if (affectedRoutes > 0) summaryParts.push(`${affectedRoutes} route(s) affected`);
if (affectedComponents > 0) summaryParts.push(`${affectedComponents} component(s) affected`);
if (affectedLayers.length > 1) summaryParts.push(`spans ${affectedLayers.length} architectural layers`);
return {
directChanges,
transitiveChanges,
affectedRoutes,
affectedComponents,
affectedLayers,
riskScore,
riskLevel,
summary: summaryParts.join(", ") + ".",
breakdown: {
filesChanged: directChanges,
routesAffected: affectedRoutes,
componentsAffected: affectedComponents,
servicesAffected,
databasesAffected,
},
};
}