rigortype 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) 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 +557 -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 +532 -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.rb +25 -3
  89. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  91. data/lib/rigor/inference/scope_indexer.rb +87 -89
  92. data/lib/rigor/plugin/isolation.rb +5 -5
  93. data/lib/rigor/plugin/loader.rb +4 -2
  94. data/lib/rigor/version.rb +1 -1
  95. data/skills/rigor-ask/SKILL.md +172 -0
  96. data/skills/rigor-doctor/SKILL.md +87 -0
  97. data/skills/rigor-editor-setup/SKILL.md +114 -0
  98. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  99. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  100. data/skills/rigor-next-steps/SKILL.md +113 -0
  101. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  102. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  103. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  104. data/skills/rigor-upgrade/SKILL.md +79 -0
  105. metadata +90 -1
@@ -0,0 +1,152 @@
1
+ # Configuration
2
+
3
+ Rigor reads a single YAML configuration file from the project
4
+ root. `rigor init` writes a starter one.
5
+
6
+ ## Discovery and precedence
7
+
8
+ With no `--config` flag, Rigor looks for, in order:
9
+
10
+ 1. `.rigor.yml`
11
+ 2. `.rigor.dist.yml`
12
+
13
+ The **first file found wins** — the two are not merged. The
14
+ convention is to commit `.rigor.dist.yml` as the shared
15
+ project config and let an individual developer drop an
16
+ (un-tracked) `.rigor.yml` to override it locally.
17
+
18
+ To *layer* configs rather than replace, a config file can name
19
+ a base with `includes:` (recursive). `--config=PATH` bypasses
20
+ discovery entirely.
21
+
22
+ All relative paths in a config file resolve against that
23
+ file's own directory.
24
+
25
+ ## A minimal config
26
+
27
+ ```yaml
28
+ target_ruby: "4.0"
29
+ paths:
30
+ - lib
31
+ plugins: []
32
+ cache:
33
+ path: .rigor/cache
34
+ ```
35
+
36
+ ## Key reference
37
+
38
+ ### Sources and target
39
+
40
+ | Key | Type | Default | Meaning |
41
+ | --- | --- | --- | --- |
42
+ | `target_ruby` | String | `"4.0"` | The Ruby version *your* project runs — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. |
43
+ | `paths` | Array | `["lib"]` | Directories or files to analyse. |
44
+ | `exclude` | Array | `[]` | Glob patterns to skip. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
45
+ | `includes` | Array | `[]` | Other config files to layer underneath this one. |
46
+ | `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
47
+
48
+ ### Type sources
49
+
50
+ | Key | Type | Default | Meaning |
51
+ | --- | --- | --- | --- |
52
+ | `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
53
+ | `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
54
+ | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches. |
55
+ | `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
56
+
57
+ ### Config validation warnings
58
+
59
+ `rigor check` warns on STDERR when a configured value silently resolves to
60
+ nothing — the class of mistake where a typo loads zero signatures (or
61
+ leaves a suppression inert) and the only symptom is downstream and
62
+ confusing. A missing RBS path, for instance, turns every call into the
63
+ types it was meant to describe into a high-confidence
64
+ `call.undefined-method`, so a one-character mistake can look like hundreds
65
+ of real type errors. The audit covers:
66
+
67
+ ```
68
+ rigor: signature_paths: "/path/to/sig" does not exist (no signatures loaded from it)
69
+ rigor: signature_paths: "/path/to/sig" matched 0 signature files
70
+ rigor: libraries: "csb" is not an available RBS library (no signatures loaded from it)
71
+ rigor: disable: "call.undefined-methdo" is not a recognized rule id; the suppression has no effect
72
+ rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the override has no effect
73
+ rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
74
+ ```
75
+
76
+ These are warnings, not errors — partial or optional bundles and
77
+ forward-looking config are valid setups. The audit only fires on explicit,
78
+ working-setup-safe signals: an unset default (auto-detected `<root>/sig`,
79
+ auto-detected bundle) is never warned about, and a `disable:` /
80
+ `severity_overrides:` token under a *plugin* family (`rspec.…`,
81
+ `rbs_extended.…`) is left alone, since its rule id cannot be enumerated
82
+ statically and may resolve at run time. The same findings appear in the
83
+ `--format=json` payload under `config_warnings` (each tagged with a
84
+ `kind`), so CI can assert on them.
85
+
86
+ ### Diagnostics
87
+
88
+ | Key | Type | Default | Meaning |
89
+ | --- | --- | --- | --- |
90
+ | `disable` | Array | `[]` | Rule IDs or families to suppress project-wide. |
91
+ | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
92
+ | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
93
+ | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
94
+ | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued diagnostic disciplines early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). The overlay is empty in this release, so every form is currently a no-op. |
95
+
96
+ ### Dependency RBS discovery
97
+
98
+ | Key | Type | Default | Meaning |
99
+ | --- | --- | --- | --- |
100
+ | `bundler.auto_detect` | Boolean | `true` | Auto-detect the Bundler install path and lockfile. |
101
+ | `bundler.bundle_path` | String | `nil` | Explicit Bundler install root. |
102
+ | `bundler.lockfile` | String | `nil` | Explicit `Gemfile.lock` path. |
103
+
104
+ `bundler.auto_detect` looks for the Bundler install root in project-local
105
+ locations first — the `path` recorded in `<project>/.bundle/config`, then a
106
+ `<project>/vendor/bundle/` directory — and falls back to a user-global
107
+ `bundle config set --global path …` (`~/.bundle/config`) when the project
108
+ has no in-tree bundle.
109
+
110
+ It deliberately does **not** read `BUNDLE_PATH` from rigor's own
111
+ environment, and it cannot reach gems installed at the *default* shared
112
+ location (the active Ruby's `GEM_HOME`, when no `path` is configured):
113
+ rigor runs in its own isolated Ruby and reads your project as data
114
+ ([ADR-27](../adr/27-tool-distribution-model.md)), so it does not know the
115
+ project Ruby's gem home without running your toolchain. If `rigor check`'s
116
+ `--stats` shows gems whose RBS it could not find, point it at the bundle
117
+ explicitly with `bundler.bundle_path:`, or supply signatures another way:
118
+ `rbs collection install` (auto-discovered) or `dependencies.source_inference:`.
119
+ | `rbs_collection.auto_detect` | Boolean | `true` | Auto-discover `rbs_collection.lock.yaml`. |
120
+ | `rbs_collection.lockfile` | String | `nil` | Explicit `rbs_collection.lock.yaml` path. |
121
+ | `dependencies.source_inference` | Array | `[]` | Per-gem source-inference modes (ADR-10). |
122
+ | `dependencies.budget_per_gem` | Integer | `5000` | Per-gem source-walk cap, counted in **method definitions** (not time): the walker stops harvesting a gem's catalog once it reaches this many `def`s, then emits `dynamic.dependency-source.budget-exceeded` and degrades the rest to `Dynamic[top]`. Range 1250–20000. |
123
+ | `dependencies.budget_overrun_strategy` | String | `"walker_cap"` | What happens to calls on a gem that hit `budget_per_gem` (ADR-10 § 5b). `walker_cap` (default) lets methods past the cap fall through to the engine's normal user-class resolution. `dependency_silence` instead resolves any call on a class from a budget-exceeded gem to `Dynamic[top]`, silencing `call.undefined-method` on that gem's unrecorded surface at the cost of weaker static checking there. |
124
+
125
+ ### Execution
126
+
127
+ | Key | Type | Default | Meaning |
128
+ | --- | --- | --- | --- |
129
+ | `cache.path` | String | `.rigor/cache` | Persistent cache directory. See [Caching](12-caching.md). |
130
+ | `cache.max_bytes` | Integer or `null` | `268435456` (256 MB) | LRU eviction cap for the cache directory; `null` disables eviction. See [Caching § Size and eviction](12-caching.md#size-and-eviction). |
131
+ | `parallel.workers` | Integer | `0` | Parallel worker processes for per-file analysis (fork-based pool today; ADR-15); `0` is sequential. CLI `--workers` and `RIGOR_RACTOR_WORKERS` take precedence. |
132
+ | `plugins_io.network` | String | `"disabled"` | Plugin network policy — `disabled` or `allowlist`. |
133
+ | `plugins_io.allowed_paths` | Array | `[]` | Filesystem paths plugins may read. |
134
+ | `plugins_io.allowed_url_hosts` | Array | `[]` | URL hosts plugins may fetch from when `network: allowlist`. |
135
+
136
+ ## A worked example
137
+
138
+ ```yaml
139
+ target_ruby: "3.4"
140
+ paths:
141
+ - lib
142
+ - app
143
+ exclude:
144
+ - "**/*_pb.rb"
145
+ plugins:
146
+ - rigor-activerecord
147
+ - rigor-rspec
148
+ severity_profile: balanced
149
+ severity_overrides:
150
+ flow.dead-assignment: warning
151
+ baseline: .rigor-baseline.yml
152
+ ```
@@ -0,0 +1,206 @@
1
+ # Diagnostics
2
+
3
+ When `rigor check` finds a problem it reports a **diagnostic**:
4
+ a file, a line and column, a severity, a rule ID, and a
5
+ message. This page is the reference for the rule catalogue,
6
+ the severity model, and suppression. For the *reasoning*
7
+ behind each rule, see
8
+ [handbook chapter 8](../handbook/08-understanding-errors.md).
9
+
10
+ ## Rule IDs
11
+
12
+ Every rule has a two-segment `family.rule` identifier:
13
+
14
+ | Family | Covers |
15
+ | --- | --- |
16
+ | `call` | Call sites — undefined methods, arity, argument types, nil receivers. |
17
+ | `flow` | Control-flow proofs — always-raises, dead branches, constant conditions. |
18
+ | `def` | Method definitions — return types, ivar writes, visibility. |
19
+ | `assert` | `assert_type` checks. |
20
+ | `dump` | `dump_type` notices. |
21
+
22
+ `rigor explain <rule>` prints the full catalogue entry for any
23
+ ID; `rigor explain` with no argument lists them all.
24
+
25
+ ### Catalogue
26
+
27
+ Each rule has a stable per-rule anchor on this page
28
+ (`#rule-<family>-<name>`, dots written as dashes) — the
29
+ `documentation_url` field in `--format json` and `rigor explain`'s
30
+ `Documentation:` line both point here. The `Evidence` column is
31
+ Rigor's confidence that a firing is a true positive (see
32
+ [Evidence tier](#evidence-tier) below).
33
+
34
+ | Rule | Fires when | Evidence |
35
+ | --- | --- | --- |
36
+ | <a id="rule-call-undefined-method"></a>`call.undefined-method` | The method is not defined on the receiver's statically known class. | high |
37
+ | <a id="rule-call-self-undefined-method"></a>`call.self-undefined-method` | An implicit-self call (no receiver) resolves to no method on a confidently-closed standalone class. Ships `:off`; opt in via `severity_overrides`. | low |
38
+ | <a id="rule-call-wrong-arity"></a>`call.wrong-arity` | The positional-argument count matches no signature. | high |
39
+ | <a id="rule-call-argument-type-mismatch"></a>`call.argument-type-mismatch` | An argument's type provably violates the parameter contract. | high |
40
+ | <a id="rule-call-possible-nil-receiver"></a>`call.possible-nil-receiver` | The receiver is `T \| nil` and the method is not defined on `NilClass`. | high |
41
+ | <a id="rule-call-unresolved-toplevel"></a>`call.unresolved-toplevel` | A top-level implicit-self call resolves against no same-file `def`, `pre_eval:` patch, or `Kernel` / `Object` method. | low |
42
+ | <a id="rule-flow-always-raises"></a>`flow.always-raises` | The expression provably raises on every reachable path. | high |
43
+ | <a id="rule-flow-unreachable-branch"></a>`flow.unreachable-branch` | An `if` / `unless` / ternary branch is statically dead. | high |
44
+ | <a id="rule-flow-always-truthy-condition"></a>`flow.always-truthy-condition` | A condition is provably always truthy or always falsey. | medium |
45
+ | <a id="rule-flow-dead-assignment"></a>`flow.dead-assignment` | A local is written but never read in the same method. | medium |
46
+ | <a id="rule-flow-unreachable-clause"></a>`flow.unreachable-clause` | A `case`/`when` or `case`/`in` clause is statically dead — its subject type is disjoint with the pattern, or a prior clause already exhausted the subject. | medium |
47
+ | <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
48
+ | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write. | high |
49
+ | <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
50
+ | <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
51
+ | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
52
+ | <a id="rule-def-override-param-narrowed"></a>`def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance). | high |
53
+ | <a id="rule-rbs_extended-unsatisfied-conformance"></a>`rbs_extended.unsatisfied-conformance` | A class declares `%a{rigor:v1:conforms-to _Interface}` in its RBS but is missing a method the interface requires. Presence-based: only definitively-absent required methods fire. | — |
54
+ | <a id="rule-assert-type-mismatch"></a>`assert.type-mismatch` | An `assert_type` expectation does not match the inferred type. | high |
55
+ | <a id="rule-dump-type"></a>`dump.type` | A `dump_type` call — informational, prints the inferred type. | — |
56
+
57
+ Plugins may contribute further families and rules; `rigor
58
+ explain` lists whatever the active configuration loads.
59
+
60
+ ## Evidence tier
61
+
62
+ Every rule in the catalogue above carries an **evidence tier** —
63
+ Rigor's own confidence that a firing is a *true positive*, derived
64
+ from the rule's firing gates. It is orthogonal to severity (impact) and to
65
+ the severity profile: the tier never changes whether a diagnostic
66
+ surfaces, it only routes attention.
67
+
68
+ | Tier | Meaning |
69
+ | --- | --- |
70
+ | `high` | Fires only on a concrete, statically-known type with no metaprogramming escape. Rigor's false-positive discipline has already filtered the uncertain cases, so a firing is almost always a real problem — a consumer can act on it (or a downstream classifier can trust it) without cross-checking another tool. |
71
+ | `medium` | Rests on a flow- or inference-level proof that inherits a documented false-positive envelope (loop / mutation / RBS-strictness modelling gaps, narrowed by the rule's *does not fire when* list). Usually right, but not literal-provable. |
72
+ | `low` | A resolution- or coverage-gap signal: a firing frequently reflects context the analyzer cannot see (an unanalyzed file, a metaprogramming patch) rather than a definite bug. Treat as "review this" — e.g. route `call.unresolved-toplevel` to a `pre_eval:` decision. |
73
+
74
+ Informational rules (`dump.type`) carry no tier. The per-rule tier
75
+ is the single source of truth in the rule catalogue — read it with
76
+ `rigor explain <rule>` or `rigor explain --format json`, and it is
77
+ echoed on each diagnostic in `rigor check --format json` (below).
78
+
79
+ ## Severity profiles
80
+
81
+ Each rule emits with an authored severity, then a **profile**
82
+ re-stamps it for the run. Three profiles, set with the
83
+ `severity_profile:` config key:
84
+
85
+ | Profile | Stance |
86
+ | --- | --- |
87
+ | `lenient` | Only proven diagnostics are errors; uncertain ones drop to `warning` / `info`. For incremental adoption on legacy code. |
88
+ | `balanced` *(default)* | Most rules `error`; `dump.type` `info`; uncertain rules `warning`. |
89
+ | `strict` | Every rule is an `error`. CI-friendly. |
90
+
91
+ For finer control, `severity_overrides:` maps a rule ID or a
92
+ family to one of `error`, `warning`, `info`, or `off`:
93
+
94
+ ```yaml
95
+ severity_profile: balanced
96
+ severity_overrides:
97
+ flow.always-truthy-condition: off
98
+ call: warning
99
+ ```
100
+
101
+ A rule-specific override beats a family override.
102
+
103
+ ## Machine-readable output (`--format json`)
104
+
105
+ `rigor check --format json` emits the diagnostics as a JSON
106
+ document for editors, CI, and AI agents. Each diagnostic is an
107
+ object with **stable, structured fields** — so a consumer filters
108
+ and groups on them directly and **never parses the human-readable
109
+ `message`** (the wording is presentation, not contract, and may be
110
+ reworded in a minor release):
111
+
112
+ | Field | Present | Meaning |
113
+ | --- | --- | --- |
114
+ | `path` / `line` / `column` | always | Location (1-based line and column). |
115
+ | `severity` | always | `error` / `warning` / `info`. |
116
+ | `rule` | always (`null` for parse / internal errors) | The `family.rule` ID. |
117
+ | `source_family` | always | `builtin`, `rbs_extended`, `generated.*`, or `plugin.<id>`. |
118
+ | `message` | always | Human-readable text — *presentation, not contract*. |
119
+ | `receiver_type` | when the rule has a receiver | The called receiver's displayed type (`String`, `Array[User]`, …). |
120
+ | `method_name` | when the rule has a method | The called / defined method name. |
121
+ | `project_definition_site` | `call.undefined-method` monkey-patch case | `path:line` where the project itself defines the method (ADR-17). |
122
+ | `evidence_tier` | built-in rules with a tier | `high` / `medium` / `low` — Rigor's confidence the firing is a true positive ([Evidence tier](#evidence-tier)). |
123
+ | `documentation_url` | built-in rules | A stable URL to the rule's entry in this catalogue. |
124
+
125
+ `evidence_tier` lets a consumer prioritise without re-deriving
126
+ confidence — e.g. surface only `high` firings in a strict CI gate,
127
+ or route `low` firings to a human review queue:
128
+
129
+ ```sh
130
+ # only the high-confidence diagnostics
131
+ rigor check --format json \
132
+ | jq '[.diagnostics[] | select(.evidence_tier == "high")]'
133
+ ```
134
+
135
+ ### Coverage block (`--coverage`)
136
+
137
+ `rigor check --coverage` adds a top-level `coverage` object so a single
138
+ run reports both *what fired* and *how much of the analyzed surface
139
+ Rigor could type* — useful when a large diagnostic count raises the
140
+ question "did it analyze all my files, or only a few?". The block
141
+ mirrors the `summary` of `rigor check`'s sibling
142
+ [`rigor coverage`](02-cli-reference.md#rigor-coverage) (same
143
+ precision-tier vocabulary), plus `scan_files`:
144
+
145
+ ```jsonc
146
+ "coverage": {
147
+ "scan_files": 203,
148
+ "parse_errors": 0,
149
+ "expressions_typed": 18394,
150
+ "precise_count": 9847,
151
+ "precise_ratio": 0.535,
152
+ "dynamic_opaque_count": 8547,
153
+ "dynamic_opaque_ratio": 0.465
154
+ }
155
+ ```
156
+
157
+ It is **off by default** — computing it is a second precision pass over
158
+ the analyzed files — so the default check path's cost is unchanged. In
159
+ text mode `--coverage` prints a one-line summary instead. For the full
160
+ per-file / per-tier breakdown, run `rigor coverage` directly.
161
+
162
+ The `receiver_type` / `method_name` pair is populated by the
163
+ call-family rules and the method-level `def.*` rules. Group a run
164
+ by the called class and method with `jq`, no message parsing:
165
+
166
+ ```sh
167
+ # every diagnostic that names a method, as {receiver, method, rule}
168
+ rigor check --format json \
169
+ | jq '[.diagnostics[] | select(.method_name) | {receiver: .receiver_type, method: .method_name, rule}]'
170
+ ```
171
+
172
+ The `check` stream is **faithful per-site** — a literal receiver
173
+ reports its literal type (`"hi"`, `42`). For the **aggregated**
174
+ view — counts per class/method across the whole run, with literal
175
+ receivers folded to their class — use
176
+ [`rigor triage`](02-cli-reference.md)'s `selectors` section.
177
+
178
+ ## Suppressing a diagnostic
179
+
180
+ Three layers, from narrowest to broadest.
181
+
182
+ **In-source, one line.** A trailing comment suppresses the
183
+ named rules on that line:
184
+
185
+ ```ruby
186
+ config.merge(extra) # rigor:disable call.undefined-method
187
+ ```
188
+
189
+ It accepts qualified IDs, family wildcards (`call`), a
190
+ comma- or space-separated list, or `all`.
191
+
192
+ **In-source, whole file.** `# rigor:disable-file <rules>`
193
+ anywhere in a file suppresses those rules for every line;
194
+ `# rigor:disable-file all` silences the file.
195
+
196
+ **Project-wide.** The `disable:` config key turns rules off
197
+ across the whole run:
198
+
199
+ ```yaml
200
+ disable:
201
+ - flow.dead-assignment
202
+ ```
203
+
204
+ For a *known backlog* you want to keep visible but not fail
205
+ on, prefer a [baseline](06-baseline.md) over a blanket
206
+ `disable:` — `disable:` also hides any new occurrences.
@@ -0,0 +1,109 @@
1
+ # Inspecting inferred types
2
+
3
+ Rigor's analysis is invisible by default — it only speaks up to
4
+ report a diagnostic. When you want to *see* what type the
5
+ engine assigns an expression, there are four tools: two source
6
+ helpers and two CLI commands.
7
+
8
+ ## `dump_type` — print a type from source
9
+
10
+ `dump_type(expr)` makes Rigor emit an `info`-severity
11
+ `dump.type` diagnostic showing the inferred type of `expr`. At
12
+ runtime it is a no-op that returns `expr` unchanged, so it is
13
+ safe to leave in or sprinkle freely while debugging.
14
+
15
+ ```ruby
16
+ require "rigor/testing"
17
+ include Rigor::Testing
18
+
19
+ dump_type(1 + 2) # rigor reports: dump.type — Constant<3>
20
+ ```
21
+
22
+ Rigor recognises the call when it is written as `dump_type(…)`
23
+ after `include Rigor::Testing`, or fully qualified as
24
+ `Rigor::Testing.dump_type(…)` / `Rigor.dump_type(…)`.
25
+
26
+ ## `assert_type` — pin a type in source
27
+
28
+ `assert_type("TypeString", expr)` compares `expr`'s inferred
29
+ type against the literal type string. On a mismatch Rigor
30
+ emits an `error`-severity `assert.type-mismatch` diagnostic; on
31
+ a match it stays silent. Like `dump_type`, it returns `expr`
32
+ unchanged at runtime.
33
+
34
+ ```ruby
35
+ assert_type("Constant<3>", 1 + 2) # silent — matches
36
+ assert_type("Integer", 1 + 2) # assert.type-mismatch
37
+ ```
38
+
39
+ The type string is matched against the engine's short display
40
+ form. `assert_type` is how the handbook's examples stay honest,
41
+ and it doubles as a regression check you can keep in a
42
+ project's own test sources.
43
+
44
+ ## `rigor annotate` — types in the margin
45
+
46
+ `rigor annotate FILE` reprints a whole file with every line
47
+ tagged by the type of the expression it evaluates to, as a
48
+ trailing `#=>` comment (the xmpfilter / seeing_is_believing
49
+ convention):
50
+
51
+ ```ruby
52
+ two = 1 + 1 #=> 2
53
+ name = gets #=> String | nil
54
+ ```
55
+
56
+ It is the fastest way to survey a file. The annotation is
57
+ idempotent — re-running replaces the previous `#=>` comment
58
+ (including hand-written ones, and the pre-v0.2.0
59
+ `#=> dump_type:` spelling) rather than stacking it. Output is
60
+ syntax-highlighted for a tty — through
61
+ [`bat`](https://github.com/sharkdp/bat) when it is found on
62
+ `PATH` (`--no-bat` opts out), otherwise via the built-in
63
+ colorizer; `--no-color` (and the `NO_COLOR` environment
64
+ variable) disable the colour.
65
+
66
+ ## `rigor type-of` — one position
67
+
68
+ When you only need one expression's type — typically while
69
+ chasing down why a diagnostic did or did not fire — query a
70
+ single position:
71
+
72
+ ```sh
73
+ rigor type-of lib/example.rb:12:8
74
+ ```
75
+
76
+ `--format=json` emits a machine-readable result for tooling.
77
+ This is the same query the editor integration answers on
78
+ hover.
79
+
80
+ ## `rigor trace` — watch the inference happen
81
+
82
+ Where `annotate` and `type-of` show the *answer*, `rigor trace
83
+ FILE` shows the *derivation*: it re-runs the engine over the
84
+ file and replays the recorded inference events as a
85
+ step-through terminal animation — the moment a local enters the
86
+ scope, the moment two branch types merge into a union, the
87
+ moment a method call resolves (or fail-softens to
88
+ `Dynamic[top]`).
89
+
90
+ ```sh
91
+ rigor trace lib/example.rb # step on key press
92
+ rigor trace --delay=0.5 lib/example.rb # autoplay
93
+ rigor trace --format=json lib/example.rb # raw event stream
94
+ ```
95
+
96
+ `--verbose` adds an enter/result frame for every expression the
97
+ typer visits; the default keeps only the three teachable event
98
+ kinds. The JSON stream is stable enough to build course
99
+ material or figures from.
100
+
101
+ ## Which to reach for
102
+
103
+ | You want… | Use |
104
+ | --- | --- |
105
+ | One expression's type, from the shell | `rigor type-of` |
106
+ | Every line of a file surveyed | `rigor annotate` |
107
+ | The derivation replayed step by step | `rigor trace` |
108
+ | A type printed mid-analysis, in context | `dump_type` |
109
+ | A type *asserted* and regression-checked | `assert_type` |
@@ -0,0 +1,104 @@
1
+ # Baselines
2
+
3
+ A **baseline** records the diagnostics a project already has,
4
+ so `rigor check` can stay silent about them and surface only
5
+ what is *new*. It is the pragmatic on-ramp for adopting Rigor
6
+ on an existing codebase: you do not have to reach zero
7
+ diagnostics before the check becomes useful in CI.
8
+
9
+ ## The baseline file
10
+
11
+ A baseline is a YAML file — `.rigor-baseline.yml` by
12
+ convention — listing buckets of known diagnostics:
13
+
14
+ ```yaml
15
+ version: 1
16
+ ignored:
17
+ - file: app/models/user.rb
18
+ rule: call.undefined-method
19
+ count: 3
20
+ - file: app/lib/legacy.rb
21
+ rule: call.argument-type-mismatch
22
+ count: 1
23
+ ```
24
+
25
+ Each row is a **bucket** keyed by `(file, rule)`, with a
26
+ `count` of how many diagnostics of that rule the file is
27
+ allowed. An optional `message:` field (a regular-expression
28
+ source) narrows a bucket to call sites whose message matches.
29
+
30
+ You do not hand-write this file — `rigor baseline generate`
31
+ produces it.
32
+
33
+ ## Turning a baseline on
34
+
35
+ A baseline file sitting in the project is **dormant** until
36
+ something activates it — presence alone does nothing. Activate
37
+ it with the config key:
38
+
39
+ ```yaml
40
+ baseline: .rigor-baseline.yml
41
+ ```
42
+
43
+ or per run with `rigor check --baseline=PATH`. `--no-baseline`
44
+ ignores a configured one for a single run, and `baseline:
45
+ false` in a config file disables a baseline inherited through
46
+ `includes:`.
47
+
48
+ ## All-or-nothing per bucket
49
+
50
+ When a baseline is active, each bucket is matched whole:
51
+
52
+ - **current count ≤ recorded count** — every diagnostic in the
53
+ bucket is silenced.
54
+ - **current count > recorded count** — every diagnostic in the
55
+ bucket surfaces, not just the excess.
56
+
57
+ The reasoning: line numbers drift as a file is edited, so a
58
+ partial match cannot reliably point at *which* diagnostic is
59
+ the new one. Surfacing the whole bucket asks you to review
60
+ that rule × file together.
61
+
62
+ The baseline filter runs **last** — after `# rigor:disable`
63
+ comments and the severity profile. A baseline never resurrects
64
+ a diagnostic another layer has already suppressed.
65
+
66
+ ## The `rigor baseline` commands
67
+
68
+ | Command | Use |
69
+ | --- | --- |
70
+ | `rigor baseline generate` | Write a fresh baseline from the current diagnostics. Refuses to clobber an existing file without `--force`. |
71
+ | `rigor baseline regenerate` | Rewrite unconditionally — run it after fixing diagnostics so the file shrinks. |
72
+ | `rigor baseline dump` | Print the baseline, filterable with `--rule` and `--file`. |
73
+ | `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
74
+ | `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
75
+
76
+ `generate` and `regenerate` take `--match-mode=rule` (the
77
+ default — one bucket per file × rule) or `--match-mode=message`
78
+ (a bucket per distinct message: more precise, more churn).
79
+
80
+ ## Working a baseline down
81
+
82
+ `rigor triage` summarises a diagnostic stream — rule
83
+ distribution, class/method selectors, the files with the most
84
+ diagnostics, and heuristic hints about likely causes — so you can
85
+ decide what to tackle first:
86
+
87
+ ```sh
88
+ rigor triage
89
+ ```
90
+
91
+ The `selectors` section (`rigor triage --format json | jq
92
+ '.selectors'`) is the best prioritisation signal: a class/method
93
+ with a high `count` spread across many `files` is a systemic cause
94
+ one fix or a `pre_eval:` entry clears in bulk, while a low-`count`
95
+ selector is a candidate genuine bug to fix at the site.
96
+
97
+ It is advisory and always exits `0`. The intended loop is
98
+ `triage` to prioritise → fix or suppress a rule → `rigor
99
+ baseline regenerate` to shrink the file. The
100
+ [`rigor-baseline-reduce` skill](08-skills.md) walks this loop
101
+ interactively.
102
+
103
+ To make CI fail when the baseline *grows*, add
104
+ `--baseline-strict` to `rigor check`.
@@ -0,0 +1,92 @@
1
+ # Using plugins
2
+
3
+ A plugin teaches Rigor about a framework, a gem, or an
4
+ application DSL that plain inference cannot see — Rails route
5
+ helpers, RSpec `let` bindings, dry-rb struct attributes, and
6
+ so on. This page is about *activating* plugins. Writing one is
7
+ covered by [`examples/`](../../examples/README.md) and the
8
+ [`rigor-plugin-author` skill](08-skills.md).
9
+
10
+ ## Activating a plugin
11
+
12
+ List plugins under the `plugins:` key in your config file:
13
+
14
+ ```yaml
15
+ plugins:
16
+ - rigor-activerecord
17
+ - rigor-rspec
18
+ - rigor-rails-routes
19
+ ```
20
+
21
+ Each name is a plugin that ships bundled inside the `rigortype`
22
+ gem — no separate installation is needed. Listing a plugin under
23
+ `plugins:` is enough to activate it. A plugin that needs
24
+ configuration takes the object form:
25
+
26
+ ```yaml
27
+ plugins:
28
+ - gem: rigor-activerecord
29
+ config:
30
+ schema: db/schema.rb
31
+ ```
32
+
33
+ ## Available plugins
34
+
35
+ Rigor ships a catalogue of production plugins under
36
+ [`plugins/`](../../plugins/README.md). The set grows between
37
+ releases — consult that directory for the current list and
38
+ each plugin's options — but the families today are:
39
+
40
+ - **Rails** — `rigor-activerecord`, `rigor-actionpack`,
41
+ `rigor-rails-routes`, `rigor-rails-i18n`,
42
+ `rigor-actionmailer`, `rigor-activejob`,
43
+ `rigor-activestorage`, `rigor-actioncable`. The
44
+ `rigor-rails` meta-gem bundles the Rails set for Gemfile
45
+ convenience; you still enumerate the individual plugins you
46
+ want under `plugins:`.
47
+ - **Testing** — `rigor-rspec`, `rigor-rspec-rails`,
48
+ `rigor-minitest`, `rigor-shoulda-matchers`,
49
+ `rigor-factorybot`.
50
+ - **dry-rb** — `rigor-dry-types`, `rigor-dry-schema`,
51
+ `rigor-dry-struct`, `rigor-dry-validation`.
52
+ - **Other ecosystems** — `rigor-sinatra`, `rigor-hanami`,
53
+ `rigor-devise`, `rigor-pundit`, `rigor-sidekiq`,
54
+ `rigor-graphql`, `rigor-statesman`, `rigor-sorbet`,
55
+ `rigor-typescript-utility-types`,
56
+ `rigor-activesupport-core-ext`.
57
+
58
+ ## `plugins/` versus `examples/`
59
+
60
+ [`plugins/`](../../plugins/README.md) holds production plugins
61
+ for real gems and frameworks — the ones you activate. The
62
+ [`examples/`](../../examples/README.md) tree holds *tutorial*
63
+ plugins over deliberately simplified DSLs; they are reading
64
+ material for plugin authors, not for activation in a real
65
+ project.
66
+
67
+ ## Sandboxing
68
+
69
+ A plugin may want to read a file (a schema dump) or reach the
70
+ network. Those are gated by the `plugins_io:` config keys —
71
+ the network is `disabled` by default, and a plugin can read
72
+ only the paths you list. See
73
+ [Configuration](03-configuration.md).
74
+
75
+ ### Isolation strategy
76
+
77
+ A few plugins call into their target library directly (for
78
+ example to ask ActiveSupport's real inflector how to pluralise a
79
+ class name). That call runs under an **isolation strategy**, set
80
+ with the `RIGOR_PLUGIN_ISOLATION` environment variable:
81
+
82
+ | Value | Behaviour |
83
+ | --- | --- |
84
+ | `process` (default) | Run the call in a forked, crash-contained worker, so the target library's monkey-patches and any crash never contaminate Rigor. Falls back to `none` where `fork` is unavailable (Windows / JRuby). |
85
+ | `none` | Load the library into Rigor's own process and call it directly. |
86
+ | `ruby_box` | Run inside an experimental `Ruby::Box` sandbox. This needs the `RUBY_BOX=1` start flag, so the `rigor` launcher re-execs itself with it set when you select this strategy. |
87
+
88
+ The legacy `RIGOR_BOX` environment variable is a back-compat
89
+ alias for `RIGOR_PLUGIN_ISOLATION=ruby_box`. The default
90
+ (`process`) is the right choice for almost everyone; the variable
91
+ exists for the rare platform where forking is unavailable or
92
+ where you want stronger containment.