ripple_effect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.ripple-effect.yml.example +56 -0
  3. data/ARCHITECTURE.md +222 -0
  4. data/CHANGELOG.md +115 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/CONTRIBUTING.md +112 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +305 -0
  9. data/SECURITY.md +73 -0
  10. data/docs/ANALYSIS_MODEL.md +275 -0
  11. data/docs/CLI.md +276 -0
  12. data/docs/CONFIGURATION.md +178 -0
  13. data/docs/DECISIONS.md +210 -0
  14. data/docs/PUBLIC_LAUNCH_CHECKLIST.md +105 -0
  15. data/docs/RELEASING.md +94 -0
  16. data/docs/TESTING.md +179 -0
  17. data/exe/ripple-effect +7 -0
  18. data/lib/ripple_effect/analyzer.rb +379 -0
  19. data/lib/ripple_effect/cache_store.rb +207 -0
  20. data/lib/ripple_effect/cli/application.rb +126 -0
  21. data/lib/ripple_effect/cli/command.rb +165 -0
  22. data/lib/ripple_effect/cli/diff_command.rb +76 -0
  23. data/lib/ripple_effect/cli/doctor_command.rb +106 -0
  24. data/lib/ripple_effect/cli/graph_command.rb +61 -0
  25. data/lib/ripple_effect/cli/inspect_command.rb +66 -0
  26. data/lib/ripple_effect/cli/tests_command.rb +109 -0
  27. data/lib/ripple_effect/cli/version_command.rb +46 -0
  28. data/lib/ripple_effect/confidence.rb +61 -0
  29. data/lib/ripple_effect/configuration.rb +264 -0
  30. data/lib/ripple_effect/diagnostic.rb +90 -0
  31. data/lib/ripple_effect/diff/changed_symbol_resolver.rb +292 -0
  32. data/lib/ripple_effect/diff/git.rb +175 -0
  33. data/lib/ripple_effect/diff/hunk.rb +80 -0
  34. data/lib/ripple_effect/edge.rb +114 -0
  35. data/lib/ripple_effect/error.rb +23 -0
  36. data/lib/ripple_effect/extractors/base.rb +292 -0
  37. data/lib/ripple_effect/extractors/rails_associations.rb +102 -0
  38. data/lib/ripple_effect/extractors/rails_callbacks.rb +144 -0
  39. data/lib/ripple_effect/extractors/rails_delegation.rb +121 -0
  40. data/lib/ripple_effect/extractors/rails_jobs.rb +131 -0
  41. data/lib/ripple_effect/extractors/rails_mailers.rb +120 -0
  42. data/lib/ripple_effect/extractors/rails_routes.rb +256 -0
  43. data/lib/ripple_effect/extractors/rails_views.rb +299 -0
  44. data/lib/ripple_effect/extractors/ruby_structure.rb +221 -0
  45. data/lib/ripple_effect/extractors/test_conventions.rb +135 -0
  46. data/lib/ripple_effect/formatters/dot.rb +69 -0
  47. data/lib/ripple_effect/formatters/json.rb +43 -0
  48. data/lib/ripple_effect/formatters/text.rb +197 -0
  49. data/lib/ripple_effect/graph.rb +199 -0
  50. data/lib/ripple_effect/node.rb +153 -0
  51. data/lib/ripple_effect/project.rb +264 -0
  52. data/lib/ripple_effect/result.rb +147 -0
  53. data/lib/ripple_effect/risk.rb +167 -0
  54. data/lib/ripple_effect/static_index/adapter.rb +84 -0
  55. data/lib/ripple_effect/static_index/rubydex_adapter.rb +356 -0
  56. data/lib/ripple_effect/traversal/impact_walker.rb +153 -0
  57. data/lib/ripple_effect/version.rb +11 -0
  58. data/lib/ripple_effect.rb +89 -0
  59. metadata +155 -0
data/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # RippleEffect
2
+
3
+ > Change-impact analysis for Ruby on Rails.
4
+
5
+ **RippleEffect maps the blast radius of Ruby on Rails changes.** It combines
6
+ Ruby-aware static indexing with Rails semantics: callbacks, associations, routes,
7
+ jobs, mailers, concerns, and test references: to explain what may be affected
8
+ before you refactor.
9
+
10
+ RippleEffect is deterministic and local. It does not boot your Rails application,
11
+ connect to your database, call an LLM, or upload your source code.
12
+
13
+ ```console
14
+ $ ripple-effect inspect 'BillingService#charge'
15
+
16
+ BillingService#charge
17
+ app/services/billing_service.rb:2
18
+
19
+ Direct dependents
20
+ app/jobs/invoice_job.rb:2 InvoiceJob#perform
21
+ because: calls method (inference.unique_method_name) at app/jobs/invoice_job.rb:3
22
+ confidence: medium, depth: 1
23
+ app/services/checkout_service.rb:2 CheckoutService#call
24
+ because: calls method (inference.unique_method_name) at app/services/checkout_service.rb:3
25
+ confidence: medium, depth: 1
26
+
27
+ Rails semantic dependents
28
+ app/jobs/invoice_job.rb:1 InvoiceJob (job)
29
+ because: enqueues job (rails.job_perform) at app/jobs/invoice_job.rb:1
30
+ confidence: medium, depth: 2
31
+ config/routes.rb:5 POST /checkout
32
+ because: route dispatches to (rails.route_to_controller) at config/routes.rb:5
33
+ confidence: medium, depth: 3
34
+
35
+ Likely affected tests
36
+ spec/services/billing_service_spec.rb reference (medium)
37
+ spec/services/checkout_service_spec.rb reference (medium)
38
+ spec/jobs/invoice_job_spec.rb convention (medium)
39
+
40
+ Blast radius
41
+ direct nodes: 4
42
+ transitive nodes: 10
43
+ max depth: 3
44
+ risk: high
45
+ because:
46
+ 4 direct dependents
47
+ 10 transitive dependents
48
+ reachable from 2 routes
49
+ reachable from 1 background job
50
+ impact spans 3 architectural layers (controllers, jobs, services)
51
+ ```
52
+
53
+ ## Why this exists
54
+
55
+ Rails hides its most important dependencies. Nothing in your source calls
56
+ `send_receipt`, an `after_commit` does. Nothing calls `InvoiceJob#perform`, a
57
+ `perform_later` two files away does. Nothing calls `OrdersController#create`, a
58
+ line in `config/routes.rb` does.
59
+
60
+ Grep cannot see any of that. RippleEffect can, and it shows you the evidence so
61
+ you can check its reasoning instead of trusting it.
62
+
63
+ ## How it differs from tools you may already use
64
+
65
+ RippleEffect does not try to replace these:
66
+
67
+ - **Packwerk** enforces package boundaries. Its static analysis focuses on
68
+ constant references and intentionally ignores method calls. RippleEffect is not
69
+ a boundary enforcer; it answers "what could this change break?" at method level.
70
+ - **Enola** provides broad multi-language architecture intelligence. RippleEffect
71
+ is a Ruby gem and library first, focused narrowly on Rails source-symbol blast
72
+ radius with an embeddable API.
73
+ - **Regression-test-selection tools** (Crystalball, gitlab-crystalball, Kaisoku)
74
+ optimise which tests run, often using runtime traces. For RippleEffect, test
75
+ relevance is supporting evidence, not the product: the explainable graph is.
76
+
77
+ ## Installation
78
+
79
+ ```console
80
+ $ gem install ripple_effect
81
+ ```
82
+
83
+ Or in a Gemfile:
84
+
85
+ ```ruby
86
+ group :development do
87
+ gem "ripple_effect", require: false
88
+ end
89
+ ```
90
+
91
+ RippleEffect does not depend on Rails and never loads your application.
92
+
93
+ ## Quick start
94
+
95
+ ```console
96
+ $ cd your-rails-app
97
+ $ ripple-effect doctor # check what RippleEffect can see
98
+ $ ripple-effect inspect 'BillingService#charge'
99
+ $ ripple-effect diff main # blast radius of your branch
100
+ $ bundle exec rspec $(ripple-effect tests main)
101
+ ```
102
+
103
+ ## Commands
104
+
105
+ | Command | What it answers |
106
+ | --- | --- |
107
+ | `ripple-effect inspect SYMBOL` | What may be affected by changing this symbol, and why |
108
+ | `ripple-effect diff BASE [HEAD]` | What may be affected by everything changed between two revisions |
109
+ | `ripple-effect tests BASE [HEAD]` | Which test files are most likely relevant |
110
+ | `ripple-effect graph SYMBOL` | The impacted subgraph, as DOT or JSON |
111
+ | `ripple-effect doctor` | Whether this project can be analysed, and what was found |
112
+ | `ripple-effect version` | The version |
113
+
114
+ Symbols may be written as `User`, `User#activate!`, `User.find`, `User::Profile`,
115
+ `OrdersController#create`, or `app/models/user.rb`.
116
+
117
+ Full details, flags and exit codes: [docs/CLI.md](docs/CLI.md).
118
+
119
+ ### Machine-readable output
120
+
121
+ Every command accepts `--format json` and emits a versioned, deterministic
122
+ document: see [docs/CLI.md](docs/CLI.md#json-output) for the contract.
123
+
124
+ ```console
125
+ $ ripple-effect diff main --format json | jq '.risk'
126
+ {
127
+ "level": "high",
128
+ "score": 15.0,
129
+ "reasons": [
130
+ "4 direct dependents",
131
+ "10 transitive dependents",
132
+ "reachable from 2 routes",
133
+ "reachable from 1 background job"
134
+ ]
135
+ }
136
+ ```
137
+
138
+ ### In CI
139
+
140
+ ```console
141
+ $ ripple-effect diff origin/main --fail-on-risk critical
142
+ ```
143
+
144
+ `diff` exits non-zero **only** when a `--fail-on-risk` threshold you asked for is
145
+ met. Finding impact is not, by itself, a failure.
146
+
147
+ ## Library API
148
+
149
+ The CLI is not the only product.
150
+
151
+ ```ruby
152
+ require "ripple_effect"
153
+
154
+ result = RippleEffect.analyze("BillingService#charge")
155
+
156
+ result.impacted_nodes.map(&:name) # => ["CheckoutService#call", ...]
157
+ result.test_files # => ["spec/services/billing_service_spec.rb", ...]
158
+ result.risk.level # => :high
159
+ result.risk.reasons # => ["2 direct dependents", ...]
160
+ result.to_h # => the JSON contract, as a Hash
161
+
162
+ RippleEffect.diff("main")
163
+ RippleEffect.tests_for("User#activate!")
164
+ ```
165
+
166
+ For more control:
167
+
168
+ ```ruby
169
+ project = RippleEffect::Project.new(root: Dir.pwd)
170
+ analyzer = RippleEffect::Analyzer.new(project: project)
171
+
172
+ result = analyzer.inspect_symbol("BillingService#charge", depth: 2, min_confidence: :high)
173
+ result.impacts.each do |impact|
174
+ puts "#{impact.node.name} (#{impact.confidence}) via #{impact.reason.description}"
175
+ end
176
+ ```
177
+
178
+ ## Evidence and confidence
179
+
180
+ Every edge in the graph carries an evidence code and a confidence band. Nothing is
181
+ asserted without a reason you can go and look at.
182
+
183
+ | Band | Meaning | Example |
184
+ | --- | --- | --- |
185
+ | `high` | An explicit static reference, or a literal Rails DSL relationship | `rails.after_commit`, `rubydex.constant_reference` |
186
+ | `medium` | A target inferred from a strong convention | `inference.unique_method_name`, `rails.delegate` |
187
+ | `low` | A naming or path heuristic only | `convention.request_spec_path` |
188
+
189
+ By default RippleEffect traverses `high` and `medium` edges. Pass
190
+ `--include-low-confidence` to widen, or `--min-confidence high` to narrow.
191
+
192
+ A path is only as confident as its weakest link, and RippleEffect reports the
193
+ *shortest* chain of reasoning, because that is the one you can check fastest.
194
+
195
+ There are no percentages. Ruby is too dynamic for a number like "91% chance of
196
+ breakage" to mean anything, and a made-up number is worse than no number.
197
+
198
+ See [docs/ANALYSIS_MODEL.md](docs/ANALYSIS_MODEL.md) for every edge type and the
199
+ exact resolution rules.
200
+
201
+ ## Safety and privacy
202
+
203
+ RippleEffect reads files. That is all.
204
+
205
+ - No network access, ever. No telemetry.
206
+ - Your application is never booted and your source is never `eval`ed.
207
+ - No database connection.
208
+ - Git is invoked with argument arrays, never a shell string.
209
+ - YAML config is loaded with safe loading: no aliases, no object deserialisation.
210
+ - The cache is JSON, never Marshal.
211
+ - Paths are normalised and confined to the project root; a symlink pointing
212
+ outside it is refused.
213
+
214
+ `ripple-effect doctor` restates these guarantees. See [SECURITY.md](SECURITY.md).
215
+
216
+ ## Supported versions
217
+
218
+ - Ruby 3.2, 3.3, 3.4: the full suite is run against each
219
+ - Rails 7.0 – 8.1 (analysed statically; Rails is not a dependency)
220
+
221
+ ### Validated against real applications
222
+
223
+ v0.1 was checked against six real Rails codebases: two private production
224
+ applications and four large open-source ones: covering Rails 7.0 through 8.1,
225
+ classic ERB apps, an API-only app, a Haml-based app, and an engine monorepo:
226
+
227
+ | Application | Ruby files | Templates | Graph | Cold |
228
+ | --- | --- | --- | --- | --- |
229
+ | lobsters | 258 | 121 erb | 1.4k nodes / 4.8k edges | 0.5s |
230
+ | rubygems.org | 1,044 | 173 erb | 5.7k nodes / 20.6k edges | 2.2s |
231
+ | Solidus (6 engines) | 1,810 | 299 erb | 8.9k nodes / 22.8k edges | 6.6s |
232
+ | Mastodon | 2,574 | 46 erb | 13.3k nodes / 47.7k edges | 4.4s |
233
+ | private app (Rails 7.0) | 521 | 688 erb | 5.0k nodes / 12.8k edges | 0.8s |
234
+ | private app (Rails 8.0) | 845 | 33 erb | 7.7k nodes / 21.1k edges | 1.2s |
235
+
236
+ None crashed, and none reported an analysis error. Warm runs are well under a
237
+ tenth of a second.
238
+
239
+ Dependents were ground-truthed against an exhaustive text search of each codebase
240
+ (`script/ground_truth.rb`). Across **18 heavily-referenced methods in those six
241
+ applications, there were zero false positives**, RippleEffect never claimed a
242
+ dependency that was not really there. Several were exact matches (151 of 151
243
+ files for one Mastodon method, 47 of 47 for one private-app helper).
244
+
245
+ Recall is the weaker side, by design: where a receiver is ambiguous RippleEffect
246
+ records a diagnostic instead of guessing, so it misses some real relationships.
247
+ See [Known limitations](#known-limitations).
248
+
249
+ That is a spot check, not a guarantee. Please report anything it gets wrong.
250
+
251
+ ## Known limitations
252
+
253
+ RippleEffect prefers a false negative to a confident false positive. It will tell
254
+ you when it does not know.
255
+
256
+ - **Dynamic dispatch.** `send`, `public_send`, `method_missing` and metaprogrammed
257
+ methods are not resolved. Where a receiver cannot be resolved and one method
258
+ name matches uniquely, a `medium`-confidence edge is created; where several
259
+ match, a diagnostic is recorded and no edge is invented.
260
+ - **Polymorphic associations** have no static target. They are reported as a
261
+ diagnostic, not resolved.
262
+ - **Non-literal Rails DSL.** Callbacks, associations and routes built from
263
+ variables, constants or loops are not read.
264
+ - **Route DSL coverage** is conservative: explicit verbs with `to:`,
265
+ `root`, `resources`/`resource` with `only:`/`except:`/`controller:`, `namespace`
266
+ and `scope`. Constraints, `concern`, and `direct` are not modelled.
267
+ - **Test selection is a recommendation.** It is never permission to skip the rest
268
+ of your suite. When a global or boot-impact file changes, `tests` refuses to
269
+ print a narrowed list unless you pass `--allow-unsafe-focus`.
270
+ - **Recall is incomplete.** Where a call's receiver is ambiguous,
271
+ RippleEffect records a diagnostic and adds no edge, so some real dependencies
272
+ are missed. Run with `--verbose` to see exactly what was skipped.
273
+ - **ERB templates are indexed**, but a call with an explicit receiver inside one
274
+ (`<%= @order.total %>`) is not resolved: the receiver's type is unknown.
275
+ Non-ERB templates (Haml, Slim, Builder), JavaScript, and SQL are not indexed.
276
+
277
+ ## Development
278
+
279
+ ```console
280
+ $ bin/setup # or: bundle install
281
+ $ bundle exec rake # specs + RuboCop
282
+ $ bundle exec rspec
283
+ $ bundle exec rubocop
284
+ $ bundle exec rake smoke # build, install into a temp GEM_HOME, run the CLI
285
+ $ bundle exec rake benchmark
286
+ ```
287
+
288
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [ARCHITECTURE.md](ARCHITECTURE.md) and
289
+ [docs/TESTING.md](docs/TESTING.md).
290
+
291
+ ## Status
292
+
293
+ v0.1.0 is an early release. The analysis model and CLI are stable enough to use,
294
+ but expect rough edges on unusual codebases. Bug reports with a small
295
+ reproduction are very welcome.
296
+
297
+ ## Acknowledgements
298
+
299
+ RippleEffect's Ruby declaration and reference index is powered by
300
+ [Rubydex](https://github.com/Shopify/rubydex) (MIT), and its Rails DSL parsing by
301
+ [Prism](https://github.com/ruby/prism).
302
+
303
+ ## License
304
+
305
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/SECURITY.md ADDED
@@ -0,0 +1,73 @@
1
+ # Security policy
2
+
3
+ ## Threat model
4
+
5
+ RippleEffect reads files in your project and runs `git`. That is the whole of it.
6
+
7
+ It is designed to be safe to run against a codebase you do not fully trust, and to
8
+ be safe to run in CI.
9
+
10
+ ### Guarantees
11
+
12
+ | Guarantee | How |
13
+ | --- | --- |
14
+ | **No network access** | Nothing in the gem opens a socket. Analysis is entirely local. |
15
+ | **No telemetry** | Nothing is collected, counted, or transmitted. |
16
+ | **Your application is never booted** | RippleEffect analyses source statically. It never loads your Rails environment or initializers. |
17
+ | **Your source is never evaluated** | No `eval`, `instance_eval`, or `Kernel#load` of analysed code. Parsing only. |
18
+ | **No database connection** | No adapter, connection, or query, ever. |
19
+ | **Safe subprocess invocation** | Git runs through `Open3.capture3` with argument arrays. A ref containing `; rm -rf /` is a ref that does not exist, not a command. There is a spec for this. |
20
+ | **Safe YAML** | Config is read with `YAML.safe_load_file`, no aliases and no permitted classes. A config file cannot construct objects. |
21
+ | **Safe cache** | The cache is JSON. Never Marshal, which would turn a corrupted cache file into arbitrary object construction. |
22
+ | **Path confinement** | Paths are normalised and symlinks resolved before comparison; anything resolving outside the project root is refused. |
23
+
24
+ `ripple-effect doctor` restates these at runtime.
25
+
26
+ ### What RippleEffect writes
27
+
28
+ Only the cache, by default `tmp/ripple_effect/`, containing `manifest.json` and
29
+ `graph.json`. Disable it with `cache.enabled: false` or `--no-cache`.
30
+
31
+ The cache contains symbol names, file paths and line numbers from your project. It
32
+ should be treated with the same care as your source, and should not be committed.
33
+
34
+ ### Residual risks
35
+
36
+ - **Subprocess:** RippleEffect executes the `git` on your `PATH`. A compromised
37
+ `git` is outside its control.
38
+ - **Dependencies:** `rubydex` (which includes a native extension) and `prism`.
39
+ - **Output:** analysis output contains your symbol names and paths. Redact before
40
+ sharing a report publicly.
41
+
42
+ ## Supported versions
43
+
44
+ | Version | Supported |
45
+ | --- | --- |
46
+ | 0.1.x | Yes |
47
+
48
+ ## Reporting a vulnerability
49
+
50
+ **Please do not open a public issue for a security problem.**
51
+
52
+ Report it privately through GitHub's private vulnerability reporting:
53
+
54
+ <https://github.com/iamzayn19/ripple-effect/security/advisories/new>
55
+
56
+ If private reporting is not enabled yet, email <iamzayn19@gmail.com> instead.
57
+
58
+ Please include:
59
+
60
+ - what the vulnerability allows an attacker to do,
61
+ - the affected version,
62
+ - reproduction steps or a proof of concept,
63
+ - any suggested fix.
64
+
65
+ ### What to expect
66
+
67
+ - **Acknowledgement** within 3 working days.
68
+ - **An initial assessment** within 10 working days.
69
+ - **A fix or a plan** shared with you before public disclosure.
70
+ - **Credit** in the release notes, unless you prefer otherwise.
71
+
72
+ We ask that you give us a reasonable opportunity to fix an issue before disclosing
73
+ it publicly.
@@ -0,0 +1,275 @@
1
+ # Analysis model
2
+
3
+ What RippleEffect claims, what it refuses to claim, and why.
4
+
5
+ ## Node kinds
6
+
7
+ | Kind | What it represents |
8
+ | --- | --- |
9
+ | `file` | A source file |
10
+ | `test_file` | A file under `spec/` or `test/` |
11
+ | `class` | A class declaration |
12
+ | `module` | A module declaration |
13
+ | `instance_method` | `Order#total` |
14
+ | `class_method` | `Order.recent` |
15
+ | `route` | One routing entry, e.g. `POST /checkout` |
16
+ | `callback` | One lifecycle callback declaration |
17
+ | `association` | Reserved for association nodes; v0.1 models associations as edges |
18
+ | `job` | An Active Job class, as a background entry point |
19
+ | `mailer_action` | One mailer action, e.g. `OrderMailer.receipt` |
20
+ | `view` | An ERB template or partial |
21
+ | `unknown` | Fallback; not emitted in v0.1 |
22
+
23
+ ### Node IDs
24
+
25
+ Deterministic and readable, never random. The path is part of the ID so a class
26
+ reopened in two files does not collide.
27
+
28
+ ```
29
+ file:app/models/user.rb
30
+ class:app/models/order.rb:Order
31
+ method:app/services/billing_service.rb:BillingService#charge
32
+ route:config/routes.rb:5:route:POST:/checkout
33
+ ```
34
+
35
+ ### Canonical symbol names
36
+
37
+ | Kind | Canonical form | Also accepted |
38
+ | --- | --- | --- |
39
+ | Instance method | `User#activate!` |, |
40
+ | Singleton method | `User.find` | `User::find` |
41
+ | Namespaced constant | `User::Profile` |, |
42
+
43
+ ## Edge direction
44
+
45
+ One rule, never varied:
46
+
47
+ > `from_id` **depends on / invokes / references** `into_id`
48
+
49
+ Walking edges backwards from a changed node therefore finds its dependents.
50
+
51
+ ## Edge types
52
+
53
+ | Type | Meaning | Typical evidence |
54
+ | --- | --- | --- |
55
+ | `constant_reference` | A constant is mentioned | `rubydex.constant_reference` |
56
+ | `method_call` | A method is called | `rubydex.method_reference`, `inference.unique_method_name` |
57
+ | `inheritance` | `class B < A` | `ruby.superclass` |
58
+ | `include` | `include M` | `ruby.include` |
59
+ | `prepend` | `prepend M` | `ruby.prepend` |
60
+ | `extend` | `extend M` | `ruby.extend` |
61
+ | `association` | An ActiveRecord association | `rails.belongs_to`, `rails.has_many`, … |
62
+ | `callback` | A lifecycle callback, or its condition | `rails.after_commit`, `rails.callback_condition`, … |
63
+ | `delegate` | `delegate ..., to:` | `rails.delegate` |
64
+ | `route_handler` | A route dispatches to a controller action | `rails.route_to_controller` |
65
+ | `job_enqueue` | A job is enqueued, or a job class owns its `#perform` | `rails.perform_later`, `rails.job_perform` |
66
+ | `mailer_delivery` | Mail is delivered, or a mailer owns its action | `rails.deliver_later`, `rails.mailer_action` |
67
+ | `test_convention` | A test covers code | `convention.rspec_path`, `convention.minitest_path`, `convention.request_spec_path` |
68
+ | `file_reference` | Structural containment, and template rendering | `ruby.file_declares`, `ruby.defines_method`, `rails.renders_template`, `rails.render_partial` |
69
+
70
+ ### Structural edges
71
+
72
+ `file_reference` edges are graph plumbing: a file "depends on" the classes it
73
+ declares, and a class on the methods it defines. They exist so a file-level change
74
+ can reach its code, and so a method change reaches everything that depends on its
75
+ class.
76
+
77
+ Text output hides them, because reporting a file as a dependent of its own method
78
+ buries the real answer. JSON keeps them.
79
+
80
+ ## Confidence
81
+
82
+ Three bands, no percentages. Ruby is too dynamic for a number like "91% chance of
83
+ breakage" to mean anything, and a fabricated number is worse than none.
84
+
85
+ | Band | Rule | Examples |
86
+ | --- | --- | --- |
87
+ | `high` | An explicit static reference, or a literal Rails DSL relationship | `rails.after_commit`, `rubydex.constant_reference`, `ruby.superclass` |
88
+ | `medium` | A target inferred from a strong convention | `inference.unique_method_name`, `rails.delegate` |
89
+ | `low` | A naming or path heuristic only | `convention.request_spec_path` |
90
+
91
+ **A path is as confident as its weakest link.** Default traversal is `high` +
92
+ `medium`; `low` requires `--include-low-confidence`.
93
+
94
+ ## Method call resolution
95
+
96
+ | Case | Example | Result |
97
+ | --- | --- | --- |
98
+ | Receiver resolved by the index | `validate!` inside the same class | `high` edge |
99
+ | Explicit constant receiver | `InvoiceJob.perform_later(id)` | `high` edge |
100
+ | Unresolved receiver, one method in the project has that name | `billing.charge(order)` | `medium` edge |
101
+ | Unresolved receiver, several methods share the name | `x.run` where `A#run` and `B#run` exist | **No edge**, `unresolved_method_receiver` diagnostic |
102
+ | `send`, `public_send`, dynamic names | `public_send(name)` | **No edge** |
103
+ | `method_missing`, generated methods |, | **No edge** |
104
+
105
+ The fourth row is the heart of the design. Choosing between candidates would
106
+ produce a confident falsehood, and one of those costs more trust than many honest
107
+ "I don't know"s.
108
+
109
+ ## Rails semantics
110
+
111
+ ### Associations
112
+
113
+ Literal macros only: `belongs_to`, `has_one`, `has_many`,
114
+ `has_and_belongs_to_many`.
115
+
116
+ - Target resolved from an explicit `class_name:` when literal, otherwise from
117
+ Rails' own naming convention (`has_many :orders` → `Order`).
118
+ - Metadata records `macro`, `association_name`, `class_name`, `foreign_key`,
119
+ `through`.
120
+ - Scopes and blocks are **never executed**.
121
+ - **Polymorphic** targets have no static answer. They produce an
122
+ `unresolved_polymorphic_association` diagnostic, never an invented constant.
123
+ - A target that is not indexed produces `unresolved_association_target`.
124
+
125
+ ### Callbacks
126
+
127
+ `before_validation`, `after_validation`, `before/around/after` × `save`, `create`,
128
+ `update`, `destroy`, plus `after_commit`, `after_rollback`, `after_touch`,
129
+ `after_initialize`, `after_find`.
130
+
131
+ Each declaration becomes a `callback` node, so the reason survives into the
132
+ explanation. The model reaches the callback; the callback reaches the method.
133
+
134
+ - Symbol and string names are both read, including several in one call.
135
+ - `if:` / `unless:` predicates get their own edge: they run on every save too.
136
+ - A block callback becomes an anonymous node anchored to its line range; the calls
137
+ inside it are indexed normally.
138
+
139
+ ### Jobs
140
+
141
+ A class is a job when it inherits from `ApplicationJob` or `ActiveJob::Base`,
142
+ directly or through intermediates.
143
+
144
+ - `perform_later` and `perform_now` with a constant receiver link the caller to
145
+ `Job#perform` at `high` confidence.
146
+ - `Job.set(...).perform_later` is followed, because the constant is still visible.
147
+ - A non-constant receiver is ignored. No queue or adapter is ever loaded.
148
+
149
+ ### Mailers
150
+
151
+ A class is a mailer when it inherits from `ApplicationMailer` or
152
+ `ActionMailer::Base`. Each instance method becomes a `mailer_action` node —
153
+ Action Mailer synthesises the class-level call, so it exists nowhere in the source.
154
+
155
+ `OrderMailer.receipt(order).deliver_later` links the sender to
156
+ `OrderMailer#receipt`.
157
+
158
+ ### Routes
159
+
160
+ conservative. Supported:
161
+
162
+ - Explicit verbs with `to: "controller#action"`, and `root to:`
163
+ - `resources` / `resource`, honouring literal `only:`, `except:` and `controller:`
164
+ - `namespace` (prefixes both path and controller module)
165
+ - `scope` with a literal path or `module:`
166
+ - Nested resources inside a `resources` block
167
+
168
+ Not modelled in v0.1: constraints, `concern`, `direct`, `resolve`, and routes
169
+ built from variables or loops. A route whose controller is not indexed produces
170
+ `unresolved_route_controller`.
171
+
172
+ ### Views
173
+
174
+ ERB templates are part of the graph. The Ruby is lifted out of the ERB tags and
175
+ parsed with Prism; a template is never rendered and its code is never executed.
176
+
177
+ Rails' own ERB handler: not stdlib `ERB`, is what understands
178
+ `<%= form_with do |f| %>`, so the tag contents are read directly rather than
179
+ compiled through `ERB#src`, which turns that form into a syntax error.
180
+
181
+ | Relationship | Evidence | Confidence |
182
+ | --- | --- | --- |
183
+ | A controller action renders its conventional template | `rails.renders_template` | `high` |
184
+ | A template renders a partial | `rails.render_partial` | `high` |
185
+ | A template references a constant | `view.constant_reference` | `high` |
186
+ | A template calls a uniquely-named helper or model method | `view.helper_call` | `medium` |
187
+ | `helper_method :foo` exposes a controller method to templates | `rails.helper_method` | `high` on the controller, `medium` via an included concern |
188
+
189
+ A call with an explicit receiver in a template (`@order.total`) is not resolved:
190
+ the receiver is a local or instance variable whose type is unknown.
191
+
192
+ ### Delegation
193
+
194
+ `delegate :name, to: :account` names a **receiver**, not a class. RippleEffect
195
+ resolves it through an association of the same name on the same class, and
196
+ otherwise through the conventional class name: at `medium` confidence, because
197
+ it is an inference.
198
+
199
+ An unresolvable receiver produces `unresolved_delegate_target` and no edge.
200
+
201
+ ## Test relevance
202
+
203
+ Ranked by *why* a test was reached:
204
+
205
+ | Rank | Reason | Meaning |
206
+ | --- | --- | --- |
207
+ | 1 | `reference` | The test actually references the changed code |
208
+ | 2 | `rails_semantic` | Reached through a Rails relationship |
209
+ | 3 | `convention` | It sits at the conventional path |
210
+ | 4 | `heuristic` | A looser path match, such as a request spec |
211
+
212
+ A changed test file always ranks first.
213
+
214
+ **Test selection is a recommendation, never permission to skip the rest of your
215
+ suite.** RippleEffect can prove a test is probably relevant; it cannot prove one is
216
+ irrelevant.
217
+
218
+ ## Risk
219
+
220
+ Discrete bands with stated reasons: a ranking aid for reviewers, not a defect
221
+ prediction.
222
+
223
+ | Points | For |
224
+ | --- | --- |
225
+ | +1 each (max 8) | Direct dependents |
226
+ | +0.5 each (max 8) | Transitive dependents |
227
+ | +2 | Reachable from a route |
228
+ | +2 | Reachable from a background job |
229
+ | +2 | Reachable from a model callback |
230
+ | +1 | Reachable from a mailer |
231
+ | +3 | The change touches a class or module body, not just method bodies |
232
+ | +3 | A global or boot-impact file changed |
233
+ | +2 | Impact spans 3 or more architectural layers |
234
+
235
+ | Score | Band |
236
+ | --- | --- |
237
+ | `>= 18` | `critical` |
238
+ | `>= 10` | `high` |
239
+ | `>= 4` | `medium` |
240
+ | `< 4` | `low` |
241
+
242
+ File nodes are excluded from the dependent counts.
243
+
244
+ ## Diagnostics
245
+
246
+ Non-fatal facts about the analysis. Match on `code`, not on message text: codes
247
+ are stable, messages are not.
248
+
249
+ | Code | Meaning |
250
+ | --- | --- |
251
+ | `unresolved_polymorphic_association` | A polymorphic `belongs_to` has no static target |
252
+ | `unresolved_association_target` | The association's target class is not indexed |
253
+ | `unresolved_delegate_target` | The delegation receiver could not be resolved |
254
+ | `unresolved_method_receiver` | A call site's receiver is ambiguous or unknown |
255
+ | `unresolved_route_controller` | A route points at a controller action that is not indexed |
256
+ | `unparsed_file` | A file could not be read or parsed; analysis continued |
257
+ | `index_note` | The indexer reported a lint finding about a file it parsed successfully |
258
+ | `unmapped_changed_lines` | Changed lines could not be mapped to a declaration |
259
+ | `global_file_changed` | A boot-impact file changed; impact may be broader than shown |
260
+ | `deleted_file` | A file was deleted; anything referencing it may break |
261
+ | `renamed_file` | A file was renamed |
262
+ | `cache_discarded` | The cache was unreadable and has been rebuilt |
263
+ | `dynamic_dispatch` | Reserved for dynamic dispatch reporting |
264
+ | `empty_index` | Files were indexed but no declarations found; results are not trustworthy |
265
+
266
+ Severities are `info`, `warning` and `error`. Text output shows warnings by
267
+ default and everything with `--verbose`; JSON always includes all of them.
268
+
269
+ ## What RippleEffect does not do
270
+
271
+ - Execute your code, boot Rails, or connect to a database.
272
+ - Resolve dynamic dispatch or metaprogrammed methods.
273
+ - Analyse non-ERB templates (Haml, Slim, Builder), JavaScript, or SQL.
274
+ - Follow impact across repositories.
275
+ - Prove that unselected tests are unnecessary.