rigortype 0.3.4 → 0.3.6

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/effects/core.yml +18 -1
  4. data/data/effects/registry.yml +31 -3
  5. data/data/gem_overlay/activesupport/core_ext.rbs +33 -0
  6. data/docs/handbook/02-everyday-types.md +1 -1
  7. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  8. data/docs/handbook/08-understanding-errors.md +1 -1
  9. data/docs/handbook/09-plugins.md +2 -2
  10. data/docs/handbook/10-sorbet.md +1 -1
  11. data/docs/handbook/README.md +2 -2
  12. data/docs/handbook/appendix-go.md +1 -1
  13. data/docs/handbook/appendix-java-csharp.md +2 -2
  14. data/docs/handbook/appendix-mypy.md +1 -1
  15. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  16. data/docs/handbook/appendix-rust.md +1 -1
  17. data/docs/handbook/appendix-type-theory.md +2 -2
  18. data/docs/handbook/appendix-typescript.md +4 -4
  19. data/docs/manual/02-cli-reference.md +73 -22
  20. data/docs/manual/03-configuration.md +19 -2
  21. data/docs/manual/04-diagnostics.md +15 -2
  22. data/docs/manual/08-skills.md +1 -1
  23. data/docs/manual/11-ci.md +38 -1
  24. data/docs/manual/12-caching.md +40 -1
  25. data/docs/manual/15-type-protection-coverage.md +1 -1
  26. data/docs/manual/16-rbs-extended-annotations.md +15 -2
  27. data/docs/manual/18-removing-dead-code.md +13 -4
  28. data/docs/manual/19-effect-labels.md +678 -0
  29. data/docs/manual/README.md +7 -2
  30. data/docs/manual/ci-templates/README.md +9 -0
  31. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  32. data/docs/manual/plugins/rigor-activejob.md +3 -3
  33. data/docs/manual/plugins/rigor-activerecord.md +4 -4
  34. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  35. data/docs/manual/plugins/rigor-rspec.md +6 -2
  36. data/lib/rigor/analysis/diagnostic.rb +17 -6
  37. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  38. data/lib/rigor/analysis/reachability/graph.rb +30 -11
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  40. data/lib/rigor/analysis/reachability/scan.rb +20 -4
  41. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  42. data/lib/rigor/analysis/rule_catalog.rb +10 -3
  43. data/lib/rigor/analysis/run_cache_key.rb +12 -0
  44. data/lib/rigor/analysis/run_cache_probe.rb +69 -1
  45. data/lib/rigor/analysis/runner/declaration_position.rb +8 -24
  46. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +28 -28
  47. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +15 -9
  48. data/lib/rigor/analysis/runner/pool_coordinator.rb +25 -0
  49. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
  50. data/lib/rigor/analysis/runner.rb +163 -31
  51. data/lib/rigor/analysis/worker_session.rb +3 -1
  52. data/lib/rigor/cache/file_digest.rb +20 -2
  53. data/lib/rigor/cli/check_command.rb +67 -48
  54. data/lib/rigor/cli/coverage_command.rb +5 -6
  55. data/lib/rigor/cli/doc_links.rb +100 -0
  56. data/lib/rigor/cli/docs_command.rb +32 -2
  57. data/lib/rigor/cli/effects_command.rb +149 -10
  58. data/lib/rigor/cli/effects_diff_renderer.rb +88 -14
  59. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  60. data/lib/rigor/cli/effects_renderer.rb +41 -3
  61. data/lib/rigor/cli/effects_report.rb +116 -6
  62. data/lib/rigor/cli/effects_snapshot_command.rb +75 -11
  63. data/lib/rigor/cli/unused_command.rb +65 -16
  64. data/lib/rigor/cli.rb +33 -9
  65. data/lib/rigor/configuration.rb +37 -25
  66. data/lib/rigor/configuration_error.rb +20 -0
  67. data/lib/rigor/effects/collector.rb +38 -1
  68. data/lib/rigor/effects/definition_lines.rb +100 -0
  69. data/lib/rigor/effects/entry_points.rb +47 -1
  70. data/lib/rigor/effects/envelope_check.rb +18 -1
  71. data/lib/rigor/effects/file_collection.rb +18 -4
  72. data/lib/rigor/effects/framework_units.rb +68 -13
  73. data/lib/rigor/effects/inline_anchor.rb +134 -0
  74. data/lib/rigor/effects/liskov_check.rb +17 -8
  75. data/lib/rigor/effects/plugin_facts.rb +62 -13
  76. data/lib/rigor/effects/propagator.rb +79 -19
  77. data/lib/rigor/effects/registry.rb +10 -3
  78. data/lib/rigor/effects/scanner.rb +21 -9
  79. data/lib/rigor/effects/signature_sources.rb +29 -2
  80. data/lib/rigor/effects/snapshot.rb +74 -26
  81. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  82. data/lib/rigor/effects/taint_cause.rb +1 -0
  83. data/lib/rigor/effects/unit_scan.rb +87 -12
  84. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  85. data/lib/rigor/plugin/base.rb +4 -0
  86. data/lib/rigor/plugin/box.rb +18 -2
  87. data/lib/rigor/plugin/effect_ancestry.rb +80 -0
  88. data/lib/rigor/plugin/manifest.rb +34 -10
  89. data/lib/rigor/plugin/registry.rb +10 -3
  90. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  91. data/lib/rigor/rbs_extended.rb +22 -2
  92. data/lib/rigor/version.rb +1 -1
  93. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +17 -2
  94. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
  95. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
  96. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
  97. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +13 -1
  98. data/skills/rigor-ci-setup/SKILL.md +2 -2
  99. data/skills/rigor-editor-setup/SKILL.md +2 -2
  100. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  101. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  102. data/skills/rigor-plugin-review/SKILL.md +3 -3
  103. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffcad04b990dc4220690a6906bb96306056a02792017fcf41200b8865c040846
4
- data.tar.gz: b62aa8767389da9ddd808df2a74d09447aee4d5a9c0deb0d7d4292da5ff7669b
3
+ metadata.gz: b2709c0d68fc616109c3300013f08fda3702484c6ca01ff67b4ccf9d0f354ad0
4
+ data.tar.gz: 77486c048ef9cd94a725b5ebb09ae4e161003b37f67919c769845ed36f6c60bf
5
5
  SHA512:
6
- metadata.gz: f55c7fb8b035ba5364d97163f6ae66e1212d544d5fa2821110a230818be1f64bd2d803ff88739af394d57c39a3bb6ed77737451a68f1cfb047276e23c6ede98a
7
- data.tar.gz: 85aafda9ac9c46b73757c5690484d97d77b1dea26cc499252d39a24aefbd9f58fcf0dfda759a11e7dea553b4d8fac3ae83db9a165e1e926ff62a8fdecad78967
6
+ metadata.gz: 15a406b49bcd62e9efc2c1c143903a85157dbaf165ccca5defdf4abd69949d0c0d0107afd55469e5a0b27fa667e3c59a0ec58de854af503cad0a9e3952568a6e
7
+ data.tar.gz: fa429f47f4ed13b53fa07c7078510524bd6d3b304f7ffa48916b3cfab0e954b42167bda011b413946448dbdaeb63e78dfeaf444ec86a68cf928cefcf3fd70813
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Rigor
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rigortype.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/rigortype)
4
- [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](https://github.com/rigortype/rigor/blob/master/LICENSE)
4
+ [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](LICENSE)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rigortype/rigor)
6
6
 
7
7
  **Type-aware bug finding for Ruby — no annotations required, and a
@@ -47,7 +47,7 @@ s.lenght
47
47
 
48
48
  ```shell-session
49
49
  $ rigor check demo.rb
50
- demo.rb:7:3: error: undefined method `lenght' for "hello-world"
50
+ demo.rb:7:3: error: undefined method `lenght' for "hello-world" [call.undefined-method]
51
51
  ```
52
52
 
53
53
  Note what the error says: not ``undefined method `lenght' for String``
@@ -185,9 +185,9 @@ plugins:
185
185
 
186
186
  The full catalogue is in [`plugins/README.md`](plugins/README.md); the
187
187
  `rigor-project-init` Skill picks the right set for you. To teach Rigor
188
- your own DSL, the [`rigor-plugin-author`](skills/rigor-plugin-author/)
188
+ your own DSL, the [`rigor-plugin-author`](skills/rigor-plugin-author)
189
189
  Skill authors a plugin step-by-step, and
190
- [`rigor-baseline-reduce`](skills/rigor-baseline-reduce/) drives the
190
+ [`rigor-baseline-reduce`](skills/rigor-baseline-reduce) drives the
191
191
  baseline down once you are running.
192
192
 
193
193
  ## Going deeper
@@ -231,7 +231,7 @@ rigor docs --list # list every bundled page
231
231
 
232
232
  ## Status
233
233
 
234
- Current release: **`v0.3.4`** (2026-08-21) — on the
234
+ Current release: **`v0.3.6`** (2026-08-30) — on the
235
235
  `0.3.x` line, continuing the evaluation era opened by `v0.2.0`, the first
236
236
  publicly-announced (general / evaluation) release. The line publishes
237
237
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -531,7 +531,19 @@ classes:
531
531
  # ---------------------------------------------------------------------------------------------
532
532
  # Network.
533
533
  # ---------------------------------------------------------------------------------------------
534
- Socket: { posture: net, why: "the BSD socket surface" }
534
+ Socket:
535
+ posture: net
536
+ why: "the BSD socket surface; the rows below read the machine's own identity and send nothing"
537
+ singleton_methods:
538
+ # `io.net` on these was a wrong label, not a conservative one, and it travelled: Redmine builds
539
+ # its Message-IDs from `Socket.gethostname`, so 215 of 4,234 rows — every model `save`, every
540
+ # `Mailer#*`, and `Redmine::IMAP.check`, whose actual IMAP connection is invisible — read as
541
+ # network traffic on the strength of a hostname lookup (#458). `io` is the honest bound: the
542
+ # call leaves the process to ask the kernel, and nothing goes over a wire.
543
+ gethostname: { effects: [io], why: "reads this machine's own name; no packet leaves the process" }
544
+ gethostbyname: { effects: [io.net], why: "resolves a name — the resolver, and possibly DNS" }
545
+ getifaddrs: { effects: [io], why: "reads this machine's own interfaces; local configuration, not resolution" }
546
+ ip_address_list: { effects: [io], why: "reads this machine's own addresses; local configuration, not resolution" }
535
547
  BasicSocket: { posture: net, why: "the shared socket surface" }
536
548
  TCPSocket: { posture: net, why: "a TCP stream" }
537
549
  TCPServer: { posture: net, why: "a listening TCP socket" }
@@ -572,6 +584,11 @@ classes:
572
584
  start: { effects: [io.net.http], why: "opens the connection; the block joins by containment" }
573
585
  new: { effects: [], why: "allocates the client; nothing is sent until start / request" }
574
586
  "Net::SMTP": { posture: net, why: "an SMTP session; `email.send` is the plugin's semantic layer, not the transport's" }
587
+ # rbs ships `net-http` / `net-smtp` and not these, so a project's `Net::IMAP` types as `Dynamic` and
588
+ # only the constant the author wrote names the class. #463 is what lets a posture answer there; before
589
+ # it, `Net::FTP` had a row that could never fire and these two were not worth writing.
590
+ "Net::IMAP": { posture: net, why: "an IMAP session" }
591
+ "Net::POP3": { posture: net, why: "a POP3 session" }
575
592
  "Net::FTP": { posture: net, why: "an FTP session" }
576
593
 
577
594
  URI:
@@ -11,6 +11,25 @@
11
11
  # still reads.
12
12
  vocabulary: 1
13
13
 
14
+ # What each root means, in one line. Read by `rigor effects --list-labels` (#429), which is the only
15
+ # way an installed Rigor can tell a reader what to write in `effects.envelopes:`, `effects.tolerated:`,
16
+ # `effects.attribution:` or an `%a{rigor:v1:effect …}` annotation — four surfaces that all require
17
+ # typing a label. `docs/manual/19-effect-labels.md`'s vocabulary table is the same wording for the same
18
+ # reason; a root without a line here simply prints without one, which is what a plugin-opened root does.
19
+ descriptions:
20
+ io: talks to something outside the process — a file, a socket, a database, a terminal, a subprocess
21
+ mutate: changes state in place, in the frame's own objects, the receiver, another object, or the class
22
+ nondet: reads something that differs between two otherwise identical runs
23
+ global: process-wide state — ENV, $stdout, a class variable
24
+ exit: can end the process
25
+ ffi: calls out through a foreign-function interface
26
+ telemetry: reports on the program rather than doing its work — logs, metrics, traces
27
+ email: sends mail, whatever the transport underneath is
28
+ job: hands work to a queue for another process to run
29
+ cache: reads or writes a cache, whatever backs it
30
+ failure: registered so a policy written for Steins, the sibling PHP analyzer, parses here; Rigor never
31
+ produces one, so a bound naming it is satisfied vacuously
32
+
14
33
  labels:
15
34
  # Steins v1, verbatim (rigortype/steins `docs/type-specification/effects.md`). Shared vocabulary:
16
35
  # a policy naming these labels must read the same against a PHP service and a Rails app.
@@ -53,9 +72,18 @@ labels:
53
72
  - io.db.write
54
73
  - io.db.transaction
55
74
 
56
- # Application-meaning roots, small and shared. These are the labels a policy actually names
57
- # ("presenters do not enqueue jobs") and the ones `tolerated:` grips, so they must spell the same
58
- # in Steins and Rigor.
75
+ # Steins carries these (its ADR-0042) and Rigor does not produce them: they name a failure arm's
76
+ # value provenance rather than an effect. Registered for the same reason `io.output.buffer` is
77
+ # so a policy written against Steins parses here — and never inferred (ADR-103 WD16).
78
+ - failure
79
+ - failure.environment
80
+ - failure.input
81
+ - failure.resource
82
+
83
+ # Application-meaning roots. These are the labels a policy actually names ("presenters do not
84
+ # enqueue jobs") and the ones `tolerated:` grips, so a project must not need a plugin before it
85
+ # can write one. Rigor-owned and proposed to Steins, which holds ecosystem labels outside its
86
+ # builtin set and supplies them through a plugin manifest (ADR-103 WD16).
59
87
  - telemetry
60
88
  - email.send
61
89
  - job.enqueue
@@ -65,6 +65,9 @@ class Object
65
65
  # `acts_like?(:string)` / `acts_like?(:date)` / `acts_like?(:time)`
66
66
  # is ActiveSupport's "duck-typing helper" predicate.
67
67
  def acts_like?: (Symbol | String) -> bool
68
+
69
+ # `core_ext/object/inclusion` — `x.in?([a, b])` / `x.in?(1..10)`.
70
+ def in?: (untyped) -> bool
68
71
  end
69
72
 
70
73
  # `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
@@ -108,6 +111,9 @@ class String
108
111
  def pluralize: (?Integer count, ?Symbol locale) -> String
109
112
  def singularize: (?Symbol locale) -> String
110
113
  def humanize: (?capitalize: bool, ?keep_id_suffix: bool) -> String
114
+ def titlecase: () -> String
115
+ def dasherize: () -> String
116
+ def upcase_first: () -> String
111
117
 
112
118
  # `core_ext/string/filters`
113
119
  def squish: () -> String
@@ -115,6 +121,8 @@ class String
115
121
  def truncate: (Integer truncate_at, ?omission: String, ?separator: String | Regexp | nil) -> String
116
122
  def truncate_bytes: (Integer truncate_at, ?omission: String) -> String
117
123
  def truncate_words: (Integer words_count, ?omission: String, ?separator: String | Regexp | nil) -> String
124
+ def remove: (*Regexp | String patterns) -> String
125
+ def remove!: (*Regexp | String patterns) -> String
118
126
 
119
127
  # `core_ext/string/output_safety` — `html_safe` returns an
120
128
  # `ActiveSupport::SafeBuffer` (a String subclass). The closest
@@ -282,6 +290,8 @@ class Time
282
290
  # Re-declaring it raised `RBS::DuplicatedMethodDefinitionError` and took the WHOLE `Time` definition
283
291
  # down with it (every `Time` call degraded to `Dynamic[top]`). This overlay only ADDS what
284
292
  # ActiveSupport adds.
293
+ def advance: (untyped options) -> Time
294
+ def all_day: () -> Range[Time]
285
295
  def acts_like_time?: () -> true
286
296
  end
287
297
 
@@ -323,6 +333,17 @@ class Date
323
333
  def at_beginning_of_day: () -> Time
324
334
  def end_of_day: () -> Time
325
335
  def at_end_of_day: () -> Time
336
+ def advance: (untyped options) -> Date
337
+ def all_day: () -> Range[Time]
338
+
339
+ # `Date#to_time` is an OVERLOAD CONTINUATION (`| ...`), not a plain declaration: stdlib `date`
340
+ # already types `to_time: () -> Time`, and re-declaring it raises
341
+ # `RBS::DuplicatedMethodDefinitionError`, which collapses `Date` to `Dynamic[top]` for every project
342
+ # this overlay reaches — the failure the `Time#utc?` note above records, and the one #437 hit on the
343
+ # plugin. The row cannot simply be omitted either: ActiveSupport genuinely widens the arity, so
344
+ # without it `date.to_time(:utc)` — correct Rails code — draws an arity diagnostic (#449). `| ...`
345
+ # appends this overload ahead of the stdlib one, so both arities resolve.
346
+ def to_time: (?Symbol form) -> Time | ...
326
347
  end
327
348
 
328
349
  # ---------------------------------------------------------------
@@ -475,3 +496,15 @@ class DateTime
475
496
  def end_of_minute: () -> DateTime
476
497
  def acts_like_time?: () -> true
477
498
  end
499
+
500
+ # ---------------------------------------------------------------
501
+ # ERB::Util — ActionView extends it with `html_escape_once`
502
+ # ---------------------------------------------------------------
503
+
504
+ # `ERB` is a CLASS in Ruby / RBS (not a module), so reopen it as a class — a `module ERB` wrapper
505
+ # collides with upstream `class ERB` once the `erb` stdlib is in scope. `ERB::Util` itself is a module.
506
+ class ERB
507
+ module Util
508
+ def self.html_escape_once: (untyped) -> String
509
+ end
510
+ end
@@ -113,7 +113,7 @@ String, Symbol, Array, and Hash. The list is not in this
113
113
  handbook (it would fill several pages); see
114
114
  [`docs/types.md`](../types.md) and the per-class catalogues
115
115
  under
116
- [`data/builtins/ruby_core/`](../../data/builtins/ruby_core/).
116
+ [`data/builtins/ruby_core/`](../../data/builtins/ruby_core).
117
117
 
118
118
  When folding is **not** safe (because a method has side
119
119
  effects, depends on the environment, or is not in a
@@ -275,7 +275,7 @@ the underlying RBS sig advertises.
275
275
 
276
276
  If you prefer the TS spellings (`Pick<T, K>` etc.) in
277
277
  directives, opt into the
278
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/)
278
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types)
279
279
  plugin. The plugin registers a `Plugin::TypeNodeResolver` that
280
280
  translates each TS name onto the canonical projection:
281
281
 
@@ -236,6 +236,6 @@ deprecations, …). Most projects will never write one; the
236
236
  chapter exists so you know the option is there.
237
237
  [Chapter 10 — Coexisting with Sorbet](10-sorbet.md) is for
238
238
  projects arriving from a Sorbet codebase: the
239
- [`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter reads
239
+ [`rigor-sorbet`](../../plugins/rigor-sorbet) adapter reads
240
240
  `sig { ... }` blocks, RBI files, and `T.let` / `T.cast` /
241
241
  `T.must` / `T.unsafe` assertions as type sources.
@@ -122,7 +122,7 @@ Reach for a plugin only when:
122
122
 
123
123
  If those are true, [`examples/README.md`](../../examples/README.md)
124
124
  is your starting point. The
125
- [`rigor-deprecations`](../../examples/rigor-deprecations/)
125
+ [`rigor-deprecations`](../../examples/rigor-deprecations)
126
126
  example is the smallest fully-shaped plugin — manifest +
127
127
  single per-file walk + a couple of diagnostic emissions —
128
128
  and is the recommended template for "I want to author my
@@ -146,7 +146,7 @@ From here:
146
146
  to deeper material in
147
147
  [`docs/type-specification/`](../type-specification/README.md),
148
148
  [`docs/internal-spec/`](../internal-spec/README.md), and
149
- [`docs/adr/`](../adr/).
149
+ [`docs/adr/`](../adr).
150
150
  - The [`CHANGELOG.md`](../../CHANGELOG.md) is the per-release
151
151
  truth for what shipped when.
152
152
 
@@ -1,7 +1,7 @@
1
1
  # Coexisting with Sorbet
2
2
 
3
3
  If your project already uses [Sorbet](https://sorbet.org/),
4
- the [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin
4
+ the [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin
5
5
  lets Rigor read your existing `sig` blocks, RBI files, and
6
6
  `T.let` / `T.cast` / `T.must` / `T.unsafe` assertions as type
7
7
  sources. You do not have to rewrite anything in RBS to start
@@ -66,7 +66,7 @@ up the flag, key, or command that *acts* on it.
66
66
  landing page.
67
67
  10. [**Coexisting with Sorbet**](10-sorbet.md) — for users
68
68
  arriving from a Sorbet-using project: the
69
- [`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter
69
+ [`rigor-sorbet`](../../plugins/rigor-sorbet) adapter
70
70
  reads `sig { ... }` blocks, RBI files, and
71
71
  `T.let` / `T.cast` / `T.must` / `T.unsafe` assertions
72
72
  as type sources without rewriting in RBS.
@@ -255,7 +255,7 @@ ADRs:
255
255
  - [`docs/internal-spec/`](../internal-spec/README.md) —
256
256
  analyzer-internal contracts (engine surface, type-object
257
257
  public API).
258
- - [`docs/adr/`](../adr/) — architecture decision records.
258
+ - [`docs/adr/`](../adr) — architecture decision records.
259
259
 
260
260
  ## Non-goals
261
261
 
@@ -105,7 +105,7 @@ type switches. Rigor has direct analogues.
105
105
  | `if x != nil` | `if x` (strips `nil`), or `unless x.nil?` |
106
106
  | `v, ok := x.(string)` (comma-ok assertion) | `x.is_a?(String)` narrowing in an `if` |
107
107
  | `switch v := x.(type) { case string: … }` | `case x; in String => v` |
108
- | `x.(T)` (assertion, panics on fail) | (no panicking assertion) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
108
+ | `x.(T)` (assertion, panics on fail) | (no panicking assertion) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet) |
109
109
  | user func returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive |
110
110
 
111
111
  Go's type switch is *not* exhaustive — you can omit cases and
@@ -58,7 +58,7 @@ advisor that only speaks when it is sure.
58
58
  | `Set<T>` | `HashSet<T>` / `ISet<T>` | `Set[T]` | |
59
59
  | `record Point(int x, int y)` | `record Point(int X, int Y)` | `Point = Data.define(:x, :y)` | See [Records ↔ Data.define](#records--datadefine). |
60
60
  | `Optional<T>` | `T?` (nullable reference type) | `T?` (i.e. `T \| nil`) | Java models it as a *container*; C# as a *type modifier*. See [Nullability](#nullability). |
61
- | `enum Color { RED, GREEN }` | `enum Color { Red, Green }` | `Constant<:red> \| Constant<:green>` (Symbol union) | Ruby has no native enum; the [`rigor-mangrove`](../../plugins/) plugin types richer enum DSLs. |
61
+ | `enum Color { RED, GREEN }` | `enum Color { Red, Green }` | `Constant<:red> \| Constant<:green>` (Symbol union) | Ruby has no native enum; the [`rigor-mangrove`](../../plugins) plugin types richer enum DSLs. |
62
62
  | `sealed interface Shape permits …` | `abstract` base + sealed hierarchy | union of the subtypes | See [Sealed types & exhaustiveness](#sealed-types-and-exhaustiveness). |
63
63
  | `<T>` (generic) | `<T>` (generic) | RBS `[T]` type parameter | |
64
64
  | `? extends T` (use-site) | `out T` (declaration-site) | covariant type parameter | See [Generics & variance](#generics-and-variance). |
@@ -124,7 +124,7 @@ behaviour matches.
124
124
  | `x instanceof String` | `x is string` | `x.is_a?(String)` |
125
125
  | `x instanceof String s` (binding) | `x is string s` (binding) | `case x; in String => s` |
126
126
  | `switch (x) { case Foo f -> … }` | `switch (x) { case Foo f => … }` | `case x; in Foo => f` |
127
- | `(Foo) x` (cast) | `(Foo)x` (cast) | (no in-source cast) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
127
+ | `(Foo) x` (cast) | `(Foo)x` (cast) | (no in-source cast) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet) |
128
128
  | `Objects.requireNonNull(x)` | `x!` (null-forgiving) | (no in-source assertion) — `unless x.nil?`, or `T.must` via `rigor-sorbet` |
129
129
  | user method returning `boolean` | user method returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive on the predicate |
130
130
 
@@ -296,7 +296,7 @@ draws the distinction in full.
296
296
  Pyright's "type alias narrowing" and mypy's overload stacks
297
297
  cover some cases; Rigor's plugin contract gives you full
298
298
  Ruby code at the dispatch point. The
299
- [`rigor-lisp-eval`](../../examples/rigor-lisp-eval/) example
299
+ [`rigor-lisp-eval`](../../examples/rigor-lisp-eval) example
300
300
  is the canonical demo — `Lisp.eval([:+, 1, 2])` returns
301
301
  `Integer`, `Lisp.eval([:<, 1, 2])` returns `bool`.
302
302
 
@@ -228,8 +228,8 @@ author):
228
228
  - The `missing-protocol-method` / `protocol-return-mismatch`
229
229
  diagnostics are **plugin diagnostics**, emitted under the
230
230
  plugin's `plugin.<id>.` provenance — not core Rigor rules. The
231
- worked references are [`examples/rigor-web/`](../../examples/rigor-web/)
232
- (the minimal tutorial) and [`plugins/rigor-hanami/`](../../plugins/rigor-hanami/)
231
+ worked references are [`examples/rigor-web/`](../../examples/rigor-web)
232
+ (the minimal tutorial) and [`plugins/rigor-hanami/`](../../plugins/rigor-hanami)
233
233
  (production Hanami 2 actions).
234
234
 
235
235
  ## Interface vs protocol contract
@@ -91,7 +91,7 @@ The reflex to drop is `unwrap()`. In Rust you reach for
91
91
  `.unwrap()` / `.expect()` when you *know* it is `Some`. Rigor
92
92
  has no in-source assertion that lies to the checker; the
93
93
  equivalents are a `nil?` guard (checked, not asserted) or
94
- `T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet/)
94
+ `T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet)
95
95
  plugin (see [Chapter 10](10-sorbet.md)).
96
96
 
97
97
  **`Result<T, E>` ↔ exceptions.** Here the models diverge. Ruby
@@ -709,7 +709,7 @@ Rigor maps onto this as:
709
709
  | Dynamic type `?` | **`Dynamic[T]`** — a carrier that *wraps* a "best-guess" type `T` while marking the value as not-statically-verified. `Dynamic[top]` is the maximally-dynamic form. |
710
710
  | Consistency `~` | The `maybe` arm of the trinary certainty — `Dynamic[T] ~ U` holds whenever `T ~ U` does. |
711
711
  | Static/dynamic boundary | Per-method, per-file, per-plugin contribution — Rigor records *why* a value became `Dynamic[T]` in its dynamic-origin algebra. |
712
- | Casts | No in-source cast operator. The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin reads `T.let` / `T.cast` / `T.must` as cast forms; `RBS::Extended` `assert_type` directives serve the same role from `.rbs`. |
712
+ | Casts | No in-source cast operator. The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin reads `T.let` / `T.cast` / `T.must` as cast forms; `RBS::Extended` `assert_type` directives serve the same role from `.rbs`. |
713
713
 
714
714
  Two Rigor-specific extensions matter:
715
715
 
@@ -794,7 +794,7 @@ against:
794
794
  ### What Rigor explicitly does NOT do
795
795
 
796
796
  - **Runtime contract insertion at the static / dynamic boundary.**
797
- The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin
797
+ The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin
798
798
  reads Sorbet's `T.let` / `T.cast` / `T.must` as cast forms, but
799
799
  the contract *enforcement* is `sorbet-runtime`'s job, not
800
800
  Rigor's. Rigor's static analysis uses the cast as a hint, not
@@ -51,7 +51,7 @@ inference cannot see further).
51
51
  | `{ name: string; age: number }` | `HashShape{name: String, age: Integer}` | Same per-key model; Ruby uses Symbol keys idiomatically. |
52
52
  | `Array<T>` / `T[]` | `Array[T]` | Same. |
53
53
  | `Record<K, V>` | `Hash[K, V]` | Same. |
54
- | `Readonly<T>` | `readonly_of[T]` (via opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/) plugin) | View-level read-only marker on every entry of a `HashShape`. Does NOT prove the underlying object is frozen — ADR-13 § "Readonly". |
54
+ | `Readonly<T>` | `readonly_of[T]` (via opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types) plugin) | View-level read-only marker on every entry of a `HashShape`. Does NOT prove the underlying object is frozen — ADR-13 § "Readonly". |
55
55
  | `Partial<T>` / `Required<T>` | `partial_of[T]` / `required_of[T]` (same plugin) | Flips every entry's required-ness on a `HashShape`. `Partial` does NOT widen value types to `nil` — Rigor's `HashShape` distinguishes "key absent" from "key present with nil value" (ADR-13 WD on required-ness flips). |
56
56
  | `Pick<T, K>` / `Omit<T, K>` | `pick_of[T, K]` / `omit_of[T, K]` (same plugin) | Restrict / remove `HashShape` entries by literal-key union; Tuple receivers project by integer index. Non-shape carriers degrade conservatively and surface `dynamic.shape.lossy-projection`. |
57
57
  | Conditional types `T extends U ? A : B` | (none in core; plugin contributions) | A plugin can vary return type by argument shape. |
@@ -159,7 +159,7 @@ inferred call-site instantiation as routinely as TypeScript.
159
159
  | `Array<T>` | `Array[T]` |
160
160
  | `Map<K, V>` | `Hash[K, V]` |
161
161
  | `Promise<T>` | (no analogue — Ruby has no built-in Promise) |
162
- | `Pick<T, K>` / `Omit<T, K>` / `Partial<T>` / `Required<T>` / `Readonly<T>` | Opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/) plugin maps each onto `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of` over `HashShape` (and `pick_of` / `omit_of` over `Tuple`). |
162
+ | `Pick<T, K>` / `Omit<T, K>` / `Partial<T>` / `Required<T>` / `Readonly<T>` | Opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types) plugin maps each onto `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of` over `HashShape` (and `pick_of` / `omit_of` over `Tuple`). |
163
163
  | Conditional types | (no analogue — would need a plugin) |
164
164
 
165
165
  Rigor reads RBS generics through its dispatcher and instantiates
@@ -214,7 +214,7 @@ Be honest about what you give up:
214
214
  variation, not type-level expressions.
215
215
  - **Mapped types.** `Pick`, `Omit`, `Partial`, `Required`, and
216
216
  `Readonly` ship as opt-in plugin-supplied vocabulary via
217
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/),
217
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types),
218
218
  which maps them onto the Rigor-canonical `pick_of` / `omit_of`
219
219
  / `partial_of` / `required_of` / `readonly_of` shape-projection
220
220
  type functions on `HashShape` (and `pick_of` / `omit_of` on
@@ -294,7 +294,7 @@ def pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]
294
294
 
295
295
  The RBS sig stays generic. If you want `Pick<T, K>`'s exact-
296
296
  key-set tracking back, opt into the
297
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/)
297
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types)
298
298
  plugin and annotate the return type with the `Pick` spelling:
299
299
 
300
300
  ```rbs
@@ -38,7 +38,7 @@ the `paths:` list from the configuration file.
38
38
  | `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. See [Caching](12-caching.md). |
39
39
  | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics are byte-identical, then exit (0 on match, 1 with the differing diagnostics on mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
40
40
  | `--clear-cache` | Delete the cache directory before running. |
41
- | `--cache-stats` | Print the on-disk cache inventory when finished. |
41
+ | `--cache-stats` | Print the on-disk cache inventory when finished — on stdout under `--format text`, on stderr under every other format, so machine-readable output stays a parseable document. `--clear-cache`'s and `--verify-incremental`'s notes follow the same rule. |
42
42
  | `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
43
43
  | `--coverage` | Add a type-precision coverage block to the output (`coverage` object under `--format json`; a one-line summary in text mode). Off by default — it is a second precision pass over the analyzed files, the same scan [`rigor coverage`](#rigor-coverage) runs, so it is opt-in. |
44
44
  | `--workers=N` | Dispatch analysis across `N` parallel worker processes (fork-based pool today; ADR-15). Default `0` (sequential). Applies to `--incremental` re-checks as well as full runs. |
@@ -169,7 +169,10 @@ Report what each method *does* — its effect labels — rather
169
169
  than what it returns, and manage the committed **effect
170
170
  snapshot** that gates drift. Opt-in and observational: nothing
171
171
  here emits a diagnostic or changes `rigor check`'s output, and
172
- only `rigor effects check` ever exits non-zero.
172
+ only `rigor effects check` ever exits non-zero. The workflow
173
+ around these verbs — the label vocabulary, how to read the
174
+ report at scale, the review loop and the CI step — is
175
+ [Effect labels](19-effect-labels.md).
173
176
 
174
177
  ```sh
175
178
  rigor effects [PATH...] # the report
@@ -183,6 +186,13 @@ configuring anything; such an ad-hoc run shares no cache with
183
186
  `rigor check`, because a run served from that cache would have
184
187
  collected nothing.
185
188
 
189
+ Passing paths narrows the **analysis**, not just the output.
190
+ Labels are transitive over what was analysed, so a method whose
191
+ callees were not analysed reports fewer labels and a wider
192
+ "possibly more" than the same method in a whole-project run —
193
+ honest, but not a cheaper way to get the whole-project answer.
194
+ Filter the output when you want the real footprint.
195
+
186
196
  Each line is one method, sorted by key:
187
197
 
188
198
  ```
@@ -199,10 +209,13 @@ lines say why. A taint is never a finding.
199
209
 
200
210
  A ` ≤ [...]` clause after the labels is the **declared** lane:
201
211
  what a source Rigor trusts but did not verify *claims* the
202
- method does, today the `effects.attribution:` table you wrote
203
- for gem methods it cannot see. It is printed apart from the
204
- proven labels and never folded in among them, because the two
205
- answer different questions. It follows call edges exactly as
212
+ method does. Two sources feed it — the plugins you activated,
213
+ which on a framework application contribute most of it
214
+ (`rails.session.write` and the rest of `rails.*` are Action
215
+ Pack's rows, not yours), and the `effects.attribution:` table
216
+ you wrote for gem methods nothing models. It is printed apart
217
+ from the proven labels and never folded in among them, because
218
+ the two answer different questions. It follows call edges exactly as
206
219
  the proven labels do, so a controller two hops above an
207
220
  attributed gem call carries the claim rather than only a
208
221
  "possibly more"; a declared label the proven list already
@@ -213,10 +226,30 @@ Gateways::Client#fetch: [] ≤ [io.net.http] …?
213
226
  plugin-attribution (Acme::Http.get)
214
227
  ```
215
228
 
216
- A method is omitted when it is exhaustive and proves nothing
217
- beyond `mutate.local` — mutation of objects its own frame
218
- allocated and never let out, which every effect envelope
219
- tolerates. `--full` lists every method instead.
229
+ Two kinds of method are omitted: one that proves nothing
230
+ beyond `mutate.local` and claims nothing — mutation of objects
231
+ its own frame allocated and never let out, which every effect
232
+ envelope tolerates and one with no label in either lane,
233
+ which exists only to record that something below it was
234
+ unresolved. `--full` lists both. `--pure` asks for the first
235
+ group by name, which is the set worth annotating `%a{pure}`.
236
+
237
+ `--label=LABEL` prints only the methods carrying `LABEL` or a
238
+ label under it, in either lane. `--limit=N` caps the rows.
239
+ `--why` expands each row's unresolved reasons and the plugin
240
+ row behind each declared label, which are collapsed to a count
241
+ by default. The report closes with a footer counting the two
242
+ lanes apart, because they have different powers: a proven label
243
+ can fail a build and a declared one cannot
244
+ ([ADR-103](../adr/103-effect-labels.md) § WD17).
245
+
246
+ A `PATH` argument selects which methods are **printed**, never
247
+ which are analysed: Rigor analyses your configured `paths:`
248
+ either way, so a selected row carries exactly the labels the
249
+ whole-project run gives it, and a note on stderr says how many
250
+ of how many you are looking at. A path outside `paths:` is
251
+ analysed as well as them, so pointing the command at a tree
252
+ your configuration does not cover still works.
220
253
 
221
254
  `--format=text|json` selects the output format; the JSON
222
255
  payload additionally carries each method's *direct* summary
@@ -226,9 +259,15 @@ subcommands and does nothing here: the report is an
226
259
  observation, and observations are undischarged.
227
260
 
228
261
  What is collected and how it propagates is
229
- [the effect-summaries internal spec](../internal-spec/effect-summaries.md);
230
- the label vocabulary is
231
- [the effect-labels specification](../type-specification/effect-labels.md).
262
+ [the effect-summaries internal spec](../internal-spec/effect-summaries.md).
263
+ `--list-labels` prints the vocabulary this project can name —
264
+ every shipped label with its root's meaning, plus whatever your
265
+ plugins and your `effects.labels:` opened — and exits without
266
+ analysing anything. The same table is in
267
+ [Effect labels § The label vocabulary](19-effect-labels.md), and
268
+ the grammar is specified normatively in the effect-labels
269
+ specification, which the gem does not ship:
270
+ <https://rigor.typedduck.fail/type-specification/effect-labels/>.
232
271
 
233
272
  ### The effect snapshot
234
273
 
@@ -265,8 +304,8 @@ supposed to fan out — the fan-out is the blast radius.
265
304
 
266
305
  ```yaml
267
306
  # .rigor-effects.yml — generated by `rigor effects update`. Commit it; review its diff.
268
- schema: 1
269
- rigor: "0.3.3"
307
+ schema: 2
308
+ rigor: "0.3.5"
270
309
  vocabulary: 1
271
310
  config_digest: "9ec82bfc…"
272
311
  methods:
@@ -275,7 +314,7 @@ methods:
275
314
  "Reports::Nightly#perform":
276
315
  effects: ["io.db.read"]
277
316
  exhaustive: false
278
- unresolved: ["dynamic-send"]
317
+ unresolved: 1
279
318
  reach:
280
319
  "OrdersController#create":
281
320
  effects: ["io.db.read", "io.net.http", "job.enqueue"]
@@ -337,14 +376,17 @@ spelled out:
337
376
  Effect drift against .rigor-effects.yml:
338
377
 
339
378
  methods:
340
- PaymentGateway#charge + io.net.http
379
+ PaymentGateway#charge + io.net.http (app/services/payment_gateway.rb:18)
341
380
 
342
381
  reach:
343
- OrdersController#create + io.net.http
382
+ OrdersController#create + io.net.http (app/controllers/orders_controller.rb:7)
344
383
 
345
- Run `rigor effects update` and commit the result if this change is intended.
384
+ Run `rigor effects explain` to see what caused this, and `rigor effects update` to accept it.
346
385
  ```
347
386
 
387
+ Each row names where the method is defined, so the reviewer reads the report
388
+ rather than searching for the method.
389
+
348
390
  The author runs `rigor effects explain` to see the route —
349
391
 
350
392
  ```
@@ -517,7 +559,7 @@ hand-adjudicated corpus target only **7% of the rows were genuinely
517
559
  unused**; the rest were reachable by means static analysis cannot
518
560
  see. That is why this is a separate command and never a `rigor check`
519
561
  diagnostic — see
520
- [ADR-102](https://github.com/rigortype/rigor/blob/master/docs/adr/102-unused-code-reachability-report.md).
562
+ [ADR-102](../adr/102-unused-code-reachability-report.md).
521
563
 
522
564
  Reachability is computed from **roots**, not by counting references,
523
565
  so a cluster of classes that only reference each other is still
@@ -1053,9 +1095,10 @@ operational knobs read the environment instead.
1053
1095
  | `RIGOR_CI_DETECT=0` | Turn off CI auto-detection — the same as `--no-ci-detect`. See [Running Rigor in CI § auto-detection](11-ci.md). |
1054
1096
  | `RIGOR_RACTOR_WORKERS=N` | Worker count for parallel analysis. Sits between the CLI flag and the config key in precedence: `--workers=N` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > `0` (sequential). |
1055
1097
  | `RIGOR_POOL_BACKEND=ractor` | Opt back into the (off-by-default) Ractor worker pool instead of the active fork-based pool ([ADR-15](../adr/15-ractor-concurrency.md)). Only relevant with a non-zero worker count; the fork pool is the supported backend. |
1098
+ | `RIGOR_LSP_POOL_MIN_BATCH=N` | Fewest buffers an [`rigor lsp`](#rigor-lsp) batch must carry before analysis is dispatched across the worker pool rather than run in-process (default `16`). Lower it if your project's per-file analysis is expensive enough that pooling pays off sooner. |
1056
1099
  | `RIGOR_PLUGIN_ISOLATION=none\|process\|ruby_box` | How a plugin's direct calls into its target library are isolated. Default `process`. See [Using plugins § Isolation strategy](07-plugins.md). `RIGOR_BOX` is a legacy alias for `ruby_box`. |
1057
1100
  | `RIGOR_STRICT_VALIDATION=1` | Force full-content cache validation for one run (the same as `cache.validation: digest`, and winning over it) — re-hash every file's content instead of trusting its stat metadata. Use it if a filesystem's timestamps or inode numbers cannot be trusted. See [Caching § How a file is checked for changes](12-caching.md#how-a-file-is-checked-for-changes). |
1058
- | `RIGOR_DISABLE_YJIT=1` | Opt out of Rigor's deferred YJIT enablement. Rigor turns YJIT on partway through a long `check` / `coverage` run so short runs never pay the JIT warm-up; this variable leaves it off entirely. Diagnostics and allocations are identical either way — the effect is wall-time only. |
1101
+ | `RIGOR_DISABLE_YJIT=1` | Opt out of Rigor's deferred YJIT enablement. Rigor turns YJIT on partway through any long run so short runs never pay the JIT warm-up; this variable leaves it off entirely. Diagnostics and allocations are identical either way — the effect is wall-time only. |
1059
1102
  | `RIGOR_YJIT_DEADLINE=<seconds>` | Advanced: tune how long a run must last before deferred YJIT enables (default `5.0`). Lower it if your runs are long and you want the JIT sooner; raise it to protect short runs. Ignored when `RIGOR_DISABLE_YJIT=1` is set or YJIT is unavailable. |
1060
1103
 
1061
1104
  Three further variables (`RIGOR_BUDGET_TRACE`,
@@ -1069,7 +1112,15 @@ diagnostics about Rigor's own inference cutoffs and memory — see
1069
1112
  | --- | --- |
1070
1113
  | `0` | Success — no error-severity diagnostics. |
1071
1114
  | `1` | Diagnostics found, or a per-command failure (parse error, missing file, new diagnostics on `diff`, effect drift on `effects check`). |
1072
- | `64` | Usage error — unknown command, bad flag, malformed argument. |
1115
+ | `64` | Usage error — unknown command, bad flag, malformed argument, or a value in `.rigor.yml` the loader cannot proceed on. |
1073
1116
 
1074
1117
  `rigor triage` is the exception: it is advisory and always
1075
1118
  exits `0`.
1119
+
1120
+ A configuration mistake prints one `rigor:` line naming the key
1121
+ and nothing else — no backtrace, on every command:
1122
+
1123
+ ```
1124
+ $ rigor effects update
1125
+ rigor: effects.attribution key is not a method key (`Owner#method` / `Owner.method`): "Net::HTTP get"
1126
+ ```