rigortype 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/docs/handbook/09-plugins.md +5 -2
- data/docs/handbook/appendix-liskov.md +5 -3
- data/docs/handbook/appendix-phpstan.md +2 -2
- data/docs/install.md +1 -1
- data/docs/manual/02-cli-reference.md +60 -2
- data/docs/manual/06-baseline.md +12 -0
- data/docs/manual/08-skills.md +21 -0
- data/docs/manual/11-ci.md +6 -6
- data/docs/manual/15-type-protection-coverage.md +29 -0
- data/docs/manual/plugins/rigor-minitest.md +1 -1
- data/lib/rigor/cli/check_command.rb +4 -33
- data/lib/rigor/cli/check_runner_factory.rb +63 -0
- data/lib/rigor/cli/coverage_command.rb +42 -10
- data/lib/rigor/cli/doctor_command.rb +295 -0
- data/lib/rigor/cli/plugins_command.rb +2 -2
- data/lib/rigor/cli/plugins_renderer.rb +1 -1
- data/lib/rigor/cli/protection_renderer.rb +32 -2
- data/lib/rigor/cli/protection_report.rb +32 -6
- data/lib/rigor/cli/skill_command.rb +52 -1
- data/lib/rigor/cli/upgrade_command.rb +25 -0
- data/lib/rigor/cli.rb +17 -1
- data/lib/rigor/environment/rbs_loader.rb +28 -0
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/inference/dynamic_origin.rb +67 -0
- data/lib/rigor/inference/expression_typer.rb +22 -10
- data/lib/rigor/inference/fallback.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +41 -2
- data/lib/rigor/inference/method_dispatcher.rb +19 -4
- data/lib/rigor/inference/mutation_widening.rb +18 -0
- data/lib/rigor/inference/protection_scanner.rb +6 -3
- data/lib/rigor/inference/statement_evaluator.rb +5 -8
- data/lib/rigor/plugin/base.rb +34 -7
- data/lib/rigor/plugin/registry.rb +1 -1
- data/lib/rigor/scope.rb +16 -5
- data/lib/rigor/sig_gen/generator.rb +25 -0
- data/lib/rigor/sig_gen/method_candidate.rb +7 -2
- data/lib/rigor/sig_gen/writer.rb +60 -13
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +63 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +2 -3
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +14 -24
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +10 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +1 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +36 -79
- data/sig/rigor/plugin/base.rbs +2 -0
- data/sig/rigor/scope.rbs +3 -1
- data/skills/rigor-ask/SKILL.md +21 -1
- data/skills/rigor-baseline-reduce/SKILL.md +16 -0
- data/skills/rigor-ci-setup/SKILL.md +96 -249
- data/skills/rigor-doctor/SKILL.md +39 -49
- data/skills/rigor-doctor/references/01-checks.md +52 -0
- data/skills/rigor-editor-setup/SKILL.md +14 -0
- data/skills/rigor-mcp-setup/SKILL.md +14 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
- data/skills/rigor-plugin-author/SKILL.md +24 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +8 -4
- data/skills/rigor-plugin-review/SKILL.md +174 -0
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
- data/skills/rigor-plugin-tune/SKILL.md +21 -2
- data/skills/rigor-project-init/SKILL.md +16 -0
- data/skills/rigor-protection-uplift/SKILL.md +15 -0
- data/skills/rigor-rbs-setup/SKILL.md +15 -0
- data/skills/rigor-upgrade/SKILL.md +16 -0
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c16266bcbae2ff766dd6579df68ca08d31563d7f9a813ef49b88bd8f72734b7a
|
|
4
|
+
data.tar.gz: c80a5f751c3f757c30acfa0fd1631918a5b3fdc388752c03f27ad56c11a723c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: deda828a4f7171b30c1e0b0531fbb52cb815905e05ebb05186a87b49107d7ce9e80c4d5d9dc81fd35ed26a1872d2d6e9bbeefe98e3b146d1acd1e4d360a81a26
|
|
7
|
+
data.tar.gz: fbe3ba7eed33351ea7ec8863f09085ed1b55f99e17ac3a0bba51234723da1a35f1e90fa9dd694aa0a680c5dbb8725855deb789728c6886c16ccc229a6d4b1be1
|
data/README.md
CHANGED
|
@@ -231,9 +231,10 @@ rigor docs --list # list every bundled page
|
|
|
231
231
|
|
|
232
232
|
## Status
|
|
233
233
|
|
|
234
|
-
Current release: **`v0.2.
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
Current release: **`v0.2.7`** (2026-07-05) — the latest cut on the
|
|
235
|
+
`0.2.x` evaluation line opened by `v0.2.0`, the first
|
|
236
|
+
publicly-announced (general / evaluation) release. The line publishes
|
|
237
|
+
an enumerated [compatibility surface](docs/compatibility.md) as a
|
|
237
238
|
minor-non-break trial, rehearsing the contract that hard-freezes at
|
|
238
239
|
`v1.0.0`. Rigor analyses real Ruby today: it has been hardened against
|
|
239
240
|
Mastodon, Redmine, and GitLab FOSS, and the deliberately conservative
|
data/docs/handbook/09-plugins.md
CHANGED
|
@@ -68,12 +68,15 @@ directory — gives a plugin five primary surfaces:
|
|
|
68
68
|
array of `Rigor::Analysis::Diagnostic` rows. The runner
|
|
69
69
|
stamps each with `source_family: "plugin.<your-id>"`.
|
|
70
70
|
2. **`dynamic_return(receivers:, methods:, file_methods:)` /
|
|
71
|
-
`
|
|
71
|
+
`narrowing_facts(methods:)`** — the per-call-site return-type
|
|
72
72
|
and flow-narrowing contribution surface (ADR-37 Slice 2).
|
|
73
73
|
A `dynamic_return` block names the inferred return type at a
|
|
74
74
|
matching call site; the analyzer's dispatcher merges the
|
|
75
75
|
contribution and uses it as if it were RBS-declared. A
|
|
76
|
-
`
|
|
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.)
|
|
77
80
|
(These replaced the removed `flow_contribution_for` hook —
|
|
78
81
|
ADR-52 WD3; a plugin that still defines it raises at load.)
|
|
79
82
|
3. **`Plugin::IoBoundary#read_file`** / **`#open_url`** —
|
|
@@ -210,10 +210,12 @@ def shout(thing)
|
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
# Clause 1 / covariant returns: the body provably returns the
|
|
213
|
-
# receiver, so #dup returns `self` (
|
|
214
|
-
# Returning Object would *weaken* the postcondition.
|
|
213
|
+
# receiver, so #dup returns `self` (the receiver's own type, not the
|
|
214
|
+
# widened Object). Returning Object would *weaken* the postcondition.
|
|
215
|
+
# Shape preservation through `dup` (ADR-76 WD2) keeps the precise
|
|
216
|
+
# tuple here rather than degrading to the nominal `Array`.
|
|
215
217
|
copy = [1, 2, 3].dup
|
|
216
|
-
assert_type("
|
|
218
|
+
assert_type("[1, 2, 3]", copy)
|
|
217
219
|
```
|
|
218
220
|
|
|
219
221
|
The connection to the type-theory appendix's **gradual guarantee**
|
|
@@ -96,13 +96,13 @@ When the assertion is recognised by **call shape** rather than
|
|
|
96
96
|
by signature — PHPStan's `TypeSpecifyingExtension` interface,
|
|
97
97
|
where you write a class that the framework instantiates and
|
|
98
98
|
asks "given this call, what narrowings does it produce?" —
|
|
99
|
-
Rigor's analogue is a plugin's `
|
|
99
|
+
Rigor's analogue is a plugin's `narrowing_facts` / `dynamic_return` and
|
|
100
100
|
`#diagnostics_for_file` hooks plus the engine's
|
|
101
101
|
`post_return_facts` substrate.
|
|
102
102
|
|
|
103
103
|
| PHPStan extension type | Rigor analogue |
|
|
104
104
|
| --- | --- |
|
|
105
|
-
| `MethodTypeSpecifyingExtension` | Plugin's `Fact(target_kind: :parameter)` returned from `
|
|
105
|
+
| `MethodTypeSpecifyingExtension` | Plugin's `Fact(target_kind: :parameter)` returned from `narrowing_facts` |
|
|
106
106
|
| `StaticMethodTypeSpecifyingExtension` | Same, with `Fact(target_kind: :receiver-class)` |
|
|
107
107
|
| `FunctionTypeSpecifyingExtension` | Same, with `Fact(target_kind: :argument)` |
|
|
108
108
|
| `DynamicMethodReturnTypeExtension` | Plugin's `dynamic_return(methods:) { |call_node, scope, ...| ... }` |
|
data/docs/install.md
CHANGED
|
@@ -152,7 +152,7 @@ without WSL). For all other environments, prefer Case A–D above.
|
|
|
152
152
|
rigor --version
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
A version string like `rigor 0.
|
|
155
|
+
A version string like `rigor 0.2.x` confirms a successful install.
|
|
156
156
|
If the command is not found, revisit Step 2 for your case.
|
|
157
157
|
|
|
158
158
|
---
|
|
@@ -403,7 +403,7 @@ catalogue** ([ADR-37](../adr/37-plugin-interface-segregation.md)):
|
|
|
403
403
|
a focused, machine-readable map of what each loaded plugin
|
|
404
404
|
contributes — the AST node types its `node_rule`s match, the
|
|
405
405
|
receiver classes its `dynamic_return`s gate on, the methods its
|
|
406
|
-
`
|
|
406
|
+
`narrowing_facts`s narrow, and the facts it `produces` /
|
|
407
407
|
`consumes`. Combine with `--format=json` for tooling (an AI
|
|
408
408
|
agent can enumerate every plugin's behaviour without reading a
|
|
409
409
|
line of plugin source). The same narrow surfaces also appear in
|
|
@@ -452,13 +452,14 @@ The positional slot is a skill *name*; alternative outputs are flags,
|
|
|
452
452
|
so a skill can never be shadowed by a verb.
|
|
453
453
|
|
|
454
454
|
```sh
|
|
455
|
-
rigor skill [<name>] [--path <name>] [--list] [--describe]
|
|
455
|
+
rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
|
|
456
456
|
```
|
|
457
457
|
|
|
458
458
|
| Form | Purpose |
|
|
459
459
|
| --- | --- |
|
|
460
460
|
| (none) / `--list` | Table of every bundled skill (name + absolute path). |
|
|
461
461
|
| `<name>` | Print the `SKILL.md` body to stdout, with a header pointing at the skill's `references/` directory. |
|
|
462
|
+
| `--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. |
|
|
462
463
|
| `--path <name>` | Print the single-line absolute `SKILL.md` path, suitable as input to a file-reading tool. |
|
|
463
464
|
| `--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. |
|
|
464
465
|
|
|
@@ -539,6 +540,63 @@ with its stable id, the severity it would impose, and whether your config
|
|
|
539
540
|
adopts it. See [`docs/compatibility.md`](../compatibility.md) for how
|
|
540
541
|
bleeding-edge fits the stability model.
|
|
541
542
|
|
|
543
|
+
## `rigor doctor`
|
|
544
|
+
|
|
545
|
+
Classify setup problems vs a clean run with routed next actions
|
|
546
|
+
([ADR-77](../adr/77-doctor-and-upgrade-commands.md) WD1).
|
|
547
|
+
|
|
548
|
+
```sh
|
|
549
|
+
rigor doctor [--config PATH] [--format text|json]
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
| Flag | Purpose |
|
|
553
|
+
| --- | --- |
|
|
554
|
+
| `--config PATH` | Use this `.rigor.yml` instead of auto-discovery. |
|
|
555
|
+
| `--format text\|json` | Output format. Default `text`. |
|
|
556
|
+
|
|
557
|
+
Runs a scoped analysis and audits:
|
|
558
|
+
|
|
559
|
+
- **Configuration audit** — unresolved `signature_paths:`, unknown
|
|
560
|
+
`libraries:`, inert `disable:` / `severity_overrides:` tokens
|
|
561
|
+
({ConfigAudit}).
|
|
562
|
+
- **RBS environment health** — whether the RBS class universe built
|
|
563
|
+
successfully (`0` classes means a broken setup).
|
|
564
|
+
- **Plugin load errors** — whether every configured plugin loaded.
|
|
565
|
+
- **Baseline drift** — whether the current diagnostics have drifted
|
|
566
|
+
from the saved baseline.
|
|
567
|
+
- **Rails plugin gap** — whether `Gemfile.lock` contains Rails gems
|
|
568
|
+
but no Rails plugin is enabled.
|
|
569
|
+
|
|
570
|
+
Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
|
|
571
|
+
routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
|
|
572
|
+
is a stable contract:
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"status": "issues_found",
|
|
577
|
+
"checks": [
|
|
578
|
+
{ "id": "config_audit", "status": "fail", "message": "...", "hint": "..." }
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Exits `1` when any check fails, `0` when all pass.
|
|
584
|
+
|
|
585
|
+
## `rigor upgrade`
|
|
586
|
+
|
|
587
|
+
Migration command skeleton ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
|
|
588
|
+
WD7). The real body lands when a concrete backwards-compatibility
|
|
589
|
+
break gives it a target (e.g. re-running `baseline regenerate`
|
|
590
|
+
against a strengthened default profile, surfacing renamed
|
|
591
|
+
suppression ids, reporting `bleeding_edge:` graduations).
|
|
592
|
+
|
|
593
|
+
```sh
|
|
594
|
+
rigor upgrade
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Until then it prints the current version and notes that upgrade is
|
|
598
|
+
queued. Exits `0`.
|
|
599
|
+
|
|
542
600
|
## Environment variables
|
|
543
601
|
|
|
544
602
|
Most behaviour is driven by flags and `.rigor.yml`; a few
|
data/docs/manual/06-baseline.md
CHANGED
|
@@ -77,6 +77,18 @@ a diagnostic another layer has already suppressed.
|
|
|
77
77
|
default — one bucket per file × rule) or `--match-mode=message`
|
|
78
78
|
(a bucket per distinct message: more precise, more churn).
|
|
79
79
|
|
|
80
|
+
`--match-mode=message` keys each bucket on the **rendered
|
|
81
|
+
message text**, which includes details such as the displayed
|
|
82
|
+
receiver type. That makes it sharper at telling two same-rule
|
|
83
|
+
diagnostics on one line apart, but also **brittle**: when a Rigor
|
|
84
|
+
upgrade rewords a message or changes how a type is displayed, the
|
|
85
|
+
key no longer matches and the previously-baselined diagnostic
|
|
86
|
+
resurfaces as if new. `--match-mode=rule` keys only on
|
|
87
|
+
`(file, rule)` and is immune to message rewording — prefer it
|
|
88
|
+
unless you specifically need per-message discrimination, and
|
|
89
|
+
expect to `regenerate` a `message`-mode baseline after upgrading
|
|
90
|
+
Rigor.
|
|
91
|
+
|
|
80
92
|
## Working a baseline down
|
|
81
93
|
|
|
82
94
|
`rigor triage` summarises a diagnostic stream — rule
|
data/docs/manual/08-skills.md
CHANGED
|
@@ -108,6 +108,7 @@ source checkout. The `rigor skill` command surfaces them:
|
|
|
108
108
|
rigor skill describe # probe the project + recommend the next skill (alias: rigor describe)
|
|
109
109
|
rigor skill --list # name + absolute path for each bundled skill
|
|
110
110
|
rigor skill <name> # print the SKILL.md body (with a references/ header)
|
|
111
|
+
rigor skill --full <name> # the body + every references/*.md inline (complete procedure)
|
|
111
112
|
rigor skill --path <name> # one-line absolute SKILL.md path, for a file-reading tool
|
|
112
113
|
```
|
|
113
114
|
|
|
@@ -133,6 +134,26 @@ npx skills add rigortype/rigor
|
|
|
133
134
|
(The contributor-only skills under `.claude/skills/` are marked internal
|
|
134
135
|
and are not installed by a bulk `npx skills add`.)
|
|
135
136
|
|
|
137
|
+
### Keeping installed skills current
|
|
138
|
+
|
|
139
|
+
A skill copied into your project this way is **frozen at install time**,
|
|
140
|
+
while Rigor keeps evolving — flags, config keys, and rule ids move release
|
|
141
|
+
to release. To keep that from going stale (陳腐化), every skill opens with
|
|
142
|
+
a **"First: load the version-current copy"** directive: before following
|
|
143
|
+
its steps, an agent re-fetches the authoritative body from the *installed*
|
|
144
|
+
Rigor with
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
rigor skill --full <name> # the current body + all its references/, in one call
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Because the vendored copy is fixed at install time but `rigor skill` always
|
|
151
|
+
reads the gem, the two diverge as you upgrade, and the directive makes the
|
|
152
|
+
agent prefer the gem's current version. You therefore do **not** need to
|
|
153
|
+
re-install the skills after upgrading Rigor — only the thin entry point
|
|
154
|
+
(`rigor-next-steps`) needs to work before Rigor exists; everything
|
|
155
|
+
version-specific is served live by the installed binary.
|
|
156
|
+
|
|
136
157
|
## Running a skill
|
|
137
158
|
|
|
138
159
|
In an agent that supports Agent Skills, invoke the skill by name (in
|
data/docs/manual/11-ci.md
CHANGED
|
@@ -192,9 +192,9 @@ remains available for any other tool that wants the raw diagnostic stream.
|
|
|
192
192
|
|
|
193
193
|
The workflow above installs whatever `rigortype` is current at run
|
|
194
194
|
time. To pin a version — and keep CI reproducible — choose one of the
|
|
195
|
-
options below. While the `0.
|
|
196
|
-
still settling, so pinning is recommended; what Rigor
|
|
197
|
-
keeping stable (and from which release) is enumerated in
|
|
195
|
+
options below. While the `0.2.x` line is an evaluation trial the
|
|
196
|
+
surface is still settling, so pinning is recommended; what Rigor
|
|
197
|
+
commits to keeping stable (and from which release) is enumerated in
|
|
198
198
|
[`docs/compatibility.md`](../compatibility.md).
|
|
199
199
|
|
|
200
200
|
### A CI-only `Gemfile` (recommended)
|
|
@@ -203,7 +203,7 @@ Commit a two-line `.github/rigor/Gemfile`:
|
|
|
203
203
|
|
|
204
204
|
```ruby
|
|
205
205
|
source "https://rubygems.org"
|
|
206
|
-
gem "rigortype", "~> 0.
|
|
206
|
+
gem "rigortype", "~> 0.2"
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
plus its `Gemfile.lock`, and point the Rigor job at it through
|
|
@@ -242,7 +242,7 @@ updates:
|
|
|
242
242
|
|
|
243
243
|
### A pinned `gem install`
|
|
244
244
|
|
|
245
|
-
`gem install rigortype -v "0.
|
|
245
|
+
`gem install rigortype -v "0.2.6"` in the workflow. Simpler, with no
|
|
246
246
|
extra files — but Dependabot does not see a version inside a `run:`
|
|
247
247
|
step, so updates to the pin are manual.
|
|
248
248
|
|
|
@@ -258,7 +258,7 @@ docker run --rm -v "$PWD:/src" ghcr.io/rigortype/rigor check
|
|
|
258
258
|
|
|
259
259
|
`rigor` is the image entrypoint, so subcommands and flags follow the
|
|
260
260
|
image name. Pin a version with an explicit tag —
|
|
261
|
-
`ghcr.io/rigortype/rigor:0.
|
|
261
|
+
`ghcr.io/rigortype/rigor:0.2.6`.
|
|
262
262
|
|
|
263
263
|
## Nix
|
|
264
264
|
|
|
@@ -172,6 +172,35 @@ breakage a *different* test would catch shows as a gap). For an
|
|
|
172
172
|
accurate map, run the command over all tests that exercise the
|
|
173
173
|
files you are measuring — trading time for completeness.
|
|
174
174
|
|
|
175
|
+
### Why a hole is untyped — provenance and tractability
|
|
176
|
+
|
|
177
|
+
For a `Dynamic`-receiver hole, `rigor coverage --protection` also
|
|
178
|
+
records *why* the receiver is untyped and *whether a type can close
|
|
179
|
+
it*, so you spend effort where it pays off. Each `add_a_type_here`
|
|
180
|
+
entry carries two fields:
|
|
181
|
+
|
|
182
|
+
- **`dynamic_origin`** — the cause the value became dynamic:
|
|
183
|
+
`external_gem_without_rbs`, `framework_dsl_boundary`,
|
|
184
|
+
`analyzer_budget_cutoff`, `explicit_untyped`, or
|
|
185
|
+
`unsupported_syntax`.
|
|
186
|
+
- **`tractability`** — the action axis derived from that cause:
|
|
187
|
+
- **`add_rbs`** — you can close it with a type: install RBS
|
|
188
|
+
(`rbs collection install`), enable `dependencies.source_inference:`,
|
|
189
|
+
or tighten an authored `untyped` signature.
|
|
190
|
+
- **`enable_plugin`** — a framework / DSL boundary; reach for a
|
|
191
|
+
plugin or [`pre_eval:`](03-configuration.md), not a hand-written
|
|
192
|
+
type.
|
|
193
|
+
- **`engine_gap`** — not closable by a user type (a budget cutoff
|
|
194
|
+
or a construct Rigor does not yet model); report it.
|
|
195
|
+
|
|
196
|
+
The text report prints a one-line `by tractability:` breakdown under
|
|
197
|
+
the "Add a type here" header, and the JSON carries the same totals as
|
|
198
|
+
`tractability_summary`. Start with the `add_rbs` holes — they are the
|
|
199
|
+
ones a type actually catches.
|
|
200
|
+
|
|
201
|
+
Provenance is precision-additive only: it never changes a type, fires
|
|
202
|
+
no diagnostic, and never affects severity or the protection ratio.
|
|
203
|
+
|
|
175
204
|
## Cost and scope
|
|
176
205
|
|
|
177
206
|
The fused tiers run real analyses and real test suites, so treat
|
|
@@ -79,7 +79,7 @@ matches.
|
|
|
79
79
|
## Plugin internals
|
|
80
80
|
|
|
81
81
|
The assertion recogniser and the contract surfaces this plugin
|
|
82
|
-
exercises — the ADR-37 `
|
|
82
|
+
exercises — the ADR-37 `narrowing_facts` narrowing gate and the ADR-38
|
|
83
83
|
`additional_initializers` for `setup` — are in the
|
|
84
84
|
[plugin's README](../../../plugins/rigor-minitest/README.md). To write a
|
|
85
85
|
plugin, see [`examples/`](../../../examples/README.md) and the
|
|
@@ -13,6 +13,7 @@ require_relative "command"
|
|
|
13
13
|
require_relative "options"
|
|
14
14
|
require_relative "diagnostic_formats"
|
|
15
15
|
require_relative "ci_detector"
|
|
16
|
+
require_relative "check_runner_factory"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
class CLI
|
|
@@ -244,42 +245,12 @@ module Rigor
|
|
|
244
245
|
end
|
|
245
246
|
|
|
246
247
|
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Cache::Store.new(
|
|
251
|
-
root: cache_root,
|
|
252
|
-
max_bytes: configuration.cache_max_bytes
|
|
253
|
-
)
|
|
254
|
-
end
|
|
255
|
-
Analysis::Runner.new(
|
|
256
|
-
configuration: configuration,
|
|
257
|
-
explain: options.fetch(:explain),
|
|
258
|
-
cache_store: cache_store,
|
|
259
|
-
collect_stats: options.fetch(:stats),
|
|
260
|
-
workers: resolve_workers(options, configuration),
|
|
261
|
-
buffer: buffer
|
|
248
|
+
CheckRunnerFactory.build(
|
|
249
|
+
configuration: configuration, options: options,
|
|
250
|
+
buffer: buffer, cache_root: cache_root
|
|
262
251
|
)
|
|
263
252
|
end
|
|
264
253
|
|
|
265
|
-
# ADR-15 Phase 4c — resolves the worker count by
|
|
266
|
-
# precedence: CLI `--workers=N` (most explicit) > env
|
|
267
|
-
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
268
|
-
# `parallel.workers:` > 0 (sequential default). Returns
|
|
269
|
-
# an Integer; non-numeric values raise so typos fail
|
|
270
|
-
# loudly. CLI / env may pass a negative value — clamped
|
|
271
|
-
# to 0 (sequential) so a stray `-1` doesn't crash the
|
|
272
|
-
# pool spawn loop.
|
|
273
|
-
def resolve_workers(options, configuration)
|
|
274
|
-
cli_value = options[:workers]
|
|
275
|
-
return [Integer(cli_value), 0].max if cli_value
|
|
276
|
-
|
|
277
|
-
env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
278
|
-
return [Integer(env_value), 0].max if env_value && !env_value.empty?
|
|
279
|
-
|
|
280
|
-
configuration.parallel_workers
|
|
281
|
-
end
|
|
282
|
-
|
|
283
254
|
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
284
255
|
options = {
|
|
285
256
|
config: nil,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analysis/runner"
|
|
4
|
+
require_relative "../cache/store"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
class CLI
|
|
8
|
+
# Shared factory for building an {Analysis::Runner} from CLI option
|
|
9
|
+
# hashes + a loaded {Configuration}.
|
|
10
|
+
#
|
|
11
|
+
# Extracted from {CheckCommand} so that `rigor doctor` and future
|
|
12
|
+
# `describe --deep` can reuse the exact same runner-setup logic
|
|
13
|
+
# (cache, workers, buffer, explain) without diverging from the
|
|
14
|
+
# primary check path (ADR-77 WD3).
|
|
15
|
+
module CheckRunnerFactory
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Builds a runner matching the configuration/plugin/cache resolution
|
|
19
|
+
# that `rigor check` itself uses.
|
|
20
|
+
#
|
|
21
|
+
# @param configuration [Rigor::Configuration]
|
|
22
|
+
# @param options [Hash] parsed CLI options (must contain at least
|
|
23
|
+
# `:no_cache`, `:explain`, `:stats`, `:workers`)
|
|
24
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
25
|
+
# @param cache_root [String]
|
|
26
|
+
# @return [Rigor::Analysis::Runner]
|
|
27
|
+
def build(configuration:, options:, buffer:, cache_root:)
|
|
28
|
+
cache_store = if options.fetch(:no_cache)
|
|
29
|
+
nil
|
|
30
|
+
else
|
|
31
|
+
Cache::Store.new(
|
|
32
|
+
root: cache_root,
|
|
33
|
+
max_bytes: configuration.cache_max_bytes
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
Analysis::Runner.new(
|
|
37
|
+
configuration: configuration,
|
|
38
|
+
explain: options.fetch(:explain),
|
|
39
|
+
cache_store: cache_store,
|
|
40
|
+
collect_stats: options.fetch(:stats),
|
|
41
|
+
workers: resolve_workers(options, configuration),
|
|
42
|
+
buffer: buffer
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resolves the worker count by precedence: CLI `--workers=N`
|
|
47
|
+
# (most explicit) > env `RIGOR_RACTOR_WORKERS` > config
|
|
48
|
+
# `.rigor.yml` `parallel.workers:` > 0 (sequential default).
|
|
49
|
+
# Returns an Integer; non-numeric values raise so typos fail
|
|
50
|
+
# loudly. CLI / env may pass a negative value — clamped to 0
|
|
51
|
+
# (sequential) so a stray `-1` doesn't crash the pool spawn loop.
|
|
52
|
+
def resolve_workers(options, configuration)
|
|
53
|
+
cli_value = options[:workers]
|
|
54
|
+
return [Integer(cli_value), 0].max if cli_value
|
|
55
|
+
|
|
56
|
+
env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
57
|
+
return [Integer(env_value), 0].max if env_value && !env_value.empty?
|
|
58
|
+
|
|
59
|
+
configuration.parallel_workers
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -144,7 +144,7 @@ module Rigor
|
|
|
144
144
|
|
|
145
145
|
def scan_protection(paths, options)
|
|
146
146
|
configuration = Configuration.load(options.fetch(:config))
|
|
147
|
-
environment =
|
|
147
|
+
environment = plugin_aware_environment(configuration)
|
|
148
148
|
scope = scope_with_inferred_params(paths, configuration, environment)
|
|
149
149
|
scanner = Inference::ProtectionScanner.new(scope: scope)
|
|
150
150
|
accumulator = ProtectionAccumulator.new
|
|
@@ -153,21 +153,38 @@ module Rigor
|
|
|
153
153
|
accumulator.to_report
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
156
|
+
# Seed the protection scan's scope with the same cross-file facts
|
|
157
|
+
# `rigor check` resolves against, so a receiver reads the type it
|
|
158
|
+
# actually has rather than a stripped-scope `Dynamic`:
|
|
159
|
+
#
|
|
160
|
+
# - `discovered_classes` — a project constant referring to a class
|
|
161
|
+
# defined in a *sibling* file (`Account`, `User`) types as
|
|
162
|
+
# `singleton(Account)` instead of `Dynamic`. Without this, a
|
|
163
|
+
# single-file scan cannot see a class it does not itself declare,
|
|
164
|
+
# so every cross-file class-constant dispatch was miscounted as
|
|
165
|
+
# unprotected (the model-constant undercount found 2026-07-04).
|
|
166
|
+
# - `param_inferred_types` (ADR-67 WD3) — an inferred-parameter
|
|
167
|
+
# receiver (`node.loc` where `node` is a `def compile(node)`
|
|
168
|
+
# parameter) counts as protected when its call sites resolve to
|
|
169
|
+
# concrete argument types.
|
|
170
|
+
#
|
|
171
|
+
# Both span the scanned `paths` only (no whole-project pre-pass) —
|
|
172
|
+
# a site that gains neither is classified exactly as before.
|
|
163
173
|
def scope_with_inferred_params(paths, configuration, environment)
|
|
164
174
|
base = Scope.empty(environment: environment)
|
|
175
|
+
seed = {}
|
|
176
|
+
|
|
177
|
+
discovered = Inference::ScopeIndexer.discovered_classes_for_paths(paths)
|
|
178
|
+
seed[:discovered_classes] = discovered unless discovered.empty?
|
|
179
|
+
|
|
165
180
|
table = Inference::ParameterInferenceCollector.collect(
|
|
166
181
|
files: paths, environment: environment, target_ruby: configuration.target_ruby
|
|
167
182
|
)
|
|
168
|
-
|
|
183
|
+
seed[:param_inferred_types] = table unless table.empty?
|
|
184
|
+
|
|
185
|
+
return base if seed.empty?
|
|
169
186
|
|
|
170
|
-
base.with_discovery(base.discovery.with(
|
|
187
|
+
base.with_discovery(base.discovery.with(**seed))
|
|
171
188
|
end
|
|
172
189
|
|
|
173
190
|
def determine_protection_exit(report, options)
|
|
@@ -196,6 +213,21 @@ module Rigor
|
|
|
196
213
|
CoverageScan.project_environment(configuration)
|
|
197
214
|
end
|
|
198
215
|
|
|
216
|
+
# The protection scan must see the same receiver types `rigor check`
|
|
217
|
+
# does — including plugin-contributed `dynamic_return` types (a
|
|
218
|
+
# controller's `params` → `ActionController::Parameters`, a
|
|
219
|
+
# `Model.where` → `ActiveRecord::Relation[Model]`). The bare
|
|
220
|
+
# `project_environment` carries only the RBS environment (no plugin
|
|
221
|
+
# registry), so every plugin-typed receiver reads `Dynamic` and its
|
|
222
|
+
# dispatch site is miscounted as *unprotected* — a systematic
|
|
223
|
+
# undercount of what Rigor actually types on a plugin-using project.
|
|
224
|
+
# `ProjectContext` builds the plugin-aware environment (registry
|
|
225
|
+
# materialised + the per-run prepare pass that primes producers like
|
|
226
|
+
# the controller / model index) exactly as the LSP and the runner do.
|
|
227
|
+
def plugin_aware_environment(configuration)
|
|
228
|
+
LanguageServer::ProjectContext.new(configuration: configuration).environment
|
|
229
|
+
end
|
|
230
|
+
|
|
199
231
|
def scan_one(path, scanner, accumulator, configuration)
|
|
200
232
|
CoverageScan.scan_into(path, scanner, accumulator, configuration)
|
|
201
233
|
end
|