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.
- checksums.yaml +7 -0
- data/.ripple-effect.yml.example +56 -0
- data/ARCHITECTURE.md +222 -0
- data/CHANGELOG.md +115 -0
- data/CODE_OF_CONDUCT.md +64 -0
- data/CONTRIBUTING.md +112 -0
- data/LICENSE.txt +21 -0
- data/README.md +305 -0
- data/SECURITY.md +73 -0
- data/docs/ANALYSIS_MODEL.md +275 -0
- data/docs/CLI.md +276 -0
- data/docs/CONFIGURATION.md +178 -0
- data/docs/DECISIONS.md +210 -0
- data/docs/PUBLIC_LAUNCH_CHECKLIST.md +105 -0
- data/docs/RELEASING.md +94 -0
- data/docs/TESTING.md +179 -0
- data/exe/ripple-effect +7 -0
- data/lib/ripple_effect/analyzer.rb +379 -0
- data/lib/ripple_effect/cache_store.rb +207 -0
- data/lib/ripple_effect/cli/application.rb +126 -0
- data/lib/ripple_effect/cli/command.rb +165 -0
- data/lib/ripple_effect/cli/diff_command.rb +76 -0
- data/lib/ripple_effect/cli/doctor_command.rb +106 -0
- data/lib/ripple_effect/cli/graph_command.rb +61 -0
- data/lib/ripple_effect/cli/inspect_command.rb +66 -0
- data/lib/ripple_effect/cli/tests_command.rb +109 -0
- data/lib/ripple_effect/cli/version_command.rb +46 -0
- data/lib/ripple_effect/confidence.rb +61 -0
- data/lib/ripple_effect/configuration.rb +264 -0
- data/lib/ripple_effect/diagnostic.rb +90 -0
- data/lib/ripple_effect/diff/changed_symbol_resolver.rb +292 -0
- data/lib/ripple_effect/diff/git.rb +175 -0
- data/lib/ripple_effect/diff/hunk.rb +80 -0
- data/lib/ripple_effect/edge.rb +114 -0
- data/lib/ripple_effect/error.rb +23 -0
- data/lib/ripple_effect/extractors/base.rb +292 -0
- data/lib/ripple_effect/extractors/rails_associations.rb +102 -0
- data/lib/ripple_effect/extractors/rails_callbacks.rb +144 -0
- data/lib/ripple_effect/extractors/rails_delegation.rb +121 -0
- data/lib/ripple_effect/extractors/rails_jobs.rb +131 -0
- data/lib/ripple_effect/extractors/rails_mailers.rb +120 -0
- data/lib/ripple_effect/extractors/rails_routes.rb +256 -0
- data/lib/ripple_effect/extractors/rails_views.rb +299 -0
- data/lib/ripple_effect/extractors/ruby_structure.rb +221 -0
- data/lib/ripple_effect/extractors/test_conventions.rb +135 -0
- data/lib/ripple_effect/formatters/dot.rb +69 -0
- data/lib/ripple_effect/formatters/json.rb +43 -0
- data/lib/ripple_effect/formatters/text.rb +197 -0
- data/lib/ripple_effect/graph.rb +199 -0
- data/lib/ripple_effect/node.rb +153 -0
- data/lib/ripple_effect/project.rb +264 -0
- data/lib/ripple_effect/result.rb +147 -0
- data/lib/ripple_effect/risk.rb +167 -0
- data/lib/ripple_effect/static_index/adapter.rb +84 -0
- data/lib/ripple_effect/static_index/rubydex_adapter.rb +356 -0
- data/lib/ripple_effect/traversal/impact_walker.rb +153 -0
- data/lib/ripple_effect/version.rb +11 -0
- data/lib/ripple_effect.rb +89 -0
- metadata +155 -0
data/docs/CLI.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
ripple-effect COMMAND [options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Exit codes
|
|
8
|
+
|
|
9
|
+
These are part of the interface and are the same for every command.
|
|
10
|
+
|
|
11
|
+
| Code | Meaning |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `0` | Success |
|
|
14
|
+
| `1` | A threshold you asked us to enforce was met (`--fail-on-risk`) |
|
|
15
|
+
| `2` | Your input: a bad query, bad configuration, bad usage, or a refused unsafe focus |
|
|
16
|
+
| `3` | Our problem: the analyzer or index failed |
|
|
17
|
+
|
|
18
|
+
Finding impact is never, by itself, a failure.
|
|
19
|
+
|
|
20
|
+
## Global options
|
|
21
|
+
|
|
22
|
+
Accepted **before or after** the subcommand.
|
|
23
|
+
|
|
24
|
+
| Option | Meaning |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `--root PATH` | Project root (default: current directory) |
|
|
27
|
+
| `--config PATH` | Path to a `.ripple-effect.yml`; must exist |
|
|
28
|
+
| `--format text\|json` | Output format (default `text`; `tests` defaults to `paths`) |
|
|
29
|
+
| `--no-cache` | Ignore and do not write the graph cache |
|
|
30
|
+
| `--verbose` | Include stats and every diagnostic |
|
|
31
|
+
| `--quiet` | Suppress non-essential output |
|
|
32
|
+
| `-v`, `--version` | Print the version |
|
|
33
|
+
| `-h`, `--help` | Show usage |
|
|
34
|
+
|
|
35
|
+
## `inspect`
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
ripple-effect inspect SYMBOL [options]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
What may be affected by changing `SYMBOL`, and why.
|
|
42
|
+
|
|
43
|
+
**Symbol forms**
|
|
44
|
+
|
|
45
|
+
| Form | Example |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Class or module | `User`, `User::Profile` |
|
|
48
|
+
| Instance method | `User#activate!` |
|
|
49
|
+
| Singleton method | `User.find` (`User::find` is also accepted) |
|
|
50
|
+
| File | `app/models/user.rb` |
|
|
51
|
+
|
|
52
|
+
| Option | Meaning |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `--depth N\|all` | Maximum hops to follow (default: unlimited) |
|
|
55
|
+
| `--path PATH` | Disambiguate a symbol declared in several files |
|
|
56
|
+
| `--min-confidence high\|medium\|low` | Only follow edges at least this confident |
|
|
57
|
+
| `--include-low-confidence` | Also follow low-confidence edges |
|
|
58
|
+
| `--direction dependents\|dependencies\|both` | Default `dependents` |
|
|
59
|
+
|
|
60
|
+
```console
|
|
61
|
+
$ ripple-effect inspect 'BillingService#charge'
|
|
62
|
+
$ ripple-effect inspect User.find --depth 2 --format json
|
|
63
|
+
$ ripple-effect inspect 'User#save' --path lib/legacy/user.rb
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
An ambiguous symbol is an error, never a guess:
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
$ ripple-effect inspect 'User#save'
|
|
70
|
+
Error: `User#save` matched 2 declarations
|
|
71
|
+
1. app/models/user.rb:42
|
|
72
|
+
2. lib/legacy/user.rb:18
|
|
73
|
+
Use --path to disambiguate.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `diff`
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
ripple-effect diff BASE [HEAD] [options]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The blast radius of everything changed between two revisions.
|
|
83
|
+
|
|
84
|
+
**Revision semantics**
|
|
85
|
+
|
|
86
|
+
| Invocation | Compares |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `diff main` | `main` against the **working tree**, committed, staged, unstaged and untracked |
|
|
89
|
+
| `diff main HEAD` | `main` against `HEAD`, committed changes only |
|
|
90
|
+
| `diff v1.2.0 HEAD` | Any two refs |
|
|
91
|
+
|
|
92
|
+
| Option | Meaning |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `--depth N\|all` | Maximum hops to follow |
|
|
95
|
+
| `--min-confidence LEVEL` | Only follow edges at least this confident |
|
|
96
|
+
| `--include-low-confidence` | Also follow low-confidence edges |
|
|
97
|
+
| `--fail-on-risk low\|medium\|high\|critical` | Exit `1` when risk reaches this level |
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
$ ripple-effect diff main
|
|
101
|
+
$ ripple-effect diff origin/main --fail-on-risk critical
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## `tests`
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
ripple-effect tests BASE [HEAD] [options]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The test files most likely relevant to a change.
|
|
111
|
+
|
|
112
|
+
The default `paths` format prints **only** test paths on stdout: every warning
|
|
113
|
+
goes to stderr: so it composes:
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
$ bundle exec rspec $(ripple-effect tests main)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Option | Meaning |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `--runner rspec\|minitest\|auto` | Test framework (default `auto`) |
|
|
122
|
+
| `--allow-unsafe-focus` | Print a narrowed list even when a global file changed |
|
|
123
|
+
| `--min-confidence LEVEL` | Only follow edges at least this confident |
|
|
124
|
+
| `--format paths\|text\|json` | Default `paths` |
|
|
125
|
+
|
|
126
|
+
### The unsafe-focus refusal
|
|
127
|
+
|
|
128
|
+
When a boot-impact file changes. `Gemfile`, `config/initializers/**`,
|
|
129
|
+
`config/routes.rb`, `spec/rails_helper.rb`, and the rest of
|
|
130
|
+
`tests.unsafe_global_files`, the graph cannot bound what it reaches. Printing a
|
|
131
|
+
short list would be an implicit claim that the other tests are unnecessary, which
|
|
132
|
+
RippleEffect does not make.
|
|
133
|
+
|
|
134
|
+
So the command prints nothing to stdout and exits `2`:
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
$ ripple-effect tests main
|
|
138
|
+
WARNING: focused tests may be incomplete because config/initializers/payments.rb changed.
|
|
139
|
+
These files affect application boot or configuration, so the impact graph cannot bound what they reach.
|
|
140
|
+
Run your full suite, or pass --allow-unsafe-focus to print a narrowed list anyway.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## `graph`
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
ripple-effect graph SYMBOL [options]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The impacted subgraph: not the whole project.
|
|
150
|
+
|
|
151
|
+
| Option | Meaning |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `--format dot\|json` | Default `dot` |
|
|
154
|
+
| `--depth N\|all` | Maximum hops to follow |
|
|
155
|
+
| `--path PATH` | Disambiguate a symbol declared in several files |
|
|
156
|
+
|
|
157
|
+
```console
|
|
158
|
+
$ ripple-effect graph 'BillingService#charge' --format dot | dot -Tsvg -o impact.svg
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Node shapes distinguish routes, jobs, callbacks and mailers; edge line styles show
|
|
162
|
+
confidence (solid = high, dashed = medium, dotted = low).
|
|
163
|
+
|
|
164
|
+
## `doctor`
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
ripple-effect doctor
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Checks the project root, Git availability, Ruby version support, Rails-like
|
|
171
|
+
layout, the static index backend, config validity, how many files were indexed,
|
|
172
|
+
diagnostic count, and cache writability: then restates the privacy guarantees.
|
|
173
|
+
|
|
174
|
+
Exits `2` if the Ruby version is unsupported. Makes no network calls.
|
|
175
|
+
|
|
176
|
+
## `version`
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
ripple-effect version
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Prints the bare semantic version, so it can be captured directly:
|
|
183
|
+
|
|
184
|
+
```console
|
|
185
|
+
$ VERSION=$(ripple-effect version)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--verbose` adds the schema version, Ruby version and index backend; `--format
|
|
189
|
+
json` emits the same as JSON.
|
|
190
|
+
|
|
191
|
+
## JSON output
|
|
192
|
+
|
|
193
|
+
Every command accepts `--format json`. The document is versioned and
|
|
194
|
+
deterministic: identical input produces byte-identical output.
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"schema_version": 1,
|
|
199
|
+
"tool": { "name": "ripple_effect", "version": "0.1.0" },
|
|
200
|
+
"query": { "type": "symbol", "value": "BillingService#charge" },
|
|
201
|
+
"changed_nodes": [],
|
|
202
|
+
"impacted_nodes": [
|
|
203
|
+
{
|
|
204
|
+
"id": "method:app/services/checkout_service.rb:CheckoutService#call",
|
|
205
|
+
"kind": "instance_method",
|
|
206
|
+
"name": "CheckoutService#call",
|
|
207
|
+
"qualified_name": "CheckoutService#call",
|
|
208
|
+
"path": "app/services/checkout_service.rb",
|
|
209
|
+
"start_line": 2,
|
|
210
|
+
"end_line": 5,
|
|
211
|
+
"metadata": {},
|
|
212
|
+
"depth": 1,
|
|
213
|
+
"confidence": "medium",
|
|
214
|
+
"source_id": "method:app/services/billing_service.rb:BillingService#charge",
|
|
215
|
+
"evidence_path": [
|
|
216
|
+
{
|
|
217
|
+
"type": "method_call",
|
|
218
|
+
"evidence": "inference.unique_method_name",
|
|
219
|
+
"confidence": "medium",
|
|
220
|
+
"location": "app/services/checkout_service.rb:3"
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
"tests": [
|
|
226
|
+
{
|
|
227
|
+
"path": "spec/services/billing_service_spec.rb",
|
|
228
|
+
"reason": "reference",
|
|
229
|
+
"evidence": "inference.unique_method_name",
|
|
230
|
+
"confidence": "medium",
|
|
231
|
+
"depth": 1
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"risk": { "level": "high", "score": 15.0, "reasons": ["4 direct dependents"] },
|
|
235
|
+
"diagnostics": [
|
|
236
|
+
{
|
|
237
|
+
"severity": "info",
|
|
238
|
+
"code": "unresolved_route_controller",
|
|
239
|
+
"path": "config/routes.rb",
|
|
240
|
+
"line": 4,
|
|
241
|
+
"message": "GET /health routes to HealthController#show, which is not indexed"
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
"stats": { "files_indexed": 23, "nodes": 75, "edges": 160, "cache": "hit" }
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Guarantees:
|
|
249
|
+
|
|
250
|
+
- `schema_version` is always present and increments on any breaking change.
|
|
251
|
+
- All file paths are project-relative. (Route URLs also begin with `/`, those are
|
|
252
|
+
URLs, in `metadata`, not paths.)
|
|
253
|
+
- No Ruby symbols; every value is JSON-native.
|
|
254
|
+
- Key order is fixed.
|
|
255
|
+
|
|
256
|
+
Unlike text output, JSON includes structural nodes (files, and the class that owns
|
|
257
|
+
a changed method). Text hides them as noise; machine consumers may want them.
|
|
258
|
+
|
|
259
|
+
### Errors in JSON mode
|
|
260
|
+
|
|
261
|
+
Errors are JSON too, on **stderr**, with a non-zero exit. stdout stays empty.
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"schema_version": 1,
|
|
266
|
+
"tool": { "name": "ripple_effect", "version": "0.1.0" },
|
|
267
|
+
"error": {
|
|
268
|
+
"code": "query_error",
|
|
269
|
+
"type": "RippleEffect::QueryError",
|
|
270
|
+
"message": "no indexed symbol matches `Nope#nope`"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Error codes: `usage`, `query_error`, `configuration_error`, `project_error`,
|
|
276
|
+
`git_error`, `index_error`, `unsafe_focus`.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
RippleEffect works with no configuration. To override anything, add
|
|
4
|
+
`.ripple-effect.yml` to your project root.
|
|
5
|
+
|
|
6
|
+
The `version` key is **required** whenever the file exists, so that a future schema
|
|
7
|
+
change can be detected rather than silently misread.
|
|
8
|
+
|
|
9
|
+
## Full schema
|
|
10
|
+
|
|
11
|
+
Every value shown is the default.
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
version: 1
|
|
15
|
+
|
|
16
|
+
paths:
|
|
17
|
+
include:
|
|
18
|
+
- app/**/*.rb
|
|
19
|
+
- lib/**/*.rb
|
|
20
|
+
- config/routes.rb
|
|
21
|
+
- config/routes/**/*.rb
|
|
22
|
+
- spec/**/*.rb
|
|
23
|
+
- test/**/*.rb
|
|
24
|
+
views:
|
|
25
|
+
- app/views/**/*.erb
|
|
26
|
+
discover_engines: true
|
|
27
|
+
exclude:
|
|
28
|
+
- vendor/**
|
|
29
|
+
- node_modules/**
|
|
30
|
+
- tmp/**
|
|
31
|
+
- log/**
|
|
32
|
+
- coverage/**
|
|
33
|
+
- public/assets/**
|
|
34
|
+
- storage/**
|
|
35
|
+
- .bundle/**
|
|
36
|
+
- .git/**
|
|
37
|
+
|
|
38
|
+
analysis:
|
|
39
|
+
min_confidence: medium
|
|
40
|
+
default_depth: all
|
|
41
|
+
include_low_confidence: false
|
|
42
|
+
|
|
43
|
+
rails:
|
|
44
|
+
routes: true
|
|
45
|
+
associations: true
|
|
46
|
+
callbacks: true
|
|
47
|
+
jobs: true
|
|
48
|
+
mailers: true
|
|
49
|
+
delegation: true
|
|
50
|
+
views: true
|
|
51
|
+
|
|
52
|
+
tests:
|
|
53
|
+
framework: auto
|
|
54
|
+
unsafe_global_files:
|
|
55
|
+
- Gemfile
|
|
56
|
+
- Gemfile.lock
|
|
57
|
+
- config/application.rb
|
|
58
|
+
- config/environment.rb
|
|
59
|
+
- config/boot.rb
|
|
60
|
+
- config/environments/**
|
|
61
|
+
- config/initializers/**
|
|
62
|
+
- config/routes.rb
|
|
63
|
+
- spec/spec_helper.rb
|
|
64
|
+
- spec/rails_helper.rb
|
|
65
|
+
- test/test_helper.rb
|
|
66
|
+
|
|
67
|
+
cache:
|
|
68
|
+
enabled: true
|
|
69
|
+
directory: tmp/ripple_effect
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Keys
|
|
73
|
+
|
|
74
|
+
### `paths`
|
|
75
|
+
|
|
76
|
+
Which files are indexed. Globs are project-relative; a leading `./` is stripped.
|
|
77
|
+
|
|
78
|
+
Test directories are included deliberately: indexing your specs is what lets
|
|
79
|
+
RippleEffect rank a test that *references* the changed code above one that merely
|
|
80
|
+
sits at the conventional path.
|
|
81
|
+
|
|
82
|
+
### `paths.views`
|
|
83
|
+
|
|
84
|
+
ERB templates. These are not Ruby files, so they need their own patterns.
|
|
85
|
+
|
|
86
|
+
In a classic server-rendered Rails application the view layer is a large part of
|
|
87
|
+
the real dependency graph: helpers and models are used almost entirely from
|
|
88
|
+
templates. Omitting views does not just lose detail, it systematically
|
|
89
|
+
*understates* every helper's blast radius while the answer still looks complete.
|
|
90
|
+
|
|
91
|
+
Set to `[]` to skip templates entirely (or use `rails.views: false`).
|
|
92
|
+
|
|
93
|
+
### `paths.discover_engines`
|
|
94
|
+
|
|
95
|
+
Default `true`. Also index directories that are Rails engines or gems in their
|
|
96
|
+
own right: a directory with both an `app/` and either a `*.gemspec` or a
|
|
97
|
+
`lib/**/engine.rb`.
|
|
98
|
+
|
|
99
|
+
Engine monorepos are common: Solidus and Spree are built this way, and plenty of
|
|
100
|
+
applications keep an `engines/` directory. Such repos often have no top-level
|
|
101
|
+
`app/` at all, so without this the default patterns would match almost nothing.
|
|
102
|
+
The gemspec-or-engine requirement is what keeps ordinary subdirectories out.
|
|
103
|
+
|
|
104
|
+
`ripple-effect doctor` lists the engines it found.
|
|
105
|
+
|
|
106
|
+
### `analysis.min_confidence`
|
|
107
|
+
|
|
108
|
+
`high`, `medium` (default) or `low`. Edges weaker than this are not traversed.
|
|
109
|
+
|
|
110
|
+
- `high`, only explicit static references and literal Rails DSL relationships.
|
|
111
|
+
- `medium`, also convention-inferred targets. The default.
|
|
112
|
+
- `low`, also naming and path heuristics. Noisy.
|
|
113
|
+
|
|
114
|
+
### `analysis.default_depth`
|
|
115
|
+
|
|
116
|
+
A positive integer, or `all` (default) for unlimited, cycle-protected traversal.
|
|
117
|
+
`--depth` overrides it per invocation.
|
|
118
|
+
|
|
119
|
+
### `analysis.include_low_confidence`
|
|
120
|
+
|
|
121
|
+
Lowers the floor to `low`. Equivalent to always passing
|
|
122
|
+
`--include-low-confidence`.
|
|
123
|
+
|
|
124
|
+
### `rails.*`
|
|
125
|
+
|
|
126
|
+
Switches individual Rails extractors off. Useful when a DSL in your codebase is
|
|
127
|
+
producing noise, or to speed up analysis of a non-Rails project.
|
|
128
|
+
|
|
129
|
+
The Ruby structure extractor cannot be disabled: every other extractor builds on
|
|
130
|
+
the nodes it creates.
|
|
131
|
+
|
|
132
|
+
### `tests.framework`
|
|
133
|
+
|
|
134
|
+
`auto` (default), `rspec` or `minitest`. `auto` looks for `spec/`, then `test/`.
|
|
135
|
+
|
|
136
|
+
### `tests.unsafe_global_files`
|
|
137
|
+
|
|
138
|
+
Files whose change can affect almost anything. When one of these changes, `diff`
|
|
139
|
+
records a `global_file_changed` diagnostic, risk scoring adds points, and `tests`
|
|
140
|
+
refuses to print a narrowed list without `--allow-unsafe-focus`.
|
|
141
|
+
|
|
142
|
+
Add anything in your project with the same property: a central initializer, a
|
|
143
|
+
generated schema, a shared factory file.
|
|
144
|
+
|
|
145
|
+
### `cache`
|
|
146
|
+
|
|
147
|
+
`enabled` (default `true`) and `directory` (default `tmp/ripple_effect`).
|
|
148
|
+
|
|
149
|
+
The cache is invalidated by content, so you rarely need to clear it by hand.
|
|
150
|
+
`--no-cache` skips it for one run.
|
|
151
|
+
|
|
152
|
+
## Validation
|
|
153
|
+
|
|
154
|
+
RippleEffect distinguishes two kinds of mistake:
|
|
155
|
+
|
|
156
|
+
- **An unknown top-level key is a warning**, surfaced by `doctor`. A typo in a key
|
|
157
|
+
name is usually harmless.
|
|
158
|
+
- **An invalid value for a known key is a hard error.** A bad value would silently
|
|
159
|
+
change what gets analysed, and a silently wrong answer is worse than no answer.
|
|
160
|
+
|
|
161
|
+
```console
|
|
162
|
+
$ ripple-effect doctor
|
|
163
|
+
Configuration warnings
|
|
164
|
+
unknown configuration key(s): analisys
|
|
165
|
+
|
|
166
|
+
$ ripple-effect inspect User
|
|
167
|
+
Error: `min_confidence` must be one of high, medium, low, got "certain"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Safety
|
|
171
|
+
|
|
172
|
+
The file is read with `YAML.safe_load_file`: no aliases, no permitted classes, no
|
|
173
|
+
object deserialisation. A config file cannot execute anything.
|
|
174
|
+
|
|
175
|
+
## Example
|
|
176
|
+
|
|
177
|
+
Copy [`.ripple-effect.yml.example`](../.ripple-effect.yml.example) to
|
|
178
|
+
`.ripple-effect.yml` and edit.
|
data/docs/DECISIONS.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Architecture decisions
|
|
2
|
+
|
|
3
|
+
A record of the choices that shaped RippleEffect, and what they cost.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Rubydex for Ruby indexing, behind an adapter
|
|
8
|
+
|
|
9
|
+
**Decision.** Use [Rubydex](https://github.com/Shopify/rubydex) for Ruby
|
|
10
|
+
declarations and references, and seal it behind `StaticIndex::Adapter`.
|
|
11
|
+
|
|
12
|
+
**Why.** Writing a correct Ruby indexer: constant resolution, lexical scope,
|
|
13
|
+
singleton classes, mixin ancestry: is a multi-year project and not what makes
|
|
14
|
+
RippleEffect valuable. Rubydex does it well and is MIT-licensed.
|
|
15
|
+
|
|
16
|
+
But Rubydex is young; version 0.4.1 is what this was built against, and its API is
|
|
17
|
+
still moving. Depending on it directly throughout the codebase would make every
|
|
18
|
+
future Rubydex release a potential rewrite.
|
|
19
|
+
|
|
20
|
+
**Cost.** A translation layer, and normalisation work at the boundary: Rubydex
|
|
21
|
+
reports zero-indexed lines and names singleton methods `Order::<Order>#recent`.
|
|
22
|
+
The adapter converts both. A spec asserts no Rubydex object escapes.
|
|
23
|
+
|
|
24
|
+
**Result.** Replacing the indexer means rewriting one file.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Prism for the Rails DSL, in addition to the index
|
|
29
|
+
|
|
30
|
+
**Decision.** Parse Rails DSL calls with Prism rather than extending the index.
|
|
31
|
+
|
|
32
|
+
**Why.** A declaration index answers "what is defined and what is referenced". It
|
|
33
|
+
tells us *that* `belongs_to` was called; it does not tell us it was called with
|
|
34
|
+
`:account, class_name: "User"`. Rails semantics live entirely in those arguments.
|
|
35
|
+
|
|
36
|
+
**Cost.** A second dependency, and a second pass over the source. Mitigated by a
|
|
37
|
+
shared `SourceCache`, so each file is parsed at most once regardless of how many
|
|
38
|
+
extractors need it.
|
|
39
|
+
|
|
40
|
+
**Note.** Prism is a default gem from Ruby 3.3, but not on 3.2, so it is an
|
|
41
|
+
explicit runtime dependency.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3. Confidence bands, never percentages
|
|
46
|
+
|
|
47
|
+
**Decision.** Three bands. `high`, `medium`, `low`. No numeric probability in any
|
|
48
|
+
human-facing output.
|
|
49
|
+
|
|
50
|
+
**Why.** A number like "91% likely to break" implies a calibrated model. There is
|
|
51
|
+
no such model, and there could not be one without a large corpus of real changes
|
|
52
|
+
and their outcomes. A fabricated number is worse than no number, because people
|
|
53
|
+
act on it.
|
|
54
|
+
|
|
55
|
+
**Cost.** Less impressive output. Coarser ranking.
|
|
56
|
+
|
|
57
|
+
**Consequence.** A numeric weight exists internally for traversal and ranking, but
|
|
58
|
+
never surfaces.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 4. Ambiguity produces a diagnostic, never a guess
|
|
63
|
+
|
|
64
|
+
**Decision.** When a call's receiver is unresolved and several methods share the
|
|
65
|
+
name, add **no edge** and record `unresolved_method_receiver`. Only a unique name
|
|
66
|
+
match earns a `medium` edge.
|
|
67
|
+
|
|
68
|
+
**Why.** This is the decision the whole product rests on. A user who finds one
|
|
69
|
+
confidently-asserted, obviously-wrong edge will stop trusting every other edge —
|
|
70
|
+
and correctly so. False negatives are disappointing; confident false positives are
|
|
71
|
+
disqualifying.
|
|
72
|
+
|
|
73
|
+
**Cost.** RippleEffect misses real relationships in codebases that use common
|
|
74
|
+
method names heavily.
|
|
75
|
+
|
|
76
|
+
**Mitigation.** The diagnostic is visible with `--verbose`, so a user can see
|
|
77
|
+
exactly what was skipped and why.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 5. Edges point from dependent to dependency
|
|
82
|
+
|
|
83
|
+
**Decision.** `from_id` depends on `into_id`, always, with no exceptions.
|
|
84
|
+
|
|
85
|
+
**Why.** A graph that mixes directions is unusable: every traversal has to know
|
|
86
|
+
which convention each edge type follows. With one rule, "what breaks if I change
|
|
87
|
+
this?" is simply a reverse walk.
|
|
88
|
+
|
|
89
|
+
**Cost.** Some edges read backwards at first (a route "depends on" its controller
|
|
90
|
+
action; a class "depends on" the methods it defines). The consistency is worth it.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 6. Breadth-first traversal
|
|
95
|
+
|
|
96
|
+
**Decision.** Breadth-first, keeping the shortest path, breaking ties on
|
|
97
|
+
confidence.
|
|
98
|
+
|
|
99
|
+
**Why.** The evidence path is the product. The shortest chain of reasoning is the
|
|
100
|
+
one a human can verify fastest, and verifiability is what makes the output
|
|
101
|
+
trustworthy. Depth-first would produce arbitrary, sometimes very long,
|
|
102
|
+
explanations for the same conclusion.
|
|
103
|
+
|
|
104
|
+
**Cost.** Slightly more bookkeeping: an equally-short alternative path must still
|
|
105
|
+
reach the tie-break even though it is not re-queued.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 7. Diff maps lines to the smallest enclosing declaration
|
|
110
|
+
|
|
111
|
+
**Decision.** Map changed line ranges to the innermost declaration, rather than
|
|
112
|
+
marking every declaration in a touched file as changed.
|
|
113
|
+
|
|
114
|
+
**Why.** File-level granularity makes a one-line fix look like a rewrite of the
|
|
115
|
+
class, which makes the entire blast radius useless in exactly the case people care
|
|
116
|
+
about most.
|
|
117
|
+
|
|
118
|
+
**Cost.** Real complexity. Deleted code exists only in the base revision, so it
|
|
119
|
+
must be fetched with `git show` and parsed separately. When a deleted symbol is
|
|
120
|
+
gone entirely, the change is attributed to its former owner: which is what its
|
|
121
|
+
dependents actually referenced.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 8. `tests` refuses to narrow after a global-file change
|
|
126
|
+
|
|
127
|
+
**Decision.** When a boot-impact file changes, print nothing to stdout and exit
|
|
128
|
+
`2` unless `--allow-unsafe-focus` is given.
|
|
129
|
+
|
|
130
|
+
**Why.** Printing a short list is an implicit claim that the omitted tests are
|
|
131
|
+
unnecessary. For a change to `config/initializers/` or the `Gemfile`, the graph
|
|
132
|
+
cannot support that claim. Since the output is designed to be piped straight into
|
|
133
|
+
`rspec`, a quiet under-approximation would silently skip tests in CI.
|
|
134
|
+
|
|
135
|
+
**Cost.** An extra flag for users who know what they are doing.
|
|
136
|
+
|
|
137
|
+
**Principle.** RippleEffect can show a test is probably relevant. It cannot show
|
|
138
|
+
one is irrelevant.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 9. Structural edges exist, but are hidden from text output
|
|
143
|
+
|
|
144
|
+
**Decision.** Keep `file -> class` and `class -> method` edges in the graph; hide
|
|
145
|
+
them in text output; keep them in JSON.
|
|
146
|
+
|
|
147
|
+
**Why.** They are load-bearing: a file-level diff needs to reach the code it
|
|
148
|
+
declares, and a method change needs to reach dependents of its class. But
|
|
149
|
+
reporting "app/models/order.rb depends on Order#total" as a *finding* buries the
|
|
150
|
+
real answer under noise.
|
|
151
|
+
|
|
152
|
+
**Cost.** Text and JSON differ in content, which must be documented. Risk scoring
|
|
153
|
+
also has to exclude file nodes, or every score inflates with the number of files
|
|
154
|
+
touched.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 10. JSON cache, never Marshal
|
|
159
|
+
|
|
160
|
+
**Decision.** Serialise the graph cache as JSON.
|
|
161
|
+
|
|
162
|
+
**Why.** A cache file is ordinary data in `tmp/` that any process can write.
|
|
163
|
+
`Marshal.load` on untrusted data constructs arbitrary objects. The performance
|
|
164
|
+
difference does not come close to justifying that.
|
|
165
|
+
|
|
166
|
+
**Cost.** Slower serialisation, and value objects must be reconstructed by hand on
|
|
167
|
+
read.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 11. Cache keyed on full content digests
|
|
172
|
+
|
|
173
|
+
**Decision.** SHA-256 every indexed file into the cache key, rather than relying on
|
|
174
|
+
mtime and size.
|
|
175
|
+
|
|
176
|
+
**Why.** A stale graph produces a *wrong answer that looks right*, which is the
|
|
177
|
+
worst failure mode this tool has. Branch switching, `git stash` and checkouts all
|
|
178
|
+
move mtimes in ways that make timestamp invalidation unreliable.
|
|
179
|
+
|
|
180
|
+
**Cost.** Hashing every file on every run.
|
|
181
|
+
|
|
182
|
+
**Escape hatches.** A corrupt cache is discarded and rebuilt rather than being
|
|
183
|
+
fatal; `--no-cache` skips it entirely.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 12. OptionParser rather than Thor
|
|
188
|
+
|
|
189
|
+
**Decision.** Use the standard library.
|
|
190
|
+
|
|
191
|
+
**Why.** Adding a runtime dependency to a developer tool means adding it to every
|
|
192
|
+
project that installs the tool. OptionParser is adequate for six subcommands.
|
|
193
|
+
|
|
194
|
+
**Cost.** Subcommand dispatch and shared-flag handling written by hand: about
|
|
195
|
+
sixty lines in `CLI::Command`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 13. Rails is analysed, never depended on
|
|
200
|
+
|
|
201
|
+
**Decision.** No Rails dependency, at any version.
|
|
202
|
+
|
|
203
|
+
**Why.** Loading Rails to analyse Rails would mean booting the target
|
|
204
|
+
application: the exact thing that makes this kind of tool slow, fragile, and
|
|
205
|
+
unsafe on an unfamiliar codebase. Static analysis also works on a broken app,
|
|
206
|
+
which is often when you most want it.
|
|
207
|
+
|
|
208
|
+
**Cost.** Rails' inflections and conventions are reimplemented in miniature. The
|
|
209
|
+
inflector is small and conservative, and an unresolved target is
|
|
210
|
+
reported rather than guessed.
|