rigortype 0.3.0 → 0.3.2

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/data/builtins/ruby_core/array.yml +416 -392
  4. data/data/builtins/ruby_core/file.yml +42 -42
  5. data/data/builtins/ruby_core/hash.yml +302 -302
  6. data/data/builtins/ruby_core/io.yml +191 -191
  7. data/data/builtins/ruby_core/numeric.yml +321 -366
  8. data/data/builtins/ruby_core/proc.yml +124 -124
  9. data/data/builtins/ruby_core/range.yml +21 -21
  10. data/data/builtins/ruby_core/rational.yml +39 -39
  11. data/data/builtins/ruby_core/re.yml +65 -65
  12. data/data/builtins/ruby_core/set.yml +106 -106
  13. data/data/builtins/ruby_core/struct.yml +14 -14
  14. data/data/core_overlay/pathname.rbs +5 -0
  15. data/data/core_overlay/resolv.rbs +31 -0
  16. data/data/core_overlay/string_scanner.rbs +11 -5
  17. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  18. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  19. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  21. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  22. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  23. data/docs/handbook/01-getting-started.md +22 -34
  24. data/docs/handbook/06-classes.md +1 -1
  25. data/docs/handbook/07-rbs-and-extended.md +76 -101
  26. data/docs/handbook/08-understanding-errors.md +114 -247
  27. data/docs/handbook/09-plugins.md +54 -144
  28. data/docs/handbook/README.md +5 -3
  29. data/docs/handbook/appendix-liskov.md +4 -2
  30. data/docs/handbook/appendix-phpstan.md +14 -7
  31. data/docs/handbook/appendix-steep.md +4 -2
  32. data/docs/handbook/appendix-type-theory.md +3 -1
  33. data/docs/manual/02-cli-reference.md +103 -11
  34. data/docs/manual/03-configuration.md +16 -1
  35. data/docs/manual/04-diagnostics.md +36 -4
  36. data/docs/manual/06-baseline.md +35 -1
  37. data/docs/manual/08-skills.md +6 -1
  38. data/docs/manual/09-editor-integration.md +3 -2
  39. data/docs/manual/12-caching.md +17 -5
  40. data/docs/manual/15-type-protection-coverage.md +122 -1
  41. data/docs/manual/plugins/rigor-actioncable.md +32 -0
  42. data/docs/manual/plugins/rigor-devise.md +4 -2
  43. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
  46. data/lib/rigor/analysis/check_rules.rb +79 -19
  47. data/lib/rigor/analysis/incremental_session.rb +156 -11
  48. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  49. data/lib/rigor/analysis/run_cache_key.rb +27 -1
  50. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  52. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  53. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  54. data/lib/rigor/analysis/runner.rb +80 -8
  55. data/lib/rigor/bleeding_edge.rb +132 -7
  56. data/lib/rigor/cache/descriptor.rb +6 -1
  57. data/lib/rigor/cache/engine_source.rb +162 -0
  58. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  59. data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
  60. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
  61. data/lib/rigor/cache/store.rb +99 -24
  62. data/lib/rigor/cli/check_command.rb +61 -34
  63. data/lib/rigor/cli/check_invocation.rb +84 -0
  64. data/lib/rigor/cli/coverage_command.rb +15 -2
  65. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  66. data/lib/rigor/cli/doctor_command.rb +6 -8
  67. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  68. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  69. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  70. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  71. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  72. data/lib/rigor/cli/protection_renderer.rb +13 -0
  73. data/lib/rigor/cli/protection_report.rb +11 -3
  74. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  75. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  76. data/lib/rigor/cli/skill_command.rb +21 -1
  77. data/lib/rigor/cli/skill_deep_probe.rb +172 -0
  78. data/lib/rigor/cli/skill_describe.rb +75 -9
  79. data/lib/rigor/configuration.rb +45 -6
  80. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  81. data/lib/rigor/environment/default_libraries.rb +5 -4
  82. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  83. data/lib/rigor/environment/rbs_loader.rb +434 -75
  84. data/lib/rigor/environment.rb +38 -14
  85. data/lib/rigor/inference/expression_typer.rb +15 -0
  86. data/lib/rigor/inference/fork_map.rb +9 -0
  87. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  88. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  89. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
  92. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  93. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  95. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  96. data/lib/rigor/inference/mutation_widening.rb +30 -25
  97. data/lib/rigor/inference/narrowing.rb +3 -1
  98. data/lib/rigor/inference/optimistic_origin.rb +58 -0
  99. data/lib/rigor/inference/protection_scanner.rb +18 -2
  100. data/lib/rigor/inference/receiver_alias.rb +57 -0
  101. data/lib/rigor/inference/scope_indexer.rb +78 -27
  102. data/lib/rigor/inference/statement_evaluator.rb +40 -2
  103. data/lib/rigor/language_server/buffer_resolution.rb +6 -3
  104. data/lib/rigor/language_server/buffer_table.rb +68 -6
  105. data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
  106. data/lib/rigor/language_server/incremental_sync.rb +159 -0
  107. data/lib/rigor/language_server/project_context.rb +45 -0
  108. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  109. data/lib/rigor/language_server/server.rb +38 -9
  110. data/lib/rigor/language_server.rb +2 -0
  111. data/lib/rigor/plugin/base.rb +29 -2
  112. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  113. data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
  114. data/lib/rigor/protection/dependency_closure.rb +59 -0
  115. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  116. data/lib/rigor/protection/discovery_seed.rb +116 -0
  117. data/lib/rigor/protection/kill_signature.rb +31 -0
  118. data/lib/rigor/protection/mutation_cache.rb +355 -0
  119. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  120. data/lib/rigor/protection/mutator.rb +26 -4
  121. data/lib/rigor/runtime/jit.rb +63 -1
  122. data/lib/rigor/scope/discovery_index.rb +9 -0
  123. data/lib/rigor/scope.rb +52 -8
  124. data/lib/rigor/sig_gen/generator.rb +130 -31
  125. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  126. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  127. data/lib/rigor/sig_gen/renderer.rb +20 -1
  128. data/lib/rigor/sig_gen/write_result.rb +6 -4
  129. data/lib/rigor/sig_gen/writer.rb +195 -47
  130. data/lib/rigor/sig_gen.rb +1 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
  133. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  134. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  135. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  136. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  138. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  139. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  140. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  141. data/sig/rigor/cache.rbs +6 -0
  142. data/sig/rigor/inference/void_origin.rbs +21 -0
  143. data/sig/rigor/plugin/base.rbs +4 -3
  144. data/sig/rigor/scope.rbs +8 -0
  145. metadata +23 -3
@@ -9,9 +9,10 @@ It does **not** teach plugin *authoring*. That lives in
9
9
  [`examples/`](../../examples/README.md) — six tutorial
10
10
  walkthroughs, each spotlighting one extension surface.
11
11
  Ready-to-install gems for real frameworks live in
12
- [`plugins/`](../../plugins/README.md). Read on to decide
13
- whether you need a plugin; go to `examples/` once you want to
14
- write one, or `plugins/` to install an existing one.
12
+ [`plugins/`](../../plugins/README.md), and activating one is
13
+ [manual Using plugins](../manual/07-plugins.md). Read on to
14
+ decide whether you need a plugin; go to `examples/` once you
15
+ want to write one.
15
16
 
16
17
  ## When you reach for a plugin
17
18
 
@@ -51,151 +52,60 @@ compares the six worked examples on architectural axes
51
52
  engine-collaboration via `Scope#type_of`, cross-plugin facts,
52
53
  return-type contributions, …) and recommends a reading order.
53
54
 
54
- ## What a plugin can do today
55
+ ## Two authoring paths
55
56
 
56
57
  > Still here? Most readers should jump to
57
58
  > [Should you write one?](#should-you-write-one) first — the
58
59
  > answer is usually "no, RBS and `RBS::Extended` get you
59
- > there." The surfaces below are for when it is "yes."
60
-
61
- The v0.1.0+ plugin contract pinned at
62
- [`docs/internal-spec/plugin.md`](../internal-spec/plugin.md)
63
- and laid out across a handful of slice specs in the same
64
- directory gives a plugin five primary surfaces:
65
-
66
- 1. **`#diagnostics_for_file(path:, scope:, root:)`** the
67
- per-file emission hook. Walk the parsed AST, return an
68
- array of `Rigor::Analysis::Diagnostic` rows. The runner
69
- stamps each with `source_family: "plugin.<your-id>"`.
70
- 2. **`dynamic_return(receivers:, methods:, file_methods:)` /
71
- `narrowing_facts(methods:)`** the per-call-site return-type
72
- and flow-narrowing contribution surface (ADR-37 Slice 2).
73
- A `dynamic_return` block names the inferred return type at a
74
- matching call site; the analyzer's dispatcher merges the
75
- contribution and uses it as if it were RBS-declared. A
76
- `narrowing_facts` block contributes branch-narrowing facts.
77
- (`narrowing_facts` was renamed from `type_specifier` in ADR-80;
78
- `type_specifier` remains as a deprecating alias removed in 0.3.0,
79
- so use `narrowing_facts` in new plugins.)
80
- (These replaced the removed `flow_contribution_for` hook
81
- ADR-52 WD3; a plugin that still defines it raises at load.)
82
- 3. **`Plugin::IoBoundary#read_file`** / **`#open_url`**
83
- sandboxed file and (since v0.1.2) HTTPS reads under the
84
- active `TrustPolicy`. Use this when the plugin needs to
85
- read project files (route tables, schemas, locale files)
86
- or fetch a stable URL.
87
- 4. **`Plugin::Base.producer` + `#cache_for`** — plugin-side
88
- cache producers. Use these for parses / lookups expensive
89
- enough to want cross-run caching. Auto-invalidates on
90
- the digest of every file (and content hash of every URL)
91
- the IoBoundary read while building the result.
92
- 5. **`Plugin::FactStore` + `#prepare(services)`** — the
93
- cross-plugin fact-publication surface (v0.1.1 Track 2,
94
- ADR-9). Plugins publish facts in `prepare`; downstream
95
- plugins consume them through `services.fact_store` so
96
- producer-side parsing (e.g., `config/routes.rb`) can be
97
- reused by every consumer (controller-side validators,
98
- factory-side validators, …).
99
-
100
- Several worked examples (`rigor-lisp-eval`, `rigor-pattern`,
101
- `rigor-units`, `rigor-activerecord`) contribute a narrowed
102
- return type via `dynamic_return` rather than only emitting
103
- diagnostics, so chained calls on plugin-typed values resolve
104
- through the analyzer's normal dispatch rather than the
105
- RBS-level `untyped` envelope. See the per-plugin README for
106
- which surface each one demonstrates.
107
-
108
- ## Macro / DSL expansion substrate (ADR-16)
109
-
110
- A second authoring path was added on top of the hand-rolled
111
- walker contract above: the **macro expansion substrate**
112
- (ADR-16). For metaprogramming-heavy DSLs — Rails-style
113
- `has_one_attached`, dry-struct's `attribute`, Devise's
114
- `devise :strategy`, Sinatra's `get '/foo' do ... end` — the
115
- substrate lets a plugin author **declare** the call shape
116
- instead of walking the AST by hand. The plugin's body becomes
117
- a single manifest entry; the substrate handles literal-symbol
118
- extraction, name interpolation, registry lookup, and per-method
119
- synthesis.
120
-
121
- Four tier shapes are recognised. The
122
- [per-library survey](../notes/20260515-macro-expansion-library-survey.md)
123
- identifies which libraries fit each tier and which fall
124
- outside the substrate's scope.
125
-
126
- | Tier | Shape | Manifest declaration | Worked example |
127
- | --- | --- | --- | --- |
128
- | **A — block-as-method** | DSL call's block runs as an instance method on the receiver class (`Sinatra::Base#generate_method`) | `block_as_methods: [Macro::BlockAsMethod.new(receiver_constraint:, method_names:)]` | [`rigor-sinatra`](../../plugins/rigor-sinatra/) |
129
- | **B — trait-inlining registry** | Class-level call enumerates symbols → bundled registry maps each to a module → substrate explodes the module's RBS methods onto the calling class | `trait_registries: [Macro::TraitRegistry.new(receiver_constraint:, method_name:, modules_by_symbol:, always_included:)]` | [`rigor-devise`](../../plugins/rigor-devise/) |
130
- | **C — heredoc template** | Class-level call interpolates a literal symbol into a method-name template; substrate emits synthetic readers | `heredoc_templates: [Macro::HeredocTemplate.new(receiver_constraint:, method_name:, symbol_arg_position:, emit:)]` | [`rigor-dry-struct`](../../plugins/rigor-dry-struct/) |
131
-
132
- The three Tier-A/B/C plugins above are each ~60–110 LoC of
133
- **purely declarative** Ruby — no walker, no
134
- `diagnostics_for_file`, no plugin-side state. The substrate's
135
- pre-pass + dispatcher integration do the work.
136
-
137
- ### Concern re-targeting
138
-
139
- `ActiveSupport::Concern.included do ... end` is a *deferred
140
- class_eval*: any DSL calls inside the block fire on whoever
141
- includes the concern, not on the concern module itself. The
142
- substrate's scanner handles this re-targeting automatically.
143
- For source like:
144
-
145
- ```ruby
146
- module Auditable
147
- extend ActiveSupport::Concern
148
- included do
149
- attribute :audited_at, Types::Time
150
- end
151
- end
152
-
153
- class Address < Dry::Struct
154
- include Auditable
155
- attribute :city, Types::String
156
- end
157
- ```
158
-
159
- `Address` gets BOTH `city` (direct) AND `audited_at` (re-targeted
160
- from `Auditable`) as synthetic readers. The same pattern works
161
- for Tier B traits (Devise modules included via Concerns).
162
-
163
- ### Floor / ceiling
164
-
165
- Per ADR-16 § WD13, the **floor** is that synthetic methods emit
166
- by NAME so cross-file dispatch resolves (no more
167
- `call.undefined-method`). The common cases also recover precise
168
- return types: **Tier B** redispatches on the origin module's
169
- authored RBS (a Devise `valid_password?` resolves to `bool`,
170
- not `Dynamic[T]`), and **Tier C** resolves a plain class-name
171
- return to its `Nominal`. What still degrades to `Dynamic[T]` is
172
- the parameterised / utility-type-shaped Tier C return
173
- (`Array[String]`, `Pick<T, K>`); routing those through the
174
- [ADR-13](../adr/13-typenode-resolver-plugin.md) resolver chain
175
- is the **ceiling**, demand-driven. The substrate never
176
- *fabricates* precision per ADR-5 robustness.
177
-
178
- ### Choosing between the substrate and a hand-rolled walker
179
-
180
- | If the DSL is… | Use the substrate | Use a hand-rolled walker |
181
- | --- | --- | --- |
182
- | `class-level call with literal symbol args + framework class_eval'd heredoc` | ✓ Tier C | — |
183
- | `class-level call with literal symbol args + registry-driven module include` | ✓ Tier B | — |
184
- | `class-level call with do…end block running as an instance method` | ✓ Tier A | — |
185
- | `external Ruby files instance_eval'd under a declared self` | ✓ Tier D (contract only as of v0.1.x) | — |
186
- | `domain DSL whose return type depends on argument shape` | — | `dynamic_return` ([`rigor-lisp-eval`](../../examples/rigor-lisp-eval/)) |
187
- | `cross-file validation (collect declarations, then validate uses)` | — | Two-pass walker ([`rigor-statesman`](../../plugins/rigor-statesman/)) |
188
- | `parsing an external project file (routes, schema, locale)` | — | `IoBoundary` + cache producer ([`rigor-routes`](../../examples/rigor-routes/)) |
189
- | `schema-graph recorder (GraphQL-Ruby-style)` | — | Schema-resolution pass (no plugin authored yet) |
190
-
191
- The substrate and the hand-rolled walker contract coexist —
192
- a plugin can mix `manifest`-declared substrate entries with a
193
- `diagnostics_for_file` walker. The
194
- [`skills/rigor-plugin-author/SKILL.md`](../../skills/rigor-plugin-author/SKILL.md)
195
- SKILL captures the decision flow in detail; the survey at
196
- [`docs/notes/20260515-macro-expansion-library-survey.md`](../notes/20260515-macro-expansion-library-survey.md)
197
- records which Ruby libraries the substrate covers and which
198
- fall outside.
60
+ > there." What follows is for when it is "yes."
61
+
62
+ The decision that shapes everything else is *which* of the two
63
+ authoring paths your DSL falls into.
64
+
65
+ **Declare it.** If the DSL is a class-level call with literal
66
+ symbol arguments — a Rails-style `has_one_attached`, a
67
+ dry-struct `attribute`, a Devise `devise :strategy`, a Sinatra
68
+ `get "/foo" do end` the **macro-expansion substrate**
69
+ ([ADR-16](../adr/16-macro-expansion.md)) already knows that
70
+ shape. You write a manifest entry describing the call, and the
71
+ substrate does the literal-symbol extraction, the name
72
+ interpolation and the per-method synthesis. The three bundled
73
+ plugins on this path are 60–110 lines of declarative Ruby with
74
+ no AST walking at all. The substrate also understands
75
+ `ActiveSupport::Concern`'s deferred `included do end` block,
76
+ so a DSL call written inside a concern lands on the class that
77
+ includes it rather than on the concern.
78
+
79
+ **Walk it.** If the type depends on something the shape of the
80
+ call cannot tell you argument *values* (`Lisp.eval` above), a
81
+ declaration made elsewhere in the project, or the contents of
82
+ an external file such as a route table or a schema dump you
83
+ write a walker instead, and the plugin contract gives you the
84
+ hooks for it: a per-file emission pass, per-call-site
85
+ return-type and flow-narrowing contributions, sandboxed file
86
+ and HTTPS reads under a trust policy, cached producers for
87
+ expensive parses, and a cross-plugin fact store so one plugin's
88
+ parse feeds another plugin's checks.
89
+
90
+ The two paths coexist one plugin can declare substrate
91
+ entries *and* walk files and where you go next depends on
92
+ which of them you need:
93
+
94
+ - [`examples/README.md`](../../examples/README.md) the six
95
+ walkthroughs, each spotlighting one contract surface, with a
96
+ map of which example demonstrates which one.
97
+ - [`docs/internal-spec/plugin.md`](../internal-spec/plugin.md)
98
+ the binding plugin contract: manifest, hooks, services,
99
+ registry, load order. Its siblings
100
+ [`plugin-trust.md`](../internal-spec/plugin-trust.md) and
101
+ [`plugin-cache-producers.md`](../internal-spec/plugin-cache-producers.md)
102
+ cover the I/O and caching surfaces.
103
+ - [`docs/internal-spec/macro-substrate.md`](../internal-spec/macro-substrate.md)
104
+ the substrate's tiers, the manifest field each one
105
+ declares, and how much return-type precision each recovers.
106
+ - [The macro-expansion library survey](../notes/20260515-macro-expansion-library-survey.md)
107
+ which real Ruby libraries fit which tier, and which fall
108
+ outside the substrate entirely.
199
109
 
200
110
  ## Should you write one?
201
111
 
@@ -56,9 +56,11 @@ up the flag, key, or command that *acts* on it.
56
56
  how to nudge it through `.rbs` files and `%a{rigor:v1:…}`
57
57
  directives.
58
58
  8. [**Understanding errors**](08-understanding-errors.md) —
59
- the rule catalogue (`call.undefined-method`,
60
- `call.argument-type-mismatch`, `flow.always-raises`, …),
61
- severity profiles, and `# rigor:disable` suppression.
59
+ reading a diagnostic: what each rule family claims, why one
60
+ fires when you did not expect it (and stays silent when you
61
+ did), and which layer to reach for when you want it quieter.
62
+ The catalogue itself is
63
+ [manual — Diagnostics](../manual/04-diagnostics.md).
62
64
  9. [**Plugins**](09-plugins.md) — when to author one,
63
65
  pointer to the [examples/](../../examples/README.md)
64
66
  landing page.
@@ -571,8 +571,10 @@ model"](appendix-type-theory.md#what-rigor-does-not-model) records.
571
571
  authoring the declared contracts `def.return-type-mismatch` checks
572
572
  against.
573
573
  - [Chapter 8 — Understanding errors](08-understanding-errors.md) for
574
- the `def.return-type-mismatch` / `call.argument-type-mismatch`
575
- rules and severity profiles.
574
+ what the `def.*` family claims, and
575
+ [manual Diagnostics](../manual/04-diagnostics.md) for the
576
+ `def.return-type-mismatch` / `call.argument-type-mismatch`
577
+ entries and the severity profiles.
576
578
 
577
579
  If you want to compare against another *tool* rather than the
578
580
  *principle*, the sibling appendices cover
@@ -72,8 +72,10 @@ in another tab, almost every advanced refinement transfers.
72
72
  ## The `@phpstan-assert` family
73
73
 
74
74
  PHPStan's assertion-narrowing PHPDoc tags map directly onto
75
- Rigor's `RBS::Extended` directive grammar. Chapter 7 covers
76
- the table in depth; here it is again for reference:
75
+ Rigor's `RBS::Extended` directive grammar. This is the mapping;
76
+ [Chapter 7](07-rbs-and-extended.md) works the Rigor side
77
+ through examples, and the directive reference itself is
78
+ [manual — RBS::Extended annotations](../manual/16-rbs-extended-annotations.md):
77
79
 
78
80
  | PHPStan PHPDoc | Rigor RBS::Extended | Effect |
79
81
  | --- | --- | --- |
@@ -149,7 +151,8 @@ Rigor has two baseline mechanisms: a **managed** baseline
149
151
  the next `rigor check` via the `baseline:` config key — the
150
152
  closest match to PHPStan's `--baseline`), and a **lightweight**
151
153
  ad-hoc snapshot (`rigor diff` over a `--format=json` dump).
152
- Chapter 8 has the walkthrough.
154
+ [Manual Baselines](../manual/06-baseline.md) walks through
155
+ both.
153
156
 
154
157
  The `includes:` semantics also match PHPStan's: declaration
155
158
  order, later overrides earlier, the current file's keys win
@@ -322,11 +325,15 @@ You probably do not need to read the rest of this appendix
322
325
  section sequentially. Three useful pointers:
323
326
 
324
327
  - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
325
- has the full directive grammar, including the PHPStan-mapping
326
- table that this page summarises.
328
+ works the directive grammar through examples; the reference
329
+ table is
330
+ [manual — RBS::Extended annotations](../manual/16-rbs-extended-annotations.md).
327
331
  - [Chapter 8 — Understanding errors](08-understanding-errors.md)
328
- covers the rule catalogue, severity profiles, baseline
329
- diffing every PHPStan onboarding analogue.
332
+ explains how to read a diagnostic and which knob to reach
333
+ for; the catalogue, severity profiles and baselines — every
334
+ PHPStan onboarding analogue — are
335
+ [manual — Diagnostics](../manual/04-diagnostics.md) and
336
+ [manual — Baselines](../manual/06-baseline.md).
330
337
  - [Chapter 9 — Plugins](09-plugins.md) for the
331
338
  Type-Specifying / Dynamic-Return analogues.
332
339
 
@@ -321,8 +321,10 @@ section sequentially. Three useful pointers:
321
321
  if you want to see how the directive grammar layers on top
322
322
  of the RBS you already write.
323
323
  - [Chapter 8 — Understanding errors](08-understanding-errors.md)
324
- for the rule catalogue, severity profiles, and baseline
325
- diffingthe analogue to Steep's diagnostic config.
324
+ for how to read a diagnostic, and
325
+ [manualDiagnostics](../manual/04-diagnostics.md) for the
326
+ rule catalogue and severity profiles — the analogue to
327
+ Steep's diagnostic config.
326
328
  - [`docs/notes/20260503-steep-cross-check-triage.md`](../notes/20260503-steep-cross-check-triage.md)
327
329
  for a worked side-by-side run of Steep and Rigor on the
328
330
  same project (the project itself).
@@ -1651,7 +1651,9 @@ practical companion:
1651
1651
  for the directive grammar that lets you teach Rigor about a
1652
1652
  custom predicate.
1653
1653
  - [Chapter 8 — Understanding errors](08-understanding-errors.md)
1654
- for the rule catalogue (the user-visible end of the trinary
1654
+ for how a diagnostic reads, and
1655
+ [manual — Diagnostics](../manual/04-diagnostics.md) for the
1656
+ rule catalogue (the user-visible end of the trinary
1655
1657
  certainty).
1656
1658
 
1657
1659
  If you want to compare against another *tool* rather than the
@@ -48,11 +48,38 @@ the `paths:` list from the configuration file.
48
48
  | `--treat-all-as-inline-rbs` | Force-load `rigor-rbs-inline` with `require_magic_comment: false`, so every analysed file is treated as inline-RBS without the `# rbs_inline: enabled` comment (ADR-32). |
49
49
  | `--bleeding-edge[=ids]` | Adopt the bleeding-edge overlay for this run, overriding the configured [`bleeding_edge:`](03-configuration.md) selection (ADR-50 § WD2). Bare adopts every queued feature; `--bleeding-edge=a,b` adopts only the named feature ids. Inspect it with [`rigor show-bleedingedge`](#rigor-show-bleedingedge). |
50
50
  | `--no-bleeding-edge` | Ignore any configured `bleeding_edge:` selection for this run (adopt none). |
51
- | `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. |
51
+ | `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. Alone, only the buffer's own file produces diagnostics; add `--incremental` for whole-project scope (see below). |
52
52
 
53
53
  Exit `0` when no error-severity diagnostics remain, `1` when
54
54
  any are reported, `64` on a usage error.
55
55
 
56
+ ### Editor mode scope
57
+
58
+ `--tmp-file` / `--instead-of` on their own analyse **only** the
59
+ buffer's file: fast, and blind to what the unsaved edit does to the
60
+ rest of the project.
61
+
62
+ Adding `--incremental` analyses the **whole project with the buffer
63
+ substituted** — the edited file and the files that depend on it are
64
+ re-analysed, everything else is served from the incremental snapshot.
65
+ An unsaved change to a method's return type therefore surfaces in its
66
+ callers, not just in the file being edited.
67
+
68
+ Two things to know:
69
+
70
+ - It needs a snapshot to reuse. Run `rigor check --incremental` once
71
+ (it is also what a normal terminal check should use) and every later
72
+ editor invocation gets whole-project scope. Without one, Rigor says
73
+ so on stderr and analyses the buffer alone.
74
+ - An editor-mode run never writes the snapshot — the buffer's bytes
75
+ exist only in your editor, and recording them would make the next
76
+ ordinary run believe it had already analysed a file in a state it
77
+ was never in.
78
+
79
+ `--verify-incremental` refuses a buffer: it compares against a full
80
+ analysis of the files on disk, which a buffer contradicts by
81
+ construction.
82
+
56
83
  ## `rigor init`
57
84
 
58
85
  Write a starter configuration file.
@@ -333,7 +360,10 @@ the effectiveness ratio and `--format=json` carries `mode`,
333
360
  `killed`, `survived`, `effectiveness_ratio`, per-file rows, and
334
361
  `add_a_type_here`. It is the truth tier behind the static
335
362
  `--protection` proxy, at the cost of many analyses — an opt-in CI
336
- deep-dive, not an interactive check.
363
+ deep-dive, not an interactive check. `--workers=N` fork-parallelizes
364
+ this tier too (the whole-project pre-pass is paid once on the parent
365
+ and the per-file measurement is spread across workers), with the
366
+ same precedence chain and the same byte-identical output.
337
367
 
338
368
  ```sh
339
369
  rigor coverage --protection --mutation [paths]
@@ -352,7 +382,10 @@ cost is proportional to the protection hole. `--format=json`
352
382
  carries `mode` (`protection-fused`), `type_killed`,
353
383
  `test_killed`, `unprotected`, `protected_ratio`, per-file rows,
354
384
  and `add_protection_here`; `--threshold` gates on the fused
355
- ratio.
385
+ ratio. This tier is always sequential — the suite hook shells out
386
+ to your test runner, and concurrent runs would race over one
387
+ working tree — so `--workers` does not apply and an explicit one
388
+ is reported as ignored on stderr.
356
389
 
357
390
  `--test-command=CMD` is the runner hook (default
358
391
  `bundle exec rake`). The suite must pass on clean code first, or
@@ -378,6 +411,18 @@ to a deterministic sample of `N` mutations per file, bounding the
378
411
  cost on large files. Per-file ratios then become estimates, noted
379
412
  on stderr so `--format=json` stdout stays clean.
380
413
 
414
+ A re-run is served from a per-file measurement cache while
415
+ nothing that could change a file's number has moved: the file
416
+ itself, any file it was recorded as reading from, the resolved
417
+ configuration, `sig/`, the gem set, the engine version, `--limit`
418
+ / `--seed`, and the adopted bleeding-edge features. It reads the
419
+ cross-file edges a `rigor check --incremental` run records, so
420
+ warm one once per project; without a usable snapshot every file
421
+ is measured, never silently served. `--no-cache` measures
422
+ everything from scratch. A one-line stderr report says which of
423
+ those happened — see
424
+ [Type-protection coverage](15-type-protection-coverage.md).
425
+
381
426
  ```sh
382
427
  rigor coverage --protection --mutation --with-tests \
383
428
  --test-command "bundle exec rspec" --include-dynamic [paths]
@@ -479,6 +524,7 @@ so a skill can never be shadowed by a verb.
479
524
 
480
525
  ```sh
481
526
  rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
527
+ rigor skill describe [--deep]
482
528
  ```
483
529
 
484
530
  | Form | Purpose |
@@ -488,6 +534,33 @@ rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
488
534
  | `--full <name>` | Print the `SKILL.md` body **followed by every `references/*.md` inline** — the complete, version-current procedure in one call. This is what a skill's "First: load the version-current copy" directive points at, so a copy vendored into a project (e.g. via `npx skills add`) re-fetches its current steps from the installed gem instead of following a frozen copy. |
489
535
  | `--path <name>` | Print the single-line absolute `SKILL.md` path, suitable as input to a file-reading tool. |
490
536
  | `--describe` | Probe the project's state (config / baseline / `sig/` / CI — presence only, never runs `rigor check`) and recommend the next skill to run. Also spelled `describe`; surfaced top-level as [`rigor describe`](#rigor-describe) below. |
537
+ | `describe --deep` | The same report, except it **runs `rigor check` first** and routes the headline recommendation on the result. Opt-in, because it costs a full analysis and writes `.rigor/cache` — the un-flagged form stays presence-only and side-effect-free. |
538
+
539
+ ### `describe --deep`
540
+
541
+ By default the recommendation comes from a presence-only probe, so it can
542
+ tell that a project *has* a config but not whether the analysis is
543
+ healthy. `--deep` runs the check for you and lets the result pick the
544
+ headline, using the same routing the `## For the agent` section already
545
+ teaches:
546
+
547
+ | Deep check result | Headline becomes |
548
+ | --- | --- |
549
+ | RBS environment built to 0 classes, or a `configuration-error` diagnostic | `rigor-doctor` — the analysis is hollow until the setup is fixed. |
550
+ | Call sites resolving to the project's own definitions (reopened core / gem classes), as at least a third of the errors | `rigor-monkeypatch-resolve` — list them in `pre_eval:` and they clear wholesale. |
551
+ | Any remaining error diagnostics | `rigor-baseline-reduce`. Proven monkey-patch sites below that share are still reported here, so the finding survives even when the headline stays on the larger problem. |
552
+ | Clean, or the project has no config yet | Unchanged — the presence-only recommendation stands. |
553
+
554
+ If the check **cannot run at all** (no config, an unloadable plugin, a
555
+ malformed config) `--deep` does not fail and does not pretend the project
556
+ is clean: it reports what went wrong, falls back to the presence-only
557
+ recommendation, and points you at `rigor doctor`. It deliberately does
558
+ *not* route on weaker signals — "framework calls typing as `Dynamic`" is
559
+ still a judgement call left to you and your agent.
560
+
561
+ ```sh
562
+ rigor skill describe --deep # also: rigor describe --deep
563
+ ```
491
564
 
492
565
  The verb spellings `rigor skill list` / `print <name>` / `path <name>`
493
566
  were **removed in v0.3.0** — the positional slot is a skill name, so
@@ -511,6 +584,10 @@ It reports a presence-only project-state probe (does a `.rigor.yml`,
511
584
  recommended next skill. It is read-only and side-effect-free — it never
512
585
  runs `rigor check`. Identical output to `rigor skill describe`.
513
586
 
587
+ `rigor describe --deep` forwards to
588
+ [`rigor skill describe --deep`](#rigor-skill), which opts into running the
589
+ check first — slow, and it writes the cache.
590
+
514
591
  ## `rigor docs`
515
592
 
516
593
  Print the documentation bundled inside the `rigortype` gem
@@ -546,7 +623,7 @@ pages from the installed gem with no HTTP request.
546
623
  ## `rigor show-bleedingedge`
547
624
 
548
625
  Print the **bleeding-edge overlay** — the Rigor-maintained set of
549
- the next major's queued diagnostic disciplines ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
626
+ the next major's queued changes ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
550
627
  § WD2) — and report which of them the project's
551
628
  [`bleeding_edge:`](03-configuration.md) configuration adopts. Read-only:
552
629
  it loads `.rigor.yml` to resolve the active selection but runs no
@@ -561,16 +638,31 @@ rigor show-bleedingedge [--config PATH] [--format text|json]
561
638
  | `--config PATH` | Use this `.rigor.yml` instead of auto-discovery. |
562
639
  | `--format text\|json` | Output format. Default `text`. |
563
640
 
564
- Each queued feature appears with its stable id, the severity it imposes,
565
- and whether your configuration adopts it. See
566
- [`docs/compatibility.md`](../compatibility.md) for how bleeding-edge fits
567
- the stability model.
641
+ Each queued feature appears with its stable id, its **kind** `severity`
642
+ or `behaviour` — and whether your configuration adopts it. A `severity`
643
+ feature also prints the rule → severity diff it imposes; a `behaviour`
644
+ feature changes a measurement, an algorithm, or a default without moving
645
+ any rule's severity, so it has no such diff and its summary is the whole
646
+ description. See [`docs/compatibility.md`](../compatibility.md) for how
647
+ bleeding-edge fits the stability model.
568
648
 
569
649
  Queued today:
570
650
 
571
- | Feature id | What it changes |
572
- | --- | --- |
573
- | `reject-unparseable-signatures` | An unparseable `.rbs` under `signature_paths:` **fails the run** (`rbs.coverage.quarantined-signature` → `error`) instead of being skipped with a warning. |
651
+ | Feature id | Kind | What it changes |
652
+ | --- | --- | --- |
653
+ | `reject-unparseable-signatures` | severity | An unparseable `.rbs` under `signature_paths:` **fails the run** (`rbs.coverage.quarantined-signature` → `error`) instead of being skipped with a warning. |
654
+ | `use-of-void-value` | severity | Using a value recovered from an author-declared `-> void` return in value context is reported as `static.value-use.void` (`warning`). |
655
+ | `discovery-seeded-mutation-sites` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) measures against the same cross-file project discovery Tier 1 already uses — both when picking the sites and when deciding whether a breakage was caught — so a call on a project class declared in a *sibling* file is measured instead of dropped, and a breakage there can actually be caught. **Adds sites to the denominator, so the reported effectiveness ratio moves** — check it against any `--threshold` you pin in CI before adopting. |
656
+ | `dependent-closure-kill-oracle` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) counts a breakage as caught when the diagnostic appears anywhere in the mutated file **or the files that depend on it**, instead of in the mutated file alone — so changing what a method returns counts as caught when the error lands in its callers. Can only **add** kills, so the ratio moves up or not at all; it costs about a third more wall time per mutant, and a ratio measured under it is not comparable with one measured without it. |
657
+
658
+ Once a feature **graduates** — it becomes the default at a major
659
+ ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD7)
660
+ — it leaves the queued list and appears under `Graduated`, an
661
+ acknowledgement that naming it in `bleeding_edge:` no longer does
662
+ anything: the behaviour is on for everyone. The section is absent while
663
+ nothing has graduated. In `--format json` the same information is the
664
+ `graduated` array, alongside `overlay` (every queued feature, each with
665
+ its `kind`), `active`, and `unknown_selected`.
574
666
 
575
667
  ## `rigor doctor`
576
668
 
@@ -116,7 +116,22 @@ statically and may resolve at run time. The same findings appear in the
116
116
  | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
117
117
  | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
118
118
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
119
- | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued diagnostic disciplines early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
119
+ | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued changes early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
120
+
121
+ A queued feature is one of two kinds, and `bleeding_edge:` selects both the
122
+ same way:
123
+
124
+ - A **severity** feature promotes one or more rules — a discipline Rigor
125
+ already reports quietly becomes an error or a warning. Its diff is a list
126
+ of rule ids, printed by `rigor show-bleedingedge`.
127
+ - A **behaviour** feature changes a measurement, an algorithm, or a
128
+ default, and moves no rule's severity. There is no rule-id diff to read,
129
+ so its summary in `rigor show-bleedingedge` is the whole description of
130
+ what adopting it does.
131
+
132
+ An id you name that this version of Rigor does not know is ignored rather
133
+ than rejected, so a shared `.rigor.yml` can name a feature that only some
134
+ of the versions in use have queued.
120
135
 
121
136
  ### Dependency RBS discovery
122
137
 
@@ -97,7 +97,21 @@ re-stamps it for the run. Three profiles, set with the
97
97
  | --- | --- |
98
98
  | `lenient` | Only proven diagnostics are errors; uncertain ones drop to `warning` / `info`. For incremental adoption on legacy code. |
99
99
  | `balanced` *(default)* | Most rules `error`; `dump.type` `info`; uncertain rules `warning`. |
100
- | `strict` | Nearly every rule is an `error` the exceptions are `call.self-undefined-method` (stays `off`, opt-in only) and `flow.unreachable-clause` (`warning`, pending its false-positive gate). CI-friendly. |
100
+ | `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method` and `static.value-use.void` stay `off` (both opt-in only), `flow.unreachable-clause` stays `warning` pending its false-positive gate, and the three `suppression.*` rules stay `warning` — a stale suppression comment is worth telling you about, but it is not a reason to fail a build. CI-friendly. |
101
+
102
+ Under `balanced`, the rules that do **not** emit as `error`
103
+ are:
104
+
105
+ | Severity | Rules |
106
+ | --- | --- |
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` |
109
+ | `off` | `call.self-undefined-method`, `static.value-use.void` |
110
+
111
+ Everything else emits as `error`. For one rule under all three
112
+ profiles, `rigor explain <rule>` prints `Authored severity:` and
113
+ `Severity by profile:` — that output is generated from the rule
114
+ catalogue itself, so it is the per-rule source of truth.
101
115
 
102
116
  For finer control, `severity_overrides:` maps a rule ID or a
103
117
  family to one of `error`, `warning`, `info`, or `off`:
@@ -109,7 +123,15 @@ severity_overrides:
109
123
  call: warning
110
124
  ```
111
125
 
112
- A rule-specific override beats a family override.
126
+ A rule-specific override beats a family override. `off` drops
127
+ the diagnostic from the result entirely, which makes
128
+ `severity_overrides:` the lighter-touch sibling of `disable:`
129
+ below — both silence a rule; the override reads as "this one
130
+ rule, at this severity" alongside the rest of the profile.
131
+
132
+ YAML reserves the bareword `off` as a boolean. If an override
133
+ that names it seems not to apply, quote it — `"off"` — and the
134
+ same for `on`.
113
135
 
114
136
  ## Machine-readable output (`--format json`)
115
137
 
@@ -198,7 +220,10 @@ config.merge(extra) # rigor:disable call.undefined-method
198
220
  ```
199
221
 
200
222
  It accepts qualified IDs, family wildcards (`call`), a
201
- comma- or space-separated list, or `all`.
223
+ comma- or space-separated list, or `all`. The comment must sit
224
+ on the line the diagnostic points at; there is no
225
+ `disable-block` form, so an expression spread over several
226
+ lines needs the comment on each line that fires.
202
227
 
203
228
  A marker that cannot work is flagged rather than silently
204
229
  ignored: a token that names no known rule (a typo like
@@ -217,7 +242,14 @@ suppressible like any other rule.
217
242
 
218
243
  **In-source, whole file.** `# rigor:disable-file <rules>`
219
244
  anywhere in a file suppresses those rules for every line;
220
- `# rigor:disable-file all` silences the file.
245
+ `# rigor:disable-file all` silences the file. Convention is to
246
+ put it near the top — typically on a generated file, a fixture,
247
+ or a vendored snippet — but every comment in the file is
248
+ scanned, so any placement works.
249
+
250
+ The three layers **compose**: a file-scope marker does not
251
+ cancel a line-scope one, and a project-wide `disable:` still
252
+ applies to a file that carries neither.
221
253
 
222
254
  **Project-wide.** The `disable:` config key turns rules off
223
255
  across the whole run: