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,59 @@
1
+ # rigor-dry-types
2
+
3
+ The foundation plugin for the dry-rb family. It recognises the
4
+ canonical dry-types alias module —
5
+
6
+ ```ruby
7
+ module Types
8
+ include Dry.Types()
9
+ end
10
+ ```
11
+
12
+ — and makes the alias names it generates resolvable to their
13
+ underlying Ruby classes, publishing them as a cross-plugin fact
14
+ (`:dry_type_aliases`) that [`rigor-dry-struct`](../07-plugins.md),
15
+ `rigor-dry-schema`, and `rigor-dry-validation` consume.
16
+
17
+ It ships bundled in `rigortype`. Activate it alongside the dry-rb
18
+ plugins that consume it:
19
+
20
+ ```yaml
21
+ plugins:
22
+ - rigor-dry-types
23
+ - rigor-dry-struct
24
+ # - rigor-dry-schema / rigor-dry-validation as needed
25
+ ```
26
+
27
+ ## No diagnostics of its own
28
+
29
+ This plugin emits **no diagnostics** and has **no config keys** —
30
+ it only supplies type information to the other dry-rb plugins, so
31
+ its visible effect surfaces through them (a `Types::String`-typed
32
+ attribute resolving downstream, etc.).
33
+
34
+ ## What it recognises
35
+
36
+ - **Canonical shortcuts** — `Types::String` / `Integer` / `Float` /
37
+ `Decimal` → `BigDecimal` / `Bool` / `Date` / `Hash` / `Array` /
38
+ `Any` → `Object`, and the rest.
39
+ - **Coercion-category namespaces** — the same names under
40
+ `Coercible::` / `Strict::` / `Params::` / `JSON::` (they share an
41
+ underlying class; the difference is runtime coercion).
42
+ - **Nested alias modules** — `module App; module Types; include
43
+ Dry.Types(); end; end` publishes `App::Types::String`, etc.
44
+ - **User-authored single-head compositions** — `Email =
45
+ String.constrained(format: …)`, `Status = Strict::Symbol`,
46
+ `PositiveInt = Integer.constrained(gt: 0).optional` publish under
47
+ the head's underlying class.
48
+
49
+ Unions (`String | Integer`), intersections, and transitive
50
+ references to other compositions (`ManagerEmail = Email`) are
51
+ skipped — there's no single underlying class to publish.
52
+
53
+ ## Plugin internals
54
+
55
+ The `prepare(services)` scan, the published `:dry_type_aliases`
56
+ table, and the slice floor/ceiling are documented in the
57
+ [plugin's README](../../../plugins/rigor-dry-types/README.md). To
58
+ write a plugin, see [`examples/`](../../../examples/README.md) and
59
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,62 @@
1
+ # rigor-dry-validation
2
+
3
+ Recognises `class T < Dry::Validation::Contract` subclasses,
4
+ publishes the set of contract class names as a cross-plugin fact
5
+ (`:dry_validation_contracts`), and ships an RBS overlay typing the
6
+ contract result API (`Contract#call → Result`, then `Result#success?`
7
+ / `#failure?` / `#to_h` / `#errors` / `#[]`).
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
10
+
11
+ ```yaml
12
+ plugins:
13
+ - rigor-dry-validation
14
+ ```
15
+
16
+ ## What it recognises
17
+
18
+ ```ruby
19
+ class NewUserContract < Dry::Validation::Contract
20
+ params do
21
+ required(:email).filled(:string)
22
+ required(:age).value(:integer)
23
+ end
24
+ end
25
+ ```
26
+
27
+ The plugin recognises both the full-path
28
+ `Dry::Validation::Contract` and the lexical-`Dry` form
29
+ `Validation::Contract`, and publishes a sorted list of the
30
+ discovered contract names.
31
+
32
+ With the RBS overlay loaded (see below):
33
+
34
+ ```ruby
35
+ result = NewUserContract.new.call(input) # Dry::Validation::Result
36
+ result.success? # bool
37
+ result.to_h # Hash[Symbol, untyped]
38
+ ```
39
+
40
+ ## RBS overlay
41
+
42
+ The plugin bundles an RBS overlay (`sig/dry_validation.rbs`) that
43
+ types the result API above, and **contributes it automatically** —
44
+ the plugin's manifest declares `signature_paths: ["sig"]`
45
+ ([ADR-25](../../adr/25-plugin-contributed-rbs.md)), so activating
46
+ `rigor-dry-validation` is enough; no project-side `signature_paths:`
47
+ wiring is required.
48
+
49
+ ## No diagnostics, no config
50
+
51
+ The plugin publishes the contract list and the overlay; it emits no
52
+ diagnostics and takes no config keys. (Future slices add
53
+ `result.to_h` typing from a paired `rigor-dry-schema` `params` block
54
+ and per-contract `rule`-key diagnostics.)
55
+
56
+ ## Plugin internals
57
+
58
+ The `prepare(services)` scan, the `:dry_validation_contracts` fact,
59
+ the RBS overlay, and the slice floor/ceiling are documented in the
60
+ [plugin's README](../../../plugins/rigor-dry-validation/README.md).
61
+ To write a plugin, see [`examples/`](../../../examples/README.md)
62
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,76 @@
1
+ # rigor-factorybot
2
+
3
+ Validates every `FactoryBot.create(:name, key: …)` / `.build(…)` /
4
+ `.build_stubbed(…)` / `.attributes_for(…)` / `*_list` call against
5
+ an index of your factory definitions: an unknown factory name or an
6
+ attribute key the factory doesn't declare is flagged (each with a
7
+ did-you-mean). When [`rigor-activerecord`](rigor-activerecord.md) is
8
+ also active, attribute keys are additionally cross-checked against
9
+ the model's columns. No FactoryBot runtime dependency.
10
+
11
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-factorybot
16
+ # - rigor-activerecord # optional: enables the AR column cross-check
17
+ ```
18
+
19
+ ## What it checks
20
+
21
+ ```ruby
22
+ # spec/factories/users.rb
23
+ FactoryBot.define do
24
+ factory :user do
25
+ name { "Alice" }
26
+ email { "alice@example.com" }
27
+ end
28
+ end
29
+
30
+ FactoryBot.create(:user, name: "X") # ✓ info trace
31
+ FactoryBot.build(:post, headline: "Hi") # ✗ unknown-attribute (suggest :title)
32
+ FactoryBot.create(:usre) # ✗ unknown-factory (suggest :user)
33
+ ```
34
+
35
+ | Rule | Severity | Fires when |
36
+ | --- | --- | --- |
37
+ | `plugin.factorybot.factory-call` | info | the call resolved to a known factory; lists its declared attributes |
38
+ | `plugin.factorybot.unknown-factory` | error | the literal `:name` isn't in the factory index (with a did-you-mean) |
39
+ | `plugin.factorybot.unknown-attribute` | error | a keyword key isn't a declared attribute (with a did-you-mean); also checked against the model's columns when `:model_index` is available |
40
+
41
+ The legacy `FactoryGirl` constant is recognised the same way.
42
+ Recognised entry methods: `create` / `build` / `build_stubbed` /
43
+ `attributes_for` and the `*_list` variants. Inside a factory it
44
+ recognises `name { … }` (modern), `name "…"` (legacy positional),
45
+ and `add_attribute(:name) { … }`.
46
+
47
+ ## Configuration
48
+
49
+ ```yaml
50
+ plugins:
51
+ - gem: rigor-factorybot
52
+ config:
53
+ factory_search_paths: ["spec/factories", "spec/factories.rb"] # default
54
+ # Minitest projects: ["test/factories"]
55
+ ```
56
+
57
+ ## Limitations
58
+
59
+ - **Literal arguments only** — `FactoryBot.create(name)` with a
60
+ variable name passes through.
61
+ - **Traits / sequences / associations not collected yet** — an
62
+ attribute defined only inside `trait :admin do … end` can surface
63
+ a false `unknown-attribute` until the trait slice ships.
64
+ - **Explicit receiver only** — bare `create(:user)` (from
65
+ `include FactoryBot::Syntax::Methods`) is not recognised in this
66
+ slice; it needs receiver-type inference, which would otherwise
67
+ false-positive on every unrelated `create` call.
68
+
69
+ ## Plugin internals
70
+
71
+ The factory discoverer / index, the cached producer, the
72
+ `:model_index` consumption, the demo, and the contract surfaces
73
+ this plugin exercises are in the
74
+ [plugin's README](../../../plugins/rigor-factorybot/README.md). To
75
+ write a plugin, see [`examples/`](../../../examples/README.md) and
76
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,89 @@
1
+ # rigor-graphql
2
+
3
+ Recognises GraphQL-Ruby schema classes — `Schema::Object`,
4
+ `Schema::Enum`, `Schema::InputObject`, `Schema::Mutation` subclasses —
5
+ and walks their `field` / `value` / `argument` DSL declarations,
6
+ publishing the resulting type tables as
7
+ [ADR-9](../../adr/9-cross-plugin-api.md) cross-plugin facts that
8
+ downstream plugins can consume. graphql-ruby's `field` DSL is a pure
9
+ metadata recorder (it synthesises no Ruby methods), so Rigor's value
10
+ here is a static type table rather than method synthesis. It reads
11
+ source only, with no `graphql` runtime dependency.
12
+
13
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
14
+
15
+ ```yaml
16
+ plugins:
17
+ - rigor-graphql
18
+ ```
19
+
20
+ ## What it infers
21
+
22
+ ```ruby
23
+ module Types
24
+ class User < GraphQL::Schema::Object
25
+ field :name, String, null: false
26
+ field :email, String, null: true
27
+ field :tags, [String], null: false # list-of form
28
+ end
29
+ end
30
+ ```
31
+
32
+ publishes a `:graphql_type_table` fact:
33
+
34
+ ```ruby
35
+ {
36
+ "Types::User" => {
37
+ "name" => { type: "String", nullable: false, list: false },
38
+ "email" => { type: "String", nullable: true, list: false },
39
+ "tags" => { type: "String", nullable: false, list: true }
40
+ }
41
+ }
42
+ ```
43
+
44
+ It publishes four independent facts from one project walk; consumers
45
+ that need only one (or none) are unaffected by the others:
46
+
47
+ | Fact | Source class | Shape |
48
+ | --- | --- | --- |
49
+ | `:graphql_type_table` | `Schema::Object` | `field` → `{type, nullable, list}` |
50
+ | `:graphql_enum_table` | `Schema::Enum` | `value "..."` → ordered value list |
51
+ | `:graphql_input_object_table` | `Schema::InputObject` | `argument` → `{type, required, list}` |
52
+ | `:graphql_mutation_table` | `Schema::Mutation` | `{arguments:, fields:}` combined |
53
+
54
+ Canonical GraphQL scalars map to Ruby classes (`String`→`String`,
55
+ `Integer`/`Int`→`Integer`, `Boolean`→`TrueClass`, `Float`→`Float`,
56
+ `ID`→`String`); user-defined types are recorded under their qualified
57
+ name. `null:` extracts to `nullable:` (defaulting to `true`, mirroring
58
+ graphql-ruby); `required:` defaults to `false`. The single-element
59
+ `[String]` list form is recognised.
60
+
61
+ ## No diagnostics, no config
62
+
63
+ The plugin emits no diagnostics and has no configuration knobs — it
64
+ contributes the type tables above for other plugins to consume. It
65
+ walks every `paths:` entry's `.rb` files for the schema-class shapes.
66
+
67
+ ## Limitations
68
+
69
+ - **No resolver-method type-check.** `field` declarations are recorded
70
+ as metadata; they are not yet cross-referenced against the Ruby
71
+ resolver methods that back them.
72
+ - **No `Schema.execute(...)` result typing.** Typing
73
+ `Schema.execute(query).to_h` against the queried fields is a future
74
+ plugin.
75
+ - **Constant-form types only.** The string form (`field :foo, "User"`)
76
+ and the `<Type>.array` / `<Type>!` sugar chains are not recognised
77
+ (the `[String]` bracket form is). Multi-element and empty list
78
+ literals are dropped.
79
+ - **Enum literal values only.** `value "ACTIVE"` registers; the
80
+ symbol-form (`value :ACTIVE`) and constant-form drop, and the
81
+ `value:` / `description:` kwargs ride along but stay out of the table.
82
+ - **No cache round-trip** — the walk re-runs each invocation.
83
+
84
+ ## Plugin internals
85
+
86
+ The type scanner and the contract surfaces this plugin exercises are in
87
+ the [plugin's README](../../../plugins/rigor-graphql/README.md). To
88
+ write a plugin, see [`examples/`](../../../examples/README.md) and the
89
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,83 @@
1
+ # rigor-hanami
2
+
3
+ Enforces the **Hanami::Action protocol** for Hanami 3.x apps: every
4
+ class under `app/actions/**/*.rb` must define `#handle(request,
5
+ response)`, and inside those bodies Rigor types the two parameters as
6
+ `Hanami::Action::Request` / `Hanami::Action::Response` so misuse is
7
+ caught precisely. It is the reference production consumer of the
8
+ [ADR-28](../../adr/28-path-scoped-protocol-contracts.md) path-scoped
9
+ method-protocol contract — Rigor *provides* the parameter types and the
10
+ plugin *checks* the method shape. It reads source only, with no
11
+ `hanami` runtime dependency (it ships RBS stubs for the Request /
12
+ Response / Params surface).
13
+
14
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
15
+
16
+ ```yaml
17
+ plugins:
18
+ - rigor-hanami
19
+ ```
20
+
21
+ ## What it checks
22
+
23
+ ```ruby
24
+ # app/actions/books/index.rb
25
+ module Bookshelf
26
+ module Actions
27
+ module Books
28
+ class Index < Bookshelf::Action
29
+ def handle(request, response)
30
+ response.status = 200 # response: Hanami::Action::Response
31
+ response.body = request.params[:q] # request: Hanami::Action::Request
32
+ request.no_such_method # error: call.undefined-method
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ ```
39
+
40
+ | Rule | Severity | Fires when |
41
+ | --- | --- | --- |
42
+ | `plugin.hanami.missing-handle-method` | error | a class in a matching file defines no `#handle` |
43
+ | `plugin.hanami.handle-arity-mismatch` | error | `#handle` is defined with a parameter count other than 2 |
44
+
45
+ Inside a conforming `#handle`, misuse of `request` / `response`
46
+ surfaces as the engine's own `call.undefined-method` — the same as if
47
+ the types had been declared in RBS. Return-type conformance is *not*
48
+ checked: `#handle` is void by contract (the response is mutated
49
+ in-place), so checking the return value would generate false positives
50
+ on every conditional branch. Parameter names are arbitrary (positions,
51
+ not names, are bound); only directly-defined `#handle` is checked, not
52
+ an inherited one.
53
+
54
+ ## Configuration
55
+
56
+ ```yaml
57
+ plugins:
58
+ - gem: rigor-hanami
59
+ config:
60
+ action_path: "app/actions/**/*.rb" # default
61
+ ```
62
+
63
+ Override `action_path` for a custom slice layout, e.g.
64
+ `"slices/main/actions/**/*.rb"`. The override retargets both the
65
+ parameter-type provision and the `#handle` check.
66
+
67
+ ## Limitations
68
+
69
+ - **Exact arity.** `#handle` must take exactly two parameters; optional
70
+ or keyword-only forms are flagged as `handle-arity-mismatch`.
71
+ - **Direct definition only.** A `#handle` inherited from a base class
72
+ (rather than defined on the class itself) is not detected.
73
+ - **Stubbed surface.** The bundled RBS covers the documented Request /
74
+ Response / Params methods; calls outside that surface resolve against
75
+ the stubs, not a live Hanami install.
76
+
77
+ ## Plugin internals
78
+
79
+ The `ProtocolContract` declaration, the action checker, the RBS stubs,
80
+ and the ADR-28 provide-and-check split are in the
81
+ [plugin's README](../../../plugins/rigor-hanami/README.md). To write a
82
+ plugin, see [`examples/`](../../../examples/README.md) and the
83
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,73 @@
1
+ # rigor-mangrove
2
+
3
+ Sharpens [Mangrove](https://github.com/) `Result` / `Option`
4
+ unwrap-family return types from `untyped` to the concrete
5
+ type-argument carried at the call site, and synthesises the
6
+ subclasses an `Enum`'s `variants do … end` DSL generates at runtime
7
+ so they resolve statically. It layers on top of
8
+ [`rigor-sorbet`](rigor-sorbet.md) (which ingests Mangrove's `sig` /
9
+ RBI surface); this plugin adds the two precision steps sig-level
10
+ types can't reach. No runtime dependency.
11
+
12
+ It ships bundled in `rigortype`. Activate it under `plugins:`
13
+ (alongside `rigor-sorbet`, which supplies the carrier types):
14
+
15
+ ```yaml
16
+ plugins:
17
+ - rigor-sorbet
18
+ - rigor-mangrove
19
+ ```
20
+
21
+ ## What it infers — no diagnostics, no config
22
+
23
+ The plugin emits no diagnostics of its own and has no
24
+ configuration. It contributes types in two places; the engine's
25
+ ordinary method-existence check then catches typos on the
26
+ now-known types.
27
+
28
+ **Unwrap-family return types.** When the receiver is a Mangrove
29
+ carrier carrying a type argument, the unwrap return narrows to that
30
+ argument:
31
+
32
+ ```ruby
33
+ # token : Result::Ok[String, StandardError]
34
+ session.token.unwrap!.uppercaze # error: undefined method `uppercaze' for String
35
+ ```
36
+
37
+ **Enum variant synthesis (ADR-36 Slice A).** The `variants` DSL
38
+ emits nested subclasses at runtime; the plugin synthesises them
39
+ statically — the variant constant resolves, `.new` dispatches, and
40
+ a typed `#inner` reader returns the declared payload type:
41
+
42
+ ```ruby
43
+ class Shape
44
+ extend Mangrove::Enum
45
+ variants do
46
+ variant Circle, Float
47
+ end
48
+ end
49
+
50
+ Shape::Circle.new(1.5).inner.floor # ok — Float
51
+ Shape::Circle.new(1.5).inner.no_such_float_method # error: undefined for Float
52
+ ```
53
+
54
+ ## Limitations
55
+
56
+ - **No generic inference from constructors.** `Result::Ok.new("x")`
57
+ yields a carrier with no type argument, so unwrap there
58
+ contributes nothing (a conservative no-op, never a false
59
+ positive).
60
+ - **Downcast narrowing keeps no type args (ADR-36 Slice B,
61
+ deferred).** Narrowing a parent generic to a variant via `is_a?`
62
+ doesn't yet carry the type arguments through the inheritance edge.
63
+ - **Non-constant payload shapes degrade.** A variant whose payload
64
+ is a shape hash (`{ name: String }`) falls back to `Dynamic[top]`.
65
+
66
+ ## Plugin internals
67
+
68
+ The carrier-generic instantiation, the `NestedClassTemplate`
69
+ variant synthesis, and the relationship to `rigor-sorbet` are in
70
+ the [plugin's README](../../../plugins/rigor-mangrove/README.md);
71
+ the synthesis tier is [ADR-36](../../adr/36-mangrove-enum-nested-class-emission.md).
72
+ To write a plugin, see [`examples/`](../../../examples/README.md)
73
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,86 @@
1
+ # rigor-minitest
2
+
3
+ Narrows local variables through **Minitest** and **Test::Unit**
4
+ assertions — and the Minitest/spec `_(x).must_*` / `.wont_*` matchers
5
+ layered on top. When the plugin recognises a supported assertion shape
6
+ it emits a `:local`-kind narrowing fact, so the rest of the test body
7
+ resolves against the asserted type. It also tells the engine that a
8
+ test framework's `setup` method initialises instance variables, which
9
+ suppresses spurious nil warnings on ivars read in the test body. It
10
+ reads source only, with no `minitest` runtime dependency.
11
+
12
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
13
+
14
+ ```yaml
15
+ plugins:
16
+ - rigor-minitest
17
+ ```
18
+
19
+ ## What it infers
20
+
21
+ ```ruby
22
+ def test_user
23
+ user = build_user
24
+ assert_kind_of(User, user) # user narrowed to User
25
+ user.name.upcase # `.name` resolves on User
26
+
27
+ found = find_user(1)
28
+ refute_nil(found) # found narrowed away from nil
29
+ found.id # `.id` resolves cleanly
30
+ end
31
+
32
+ it "narrows the spec way" do
33
+ _(value).must_be_kind_of(String) # value narrowed to String
34
+ value.upcase
35
+ end
36
+ ```
37
+
38
+ `_(x)`, `value(x)`, and `expect(x)` are all accepted as the spec
39
+ wrapper. Test::Unit's `assert_not_kind_of` / `assert_not_nil` /
40
+ `assert_not_equal` / `assert_not_instance_of` share the recogniser with
41
+ their `refute_*` equivalents.
42
+
43
+ | Assertion / matcher | Effect on `x` |
44
+ | --- | --- |
45
+ | `assert_kind_of(T, x)` / `assert_instance_of(T, x)` | narrow to `T` |
46
+ | `assert_nil(x)` | narrow to `Constant<nil>` |
47
+ | `assert_equal(literal, x)` | narrow to `Constant<literal>` |
48
+ | `assert_match(regex, x)` | narrow to `String` |
49
+ | `refute_kind_of` / `refute_instance_of` (+ `assert_not_*`) | narrow away from `T` |
50
+ | `refute_nil(x)` / `assert_not_nil(x)` | narrow away from nil |
51
+ | `refute_equal(literal, x)` / `assert_not_equal(...)` | narrow away from `Constant<literal>` |
52
+ | `_(x).must_be_kind_of(T)` / `must_be_a(T)` / `must_be_instance_of(T)` / `must_be_an_instance_of(T)` | narrow to `T` |
53
+ | `_(x).must_be_nil` | narrow to `Constant<nil>` |
54
+ | `_(x).must_equal(literal)` | narrow to `Constant<literal>` |
55
+ | `_(x).must_match(regex)` | narrow to `String` |
56
+ | `_(x).wont_be_kind_of(T)` / `wont_be_instance_of(T)` | narrow away from `T` |
57
+ | `_(x).wont_be_nil` | narrow away from nil |
58
+ | `_(x).wont_equal(literal)` | narrow away from `Constant<literal>` |
59
+
60
+ ## No diagnostics, no config
61
+
62
+ The plugin emits no diagnostics and has no configuration knobs — it
63
+ only contributes narrowing facts to the engine. It walks every analysed
64
+ file; non-test files fall through untouched when no recognised shape
65
+ matches.
66
+
67
+ ## Limitations
68
+
69
+ - **No block-shape matchers** — `assert_raises(T) { ... }`,
70
+ `assert_throws(:tag) { ... }`. Narrowing targets straight-line locals.
71
+ - **No predicate / respond-to matchers** — `assert_predicate(x, :foo?)`,
72
+ `assert_respond_to(x, :m)` need carriers Rigor doesn't model.
73
+ - **No `assert_in_delta` / `assert_operator`** — float-range / generic
74
+ operator narrowing is future work.
75
+ - **No legacy bare `x.must_be_kind_of(T)`** (Minitest < 6.0) — the
76
+ receiver *is* the value, so there's nothing to narrow against; migrate
77
+ to `_(x).must_*`.
78
+
79
+ ## Plugin internals
80
+
81
+ The assertion recogniser and the contract surfaces this plugin
82
+ exercises — the ADR-37 `type_specifier` narrowing gate and the ADR-38
83
+ `additional_initializers` for `setup` — are in the
84
+ [plugin's README](../../../plugins/rigor-minitest/README.md). To write a
85
+ plugin, see [`examples/`](../../../examples/README.md) and the
86
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,72 @@
1
+ # rigor-pundit
2
+
3
+ Validates Pundit `authorize` / `policy` / `policy_scope` calls
4
+ against a statically-discovered policy index: the policy class must
5
+ exist, and an `authorize(record, :action)` action must correspond
6
+ to a defined `<action>?` predicate on the policy. It reads source
7
+ only — no Pundit runtime dependency.
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
10
+
11
+ ```yaml
12
+ plugins:
13
+ - rigor-pundit
14
+ ```
15
+
16
+ ## What it checks
17
+
18
+ ```ruby
19
+ # app/policies/post_policy.rb
20
+ class PostPolicy < ApplicationPolicy
21
+ def show?; true; end
22
+ def update?; true; end
23
+ def destroy?; true; end
24
+ end
25
+
26
+ authorize(Post, :show) # info: resolves to PostPolicy#show?
27
+ authorize(Post, :destory) # error: PostPolicy#destory? is not defined (did you mean :destroy?)
28
+ authorize(Comment, :edit) # error: no policy class CommentPolicy (did you mean … ?)
29
+ ```
30
+
31
+ | Rule | Severity | Fires when |
32
+ | --- | --- | --- |
33
+ | `plugin.pundit.policy-call` | info | an `authorize` / `policy` / `policy_scope` call resolved to a discovered policy |
34
+ | `plugin.pundit.unknown-policy-class` | error | the record maps to a `<Type>Policy` with no entry in the index (with a did-you-mean) |
35
+ | `plugin.pundit.unknown-policy-method` | error | the policy exists but the `:action` has no `<action>?` predicate (lists known predicates + a did-you-mean) |
36
+ | `plugin.pundit.load-error` | warning | policy discovery failed (parse/read error) — once per file |
37
+
38
+ The record maps to a policy by constant name or inferred
39
+ `Nominal[T]` (`Post` → `PostPolicy`); `:update` normalises to
40
+ `update?`. `authorize(record)` without an action validates only
41
+ the policy class (the action is controller-runtime-bound).
42
+
43
+ ## Configuration
44
+
45
+ ```yaml
46
+ plugins:
47
+ - gem: rigor-pundit
48
+ config:
49
+ policy_search_paths: ["app/policies"] # default
50
+ policy_base_classes: ["ApplicationPolicy"] # default
51
+ ```
52
+
53
+ ## Limitations
54
+
55
+ - **Direct-superclass match only.** `class AdminPostPolicy <
56
+ AdminPolicy` (where `AdminPolicy < ApplicationPolicy`) isn't
57
+ discovered unless `AdminPolicy` is added to `policy_base_classes`.
58
+ - **Predicate methods only.** Non-`?` methods, and predicates built
59
+ with `define_method` or inherited from concerns, are out of
60
+ scope.
61
+ - **Untyped records pass through.** `authorize(local, :show)` is
62
+ not validated when `local` has no inferred `Nominal[T]`.
63
+ - **`Scope` policies** are validated for class existence, not for
64
+ `Scope#resolve`.
65
+
66
+ ## Plugin internals
67
+
68
+ The policy discoverer / index and the contract surfaces this plugin
69
+ exercises are in the
70
+ [plugin's README](../../../plugins/rigor-pundit/README.md). To
71
+ write a plugin, see [`examples/`](../../../examples/README.md) and
72
+ the [`rigor-plugin-author`](../08-skills.md) skill.