Your AI agent's PR passed tests and still broke 37 routes
Tests prove behavior. ** They don't show reach. Here's how to gate the blast radius of a TypeScript change.
An AI agent opens a PR. The diff is 12 lines. The tests pass. CI is green. Then it ships, and a helper you thought was local turns out to be imported by four routes, two components, and a background job you forgot existed.
The tests covered the behavior the agent changed. They didn't cover the surface it reached.
This is the failure mode I kept seeing, so I built Ripple: a CI gate that scores every changed TypeScript/JavaScript file by its blast radius, then blocks the merge when a change reaches too far.
The 37-route problem
Call it 37 routes, 12 components, or 80 files. The exact number doesn't matter. The pattern does:
- The diff is small.
- The tests pass.
- The change lands in a module with more dependents than the author realized.
- Review focuses on the lines, not the reach.
AI agents make this worse because they produce plausible changes at machine speed. The PR looks reviewable. The graph underneath is not.
Why tests miss blast radius
Tests are a sample of behavior. They tell you the paths you thought to exercise. They don't tell you which paths exist.
An import graph tells you what a file depends on. The interesting question is the reverse: what depends on this file? ** That's the blast radius. Package managers don't answer it. grep doesn't answer it. Most dependency graphs are drawn in the wrong direction.
What Ripple actually does
Ripple parses the project with ts-morph, builds the import graph, and walks it in reverse from the changed file. It categorizes everything it reaches:
- routes
- layouts
- components
- utilities
- tests
- entry points
Then it scores the change from 0 to 100 using seven weighted signals:
| Signal | Weight |
|---|---|
| Affected files | 30% |
| Entry point impacted | 15% |
| Shared utility impacted | 15% |
| Public export surface | 10% |
| Tests affected | 10% |
| Routes affected | 10% |
| Circular dependency membership | 10% |
The score is deterministic and explainable. --verbose prints every factor and its contribution. No model, no cloud, no telemetry.
A real example
The repo ships a small fixture so you can reproduce this line for line:
git clone https://github.com/alimaandev/ripple.git cd ripple/tests/fixtures/basic npx @alimaandev/ripple analyze src/authentication/login.ts Output (trimmed):
ripple · impact analysis · v0.7.2 File src/authentication/login.ts Risk MEDIUM · 34.7/100 ███░░░░░░░ Impact 4 routes · 2 components · 1 test Affected 7 files Max depth 2 Confidence 100% › Top impact ● Dashboard (2) ● Admin (1) ● Index TS (1) ● Main TS (1) ● Mobile (1) › Affected files (7) ├─ src/admin/page.tsx · depth 1 ├─ src/dashboard/page.tsx · depth 1 ├─ src/dashboard/route.ts · depth 1 ├─ src/index.ts · depth 1 ├─ src/mobile/route.ts · depth 1 ├─ src/tests/login.test.ts · depth 1 └─ src/main.ts · depth 2 One file. Seven dependents. Four routes. Two components. One test. The change is not just the lines in the diff.
Gate the PR in one step
name: Ripple on: pull_request permissions: contents: read pull-requests: write jobs: ripple: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: alimaandev/ripple-action@v1 with: gate: high fetch-depth: 0 matters. Ripple needs the base commit to compute the change set; a shallow checkout gives it the wrong diff.
When a changed file is at or above the gate, the job fails, and the affected files are annotated on the PR's Files tab. The action can also emit a SARIF report you can upload to Code Scanning, so the same findings show up as code scanning alerts.
For a legacy codebase, start with gate: critical, allowlist the worst offenders, and tighten it over time. For a monorepo, point it at the right package:
with: gate: high working-directory: apps/web What this doesn't solve
- It doesn't replace tests, review, or type checking.
- It doesn't know whether the affected routes matter to your business.
- It can be noisy in a codebase with a few god modules. Start loose, then tighten the gate.
- It's a prioritization tool, not a proof of safety.
The goal is not to block every change. It's to make the risky ones obvious before they merge.
Try it
npx @alimaandev/ripple analyze src/your-file.ts npx @alimaandev/ripple diff --base origin/main - Repo: https://github.com/alimaandev/ripple
- Marketplace: https://github.com/marketplace/actions/ripple-pr-risk-gate
- npm: https://www.npmjs.com/package/@alimaandev/ripple
If you run it on a large codebase, I'd like to know where the score is wrong. False positives are what will kill this tool. Tell me which files it flags that you'd ship anyway.
