Appearance
Blast Radius
Grade a PR's blast radius (magnitude × importance) and produce a review plan — what to read first, what is safe to skim. Advisory, never a merge gate.
Repo configuration — the only per-repo edit
- Facts command:
npx tsx tools/blast-radius/facts.ts - Context file:
tools/blast-radius/context.md
To port to another repo: provide (1) a facts producer that emits the shape in Facts contract below, native to that stack (nx here; mix xref / a Rails graph / madge elsewhere), and (2) a context file; then update these two values. Everything else in this skill is shared as-is.
Step 1 — Facts (magnitude, deterministic)
Run the facts command from the repo root and capture stdout as JSON:
bash
npx tsx tools/blast-radius/facts.ts --base origin/master(Use --base <ref> for a different comparison point.) If it fails entirely, say you couldn't gather facts and stop. If the JSON's missing array is non-empty, note which signals are absent and lean harder on what remains. A missing entry may carry a reason (e.g. reach: nx output unparseable) — reach genuinely unavailable and reach failed to compute are different; report which.
Long-lived branches: the default base is
git merge-base HEAD origin/master. On a branch that's been open a while, that merge-base is stale, sobase...HEADsweeps in unrelated commits and master merges — magnitude then reflects the range, not the PR's own diff (GitHub's file count is the honest one). If the numbers look inflated versus GitHub, pass--base <pr-target-branch>(the branch the PR actually merges into) and say the range was adjusted.
Facts contract (what the producer emits)
jsonc
{
"base": "<sha>", "head": "HEAD",
"totalFiles": 5, // raw PR size
"areasTouched": 3, // distinct subsystems (2-segment path prefixes) — diffusion
"reach": { "affectedProjects": 47 }, // nx-affected project count; null if unavailable
"missing": [], // signals this run couldn't compute, e.g. ["reach"]
"files": [{
"path": "…", "status": "A|M|D|R",
"added": 8, "deleted": 3, "churn": 11, "size": 120,
"layer": "contract|lib|logic|ui|config|test|other",
"isInfra": false, // lockfile / nx.json / root tsconfig / CI — ripples across builds
"isBarrel": false, // index.ts re-export — export surface may have changed
"churnRank": 3 // 1 = biggest churn in THIS PR (within-PR frame of reference)
}]
}Facts are magnitude only — they never grade. Grading is your job, below.
Step 2 — Lenses (importance + ripple, in parallel)
Cost fast-path — skip the lenses when there's nothing to judge. If reach is 0 (or 1) and no file is contract, isInfra, or isBarrel and every file is test, config, or i18n/translation data → grade LOW and skip both lenses. This is pure data/config churn with no export surface and nothing affected; dispatching cheap agents over it is wasted spend. Say you took the fast-path and why. Otherwise:
Dispatch BOTH lens agents concurrently — one message, two Agent calls — giving each: the diff range <base>...HEAD, the changed-files list with their facts, and (importance lens only) the context-file path.
Agent(subagent_type: 'blast-radius-breaking-lens', …)Agent(subagent_type: 'blast-radius-importance-lens', …)
Each returns a markdown finding list with a Scan: line and per-file reasoning.
Step 3 — Merge, audit, and apply general risk reasoning
Merge conservatively: believe any breaking finding from the breaking lens; keep every crucial-area and flag caveat from the importance lens.
Audit the lenses — they run on a cheap model. Read each lens's Reasoning, not just its verdict. If a SAFE/cleared rationale is thin or looks wrong, or a lens reported nothing for a file the facts flag as contract / isInfra / isBarrel / rank-1 churn, verify that file yourself (git diff <base>...HEAD -- <file> + grep importers) before trusting the pass. A cheap agent's clean bill of health on a high-magnitude file is exactly where you spend your own attention.
"Type-only" / "mechanical" / "no behavioral change" is a claim about the diff, and the only way to verify it is to read the hunks. A lens can correctly clear ripple risk (no importer breaks — export surface unchanged, no stale imports) yet miss behavioral risk (runtime changes for existing users). Those are different axes; clearing one does not clear the other. So for a rank-1-churn file in a crucial area, an export-surface / importer check is not enough to downgrade the lens's caution — read the actual diff for changed guards, defaults, and conditionals. A strict-mode ?./??/! "fix" can flip a null-case branch; a reordered if/else if can reclassify data; a new x ? … : false default changes the no-x case. Only relax the grade after you've read those lines — never on "it's not an exported surface."
Then grade with the heuristics that hold in any repo:
- Brand-new isolated file/feature (
status: A, low reach, nothing depends on it yet) → low. - Removed/changed export with real dependents (breaking lens confirmed importers) → high.
- Infra / lockfile / build config (
isInfra) → elevated (ripples across builds). - Pure test / styling / copy → low.
- Gated behind a feature flag → conditional — surface as needs-human, never assume flag state.
Use facts for magnitude (churnRank, reach, areasTouched, layer) and lenses for judgment. Where they disagree, the lens (which read the diff) wins on whether something breaks.
Monolithic / non-nx projects undercount
reachandareasTouched. Manage and mobile are each a single nx project with no intra-project granularity, so a 68-file shared-infra refactor can showreach: 2,areasTouched: 1— an artifact of the tooling, not low blast radius. Do not let a low reach headline down-grade a wide diff in these areas; fall back to the file count, layers, and the lens's diff reading. The raw numbers are trustworthy for recruitee's many-project layout, not for one-project monoliths.
Overall grade:
- CRITICAL — a confirmed break in a high-reach/contract area, or a break in a crucial area.
- HIGH — a confirmed break with real dependents, or a heavy change to a crucial area.
- MEDIUM — elevated magnitude (infra, wide churn, many areas) but no confirmed break.
- LOW — isolated / new / test / styling only.
If the PR spans many areas or bundles unrelated clusters, note it as large. Don't advise a split — the reader is reviewing, not rewriting. Instead, in the report, tell the reviewer how to divide their attention across the clusters (see Step 4's wording rules).
Step 4 — Report (one fixed block, scaled by severity)
The whole report is a single ruled block — nothing is printed outside it. Same five labels, same order, every time; that constant shape is what makes any run scannable at a glance. The box grows with severity: on a LOW change every row is a terse one-liner; on a HIGH/CRITICAL one the Review and Skim rows expand into tagged lists. A heavier PR gets a taller box, never a second section.
The block — always printed, verbatim shape
Print it inside a ``` code fence so the box and its column alignment hold:
━━━━━━━━━━ BLAST RADIUS ━━━━━━━━━━
Grade <🟢 LOW | 🟡 MEDIUM | 🔴 HIGH | ⛔ CRITICAL>
Scope <what changed + how contained, one line>
Reach <where a mistake could surface — spatial, hedged>
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
Review <one line on LOW; a ⚠/✓-tagged list on MEDIUM/HIGH>
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
Skim <what needs no attention>
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Rules for the box:
- Heavy frame, dotted dividers. The
━frame (top row carries the title) wraps the box; a light┈divider separates the three sections — after theGrade/Scope/Reachheader, and betweenReviewandSkim. Both dividers print on every grade, even when the rows are one-liners — the splits are what keep it skimmable. - All five rows, always, in this order. A row with nothing to say still prints its label with a terse value (
Review nothing — safe to skim,Skim everything else) — never drop a label. The fixed shape is the whole point. Grade/Scope/Reachstay one line each (wrap to an indented continuation line if long).Review/Skimscale with the grade. LOW: one short line. MEDIUM/HIGH: a bulleted list, one file or cluster per bullet, each with its plain reason and a leading⚠or✓. This is the only part that grows.- It's inside a code fence, so no markdown renders — no bold, no clickable links.
⚠and✓are plain characters and show fine. Name real files as plain text (path or filename) so the reviewer can find them; they just won't be clickable.
Severity tags on Review / Skim items:
⚠— a human needs to eyeball this (behavior changed, crucial area, unverified).✓— you checked it yourself and it's fine; listed so the reader knows it was considered, not missed.
Nothing goes outside the box — no <details>, no raw signal dump, no prose header above it.
Wording rules (apply to every row of the block)
- No tool jargon. Never surface
reach,nx,barrel,contract, "affected projects", "export surface", or "feature-internal" to the reader — these mean nothing to someone skimming. Translate every signal into what it means for whether they can skip this file: write "the change stays inside the Reports feature, so a mistake here can't spread to other screens" — not "reach: 88, exports feature-internal". The block'sScope/Reachrows are where this discipline matters most. - Write for the reviewer, never the author. Don't tell anyone to change, split, or restructure the PR — the reader is reviewing it, not rewriting it. When a PR bundles unrelated work, tell the reviewer how to spend their attention instead: "this is really two changes — the candidate-profile work and some unrelated lint cleanup; skim the cleanup fast, spend your time on the profile."
- Reach is radius, not a safety claim. "Nothing breaks" / "Could break — none" overclaims for a cheap-model pass and isn't this skill's job (it grades blast radius, not correctness). Prefer spatial, hedged phrasing: "the risky changes are all inside the candidate profile — that's where a problem would show up, not other screens", or "unlikely to reach beyond the Reports feature".
Advisory, never a gate. New files are low-risk by default (they add surface but can't break existing importers) — review them as new features, separately.