0.0
The project is in a healthy, maintained state
Ripple Effect maps the blast radius of Ruby on Rails changes. It combines Ruby-aware static indexing with Rails semantics (callbacks, associations, routes, jobs, mailers, concerns, and test references) to explain what may be affected before you refactor. It is deterministic and local: it does not boot your application, connect to your database, call an LLM, or upload your source code.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 1.0, < 3.0
>= 0.4, < 1.0
 Project Readme

RippleEffect

Change-impact analysis for Ruby on Rails.

RippleEffect maps the blast radius of Ruby on Rails changes. It combines Ruby-aware static indexing with Rails semantics: callbacks, associations, routes, jobs, mailers, concerns, and test references: to explain what may be affected before you refactor.

RippleEffect is deterministic and local. It does not boot your Rails application, connect to your database, call an LLM, or upload your source code.

$ ripple-effect inspect 'BillingService#charge'

BillingService#charge
  app/services/billing_service.rb:2

Direct dependents
  app/jobs/invoice_job.rb:2                  InvoiceJob#perform
    because: calls method (inference.unique_method_name) at app/jobs/invoice_job.rb:3
    confidence: medium, depth: 1
  app/services/checkout_service.rb:2         CheckoutService#call
    because: calls method (inference.unique_method_name) at app/services/checkout_service.rb:3
    confidence: medium, depth: 1

Rails semantic dependents
  app/jobs/invoice_job.rb:1                  InvoiceJob (job)
    because: enqueues job (rails.job_perform) at app/jobs/invoice_job.rb:1
    confidence: medium, depth: 2
  config/routes.rb:5                         POST /checkout
    because: route dispatches to (rails.route_to_controller) at config/routes.rb:5
    confidence: medium, depth: 3

Likely affected tests
  spec/services/billing_service_spec.rb          reference (medium)
  spec/services/checkout_service_spec.rb         reference (medium)
  spec/jobs/invoice_job_spec.rb                  convention (medium)

Blast radius
  direct nodes: 4
  transitive nodes: 10
  max depth: 3
  risk: high
  because:
    4 direct dependents
    10 transitive dependents
    reachable from 2 routes
    reachable from 1 background job
    impact spans 3 architectural layers (controllers, jobs, services)

Why this exists

Rails hides its most important dependencies. Nothing in your source calls send_receipt, an after_commit does. Nothing calls InvoiceJob#perform, a perform_later two files away does. Nothing calls OrdersController#create, a line in config/routes.rb does.

Grep cannot see any of that. RippleEffect can, and it shows you the evidence so you can check its reasoning instead of trusting it.

How it differs from tools you may already use

RippleEffect does not try to replace these:

  • Packwerk enforces package boundaries. Its static analysis focuses on constant references and intentionally ignores method calls. RippleEffect is not a boundary enforcer; it answers "what could this change break?" at method level.
  • Enola provides broad multi-language architecture intelligence. RippleEffect is a Ruby gem and library first, focused narrowly on Rails source-symbol blast radius with an embeddable API.
  • Regression-test-selection tools (Crystalball, gitlab-crystalball, Kaisoku) optimise which tests run, often using runtime traces. For RippleEffect, test relevance is supporting evidence, not the product: the explainable graph is.

Installation

$ gem install ripple_effect

Or in a Gemfile:

group :development do
  gem "ripple_effect", require: false
end

RippleEffect does not depend on Rails and never loads your application.

Quick start

$ cd your-rails-app
$ ripple-effect doctor                        # check what RippleEffect can see
$ ripple-effect inspect 'BillingService#charge'
$ ripple-effect diff main                     # blast radius of your branch
$ bundle exec rspec $(ripple-effect tests main)

Commands

Command What it answers
ripple-effect inspect SYMBOL What may be affected by changing this symbol, and why
ripple-effect diff BASE [HEAD] What may be affected by everything changed between two revisions
ripple-effect tests BASE [HEAD] Which test files are most likely relevant
ripple-effect graph SYMBOL The impacted subgraph, as DOT or JSON
ripple-effect doctor Whether this project can be analysed, and what was found
ripple-effect version The version

Symbols may be written as User, User#activate!, User.find, User::Profile, OrdersController#create, or app/models/user.rb.

Full details, flags and exit codes: docs/CLI.md.

Machine-readable output

Every command accepts --format json and emits a versioned, deterministic document: see docs/CLI.md for the contract.

$ ripple-effect diff main --format json | jq '.risk'
{
  "level": "high",
  "score": 15.0,
  "reasons": [
    "4 direct dependents",
    "10 transitive dependents",
    "reachable from 2 routes",
    "reachable from 1 background job"
  ]
}

In CI

$ ripple-effect diff origin/main --fail-on-risk critical

diff exits non-zero only when a --fail-on-risk threshold you asked for is met. Finding impact is not, by itself, a failure.

Library API

The CLI is not the only product.

require "ripple_effect"

result = RippleEffect.analyze("BillingService#charge")

result.impacted_nodes.map(&:name)   # => ["CheckoutService#call", ...]
result.test_files                   # => ["spec/services/billing_service_spec.rb", ...]
result.risk.level                   # => :high
result.risk.reasons                 # => ["2 direct dependents", ...]
result.to_h                         # => the JSON contract, as a Hash

RippleEffect.diff("main")
RippleEffect.tests_for("User#activate!")

For more control:

project  = RippleEffect::Project.new(root: Dir.pwd)
analyzer = RippleEffect::Analyzer.new(project: project)

result = analyzer.inspect_symbol("BillingService#charge", depth: 2, min_confidence: :high)
result.impacts.each do |impact|
  puts "#{impact.node.name} (#{impact.confidence}) via #{impact.reason.description}"
end

Evidence and confidence

Every edge in the graph carries an evidence code and a confidence band. Nothing is asserted without a reason you can go and look at.

Band Meaning Example
high An explicit static reference, or a literal Rails DSL relationship rails.after_commit, rubydex.constant_reference
medium A target inferred from a strong convention inference.unique_method_name, rails.delegate
low A naming or path heuristic only convention.request_spec_path

By default RippleEffect traverses high and medium edges. Pass --include-low-confidence to widen, or --min-confidence high to narrow.

A path is only as confident as its weakest link, and RippleEffect reports the shortest chain of reasoning, because that is the one you can check fastest.

There are no percentages. Ruby is too dynamic for a number like "91% chance of breakage" to mean anything, and a made-up number is worse than no number.

See docs/ANALYSIS_MODEL.md for every edge type and the exact resolution rules.

Safety and privacy

RippleEffect reads files. That is all.

  • No network access, ever. No telemetry.
  • Your application is never booted and your source is never evaled.
  • No database connection.
  • Git is invoked with argument arrays, never a shell string.
  • YAML config is loaded with safe loading: no aliases, no object deserialisation.
  • The cache is JSON, never Marshal.
  • Paths are normalised and confined to the project root; a symlink pointing outside it is refused.

ripple-effect doctor restates these guarantees. See SECURITY.md.

Supported versions

  • Ruby 3.2, 3.3, 3.4: the full suite is run against each
  • Rails 7.0 – 8.1 (analysed statically; Rails is not a dependency)

Validated against real applications

v0.1 was checked against six real Rails codebases: two private production applications and four large open-source ones: covering Rails 7.0 through 8.1, classic ERB apps, an API-only app, a Haml-based app, and an engine monorepo:

Application Ruby files Templates Graph Cold
lobsters 258 121 erb 1.4k nodes / 4.8k edges 0.5s
rubygems.org 1,044 173 erb 5.7k nodes / 20.6k edges 2.2s
Solidus (6 engines) 1,810 299 erb 8.9k nodes / 22.8k edges 6.6s
Mastodon 2,574 46 erb 13.3k nodes / 47.7k edges 4.4s
private app (Rails 7.0) 521 688 erb 5.0k nodes / 12.8k edges 0.8s
private app (Rails 8.0) 845 33 erb 7.7k nodes / 21.1k edges 1.2s

None crashed, and none reported an analysis error. Warm runs are well under a tenth of a second.

Dependents were ground-truthed against an exhaustive text search of each codebase (script/ground_truth.rb). Across 18 heavily-referenced methods in those six applications, there were zero false positives, RippleEffect never claimed a dependency that was not really there. Several were exact matches (151 of 151 files for one Mastodon method, 47 of 47 for one private-app helper).

Recall is the weaker side, by design: where a receiver is ambiguous RippleEffect records a diagnostic instead of guessing, so it misses some real relationships. See Known limitations.

That is a spot check, not a guarantee. Please report anything it gets wrong.

Known limitations

RippleEffect prefers a false negative to a confident false positive. It will tell you when it does not know.

  • Dynamic dispatch. send, public_send, method_missing and metaprogrammed methods are not resolved. Where a receiver cannot be resolved and one method name matches uniquely, a medium-confidence edge is created; where several match, a diagnostic is recorded and no edge is invented.
  • Polymorphic associations have no static target. They are reported as a diagnostic, not resolved.
  • Non-literal Rails DSL. Callbacks, associations and routes built from variables, constants or loops are not read.
  • Route DSL coverage is conservative: explicit verbs with to:, root, resources/resource with only:/except:/controller:, namespace and scope. Constraints, concern, and direct are not modelled.
  • Test selection is a recommendation. It is never permission to skip the rest of your suite. When a global or boot-impact file changes, tests refuses to print a narrowed list unless you pass --allow-unsafe-focus.
  • Recall is incomplete. Where a call's receiver is ambiguous, RippleEffect records a diagnostic and adds no edge, so some real dependencies are missed. Run with --verbose to see exactly what was skipped.
  • ERB templates are indexed, but a call with an explicit receiver inside one (<%= @order.total %>) is not resolved: the receiver's type is unknown. Non-ERB templates (Haml, Slim, Builder), JavaScript, and SQL are not indexed.

Development

$ bin/setup                    # or: bundle install
$ bundle exec rake             # specs + RuboCop
$ bundle exec rspec
$ bundle exec rubocop
$ bundle exec rake smoke       # build, install into a temp GEM_HOME, run the CLI
$ bundle exec rake benchmark

See CONTRIBUTING.md, ARCHITECTURE.md and docs/TESTING.md.

Status

v0.1.0 is an early release. The analysis model and CLI are stable enough to use, but expect rough edges on unusual codebases. Bug reports with a small reproduction are very welcome.

Acknowledgements

RippleEffect's Ruby declaration and reference index is powered by Rubydex (MIT), and its Rails DSL parsing by Prism.

License

MIT. See LICENSE.txt.