rigortype 0.4.2 → 0.4.3

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/handbook/11-sig-gen.md +12 -5
  4. data/docs/manual/02-cli-reference.md +19 -1
  5. data/docs/manual/03-configuration.md +6 -4
  6. data/docs/manual/05-inspecting-types.md +50 -0
  7. data/docs/manual/12-caching.md +18 -1
  8. data/docs/manual/14-rails-quickstart.md +2 -2
  9. data/docs/manual/18-removing-dead-code.md +16 -9
  10. data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
  11. data/lib/rigor/analysis/check_rules.rb +23 -22
  12. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  13. data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
  14. data/lib/rigor/analysis/incremental_session.rb +89 -22
  15. data/lib/rigor/analysis/path_expansion.rb +62 -6
  16. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  17. data/lib/rigor/analysis/reachability/graph.rb +119 -15
  18. data/lib/rigor/analysis/reachability/scan.rb +332 -173
  19. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  20. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  21. data/lib/rigor/analysis/run_stats.rb +28 -6
  22. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
  23. data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
  24. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  25. data/lib/rigor/analysis/runner.rb +29 -11
  26. data/lib/rigor/analysis/worker_session.rb +1 -1
  27. data/lib/rigor/cache/engine_source.rb +21 -7
  28. data/lib/rigor/cache/incremental_snapshot.rb +36 -8
  29. data/lib/rigor/cli/check_command.rb +138 -39
  30. data/lib/rigor/cli/docs_command.rb +5 -4
  31. data/lib/rigor/cli/lens_command.rb +144 -0
  32. data/lib/rigor/cli/lsp_command.rb +5 -2
  33. data/lib/rigor/cli/skill_command.rb +3 -3
  34. data/lib/rigor/cli/skill_describe.rb +1 -1
  35. data/lib/rigor/cli/type_of_command.rb +2 -1
  36. data/lib/rigor/cli/unused_command.rb +157 -24
  37. data/lib/rigor/cli.rb +30 -4
  38. data/lib/rigor/configuration.rb +15 -0
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  40. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  41. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  42. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  43. data/lib/rigor/environment/rbs_loader.rb +33 -0
  44. data/lib/rigor/environment/required_features.rb +4 -2
  45. data/lib/rigor/environment.rb +12 -5
  46. data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
  47. data/lib/rigor/inference/captured_locals.rb +43 -3
  48. data/lib/rigor/inference/expression_typer.rb +58 -25
  49. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
  50. data/lib/rigor/inference/match_rebinding.rb +1 -1
  51. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  52. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  53. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  54. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  55. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  56. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
  57. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  58. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  59. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  60. data/lib/rigor/inference/method_dispatcher.rb +52 -3
  61. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  62. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  63. data/lib/rigor/inference/project_patched_scanner.rb +38 -24
  64. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  65. data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
  66. data/lib/rigor/inference/refined_dispatch.rb +62 -14
  67. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  68. data/lib/rigor/inference/scope_indexer.rb +140 -5
  69. data/lib/rigor/inference/statement_evaluator.rb +223 -31
  70. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  71. data/lib/rigor/language_server/project_context.rb +56 -5
  72. data/lib/rigor/lens/anchor.rb +115 -0
  73. data/lib/rigor/lens/contract_reader.rb +118 -0
  74. data/lib/rigor/lens/document.rb +113 -0
  75. data/lib/rigor/lens/file_reader.rb +433 -0
  76. data/lib/rigor/lens/members.rb +153 -0
  77. data/lib/rigor/lens/parameters.rb +91 -0
  78. data/lib/rigor/lens/plugins.rb +40 -0
  79. data/lib/rigor/lens/run.rb +280 -0
  80. data/lib/rigor/lens/slot.rb +79 -0
  81. data/lib/rigor/lens.rb +40 -0
  82. data/lib/rigor/project_environment.rb +20 -1
  83. data/lib/rigor/protection/mutation_cache.rb +2 -1
  84. data/lib/rigor/scope.rb +22 -11
  85. data/lib/rigor/sig_gen/generator.rb +45 -5
  86. data/lib/rigor/version.rb +1 -1
  87. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  88. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  89. data/sig/rigor/scope.rbs +3 -0
  90. data/sig/rigor.rbs +6 -3
  91. data/skills/rigor-project-init/references/02-configure.md +2 -2
  92. metadata +18 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a812e377e347e70448e42c33e6f4fe79152d40851b999609e9e5fdd98dc5a5a0
4
- data.tar.gz: 285b195c8058fa30063c5ad40ac6718cd59a3f1d79bb79ccac32184093dba5b0
3
+ metadata.gz: b20cef50f4d076f1afefb8c20563cd21a95a56b01bf4db3257371adfe7c64d5d
4
+ data.tar.gz: 9c4f5f8825106ece96fb2ac00dc1ad4ef43f302368f1a12f87521c725530b924
5
5
  SHA512:
6
- metadata.gz: 148890acd58affcbc6eb81afe948d882cf254930ff7ad83635592a065bf038de149865891767318721bdc103e7def6e4c71686b8d40a8ff9b55034c446cf8d32
7
- data.tar.gz: e6d458ec387e004c83b8751e5381eda53ec5979433580263ea1586cbb574245875f50a94fce86540badae485d08d20ff73c7e3aea4d80d33c4226af3a4b7f584
6
+ metadata.gz: 4418a4a75a22d27038d0607caa5a79723470e68f9662da1aec0b79e8056a1b157f78cf85f83cb51f646aa22d2634b0d851bb9f0132a548f5104acdee2372d1d2
7
+ data.tar.gz: 026710fb13eebddf98776d6a9a3fb8e1738a7cc5fdd77c326afa0adb680c0760462d9e41b4aff57d1c41d1878fc05c41a8658be7b69ed4f265709aa27f6fc825
data/README.md CHANGED
@@ -232,7 +232,7 @@ rigor docs --list # list every bundled page
232
232
 
233
233
  ## Status
234
234
 
235
- Current release: **`v0.4.2`** (2026-10-11) — on the
235
+ Current release: **`v0.4.3`** (2026-10-15) — on the
236
236
  `0.4.x` line, continuing the evaluation era opened by `v0.2.0`, the first
237
237
  publicly-announced (general / evaluation) release. The line publishes
238
238
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -188,11 +188,11 @@ changes which calls the signature accepts:
188
188
  $ rigor sig-gen --diff lib/c.rb
189
189
  --- lib/c.rb: C#m
190
190
  - def m: (Integer x) -> untyped
191
- + def m: (Integer x) -> nil
191
+ + def m: (Integer x) -> Float
192
192
 
193
193
  --- lib/c.rb: C#n
194
194
  - def n: (?) -> untyped
195
- + def n: (?) -> nil
195
+ + def n: (?) -> Float
196
196
  ```
197
197
 
198
198
  The `-` line is the declaration the proposal replaces. A method
@@ -523,9 +523,16 @@ Neither does a declaration the body proves as a literal.
523
523
  -> String` whose body is the string `"bot"`: the declared
524
524
  type is your abstraction over that body, and pinning the
525
525
  literal would rewrite a contract every sibling class shares.
526
- A method no `.rbs` declares is unaffected — the literal is
527
- still the strictest thing the body proves, and that is what
528
- gets written.
526
+ A declared `untyped` that you wrote in `sig/` is treated
527
+ the same way for a value literal: an Integer, String or
528
+ Symbol the body proves is widened to its class (`-> 1`
529
+ becomes `-> Integer`, `-> ("S" | nil)` becomes
530
+ `-> (String | nil)`), while `nil`, `true`, `false` and a
531
+ computed `Float` are proposed as they are. An `untyped` that
532
+ only the inline reader synthesizes for a `def` you did not
533
+ annotate keeps the literal, as does a method no `.rbs`
534
+ declares — the literal is still the strictest thing the body
535
+ proves, and that is what gets written.
529
536
 
530
537
  A comment rather than a `%a{…}` annotation, for three
531
538
  reasons: it stays out of the `rigor:v1:` directive namespace
@@ -36,7 +36,7 @@ the `paths:` list from the configuration file.
36
36
  | `--explain` | Surface fail-soft fallback events as `info` diagnostics. |
37
37
  | `--no-cache` | Skip the persistent cache for this run. With `--incremental` it is a cold full analysis: the incremental snapshot and the run-result cache are neither read nor written, so the next plain `--incremental` run still uses the snapshot it had before. |
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. A run with nothing changed is answered from the cache without starting the analyzer. See [Caching](12-caching.md). |
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. |
39
+ | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics agree as a set and in the same order, then exit (0 on match, 1 on mismatch: the differing diagnostics for a set mismatch, or the first index where the order diverges for a same-set, different-order mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
40
40
  | `--clear-cache` | Delete the cache directory before running. |
41
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. |
@@ -171,6 +171,24 @@ the two builds against each other — but if you are chasing a
171
171
  disagreement between a probe and a `check` run, comparing
172
172
  against `rigor check --no-cache` removes that variable.
173
173
 
174
+ ## `rigor lens`
175
+
176
+ Map the declarations of one or more files with their types and
177
+ type provenance (experimental). See
178
+ [Inspecting inferred types](05-inspecting-types.md).
179
+
180
+ ```sh
181
+ rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE...
182
+ ```
183
+
184
+ A lens runs the same incremental analysis as
185
+ `rigor check --incremental` and reads and writes its cache.
186
+ `--workers` applies to a cold run. `--format=json` writes one
187
+ `lens/v0` document ([specification](../internal-spec/lens.md)).
188
+ Exit `0` when at least one file has a result, `1` when none
189
+ does, `64` on a usage error; diagnostics in the code never
190
+ change the exit status.
191
+
174
192
  ## `rigor trace`
175
193
 
176
194
  Replay HOW the engine typed a file, step by step, as a
@@ -19,8 +19,10 @@ To *layer* configs rather than replace, a config file can name
19
19
  a base with `includes:` (recursive). `--config=PATH` bypasses
20
20
  discovery entirely.
21
21
 
22
- All relative paths in a config file resolve against that
23
- file's own directory.
22
+ Relative paths in a config file resolve against that file's own directory, with three exceptions:
23
+ `bundler.bundle_path:`, `bundler.lockfile:` and `rbs_collection.lockfile:` resolve against the working directory
24
+ `rigor` runs from. They match only when you run `rigor` from the directory that holds the config file, which is
25
+ the usual case; with `--config path/to/.rigor.yml` from elsewhere, give these three as absolute paths.
24
26
  A leading `~/` (or a bare `~`) in a path setting expands to your home directory; `~name` is an ordinary
25
27
  directory called `~name`, not another user's home. This covers `paths:`, `signature_paths:`, `test_paths:`,
26
28
  `pre_eval:`, `plugins_io.allowed_paths:`, `includes:`, `bundler.bundle_path:`, `bundler.lockfile:` and
@@ -64,7 +66,7 @@ cache:
64
66
  | --- | --- | --- | --- |
65
67
  | `target_ruby` | String | `"4.0"` | The lowest Ruby version *your* project runs on — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. Every value selects the syntax Rigor parses. A version you set explicitly also turns on the Ruby-deprecation rules for that version ([`call.deprecated-ruby2-keywords`](04-diagnostics.md#rule-call-deprecated-ruby2-keywords) from `"4.1"`); the default and `"latest"` never do, which is why `rigor init` writes the key commented out. |
66
68
  | `paths` | Array | `["lib"]` | Directories or files to analyse. |
67
- | `exclude` | Array | `[]` | Glob patterns to skip. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
69
+ | `exclude` | Array | `[]` | Glob patterns to skip. A relative pattern is matched against the file's path relative to the directory of the config file in use (the working directory when no config file is used), so it excludes the same files whether the path came from `paths:` or from a command-line argument, and it never matches on a directory above that base (a file outside it is seen as `../…`, so `../shared/lib/gen/**` reaches a sibling directory that `paths:` names). A leading `./` is ignored, and a pattern starting with `.` (such as `.*`) also matches files outside the base, which appear as `../…`. A symlink to a file is matched only under its own name: the realpath pass resolves directories, not files. `*` and `**` also cross `/`, so `lib/*.rb` matches nested files too. An absolute pattern is matched against the absolute path. Patterns from an `includes:` file resolve against the top-level config's directory, not the included file's. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
68
70
  | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
69
71
  | `includes` | Array | `[]` | Other config files to layer underneath this one. |
70
72
  | `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
@@ -76,7 +78,7 @@ cache:
76
78
  | --- | --- | --- | --- |
77
79
  | `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
78
80
  | `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
79
- | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. |
81
+ | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. The walker does not follow `require` / `require_relative`, so list the files in load order: a method declared in two files takes the last-listed one, and a file that requires a later-listed one is still read in list order. |
80
82
  | `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
81
83
 
82
84
  ### Signature generation
@@ -89,6 +89,14 @@ array. Line queries add `line_enumerations` metadata with the
89
89
  shown and total expression counts. An exact position is the same
90
90
  query the editor integration answers on hover.
91
91
 
92
+ Inside a `rescue` or `ensure` clause, `type-of` and hover can
93
+ disagree with `rigor check`. `check` reads a variable that the
94
+ `begin` body assigns as untyped there, because the clause may run
95
+ after the body raised partway through. `type-of` shows the value
96
+ the variable had before the `begin`. In
97
+ `conn = nil; begin; conn = open_conn; work; rescue; conn.close; end`,
98
+ `type-of` reports `conn` as `nil` and `check` reports nothing.
99
+
92
100
  ## `rigor trace` — watch the inference happen
93
101
 
94
102
  Where `annotate` and `type-of` show the *answer*, `rigor trace
@@ -110,11 +118,53 @@ typer visits; the default keeps only the three teachable event
110
118
  kinds. The JSON stream is stable enough to build course
111
119
  material or figures from.
112
120
 
121
+ ## `rigor lens` — a file's declarations and where their types come from
122
+
123
+ `rigor lens` lists every class, module, method and attribute a
124
+ file declares, with the type each one has and where that type
125
+ came from: `sig/`, an inline `#:` / `@rbs` comment, a plugin, a
126
+ library, or Rigor's inference from the body. It is experimental:
127
+ the command and its `lens/v0` JSON may still change.
128
+
129
+ ```sh
130
+ rigor lens lib/shop.rb # text, for reading
131
+ rigor lens --format=json lib/*.rb # one JSON document
132
+ ```
133
+
134
+ ```text
135
+ lib/shop.rb xxh3-64:845f7426b6acfe12 (2 omitted)
136
+ 4:7c90 class Shop
137
+ 8:e37e Shop#label: (::Integer n) -> ::String [contract inline]
138
+ 14:ce93 Shop#total: () -> Integer [single_site inferred]
139
+ 16:182b Shop#fetch [contested; also lib/legacy_shop.rb:2]
140
+ omitted constant Shop::LIMIT (line 5)
141
+ ```
142
+
143
+ The answer is the one `rigor check` reaches. A lens runs the
144
+ same incremental analysis as `rigor check --incremental` and
145
+ shares its cache, so the first run costs a full check and later
146
+ runs reuse it. The diagnostics it reports for a file are the
147
+ ones `check` reports.
148
+
149
+ - A method Ruby could take from more than one definition (the
150
+ same method defined in two files) is `contested`. Rigor does
151
+ not guess which one runs, so it prints no signature for it.
152
+ - A declaration this version does not model yet (a constant, an
153
+ `alias`, a `define_method`) is listed as omitted, never
154
+ dropped.
155
+ - `LINE:HASH` is an anchor: it changes when the declaration or
156
+ the type comments above it change, and not when prose
157
+ comments do.
158
+
159
+ The JSON document is specified in
160
+ [the lens output contract](../internal-spec/lens.md).
161
+
113
162
  ## Which to reach for
114
163
 
115
164
  | You want… | Use |
116
165
  | --- | --- |
117
166
  | One expression's type, from the shell | `rigor type-of` |
167
+ | A file's declarations, their types and where each came from | `rigor lens` |
118
168
  | Every line of a file surveyed | `rigor annotate` |
119
169
  | The derivation replayed step by step | `rigor trace` |
120
170
  | A type printed mid-analysis, in context | `dump_type` |
@@ -184,7 +184,8 @@ file, which other files its analysis read from, so it knows
184
184
  exactly which files an edit can affect. A continuous-integration
185
185
  gate, `rigor check --verify-incremental`, asserts this on every
186
186
  build: it runs the incremental analyzer and a full analysis and
187
- fails if they disagree on a single diagnostic.
187
+ fails if they disagree on a single diagnostic, or list the same
188
+ diagnostics in a different order.
188
189
 
189
190
  Rigor is also precise about *what* an edit changed. Re-checking a
190
191
  file that many others depend on — a base class, a widely-used
@@ -259,6 +260,22 @@ version change does, or `--clear-cache`. So the shortcut can stay
259
260
  off indefinitely; run `rigor check --incremental --clear-cache`
260
261
  once to turn it back on.
261
262
 
263
+
264
+ A warm `--incremental` run says on stderr how far the edit spread,
265
+ for example `rigor: --incremental warm — re-analysed 3 of 1295 files
266
+ (1 changed), reused the rest`. The first count is the files the
267
+ recheck re-analysed, out of M, the analysed set: every project file
268
+ the run covers, or only the subset under a path argument such as
269
+ `rigor check --incremental lib`. The re-analysed files are M minus
270
+ those served from the snapshot. They include files that a change
271
+ reached without editing them, such as a file whose inferred
272
+ parameter types moved (ADR-67), not only the edited files and their
273
+ dependents. The parenthesis lists `changed`, and `added` or
274
+ `removed` when there are some. View templates from a plugin are not
275
+ in M. The recheck re-runs every template on every warm run, so they
276
+ are counted after the file clause, as `plus 2 view templates`. An
277
+ unchanged run reads `re-analysed 0 of 1295 files, reused all`.
278
+
262
279
  `--incremental` is most useful for fast local re-checks and CI
263
280
  on a changed branch.
264
281
 
@@ -185,8 +185,8 @@ paths:
185
185
  - lib
186
186
 
187
187
  exclude:
188
- - vendor
189
- - tmp
188
+ - "vendor/**"
189
+ - "tmp/**"
190
190
 
191
191
  plugins:
192
192
  # Rails core
@@ -116,8 +116,9 @@ gap in coverage rather than a finding about your code.
116
116
  rigor unused --entry-point='lib/cli.rb' --entry-point='lib/workers/**/*.rb'
117
117
  ```
118
118
 
119
- Repeatable. Each glob is matched against every declaration's path, and
120
- roots every declaration in a matching file.
119
+ Repeatable. Each glob is matched against every declaration's path as
120
+ written, relative to the project root, and relative to the directory
121
+ you run in, and roots every declaration in a matching file.
121
122
 
122
123
  **File-level references**, free. Code that runs on load — an
123
124
  initializer naming a class, a `.rake` task — roots what it names. This
@@ -162,9 +163,12 @@ section held 22 policy classes whose only remaining caller was
162
163
  `spec/policies/`. Those 22 were not adjudicated, so treat the section
163
164
  as high-signal rather than high-yield; the yield is unmeasured.
164
165
 
165
- "Test code" is decided by path, relative to the directory you run `rigor unused` in (run it from the
166
- project root): `spec/` and `test/` anywhere below it, `*_spec.rb` and `*_test.rb`, and the end-to-end
167
- trees `qa/`, `e2e/` and `features/` at that root. Tooling directories such as
166
+ "Test code" is decided by path, relative to the project root: the directory of the `.rigor.yml` in
167
+ use, or the directory you run `rigor unused` in when there is none or when the config sits below it
168
+ (`--config=ci/rigor.yml` from the project root). Running it from a subdirectory with `--config` reads
169
+ the same project, and references are read from the directory you run it in as well. A file counts
170
+ as test code when its path as written below either directory says so. The test paths are `spec/` and `test/` anywhere below the root,
171
+ `*_spec.rb` and `*_test.rb`, and the end-to-end trees `qa/`, `e2e/` and `features/` at the root. Tooling directories such as
168
172
  `rubocop/`, `keeps/`, `tooling/` and `scripts/` count as production, so a class used only by them is
169
173
  rooted rather than listed: deleting it would break the tooling.
170
174
 
@@ -257,10 +261,13 @@ can only bound the namespace, so everything under `Foo` is demoted;
257
261
  `"V#{version}"` is the start of a name, so `V1`, `V2_0` and everything
258
262
  under them are. `const_get` looks the name up from its receiver, so
259
263
  `const_get("V#{version}")` inside `class Migration` demotes
260
- `Migration::V1` — and the top-level `V*` names as well, unless the call
261
- passes `false` as its second argument. A name built into a local
262
- variable first (`name = "V#{version}"; const_get(name)`) is read the
263
- same way. A
264
+ `Migration::V1` — and the `V*` names under `Migration`'s superclasses
265
+ and mixins and at the top level as well, unless the call passes `false`
266
+ as its second argument. A literal `const_get("Handler")` is a reference
267
+ looked up in the same order, so it names `Migration::Handler` when
268
+ that exists rather than a top-level `Handler`. A name built into a
269
+ local variable first (`name = "V#{version}"; const_get(name)`) is read
270
+ the same way. A
264
271
  class name appearing as a string in a `.yml`, `.json` or template file
265
272
  demotes the same way — weaker evidence than a constant reference, and
266
273
  neither proof of use nor grounds to call it dead.
@@ -156,6 +156,13 @@ module Rigor
156
156
  # first segment appears here as KNOWN and stays silent: these ids are legitimate suppression /
157
157
  # `severity_overrides:` vocabulary the light rule-id table cannot enumerate (plugins load dynamically;
158
158
  # aggregator ids live in the engine-heavy runner), so under-warning is the FP-safe direction.
159
+ # The `pre-eval.*` ids (ADR-17 WD6) the bare family wildcard `pre-eval` expands to. Emitted by the
160
+ # `pre_eval:` pre-pass, outside the CheckRules catalogue, so the family cannot ride `RULE_FAMILIES`
161
+ # (which also drives the catalogue and the config audit over `ALL_RULES`).
162
+ PRE_EVAL_RULES = %w[
163
+ pre-eval.parse-error pre-eval.file-not-found pre-eval.duplicate-declaration
164
+ ].freeze
165
+
159
166
  NON_CHECK_DIAGNOSTIC_FAMILIES = %w[rbs_extended dynamic rbs pre-eval plugin plugin_trust].freeze
160
167
 
161
168
  # Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
@@ -87,6 +87,7 @@ module Rigor
87
87
  def self.resolve_rule_token(token)
88
88
  return nil if token == "all"
89
89
  return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
90
+ return PRE_EVAL_RULES if token == "pre-eval"
90
91
  return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
91
92
 
92
93
  ALL_RULES.include?(token) ? [token] : []
@@ -654,7 +655,7 @@ module Rigor
654
655
  # `dynamic.*`, `rbs.*`, `pre-eval.*`, and any `plugin.`-prefixed id — plugins load dynamically, so
655
656
  # their rule vocabulary cannot be enumerated here and under-warning is the FP-safe direction).
656
657
  def known_suppression_token?(token)
657
- return true if token == "all"
658
+ return true if %w[all pre-eval].include?(token)
658
659
  return true if ALL_RULES.include?(token) || LEGACY_RULE_ALIASES.key?(token) ||
659
660
  RULE_FAMILIES.include?(token) || NON_CHECK_DIAGNOSTIC_IDS.include?(token)
660
661
 
@@ -863,29 +864,23 @@ module Rigor
863
864
 
864
865
  # Is a refinement of `call_node`'s method name into `class_name` (or an ancestor) in effect at the call?
865
866
  # Issue #1120 asks it for a method the class lacks. The two argument rules ask
866
- # {#refined_redefinition_in_effect?} instead (issue #1740), and this only for a class-object receiver.
867
- # Typing the call from the refine body is #1664.
868
- # A refinement answers instance-side receivers only: a refined singleton (`refine X.singleton_class`) names
869
- # no constant target, so nothing records it. Issue #1673 — the answer is derived from the file's in-effect
870
- # refinements ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded
871
- # through its module's includes (issue #1671).
867
+ # {#refined_redefinition_in_effect?} instead (issue #1740). Typing the call from the refine body is #1664.
868
+ # Issue #1752 — a class-object receiver (`kind` `:singleton`) is reached by a refinement of any class or module
869
+ # on its lookup: `Class`, `Module`, `Object`, `Kernel`, `BasicObject`, or a module it extends. A refined
870
+ # singleton class (`refine X.singleton_class`) names no constant target, so its rows are class-unknown, which
871
+ # reach every receiver. Issue #1673 — the answer is derived from the file's in-effect refinements
872
+ # ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded through its
873
+ # module's includes (issue #1671).
872
874
  #
873
875
  # ADR-121 WD7 — a module in effect whose refinements Rigor cannot read
874
876
  # ({Inference::InEffectRefinements.opaque_module?}: a gem's, one from outside the analysed paths, one with an
875
877
  # unreadable `refine`) may refine any class, a class object's included, so the call declines whatever its
876
- # receiver. Issue #1796; both argument rules ask this predicate too.
878
+ # receiver. Issue #1796.
877
879
  def refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites)
878
880
  return false if lexical_sites.nil?
879
881
  return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
880
882
 
881
- modules =
882
- if kind == :instance
883
- Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name)
884
- else
885
- # ADR-121 WD7 — a row whose class the walk could not name (`refine(k)` with `k` a singleton class)
886
- # reaches a class object too.
887
- Inference::InEffectRefinements.class_unknown_refining_modules(scope, call_node.name)
888
- end
883
+ modules = Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name, kind)
889
884
  !modules.nil? &&
890
885
  lexical_sites.refinements.refinement_active?(call_node.location.start_offset, modules,
891
886
  scope.declared_refinements) do |name|
@@ -902,8 +897,8 @@ module Rigor
902
897
  #
903
898
  # A call with nothing in effect is checked. Otherwise it reports only from positive knowledge: the list
904
899
  # carries no unknown marker and no opaque module, and {Inference::RefinedDispatch::OwnMethod} proves the
905
- # receiver's own definer answers first. A class-object receiver keeps {#refined_method_in_effect?}'s
906
- # answer: its singleton-side levels are not walked yet.
900
+ # receiver's own definer answers first. Issue #1752 — a class-object receiver (`kind` `:singleton`) walks its
901
+ # singleton levels: `Integer.sqrt(1, 2)` under `refine Object do def sqrt(a, b)` runs `Integer.sqrt` and raises.
907
902
  def refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
908
903
  return false if lexical_sites.nil?
909
904
 
@@ -911,9 +906,8 @@ module Rigor
911
906
  return false if list.empty?
912
907
  return true if list.include?(Inference::InEffectRefinements::UNKNOWN)
913
908
  return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
914
- return refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites) unless kind == :instance
915
909
 
916
- !Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list)
910
+ !Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list, kind: kind)
917
911
  end
918
912
 
919
913
  # The in-effect list at the call as the typer reads it: the lexical activations, each `using` expanded,
@@ -2093,7 +2087,13 @@ module Rigor
2093
2087
  # arm restores the polarity without touching the shared helper:
2094
2088
  # `String | nil` keeps firing, and so does `Dynamic | String | nil` —
2095
2089
  # the nameless arm stays permissive inside the all-arms check, which is
2096
- # only about the arms' method surface.
2090
+ # only about the arms' method surface. That union no longer arises from
2091
+ # a merge re-joining the narrowed edges of one untyped local, which
2092
+ # binds it back to `Dynamic[top]` (issue #1840,
2093
+ # `Inference::UntypedRejoin`); it still does when the program writes
2094
+ # the `nil` (`x = nil if c`) and no `nil?` edge strips it before the
2095
+ # re-joining merge, or when the arms are not a narrowing of one
2096
+ # untyped value.
2097
2097
  def union_has_nameable_non_nil_arm?(union, scope)
2098
2098
  union.members.any? do |m|
2099
2099
  next false if nil_member?(m)
@@ -2195,7 +2195,8 @@ module Rigor
2195
2195
  # ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
2196
2196
  def refined_on_some_arm?(members, call_node, scope, lexical_sites)
2197
2197
  members.any? do |member|
2198
- refined_method_in_effect?(concrete_class_name(member), call_node, scope, :instance, lexical_sites)
2198
+ kind = member.is_a?(Type::Singleton) ? :singleton : :instance
2199
+ refined_method_in_effect?(concrete_class_name(member), call_node, scope, kind, lexical_sites)
2199
2200
  end
2200
2201
  end
2201
2202
 
@@ -80,7 +80,7 @@ module Rigor
80
80
  private
81
81
 
82
82
  def effects_descriptor(paths)
83
- files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
83
+ files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
84
84
  base = RunCacheKey.descriptor(
85
85
  configuration: @configuration, files: files, explain: false,
86
86
  rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
@@ -42,8 +42,8 @@ module Rigor
42
42
  # plugin's identity and the engine's (key slots), so a project whose plugins claim template globs is served
43
43
  # too. The ADR-87 probe misses on one: the plain slot's key carries the `template-units` slot.
44
44
  #
45
- # `explain:` is always false: the session's runners never set it (#1533), and the CLI declines the probe under
46
- # `--explain`.
45
+ # `explain:` is always false: the session writes no slot for an `--explain` run (#1533), and the CLI declines
46
+ # the probe under `--explain`.
47
47
  module IncrementalRunSlot
48
48
  module_function
49
49
 
@@ -115,7 +115,7 @@ module Rigor
115
115
  def serve(configuration:, cache_root:, paths:)
116
116
  return nil if configuration.effects_enabled?
117
117
 
118
- files = PathExpansion.ruby_files(paths, configuration.exclude_patterns)
118
+ files = PathExpansion.ruby_files(paths, configuration.exclude_patterns, base: configuration.exclude_base)
119
119
  slot_key = key(configuration: configuration, target: Target.new(files: files, roots: paths))
120
120
  return nil if slot_key.nil?
121
121