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_effectOr in a Gemfile:
group :development do
gem "ripple_effect", require: false
endRippleEffect 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 criticaldiff 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}"
endEvidence 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_missingand metaprogrammed methods are not resolved. Where a receiver cannot be resolved and one method name matches uniquely, amedium-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/resourcewithonly:/except:/controller:,namespaceandscope. Constraints,concern, anddirectare 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,
testsrefuses 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
--verboseto 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 benchmarkSee 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.