How CRG Implements Blast Radius Analysis for Code Changes
CRG determines a change's blast radius by walking the dependency graph from modified files and scoring every reachable node using weighted, direction-aware traversal with best-score BFS relaxation.
The Code-Review-Graph (CRG) is an open-source dependency analysis tool that quantifies how far a code change propagates through a codebase. Its blast radius analysis helps developers identify which components, tests, and downstream consumers are affected by a modification before it ships.
How Blast Radius Analysis Works in CRG
CRG's blast radius computation combines three interconnected components: seed generation from changed files, weighted edge traversal with direction rules, and a bounded best-score breadth-first search. The implementation spans graph.py, constants.py, and supporting tool files.
Step 1: Collect Impact Seeds from Changed Files
The analysis begins by identifying seed nodes—every qualified name contained in the modified files. This happens in Graph._impact_seed_qns at graph.py:L1307-L1315.
For each file in changed_files, CRG retrieves all nodes via self.get_nodes_by_file and extracts their qualified_name. C# files receive special handling: their declared csharp_namespaces are also added as seeds, since namespaces act as bridges for import-related traversals.
# From graph.py:L1307-L1315
def _impact_seed_qns(self, changed_files: list[str]) -> set[str]:
seeds = set()
for f in changed_files:
for node in self.get_nodes_by_file(f):
seeds.add(node.qualified_name)
if node.language == "csharp":
seeds.update(node.csharp_namespaces or [])
return seeds
Step 2: Initialize Impact Tracking Tables
The SQLite-based implementation creates four temporary tables to manage traversal state:
| Table | Purpose |
|---|---|
_impact_seeds |
Stores the original seed qualified names |
_impact_best |
Tracks the highest score seen for each visited node (initialized to 1.0 for seeds) |
_impact_frontier |
Current "wave" of nodes to expand in the next iteration |
_impact_next |
Buffer for candidates that survive score filtering |
Step 3: Execute Weighted, Direction-Aware BFS
The core traversal logic resides in Graph.get_impact_radius_sql (graph.py:L1450-L1500). For each depth level up to max_depth, CRG:
- Joins the frontier with edges, respecting direction rules from
IMPACT_EDGE_DIRECTIONS - Multiplies scores by the edge's weight and the depth decay factor (
IMPACT_DEPTH_DECAY, default0.6) - Filters candidates against the current best score and global floor (
IMPACT_SCORE_FLOOR, default0.05) - Promotes survivors to
_impact_bestand advances the frontier
# Edge weights and directions defined in constants.py:L57-L84
IMPACT_EDGE_WEIGHTS: dict[EdgeKind, float] = {
EdgeKind.CALLS: 1.0,
EdgeKind.INHERITS_FROM: 0.9,
EdgeKind.IMPORTS_FROM: 0.5,
EdgeKind.TESTED_BY: 0.7,
# ... additional edge kinds
}
IMPACT_EDGE_DIRECTIONS: dict[EdgeKind, str] = {
EdgeKind.CALLS: "incoming", # changes to callee impact callers
EdgeKind.INHERITS_FROM: "incoming",
EdgeKind.IMPORTS_FROM: "incoming",
EdgeKind.TESTED_BY: "outgoing", # changes to production code impact tests
# ... additional edge kinds
}
The best-score relaxation guarantees each node is retained with its strongest path score, preventing exponential explosion in cyclic graphs.
Step 4: Return Structured Impact Results
When traversal terminates (depth limit reached, score floor hit, or no new candidates), CRG returns:
changed_nodes: original seed nodesimpacted_nodes: all reached nodes sorted by decreasing scoreimpacted_files: files containing impacted nodesedges: traversed edges for visualizationimpact_scores: mapping of qualified names to final scores
Using CRG Blast Radius Analysis
Command-Line Interface
The impact subcommand in cli.py provides direct access:
# Basic usage
$ crg impact --files src/auth.py --depth 3
# Multiple files with custom limits
$ crg impact --files services/payment.py models/user.py --depth 2 --max-nodes 500
Python API
For programmatic access, use GraphStore.get_impact_radius:
from code_review_graph.graph import GraphStore
store = GraphStore(db_path="crg.db", repo_root="/path/to/repo")
impact = store.get_impact_radius(
changed_files=["services/payment.py"],
max_depth=2,
max_nodes=500,
)
print(f"Seeds: {len(impact['changed_nodes'])}")
print(f"Total impacted: {len(impact['impacted_nodes'])}")
for node in impact["impacted_nodes"][:10]:
score = impact["impact_scores"][node.qualified_name]
print(f"- {node.qualified_name} (score={score:.2f})")
MCP Tool Integration
The analysis is exposed as an MCP tool for IDE and agent integration via get_impact_radius_tool in main.py:
from code_review_graph.main import get_impact_radius_tool
result = get_impact_radius_tool(
changed_files=["services/payment.py"],
max_depth=2,
repo_root="/path/to/repo",
)
Key Configuration Parameters
Tune blast radius behavior through constants in constants.py:
| Parameter | Default | Effect |
|---|---|---|
IMPACT_EDGE_WEIGHTS |
varies by EdgeKind |
How much each edge type contributes to impact score |
IMPACT_EDGE_DIRECTIONS |
incoming or outgoing |
Which direction impact propagates per edge type |
IMPACT_DEPTH_DECAY |
0.6 |
Multiplicative score reduction per traversal depth |
IMPACT_SCORE_FLOOR |
0.05 |
Minimum score to retain a node |
CRG_MAX_IMPACT_DEPTH |
5 |
Hard stop for traversal depth |
CRG_MAX_IMPACT_NODES |
10000 |
Hard stop for total nodes visited |
Summary
-
Seed generation (
_impact_seed_qns) extracts all qualified names from changed files plus C# namespaces -
Edge weights and directions (
IMPACT_EDGE_WEIGHTS,IMPACT_EDGE_DIRECTIONS) assign semantic meaning to different dependency types -
Best-score BFS (
get_impact_radius_sql) propagates impact with score decay and bounded growth -
Multiple interfaces expose the same core logic: CLI (
crg impact), Python API (GraphStore.get_impact_radius), and MCP tools (get_impact_radius_tool)
Frequently Asked Questions
What makes CRG's blast radius analysis different from simple file-level dependency tracking?
CRG operates at the symbol level (functions, classes, modules) rather than files, and uses weighted edge semantics to distinguish tight coupling (CALLS) from loose references (IMPORTS_FROM). The best-score BFS with configurable decay produces a ranked list of impacted components rather than a flat set.
How does CRG handle circular dependencies during blast radius analysis?
The best-score relaxation in get_impact_radius_sql ensures each node keeps only its highest-scoring path. When cycles are encountered, the decay factor (IMPACT_DEPTH_DECAY) progressively reduces scores, and the global floor (IMPACT_SCORE_FLOOR) eventually prunes low-impact paths. This guarantees termination without exponential blowup.
Can I customize which edge types contribute to blast radius scores?
Yes. Modify IMPACT_EDGE_WEIGHTS and IMPACT_EDGE_DIRECTIONS in constants.py before building the graph, or override constants at runtime. For example, setting TESTED_BY: 0.0 would exclude test coverage from impact propagation, while increasing IMPORTS_FROM would amplify cross-module dependency effects.
What is the performance characteristic of blast radius analysis on large codebases?
The SQLite-based implementation runs in O(E × D) where E is edges examined and D is max_depth, bounded by CRG_MAX_IMPACT_NODES. For massive graphs, CRG also provides Graph._get_impact_radius_networkx as an alternative backend, though get_impact_radius_sql remains the default for memory efficiency.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →