DEV Community

Cover image for Ripple
ali sher
ali sher

Posted on

Ripple

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)