rigortype 0.3.3 → 0.3.5

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/array.yml +11 -24
  4. data/data/builtins/ruby_core/enumerable.yml +4 -8
  5. data/data/builtins/ruby_core/exception.yml +6 -9
  6. data/data/builtins/ruby_core/hash.yml +13 -20
  7. data/data/builtins/ruby_core/io.yml +6 -9
  8. data/data/builtins/ruby_core/numeric.yml +2 -3
  9. data/data/builtins/ruby_core/proc.yml +4 -8
  10. data/data/builtins/ruby_core/re.yml +6 -9
  11. data/data/builtins/ruby_core/set.yml +2 -3
  12. data/data/builtins/ruby_core/string.yml +4 -7
  13. data/data/builtins/ruby_core/struct.yml +0 -6
  14. data/data/builtins/ruby_core/time.yml +3 -6
  15. data/data/effects/core.yml +860 -0
  16. data/data/effects/registry.yml +95 -0
  17. data/docs/handbook/07-rbs-and-extended.md +106 -5
  18. data/docs/manual/02-cli-reference.md +359 -2
  19. data/docs/manual/03-configuration.md +101 -1
  20. data/docs/manual/04-diagnostics.md +8 -3
  21. data/docs/manual/11-ci.md +37 -0
  22. data/docs/manual/12-caching.md +39 -0
  23. data/docs/manual/16-rbs-extended-annotations.md +90 -0
  24. data/docs/manual/18-removing-dead-code.md +326 -0
  25. data/docs/manual/19-effect-labels.md +671 -0
  26. data/docs/manual/README.md +10 -0
  27. data/docs/manual/ci-templates/README.md +9 -0
  28. data/docs/manual/plugins/rigor-actionmailer.md +5 -0
  29. data/docs/manual/plugins/rigor-activejob.md +47 -0
  30. data/docs/manual/plugins/rigor-factorybot.md +16 -0
  31. data/docs/manual/plugins/rigor-pundit.md +26 -0
  32. data/docs/manual/plugins/rigor-rails-routes.md +41 -2
  33. data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
  34. data/docs/manual/plugins/rigor-rspec.md +8 -0
  35. data/docs/manual/plugins/rigor-sidekiq.md +54 -0
  36. data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
  37. data/lib/rigor/analysis/incremental_session.rb +140 -6
  38. data/lib/rigor/analysis/reachability/graph.rb +244 -0
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
  40. data/lib/rigor/analysis/reachability/project_files.rb +58 -0
  41. data/lib/rigor/analysis/reachability/scan.rb +241 -0
  42. data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
  43. data/lib/rigor/analysis/rule_catalog.rb +168 -3
  44. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  45. data/lib/rigor/analysis/run_cache_probe.rb +69 -1
  46. data/lib/rigor/analysis/runner/declaration_position.rb +38 -0
  47. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  48. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  49. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  50. data/lib/rigor/analysis/runner/pool_coordinator.rb +93 -10
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
  52. data/lib/rigor/analysis/runner.rb +429 -10
  53. data/lib/rigor/analysis/worker_session.rb +60 -2
  54. data/lib/rigor/bleeding_edge.rb +22 -0
  55. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  56. data/lib/rigor/cli/check_command.rb +13 -1
  57. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  58. data/lib/rigor/cli/effects_command.rb +245 -0
  59. data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
  60. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  61. data/lib/rigor/cli/effects_renderer.rb +92 -0
  62. data/lib/rigor/cli/effects_report.rb +163 -0
  63. data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
  64. data/lib/rigor/cli/unused_command.rb +288 -0
  65. data/lib/rigor/cli.rb +28 -1
  66. data/lib/rigor/configuration/severity_profile.rb +23 -0
  67. data/lib/rigor/configuration.rb +322 -28
  68. data/lib/rigor/configuration_error.rb +20 -0
  69. data/lib/rigor/effects/attribution.rb +76 -0
  70. data/lib/rigor/effects/catalog.rb +275 -0
  71. data/lib/rigor/effects/collector.rb +219 -0
  72. data/lib/rigor/effects/config_envelopes.rb +185 -0
  73. data/lib/rigor/effects/discharge.rb +69 -0
  74. data/lib/rigor/effects/effect_table.rb +92 -0
  75. data/lib/rigor/effects/entry_points.rb +139 -0
  76. data/lib/rigor/effects/envelope.rb +86 -0
  77. data/lib/rigor/effects/envelope_check.rb +172 -0
  78. data/lib/rigor/effects/envelope_index.rb +157 -0
  79. data/lib/rigor/effects/file_collection.rb +174 -0
  80. data/lib/rigor/effects/framework_units.rb +222 -0
  81. data/lib/rigor/effects/identity.rb +104 -0
  82. data/lib/rigor/effects/inline_anchor.rb +134 -0
  83. data/lib/rigor/effects/label.rb +77 -0
  84. data/lib/rigor/effects/label_intent.rb +73 -0
  85. data/lib/rigor/effects/label_set.rb +136 -0
  86. data/lib/rigor/effects/liskov_check.rb +167 -0
  87. data/lib/rigor/effects/local_ownership.rb +132 -0
  88. data/lib/rigor/effects/method_key.rb +40 -0
  89. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  90. data/lib/rigor/effects/narrowing.rb +202 -0
  91. data/lib/rigor/effects/origin.rb +66 -0
  92. data/lib/rigor/effects/path_finder.rb +89 -0
  93. data/lib/rigor/effects/plugin_facts.rb +384 -0
  94. data/lib/rigor/effects/propagator.rb +335 -0
  95. data/lib/rigor/effects/registry.rb +200 -0
  96. data/lib/rigor/effects/scanner.rb +302 -0
  97. data/lib/rigor/effects/signature_sources.rb +90 -0
  98. data/lib/rigor/effects/snapshot.rb +396 -0
  99. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  100. data/lib/rigor/effects/summary.rb +154 -0
  101. data/lib/rigor/effects/taint_cause.rb +39 -0
  102. data/lib/rigor/effects/unit_scan.rb +647 -0
  103. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  104. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  105. data/lib/rigor/effects.rb +47 -0
  106. data/lib/rigor/environment/rbs_loader.rb +34 -0
  107. data/lib/rigor/environment.rb +8 -1
  108. data/lib/rigor/flow_contribution/element.rb +1 -0
  109. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  110. data/lib/rigor/flow_contribution/merger.rb +16 -1
  111. data/lib/rigor/flow_contribution.rb +20 -4
  112. data/lib/rigor/inference/expression_typer.rb +18 -4
  113. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  114. data/lib/rigor/inference/scope_indexer.rb +15 -3
  115. data/lib/rigor/plugin/base.rb +28 -0
  116. data/lib/rigor/plugin/box.rb +18 -2
  117. data/lib/rigor/plugin/effect_ancestry.rb +80 -0
  118. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  119. data/lib/rigor/plugin/effect_edge.rb +101 -0
  120. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  121. data/lib/rigor/plugin/first_party.rb +57 -0
  122. data/lib/rigor/plugin/isolation.rb +4 -1
  123. data/lib/rigor/plugin/manifest.rb +127 -3
  124. data/lib/rigor/plugin/registry.rb +55 -0
  125. data/lib/rigor/plugin.rb +1 -0
  126. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  127. data/lib/rigor/rbs_extended.rb +188 -0
  128. data/lib/rigor/reflection.rb +131 -30
  129. data/lib/rigor/version.rb +1 -1
  130. data/lib/rigor.rb +1 -0
  131. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  132. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  133. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  134. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  135. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  136. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  137. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  138. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  139. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  140. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  141. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  142. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  143. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  144. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  145. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  146. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  147. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +401 -29
  148. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
  149. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  150. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  151. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  152. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  153. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  154. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  157. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  158. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  159. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  160. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  161. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  162. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
  164. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  165. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -3
  166. data/sig/rigor/analysis/fact_store.rbs +1 -0
  167. data/sig/rigor/inference.rbs +2 -0
  168. data/sig/rigor/rbs_extended.rbs +6 -0
  169. data/sig/rigor.rbs +17 -1
  170. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  171. metadata +84 -1
@@ -70,7 +70,7 @@ cache:
70
70
  | --- | --- | --- | --- |
71
71
  | `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
72
72
  | `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
73
- | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches. |
73
+ | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. |
74
74
  | `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
75
75
 
76
76
  ### Config validation warnings
@@ -174,6 +174,106 @@ explicitly with `bundler.bundle_path:`, or supply signatures another way:
174
174
  | `plugins_io.allowed_paths` | Array | `[]` | Filesystem paths plugins may read. |
175
175
  | `plugins_io.allowed_url_hosts` | Array | `[]` | URL hosts plugins may fetch from when `network: allowlist`. |
176
176
 
177
+ ### Effect labels
178
+
179
+ The key reference is below; the workflow these keys serve — the vocabulary, the report, the committed
180
+ snapshot, the CI gate — is [Effect labels](19-effect-labels.md).
181
+
182
+ | Key | Type | Default | Meaning |
183
+ | --- | --- | --- | --- |
184
+ | `effects` | Hash | absent | **Opt-in to effect labels ([ADR-103](../adr/103-effect-labels.md)).** The *presence* of this block is the switch — `effects: {}` enables collection with every sub-key at its default, and leaving it out keeps `rigor check` byte-identical and free. Nothing else turns collection on: an `%a{pure}` or `%a{rigor:v1:effect …}` annotation in your RBS does not, because an annotation must not silently make every run more expensive — such a project gets one `effect.annotations-unchecked` `:info` per run instead, saying the annotations are inert. `rigor effects` runs under an implicit empty block when the key is absent, so you can try the report before configuring anything. Effect summaries are cached under their own identity (Rigor's effect vocabulary, its built-in catalogue and this block), so `rigor effects` after `rigor check` in the same job is a cache hit plus the propagation, and turning this block on or off does not invalidate your diagnostics cache. The sub-keys are below. `views` is declared in the schema and reserved: accepted and **not yet read**. See [`rigor effects`](02-cli-reference.md#rigor-effects). |
185
+ | `effects.check` | Boolean | `true` | Whether the envelopes you declared — `%a{pure}` and `%a{rigor:v1:effect …}` in RBS, and the `effects.envelopes:` stanzas below — are checked against what Rigor proved, surfacing `effect.envelope-exceeded` and, for a label the vocabulary does not recognise, `effect.unknown-label`. Set it to `false` to keep the report and the snapshot while silencing both. Never on without an `effects:` block. |
186
+ | `effects.snapshot.path` | String | `.rigor-effects.yml` | Where `rigor effects update` writes the committed record. |
187
+ | `effects.snapshot.reach` | Array | `[]` | Entry points whose **transitive** footprint the snapshot records under `reach:`. The default is empty **deliberately**: Rigor could infer `[rails]` from your plugin list, but a snapshot is a record you agree to and review the diff of, and one whose contents moved because your plugin list moved would be the worse artefact. `rigor effects update` names the presets your plugins registered and leaves the same hint in the written file. Each entry is a project-relative file glob (the `unused --entry-point` semantics — `**` is the only way across a directory boundary) or the name of an entry-point **preset** a plugin registered. On a Rails app, `reach: [rails]` is the one you want — see below. A name nothing registered is an error when the snapshot is built (not when the configuration loads, because the plugins that name presets load *from* that configuration); the error lists the presets your plugins did register, so it names the fix. |
188
+ | `effects.snapshot.gate` | String | `symmetric` | What `rigor effects check` treats as drift. `symmetric` fails on any difference — a job that *stopped* enqueueing is news too; `additions` is the growth-only ratchet. |
189
+ | `effects.labels` | Array | `[]` | Effect labels **your project** registers, layered over Rigor's shipped vocabulary. A project may open any root (`acme.cache`) — listing the label here is the vouching act. Once registered, a label is usable in every other key below and stops being reported as unknown. Malformed spellings are a load error. |
190
+ | `effects.attribution` | Hash | `{}` | What a call into code Rigor cannot see *does*, keyed by method: `{"Net::HTTP.get": [io.net.http], "Logger#info": [telemetry]}`. Keys are method keys — `Owner#instance_method` or `Owner.singleton_method` — and anything else is a load error. The labels land in the caller's **declared** lane and never in the proven one, so an attribution can never make a diagnostic fire; the call still counts as unresolved, because you told Rigor what that code does and Rigor did not read it. Use it for gems nobody has written a plugin for. |
191
+ | `effects.envelopes` | Array | `[]` | Effect envelopes by **convention**, so a whole architectural layer is bounded by one stanza instead of a per-method annotation. Each entry names exactly one of `match:` (a project-relative path glob over the files a class is defined in) or `namespace:` (a constant glob: `*` is one segment, `**` is one or more), plus `effect:` — the labels the selected classes may perform, or `[]` for pure. Nearest wins: a per-method annotation beats a class-level one, which beats a stanza; among stanzas the **first** match wins. See the example below. |
192
+ | `effects.tolerated` | Array | `[]` | Labels your project has decided not to act on. Applied when a bound or a difference is **judged**, never when a record is written, and **per origin**: `Logger#info` carries `io` and `telemetry` together, so `tolerated: [telemetry]` frees the `io` that came with the logging and leaves an `io.fs.read` from a `File.read` in the same method exactly where it was. `rigor check --no-tolerated-effects` (and the same flag on `rigor effects check`) re-judges as if the list were empty — the audit switch for the policy. |
193
+
194
+ Want the block on without writing it? The
195
+ [`effects-on-by-default`](02-cli-reference.md#rigor-show-bleedingedge)
196
+ bleeding-edge feature (`bleeding_edge: [effects-on-by-default]`) makes a
197
+ config with no `effects:` key at all behave as `effects: {}` — collection,
198
+ `effects.check`, and everything else on this page all turn on at their
199
+ defaults. It only fills an *absence*: write `effects: false` and you stay
200
+ opted out regardless, and any `effects:` block you do write is left exactly
201
+ as written. It previews what becomes the default at **v0.4.0**
202
+ ([ADR-103](../adr/103-effect-labels.md) § WD15).
203
+
204
+ #### Entry-point presets
205
+
206
+ `reach:` asks "whose footprint should the record cover", and on a framework the honest answer is a fact
207
+ about the framework rather than about your code. So the plugin that models it names the set, and you
208
+ adopt it:
209
+
210
+ ```yaml
211
+ plugins:
212
+ - rigor-railties
213
+ - rigor-activerecord
214
+ - rigor-actionpack
215
+
216
+ effects:
217
+ snapshot:
218
+ reach: [rails]
219
+ ```
220
+
221
+ `rails` — registered by `rigor-railties`, which is a distinct plugin from the
222
+ [`rigor-rails`](plugins/rigor-rails.md) convenience grouping — stands for `app/controllers/**`,
223
+ `app/jobs/**`, `app/mailers/**` and `app/channels/**`: every way the outside world enters the
224
+ application. The component plugins also register the narrower `rails-controllers`, `rails-jobs`,
225
+ `rails-mailers` and `rails-channels` if you want one layer's footprint rather than all four. A preset is
226
+ just a name for globs; mixing the two in one list is fine.
227
+
228
+ Listing the plugin is what registers its preset, so `reach: [rails]` without `rigor-railties` in
229
+ `plugins:` is an error saying so.
230
+
231
+ #### Envelopes by convention
232
+
233
+ The `envelopes:` list is the surface that pays on day one, before you have written any RBS:
234
+
235
+ ```yaml
236
+ effects:
237
+ envelopes:
238
+ - match: "app/presenters/**/*.rb" # presenters render; they do not query
239
+ effect: []
240
+ - namespace: "Policies::*" # Policies::Edit, not Policies::Admin::Edit
241
+ effect: [mutate.local]
242
+ - match: "app/jobs/**/*.rb"
243
+ effect: [io]
244
+ tolerated: [telemetry]
245
+ ```
246
+
247
+ A stanza attaches its bound to every method of every class it selects, exactly as an annotation on the
248
+ class would. A method that exceeds it gets one `effect.envelope-exceeded` at its `def`, naming the
249
+ stanza it broke:
250
+
251
+ ```
252
+ app/presenters/user_presenter.rb:14:1: warning: Method Presenters::User#render performs io.fs.read
253
+ (File.read), but is declared effect: [] at .rigor.yml effects.envelopes[0], so io.fs.read exceeds
254
+ the envelope.
255
+ ```
256
+
257
+ When one method is a deliberate exception, write the narrower envelope on it in RBS — nearest wins,
258
+ and no `except:` key is needed. When a whole *kind* of effect is acceptable everywhere, name it in
259
+ `tolerated:` instead of loosening every stanza.
260
+
261
+ **One stanza can land hundreds of warnings**, one per (method, label) pair — on a mid-size Rails
262
+ application, `match: "app/helpers/**/*.rb"` with `effect: []` produced 343 across 18 files. That is
263
+ the layer telling you what it does rather than a misconfiguration, but it is not a work list either;
264
+ [Effect labels § Envelopes by convention](19-effect-labels.md) walks the recipe for working it down.
265
+
266
+ A stanza is checked against **proven** labels only, so on a Rails application it cannot fire on
267
+ `io.db.*`, `cache.*`, `telemetry`, `email.send`, `job.enqueue` or any `rails.*`: those come from a
268
+ plugin modelling a framework Rigor did not read, and a claim about unread code must not be able to
269
+ fail your build. The enforcement path for that half is the committed snapshot, whose `rigor effects
270
+ check` marks a declared-lane addition with `≤+` — [Effect labels § What a bound can and cannot
271
+ see](19-effect-labels.md#what-a-bound-can-and-cannot-see), and
272
+ [ADR-103](../adr/103-effect-labels.md) § WD17 for why.
273
+
274
+ If a layer is not ready for a bound yet, leave `envelopes:` out and start with the committed snapshot
275
+ ([`rigor effects update`](02-cli-reference.md#the-effect-snapshot)) — it needs no declaration at all,
276
+ and stanzas are the second step, written once the record has told you what the layer actually does.
177
277
  ### Reserved for other implementations
178
278
 
179
279
  | Key | Type | Default | Meaning |
@@ -27,7 +27,8 @@ built-in rule ID; `rigor explain` with no argument lists them all.
27
27
  Each built-in rule has a stable per-rule anchor on this page
28
28
  (`#rule-<family>-<name>`, dots written as dashes) — the
29
29
  `documentation_url` field in `--format json` and `rigor explain`'s
30
- `Documentation:` line both point here. The `Evidence` column is
30
+ `Documentation:` line both point here, at this chapter's published
31
+ copy on <https://rigor.typedduck.fail/manual/04-diagnostics/>. The `Evidence` column is
31
32
  Rigor's confidence that a firing is a true positive (see
32
33
  [Evidence tier](#evidence-tier) below). The one exception is
33
34
  `rbs_extended.unsatisfied-conformance`, an `rbs_extended`-family rule
@@ -58,6 +59,10 @@ carries no `documentation_url`.
58
59
  | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
59
60
  | <a id="rule-def-override-param-narrowed"></a>`def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance). | high |
60
61
  | <a id="rule-static-value-use-void"></a>`static.value-use.void` | A value recovered from an author-declared `-> void` return is used in value context (an assignment right-hand side, a call receiver, or a call argument). Off by default; reaches a run only through the `use-of-void-value` bleeding-edge feature (ADR-100). A bare-statement `void` call and a legitimate `top` value both stay silent. | high |
62
+ | <a id="rule-effect-envelope-exceeded"></a>`effect.envelope-exceeded` | A method performs an effect its declared envelope does not admit — its proven effect labels (its own body plus everything it calls) are not covered by the `%a{pure}` or `%a{rigor:v1:effect …}` bound written on it or on its class. Opt-in twice over: it needs an `effects:` block in `.rigor.yml` and an envelope you wrote. Positioned at the Ruby `def`. Unproven ("and possibly more") effects never fire, and `mutate.local` is tolerated by every envelope. | high |
63
+ | <a id="rule-effect-liskov-widened"></a>`effect.liskov-widened` | An override escapes the envelope written on the method it overrides. A `PgRepo` is usable wherever a `Repo` is, so a `%a{rigor:v1:effect io.db}` on `Repo#find` binds `PgRepo#find` too: an implementation may be purer than the bound it inherits, never less pure. Either what the override *does* exceeds the inherited bound, or the envelope the override *declares for itself* is wider than it. Both sides must be authored — nothing fires unless someone wrote an envelope on the ancestor — and only subclassing counts, not `include`. Positioned at the override's `def`. Needs an `effects:` block. | high |
64
+ | <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or `.rigor.yml` for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
65
+ | <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments — except on a run that analyses no file at all (a warm `--incremental` with nothing changed), where only the `.rbs` lane is visible. | — |
61
66
  | <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
62
67
  | <a id="rule-suppression-empty"></a>`suppression.empty` | A `# rigor:disable[-file]` comment lists no rules, so it suppresses nothing. | high |
63
68
  | <a id="rule-suppression-unknown-marker"></a>`suppression.unknown-marker` | A comment uses a suppression marker Rigor does not recognise — typically the RuboCop reflex `# rigor:disable-next-line <rule>` or `# rigor:enable <rule>`. Rigor's only markers are `# rigor:disable <rules>` (suppresses on its own line) and `# rigor:disable-file <rules>`, so the comment suppresses nothing. | high |
@@ -104,8 +109,8 @@ are:
104
109
 
105
110
  | Severity | Rules |
106
111
  | --- | --- |
107
- | `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker` |
108
- | `info` | `flow.unreachable-clause`, `dump.type` |
112
+ | `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker`, `effect.envelope-exceeded`, `effect.liskov-widened` |
113
+ | `info` | `flow.unreachable-clause`, `dump.type`, `effect.unknown-label`, `effect.annotations-unchecked` |
109
114
  | `off` | `call.self-undefined-method`, `static.value-use.void` |
110
115
 
111
116
  Everything else emits as `error`. For one rule under all three
data/docs/manual/11-ci.md CHANGED
@@ -188,6 +188,43 @@ The exit code is unchanged by `--format` — `0` when there are no errors,
188
188
  `1` otherwise — so the job still gates the pipeline. `--format json`
189
189
  remains available for any other tool that wants the raw diagnostic stream.
190
190
 
191
+ ## Gating effect drift
192
+
193
+ If the project uses [effect labels](19-effect-labels.md), add a second
194
+ step beside `rigor check`. It answers a different question — "did this
195
+ branch change what the code *does*?" — and fails for different reasons,
196
+ so keep it as its own step and let the log say which one went red:
197
+
198
+ ```yaml
199
+ - run: gem install rigortype
200
+ - run: rigor check
201
+ - run: rigor effects check
202
+ ```
203
+
204
+ `rigor effects check` exits `0` when the committed
205
+ `.rigor-effects.yml` matches what Rigor just recomputed and `1`
206
+ otherwise — including when the file is missing from the checkout, which
207
+ is what catches the pull request that changed a method and forgot to
208
+ regenerate it. The author's fix is `rigor effects update` plus a commit;
209
+ the regenerated diff is what the reviewer reads.
210
+
211
+ A bot that wants to report *what this branch changes*, regardless of how
212
+ stale the default branch is, compares against the base instead of the
213
+ committed file:
214
+
215
+ ```sh
216
+ rigor effects diff --baseline <(git show origin/main:.rigor-effects.yml)
217
+ ```
218
+
219
+ `diff` prints the identical comparison and never gates, so it is safe as
220
+ a comment-posting step; `check` takes the same `--baseline`.
221
+
222
+ With an `effects:` block configured, the snapshot verbs share their
223
+ cached effect summaries with `rigor check`, so running both in one job
224
+ does not re-read the project twice. Exit codes, `--strict-tolerated`, the
225
+ `--no-tolerated-effects` audit switch and the review loop are covered in
226
+ [Effect labels § In CI](19-effect-labels.md).
227
+
191
228
  ## Pinning Rigor's version
192
229
 
193
230
  The workflow above installs whatever `rigortype` is current at run
@@ -49,6 +49,45 @@ The cache is also schema-versioned: after a Rigor upgrade that
49
49
  changes the cache format, the stale cache is purged on the
50
50
  first writable run.
51
51
 
52
+ ## Effect summaries
53
+
54
+ [Effect labels](19-effect-labels.md) are cached alongside the rest,
55
+ but under their **own** identity: Rigor's effect vocabulary, its
56
+ built-in effect catalogue, and your `effects:` block. Two
57
+ consequences worth knowing:
58
+
59
+ - **Turning `effects:` on or off does not invalidate the entries
60
+ your `rigor check` already relies on.** The two identities are
61
+ separate, so adopting effect labels costs a first collection
62
+ pass and leaves the diagnostics cache alone. Upgrading to a
63
+ Rigor whose catalogue changed does the reverse — it re-reads
64
+ your effects without re-running your check.
65
+ - **The `rigor effects` verbs share those summaries with `rigor
66
+ check`.** In a job that runs both, the second command pays for
67
+ the propagation rather than for a second analysis.
68
+
69
+ The exception is a `rigor effects` run on a project with **no**
70
+ `effects:` block: it collects under an implicit empty block and
71
+ shares no cache with `rigor check`, because a run served from that
72
+ cache would have collected nothing.
73
+
74
+ **Everything the effect system produces survives a cache hit.** A
75
+ warm `rigor check` reports exactly the `effect.envelope-exceeded`
76
+ warnings a cold one does; there is no configuration in which the
77
+ effect checks fire on the first build of the day and go quiet
78
+ afterwards. The judgment itself is never stored — it is recomputed
79
+ each run from the summaries — so editing an envelope, a
80
+ `tolerated:` list or an `%a{pure}` annotation re-judges your code
81
+ on the next run even though no Ruby file changed, and a warning
82
+ can never outlive the stanza that justified it.
83
+
84
+ That costs speed in exactly one case. Declaring an envelope
85
+ anywhere opts the project out of the cache-hit fast path, because
86
+ Rigor has to load the analyzer to re-judge: a warm run takes about
87
+ a second on a mid-sized Rails application instead of about a
88
+ quarter of one. A project with `effects:` on and nothing declared
89
+ anywhere pays nothing.
90
+
52
91
  ## How a file is checked for changes
53
92
 
54
93
  To decide whether a cached entry is still valid, Rigor needs to
@@ -20,6 +20,27 @@ def read_name: () -> String
20
20
 
21
21
  The plain `() -> String` stays the compatibility contract; the
22
22
  annotation tells Rigor the return is a non-empty string.
23
+
24
+ You may also write any of them **in a `.rb` file**, as an
25
+ rbs-inline `# @rbs %a{…}` comment — `%a{}` is rbs-inline's own
26
+ upstream grammar, and the annotation reaches Rigor on the same
27
+ path the generated signature does:
28
+
29
+ ```rb
30
+ # rbs_inline: enabled
31
+
32
+ class Reader
33
+ # @rbs %a{rigor:v1:return: non-empty-string}
34
+ # @rbs return: String
35
+ def read_name = "x"
36
+ end
37
+ ```
38
+
39
+ This needs the `rbs-inline` library installed; Rigor ingests
40
+ inline annotations by default when it is
41
+ ([ADR-93](../adr/93-default-rbs-inline-ingestion.md)). There is
42
+ no Rigor-only comment dialect: `# rigor:` comments remain
43
+ suppression-only.
23
44
  This page is the *operational* reference — the directives you can
24
45
  write and their syntax. For the normative rules (conflict
25
46
  handling, merging, provenance) see
@@ -135,6 +156,75 @@ directives on one class combine like an intersection of
135
156
  interfaces. The directive is purely additive — a class that
136
157
  already satisfies the interface type-checks with or without it.
137
158
 
159
+ ## Effect envelopes — bounding what a method *does*
160
+
161
+ Every directive above describes what a method returns. Two
162
+ describe what it *does*: `%a{pure}` — rbs' own purity
163
+ annotation, read as "nothing at all" — and
164
+ `%a{rigor:v1:effect <labels>}`, a comma-separated list of bare
165
+ [effect labels](19-effect-labels.md) the
166
+ method may not exceed. Both attach to a method or to a `class` /
167
+ `module`, where they distribute to that class's own methods
168
+ (nearest wins), and both tolerate mutating objects the method
169
+ itself allocated and never let out:
170
+
171
+ ```rbs
172
+ class UserRepository
173
+ %a{rigor:v1:effect io.db, nondet.time}
174
+ def find: (Integer) -> User
175
+
176
+ %a{pure}
177
+ def slug: (String) -> String
178
+ end
179
+ ```
180
+
181
+ The same two work as rbs-inline comments in a `.rb` file:
182
+
183
+ ```rb
184
+ # rbs_inline: enabled
185
+
186
+ class UserRepository
187
+ # @rbs %a{rigor:v1:effect io.db}
188
+ # @rbs id: Integer
189
+ # @rbs return: User
190
+ def find(id) = User.find(id)
191
+
192
+ # @rbs %a{pure}
193
+ # @rbs return: String
194
+ def slug(s) = s.strip.downcase
195
+ end
196
+ ```
197
+
198
+ Three things follow, and all three need an `effects:` block in
199
+ `.rigor.yml` — an annotation alone never turns effect collection
200
+ on:
201
+
202
+ - A method whose proven effects escape its bound fires
203
+ [`effect.envelope-exceeded`](04-diagnostics.md#rule-effect-envelope-exceeded),
204
+ positioned at the Ruby `def`.
205
+ - A label the registry does not recognise makes the **whole
206
+ annotation** read as unbounded — a typo can never manufacture
207
+ a finding — and, where the spelling is evidently meant to be a
208
+ label, says so as
209
+ [`effect.unknown-label`](04-diagnostics.md#rule-effect-unknown-label)
210
+ at the declaration.
211
+ - Without the block, one
212
+ [`effect.annotations-unchecked`](04-diagnostics.md#rule-effect-annotations-unchecked)
213
+ `:info` per run tells you the annotations are inert — from either
214
+ lane, with one exception: a run that analyses **no file at all**
215
+ (a warm `rigor check --incremental` with nothing changed) has no
216
+ synthesised RBS to read, so it reports a `.rbs` annotation and not
217
+ an inline one. Any run that analyses something reports both.
218
+
219
+ Two practical notes. Annotating one method in a `.rbs` file forces you to
220
+ declare its whole signature — RBS has no way to annotate a method it does
221
+ not declare — while the rbs-inline form above does not, so prefer the
222
+ inline lane when the bound is all you want. And an envelope is checked
223
+ against the **proven** lane only: a method whose labels all sit in the
224
+ declared (`≤`) lane passes `%a{pure}` in silence.
225
+ [Effect labels](19-effect-labels.md) covers both, with the vocabulary
226
+ these annotations draw from.
227
+
138
228
  ## Higher-kinded type directives
139
229
 
140
230
  Two declaration-level directives register and define the