rigortype 0.2.1 → 0.2.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +569 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +539 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -0,0 +1,332 @@
1
+ # Rigor for Rails — step-by-step setup with mise
2
+
3
+ This walkthrough takes a Rails project from zero to a first
4
+ `rigor check` run. It uses [`mise`](https://mise.jdx.dev/) to
5
+ install Rigor alongside Ruby 4.0, keeping the analyser out of
6
+ your project's `Gemfile`.
7
+
8
+ There are two ways through this setup:
9
+
10
+ | | Approach | Best for |
11
+ | --- | --- | --- |
12
+ | **A** | [The `rigor-project-init` skill](#path-a--the-rigor-project-init-skill-recommended) | Most projects — the skill detects your stack, proposes plugins, and writes the config for you. |
13
+ | **B** | [Manual step-by-step](#path-b--manual-step-by-step) | When you want explicit control over each decision. |
14
+
15
+ Both produce the same result. **If you are unsure, follow Path A.**
16
+
17
+ ---
18
+
19
+ ## Before you start
20
+
21
+ You need:
22
+
23
+ - **`mise` installed** — if not, follow
24
+ [mise's getting-started guide](https://mise.jdx.dev/getting-started.html).
25
+ If you prefer `asdf` or a plain `gem install`, see
26
+ [Installing Rigor](01-installation.md).
27
+ - **`mise` wired into your shell** — add
28
+ `eval "$(mise activate zsh)"` (or the equivalent for your
29
+ shell) to your shell rc so that `rigor` reaches your `PATH`.
30
+ See [Installing Rigor § Putting rigor on your PATH](01-installation.md)
31
+ for detail.
32
+ - **An existing Rails project** at a known path.
33
+
34
+ ---
35
+
36
+ ## Step 1 — Install Ruby 4.0 and Rigor (common to both paths)
37
+
38
+ > **Using an AI agent?** Point it at the machine-readable install
39
+ > guide instead:
40
+ >
41
+ > ```
42
+ > Install Rigor in this project by following the instructions at
43
+ > https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
44
+ > ```
45
+ >
46
+ > The agent will detect your environment (mise / asdf / plain Ruby),
47
+ > install the right tools, and hand off to `rigor-project-init`
48
+ > automatically.
49
+
50
+ **Set up in your language** — the prompt is plain natural
51
+ language, so you can write it (and run the whole setup
52
+ conversation) in your mother tongue. Ready-made prompts in 16
53
+ languages are in
54
+ [Installing Rigor § Set up in your language](01-installation.md#set-up-in-your-language).
55
+
56
+ Open a terminal **in your project root** and run:
57
+
58
+ ```sh
59
+ mise use ruby@4.0
60
+ mise use gem:rigortype
61
+ ```
62
+
63
+ `mise use` records the chosen versions in a `mise.toml` in the
64
+ current directory and installs them in one step. Verify:
65
+
66
+ ```sh
67
+ rigor --version
68
+ ```
69
+
70
+ All plugins ship **bundled inside `rigortype`** — no additional
71
+ gems to install. Plugins are inactive by default; you enable the
72
+ ones you need in `.rigor.dist.yml`. That is the only step that
73
+ differs between projects.
74
+
75
+ ---
76
+
77
+ ## Path A — the rigor-project-init skill (recommended)
78
+
79
+ The `rigor-project-init` skill automates the rest of the setup.
80
+ It works inside any AI coding agent that can read a file and run
81
+ a shell command — there is no Claude-specific machinery involved.
82
+
83
+ ### What the skill does
84
+
85
+ It runs eight phases in order:
86
+
87
+ 1. **Detect** — reads your `Gemfile` / `Gemfile.lock` to
88
+ identify the framework family (Rails, dry-rb, Sinatra, …)
89
+ and which gems are present.
90
+ 2. **Choose an adoption mode** — proposes either *acknowledge*
91
+ (snapshot today's diagnostics into a baseline; catch
92
+ regressions going forward) or *strict* (drive the project to
93
+ zero and keep it there). It recommends acknowledge for
94
+ codebases with more than ~100 initial diagnostics.
95
+ 3. **Select plugins** — proposes the plugin set matching your
96
+ detected stack; you confirm or trim the list.
97
+ 4. **Write `.rigor.dist.yml`** — the committed shared config,
98
+ with `severity_profile:` tied to the chosen mode.
99
+ 5. **Sig uplift** — runs `rigor sig-gen --write` to generate a
100
+ baseline `sig/` from Rigor's own inference.
101
+ 6. **Triage** — runs `rigor triage --format json` to diagnose
102
+ the diagnostic stream by cluster.
103
+ 7. **Baseline** *(acknowledge mode only)* — generates
104
+ `.rigor-baseline.yml` and wires `baseline:` in the config.
105
+ 8. **Surface real bugs** — highlights the clusters most likely
106
+ to be genuine bugs; offers escalation paths for
107
+ application-specific metaprogramming and gaps in Rigor's
108
+ built-in coverage.
109
+
110
+ ### How to invoke it
111
+
112
+ Say one of the following to your AI coding agent:
113
+
114
+ > "Set up Rigor in this project."
115
+ > "Configure Rigor for this Rails app."
116
+ > "Add type checking."
117
+
118
+ The agent should respond by running:
119
+
120
+ ```sh
121
+ rigor skill rigor-project-init
122
+ ```
123
+
124
+ That prints the SKILL definition to stdout — a short header
125
+ (with the absolute paths of the SKILL file and its `references/`
126
+ directory) followed by the SKILL body. The agent then follows
127
+ those instructions, reading the `references/NN-*.md` files in
128
+ turn from the directory the header points at.
129
+
130
+ If the agent does not pick the command up on its own, ask
131
+ explicitly: **"Run `rigor skill rigor-project-init` and
132
+ follow the instructions it prints."**
133
+
134
+ The same flow works against any bundled skill:
135
+
136
+ - `rigor skill --list` — list every bundled skill with its path.
137
+ - `rigor skill <name>` — print the SKILL body.
138
+ - `rigor skill --path <name>` — print just the absolute SKILL.md
139
+ path (handy if your agent prefers to read the file directly).
140
+
141
+ The skill lives inside the installed `rigortype` gem at
142
+ the path printed by `rigor skill --path rigor-project-init`. The
143
+ source-of-truth copy is
144
+ [`skills/rigor-project-init/SKILL.md`](../../skills/rigor-project-init/SKILL.md).
145
+
146
+ ---
147
+
148
+ ## Path B — manual step-by-step
149
+
150
+ ### Step 2 — Choose an adoption mode
151
+
152
+ | Mode | When | What happens |
153
+ | --- | --- | --- |
154
+ | **Acknowledge** | Existing codebase with many diagnostics | Record today's diagnostics in a baseline; surface only new ones on each PR. |
155
+ | **Strict** | New or small project | Zero outstanding diagnostics; no baseline. |
156
+
157
+ If your first `rigor check` reports more than ~100 diagnostics,
158
+ acknowledge mode is the natural starting point. You can tighten
159
+ it later.
160
+
161
+ ### Step 3 — Write .rigor.dist.yml
162
+
163
+ The convention is to commit `.rigor.dist.yml` as the shared
164
+ project config and leave `.rigor.yml` for per-developer local
165
+ overrides (gitignored). When both files exist, `.rigor.yml`
166
+ takes precedence.
167
+
168
+ Create `.rigor.dist.yml` at your project root:
169
+
170
+ ```yaml
171
+ # .rigor.dist.yml — Rigor configuration (committed; shared).
172
+
173
+ target_ruby: "3.3" # the Ruby version your Rails app targets
174
+
175
+ paths:
176
+ - app
177
+ - lib
178
+
179
+ exclude:
180
+ - vendor
181
+ - tmp
182
+
183
+ plugins:
184
+ # Rails core
185
+ - rigor-activerecord
186
+ - rigor-actionpack
187
+ - rigor-rails-routes
188
+ - rigor-rails-i18n
189
+ - rigor-actionmailer
190
+ - rigor-activejob
191
+ # Optional — the ActiveSupport core_ext RBS is now auto-applied when
192
+ # activesupport is in your Gemfile.lock (ADR-72); add this plugin only
193
+ # for the fuller, opt-in surface (it stands in for the auto overlay).
194
+ - rigor-activesupport-core-ext
195
+ # Testing — keep the ones that match your project
196
+ - rigor-rspec
197
+ - rigor-factorybot
198
+
199
+ severity_profile: lenient # "strict" for strict mode; omit for "balanced"
200
+
201
+ # baseline: .rigor-baseline.yml # uncomment after Step 6 (acknowledge mode only)
202
+ ```
203
+
204
+ Adjust `target_ruby:` to match your project's Ruby version (the
205
+ value in your `Gemfile` or `.ruby-version`) and trim the
206
+ `plugins:` list to what you actually use.
207
+
208
+ > **ActiveSupport core_ext is covered automatically.** When
209
+ > `activesupport` is in your `Gemfile.lock` but ships no RBS,
210
+ > Rigor auto-loads a bundled core-ext RBS overlay
211
+ > ([ADR-72](../adr/72-gemfile-lock-gated-rbs-overlays.md)), so
212
+ > extension calls (`3.days`, `"x".squish`, `Time.current`, …)
213
+ > resolve without any plugin or config — on a real Rails app this
214
+ > is reliably the single largest false-positive cluster (a
215
+ > Mastodon measurement found ~365 of 489 `call.undefined-method`
216
+ > diagnostics were exactly this source). The
217
+ > `rigor-activesupport-core-ext` plugin above is now optional: add
218
+ > it for the fuller, opt-in surface, and the auto overlay stands
219
+ > down for it. A plain-Ruby project without `activesupport` still
220
+ > gets the genuine diagnostic.
221
+
222
+ Other plugins to consider depending on your stack:
223
+
224
+ | Plugin | When |
225
+ | --- | --- |
226
+ | `rigor-activestorage` | `has_one_attached` / `has_many_attached` |
227
+ | `rigor-actioncable` | ActionCable channels |
228
+ | `rigor-devise` | Devise authentication |
229
+ | `rigor-pundit` | Pundit policies |
230
+ | `rigor-sidekiq` | Sidekiq workers |
231
+ | `rigor-rspec-rails` | RSpec HTTP status matchers |
232
+ | `rigor-shoulda-matchers` | shoulda-matchers |
233
+ | `rigor-minitest` | Minitest / Test::Unit |
234
+
235
+ See [`plugins/README.md`](../../plugins/README.md) for the full
236
+ catalogue.
237
+
238
+ Add the cache directory to `.gitignore`:
239
+
240
+ ```
241
+ .rigor/
242
+ ```
243
+
244
+ ### Step 4 — First run
245
+
246
+ ```sh
247
+ rigor check
248
+ ```
249
+
250
+ A large initial count is normal for a project that has never
251
+ been type-checked.
252
+
253
+ ### Step 5 — Understand the output
254
+
255
+ `rigor triage` summarises the diagnostic stream instead of
256
+ listing every occurrence:
257
+
258
+ ```sh
259
+ rigor triage
260
+ ```
261
+
262
+ It groups results by rule ID, shows per-file hotspots, and
263
+ prints a brief "why" hint for common clusters — for example,
264
+ flagging that a large block of `call.undefined-method` errors
265
+ likely comes from a missing ActiveSupport core_ext bundle, or
266
+ that a gem ships no RBS and `rbs collection install` would help.
267
+
268
+ Use the triage output to decide where to start: genuine bugs
269
+ first, then large clusters to record in a baseline.
270
+
271
+ > **Rails route diagnostics.** `rigor-rails-routes` checks route
272
+ > helpers statically. Most standard Rails patterns are supported,
273
+ > but a few produce false-positive `unknown-helper` diagnostics in
274
+ > v0.1.x:
275
+ >
276
+ > - Routes defined only inside `concern :name do ... end` blocks.
277
+ > The concern body is skipped at definition time (to avoid
278
+ > wrong-arity false positives); helpers injected via
279
+ > `concerns: :name` appear as unknown.
280
+ > - Routes generated by `devise_for :users` and other engine
281
+ > macros — the parser does not execute Ruby code.
282
+ >
283
+ > If you see a cluster of `unknown-helper` on routes you know exist,
284
+ > acknowledge mode is the right approach — record them in a baseline
285
+ > and let the remaining diagnostics surface real issues.
286
+
287
+ ### Step 6 — Generate a baseline (acknowledge mode)
288
+
289
+ *Skip this step if you chose strict mode.*
290
+
291
+ ```sh
292
+ rigor baseline generate
293
+ ```
294
+
295
+ This writes `.rigor-baseline.yml` at the project root. Activate
296
+ it by uncommenting the `baseline:` line in `.rigor.dist.yml`:
297
+
298
+ ```yaml
299
+ baseline: .rigor-baseline.yml
300
+ ```
301
+
302
+ With a baseline active, `rigor check` exits clean on the current
303
+ codebase and surfaces only diagnostics that appear *after* the
304
+ baseline was captured. See [Baselines](06-baseline.md) for the
305
+ full baseline workflow.
306
+
307
+ ### Step 7 — Commit
308
+
309
+ ```sh
310
+ git add mise.toml .rigor.dist.yml .gitignore
311
+ git add .rigor-baseline.yml # if generated in Step 6
312
+ git commit -m "Add Rigor type checker"
313
+ ```
314
+
315
+ `mise.toml` pins Ruby 4.0 and Rigor's version for every
316
+ contributor — `mise install` on another machine restores the
317
+ exact same tools without any project `Gemfile` changes.
318
+
319
+ ---
320
+
321
+ ## What's next
322
+
323
+ - **CI** — add a standalone Rigor job so pull requests are
324
+ gated automatically: [Running Rigor in CI](11-ci.md).
325
+ - **Editor** — inline diagnostics as you type:
326
+ [Editor integration](09-editor-integration.md).
327
+ - **Reducing the baseline** — work through the backlog rule by
328
+ rule using the `rigor-baseline-reduce` skill:
329
+ [Baselines](06-baseline.md).
330
+ - **Plugins** — each plugin's documentation describes its config
331
+ options in detail: [Using plugins](07-plugins.md) and
332
+ [`plugins/`](../../plugins/README.md).
@@ -0,0 +1,204 @@
1
+ # Type-protection coverage
2
+
3
+ Most quality metrics tell you how *much* of something you have:
4
+ lines covered, expressions typed. They rarely answer the question
5
+ that matters most: **if I introduce a bug, would anything catch
6
+ it?**
7
+
8
+ Rigor's type-protection coverage answers that by measuring your
9
+ **types and your tests as one safety net**. A call site is safe
10
+ when *either* the type checker would reject a wrong call *or* a
11
+ test would go red, and it is unguarded only when **neither**
12
+ would. That union is the picture this command draws, with the
13
+ cheaper missing half named at every gap.
14
+
15
+ ## Precision is not protection
16
+
17
+ `rigor coverage` on its own reports **type precision** — the
18
+ fraction of expressions Rigor gives a precise (non-`Dynamic`)
19
+ type:
20
+
21
+ ```sh
22
+ rigor coverage [paths]
23
+ ```
24
+
25
+ That tells you how much Rigor is inferring, which is useful, but
26
+ precision is not protection. A precisely-typed expression you
27
+ never call the wrong way buys you nothing; an untyped one a test
28
+ hammers is safe. To measure *protection* (would a bug be caught)
29
+ add `--protection`. It comes in three tiers, cheapest first.
30
+
31
+ ## Tier 1 — could a bug be caught here? (static, instant)
32
+
33
+ ```sh
34
+ rigor coverage --protection [paths]
35
+ ```
36
+
37
+ Tier 1 classifies every **dispatch site** (a method call with a
38
+ receiver) by whether the receiver has a concrete type — a site
39
+ where Rigor's rules *can* catch a wrong method or argument. It is
40
+ one analysis pass, fast enough to run interactively and in CI, and
41
+ a **sound upper bound**: a concrete receiver is necessary for a
42
+ diagnostic to fire, but not sufficient.
43
+
44
+ The report leads with the protected ratio, then a ranked **"add a
45
+ type here"** list — the methods most often called on a `Dynamic`
46
+ receiver, where a type annotation would buy the most catching
47
+ power. `--threshold=RATIO` turns it into a CI gate (exit `1`
48
+ below the ratio) and `--format=json` carries the structured
49
+ fields.
50
+
51
+ This is the everyday number. When you want the truth behind it,
52
+ move to Tier 2.
53
+
54
+ ## Tier 2 — would a bug *actually* be caught? (mutation)
55
+
56
+ ```sh
57
+ rigor coverage --protection --mutation [paths]
58
+ ```
59
+
60
+ Tier 1 says a site *can* be protected; Tier 2 proves whether it
61
+ *is*. It introduces type-visible breakages at each site — dropping
62
+ a call-argument to `nil`, swapping its type, renaming a call to a
63
+ missing method — re-analyses the mutated code, and reports how many
64
+ Rigor **catches** (the kill rate). A breakage Rigor misses is a
65
+ real "add a type here" site, surfaced with no guesswork.
66
+
67
+ It runs many analyses, so it defaults to the **git-changed** `.rb`
68
+ files (pass explicit paths to widen — whole-project is minutes) and
69
+ is an opt-in CI deep-dive, not an interactive check. The framing is
70
+ always *effectiveness / where to add a type*, never "your code is
71
+ broken": a surviving breakage at a `Dynamic` site is a place
72
+ the type net does not reach.
73
+
74
+ What *does* reach it is your tests.
75
+
76
+ ## The fused view — types **and** tests (`--with-tests`)
77
+
78
+ ```sh
79
+ rigor coverage --protection --mutation --with-tests \
80
+ --test-command "bundle exec rspec" [paths]
81
+ ```
82
+
83
+ This is the heart of the feature. For every breakage the type
84
+ checker does **not** catch, Rigor runs your test suite and asks
85
+ whether a **test** catches it. Each dispatch site lands in one of
86
+ three buckets:
87
+
88
+ | Classification | Meaning |
89
+ | --- | --- |
90
+ | **type-protected** | the type checker would reject the bug |
91
+ | **test-protected** | the types miss it, but a test goes red |
92
+ | **unprotected** | neither — a real, unguarded dispatch site |
93
+
94
+ The report names the **cheaper missing axis** at every gap: a
95
+ `Dynamic`-receiver hole says *add a type*; a typed-but-untested
96
+ hole says *add a test*. A site is reported unprotected only when
97
+ **both** halves miss, which is where the real risk lives, and what
98
+ no types-only or tests-only tool can show you.
99
+
100
+ Cost stays proportional to the hole. A breakage the type checker
101
+ already kills **never reaches the suite** (a gradual short-circuit),
102
+ so the expensive test runs are spent only where the static net has
103
+ a gap. The honest headline becomes *"of the bugs my types let
104
+ through, how many do my tests catch?"*
105
+
106
+ `--format=json` carries `mode` (`protection-fused`), `type_killed`,
107
+ `test_killed`, `unprotected`, `protected_ratio`, per-file rows, and
108
+ `add_protection_here`; `--threshold` gates on the fused ratio.
109
+
110
+ ### The test-command hook
111
+
112
+ `--test-command=CMD` is how Rigor runs your suite (default
113
+ `bundle exec rake`). Two things to know:
114
+
115
+ - **The suite must pass on clean code first**, or "a breakage
116
+ survived" would be meaningless — the run aborts with a clear
117
+ message. Point the command at a plain pass/fail runner; a
118
+ coverage floor that exits non-zero on an otherwise-green
119
+ single-file run will trip this.
120
+ - **The command runs without a shell** (it is split into an argv
121
+ and executed directly), and Rigor strips its own Bundler
122
+ environment so a `bundle exec` command resolves *your* project's
123
+ bundle. So no `env` wrapper is needed — but shell constructs,
124
+ including an inline `BUNDLE_GEMFILE=… ` prefix, are not
125
+ interpreted. For a non-default Gemfile, use
126
+ `bundle config set --local gemfile PATH` or wrap the command in
127
+ `bash -c '…'`.
128
+
129
+ ## `--include-dynamic` — covering your untyped code
130
+
131
+ By default the fused view only mutates sites Rigor can type-check.
132
+ But on dynamic Ruby that is the minority — the receiver of most
133
+ calls is `Dynamic`, and there a **test is the only possible
134
+ protection**. `--include-dynamic` mutates those sites too:
135
+
136
+ ```sh
137
+ rigor coverage --protection --mutation --with-tests --include-dynamic \
138
+ --test-command "bundle exec rspec" --limit 40 [paths]
139
+ ```
140
+
141
+ This completes the map to *every* dispatch site, not just the
142
+ typed ones, and is where the fused view earns its keep — it shows
143
+ which of your untyped code is held up by tests and which is held up
144
+ by nothing. Because every such site is, by definition, something
145
+ the type checker cannot catch, it runs the suite far more, so it is
146
+ an explicit opt-in.
147
+
148
+ `--limit=N` (with `--seed=N`) caps the measurement to a
149
+ deterministic sample of `N` mutations per file, bounding the cost
150
+ on large files; per-file ratios then become estimates, noted on
151
+ stderr so `--format=json` stdout stays clean.
152
+
153
+ ## Reading the report — the "add a type **or** a test here" list
154
+
155
+ The unprotected sites are the payload. In practice they fall into
156
+ a few recognisable kinds, each with a natural fix:
157
+
158
+ - **An untested method body** — a helper the suite never exercises.
159
+ *Add a test*, or widen the test command to cover it.
160
+ - **An unreached branch** — an error path (`raise` in a `rescue`),
161
+ a version-dispatch arm. *Add a test* for that branch, if it
162
+ matters.
163
+ - **A `Dynamic`-receiver collaborator** — a call on an external
164
+ gem object, a framework facade, a duck-typed parameter, or a
165
+ metaprogramming DSL. *Add a type* — often a one-liner from
166
+ [`rigor sig-gen`](02-cli-reference.md#rigor-sig-gen) — so the
167
+ static net starts catching it.
168
+
169
+ A note on completeness: when `--test-command` is scoped to a
170
+ *subset* of your tests, the report over-reports `unprotected` (a
171
+ breakage a *different* test would catch shows as a gap). For an
172
+ accurate map, run the command over all tests that exercise the
173
+ files you are measuring — trading time for completeness.
174
+
175
+ ## Cost and scope
176
+
177
+ The fused tiers run real analyses and real test suites, so treat
178
+ them as a deep-dive, not a per-keystroke check:
179
+
180
+ - **Scope tight.** The changed-files default (no paths given) keeps
181
+ a run proportional to a diff; pass explicit paths to widen
182
+ deliberately.
183
+ - **Keep the suite fast.** Cost is `(breakages measured) ×
184
+ (suite runtime)`. A fast, well-scoped test command is the biggest
185
+ lever.
186
+ - **Cap with `--limit`** on `--include-dynamic` or large files.
187
+
188
+ ## In CI
189
+
190
+ Tier 1 (`--protection`) is cheap enough to gate every run; the
191
+ mutation and fused tiers are better as a scheduled or
192
+ label-triggered deep-dive. All tiers honour `--threshold` (a fused-
193
+ or effectiveness-ratio gate) and `--format=json`, so wiring them
194
+ into a pipeline reuses the same machinery as `rigor check`. See
195
+ [Running Rigor in CI](11-ci.md).
196
+
197
+ ## See also
198
+
199
+ - [CLI command reference](02-cli-reference.md#rigor-coverage) —
200
+ the full flag list for `rigor coverage`.
201
+ - [Inspecting inferred types](05-inspecting-types.md) — when a
202
+ receiver is `Dynamic` and you want to know why.
203
+ - [Provided skills](08-skills.md) — the agent skills that turn an
204
+ "add a type here" list into annotations.