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,74 @@
1
+ # rigor-actionmailer
2
+
3
+ Validates `Mailer.action(args).deliver_*` call sites for action
4
+ existence and argument arity, and flags mailer actions whose view
5
+ template is missing under `app/views/`. Actions inherited from
6
+ included concern modules are merged into the mailer's action set,
7
+ so a mailer that derives its actions from `include`d `Emails::*`
8
+ concerns still type-checks. No Rails runtime dependency.
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-actionmailer
15
+ ```
16
+
17
+ ## What it checks
18
+
19
+ ```text
20
+ demo.rb:7:1: info: `UserMailer.welcome` matches mailer action (arity 1..2)
21
+ errors_demo.rb:7:1: error: `UserMailer.welcome` expects 1..2 argument(s), got 0
22
+ errors_demo.rb:15:1: error: `UserMailer.does_not_exist` is not a defined mailer action (known actions: digest, reset_password, welcome)
23
+ app/mailers/user_mailer.rb:14:7: warning: `UserMailer#digest` has no view template under `app/views/user_mailer/`
24
+ ```
25
+
26
+ 1. **Action existence** — `Mailer.unknown_action(...)` →
27
+ `unknown-action` (an unresolved `include` silences this rather
28
+ than guessing).
29
+ 2. **Argument arity** — too few / too many positional args →
30
+ `wrong-arity`.
31
+ 3. **View template existence** — each action needs at least one
32
+ `app/views/<mailer_underscore>/<action>.{html,text}.{erb,haml,slim}`;
33
+ a missing one → `missing-view`, anchored on the action's `def`.
34
+
35
+ Recognised call shapes: a direct action call
36
+ (`UserMailer.welcome(user)`), a `.with(...)` chain
37
+ (`UserMailer.with(user: u).welcome(user)`), and a trailing
38
+ `.deliver_now` / `.deliver_later` (accepted, not interpreted).
39
+
40
+ ## Configuration
41
+
42
+ ```yaml
43
+ plugins:
44
+ - gem: rigor-actionmailer
45
+ config:
46
+ mailer_search_paths: ["app/mailers"] # default
47
+ mailer_base_classes: ["ApplicationMailer", "ActionMailer::Base"] # default
48
+ views_root: "app/views" # default
49
+ ```
50
+
51
+ ## Limitations
52
+
53
+ - **Direct-superclass match only.** `class CustomerMailer <
54
+ BaseMailer` where `BaseMailer < ApplicationMailer` is not
55
+ discovered unless `BaseMailer` is in `mailer_base_classes`.
56
+ (Actions from `include`d concern *modules* are merged; this is
57
+ about the superclass chain.)
58
+ - **Syntactic action list.** Actions are read from instance-side
59
+ `def`s; `define_method`, `initialize`, and `_`-prefixed names are
60
+ excluded.
61
+ - **Standard view filename pattern only**
62
+ (`<action>.{html,text}.{erb,haml,slim}`); custom engines / view
63
+ paths are out of scope.
64
+ - A brand-new view file does not invalidate the cached index until
65
+ something the mailer file touches changes (the read-tracking
66
+ trade-off).
67
+
68
+ ## Plugin internals
69
+
70
+ The mailer/concern discoverer, the cached `:mailer_index` producer,
71
+ the demo, and the contract surfaces this plugin exercises are in
72
+ the [plugin's README](../../../plugins/rigor-actionmailer/README.md).
73
+ To write a plugin, see [`examples/`](../../../examples/README.md)
74
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,80 @@
1
+ # rigor-actionpack
2
+
3
+ Checks controller-side Action Pack code across four areas, by
4
+ consuming facts other Rails plugins publish (ADR-9):
5
+
6
+ - **Route-helper calls** — `redirect_to user_path(@user)` against
7
+ the `:helper_table` from [`rigor-rails-routes`](rigor-rails-routes.md).
8
+ - **Filter chains** — `before_action :name` against the
9
+ controller's (and its parents') defined methods.
10
+ - **Render targets** — `render :show` / `render partial:` against
11
+ the view templates under `view_search_paths`.
12
+ - **Strong parameters** — `params.require(:user).permit(:name, …)`
13
+ keys against the model's columns (via `:model_index` from
14
+ [`rigor-activerecord`](rigor-activerecord.md)).
15
+
16
+ It ships bundled in `rigortype`. Activate it under `plugins:`,
17
+ alongside the producers whose facts it consumes:
18
+
19
+ ```yaml
20
+ plugins:
21
+ - rigor-rails-routes # publishes :helper_table (optional)
22
+ - rigor-activerecord # publishes :model_index (optional)
23
+ - rigor-actionpack
24
+ ```
25
+
26
+ Both dependencies are declared `optional` — a project that omits a
27
+ producer still loads; the area that needed that fact degrades to a
28
+ no-op rather than erroring.
29
+
30
+ ## What it checks
31
+
32
+ | Rule | Severity | Fires when |
33
+ | --- | --- | --- |
34
+ | `plugin.actionpack.helper-call` | info | a `*_path` / `*_url` call resolved against the helper table |
35
+ | `plugin.actionpack.unknown-helper` | error | the helper name is not in the table (with a did-you-mean) |
36
+ | `plugin.actionpack.wrong-helper-arity` | error | the call's positional-arg count ≠ the helper's recorded arity |
37
+ | `plugin.actionpack.filter-call` | info | a filter reference (`before_action :name`, `skip_around_action`, …) resolved to a defined method |
38
+ | `plugin.actionpack.unknown-filter-method` | error | a filter reference names a method not defined on the controller or a parent (with a did-you-mean) |
39
+ | `plugin.actionpack.render-target` | info | an explicit `render :symbol` / `"string"` / `partial:` resolved to a view template |
40
+ | `plugin.actionpack.missing-template` | error | an explicit `render` resolved to a view path that doesn't exist under any `view_search_paths` |
41
+ | `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
42
+ | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` isn't a column on the model (with a did-you-mean) |
43
+
44
+ Filter and render resolution honours nested-module controller
45
+ qualification (`module Admin; class WidgetsController` resolves
46
+ views under `admin/widgets/…`) and silences gem-shipped parent
47
+ classes it can't see.
48
+
49
+ ## Configuration
50
+
51
+ ```yaml
52
+ plugins:
53
+ - gem: rigor-actionpack
54
+ config:
55
+ controller_search_paths: ["app/controllers"] # default
56
+ view_search_paths: ["app/views"] # default
57
+ ```
58
+
59
+ ## Limitations
60
+
61
+ - **Implicit-self helpers only.** `*_path` / `*_url` calls with an
62
+ explicit receiver (`Rails.application.routes.url_helpers.x_path`)
63
+ are passed through.
64
+ - **Path-based file filter.** Files under
65
+ `controller_search_paths` are checked regardless of class
66
+ hierarchy; a non-controller file placed there (rare) would be
67
+ scanned.
68
+ - **Coverage follows the upstream facts.** Helper validation only
69
+ knows what `rigor-rails-routes` published, and `permit`
70
+ validation only what `rigor-activerecord` published — enabling
71
+ those producers widens what this plugin can check.
72
+
73
+ ## Plugin internals
74
+
75
+ The cross-plugin fact contract (`:helper_table` / `:model_index`),
76
+ the controller/view discovery producers, the demo, and the
77
+ contract surfaces this plugin exercises are in the
78
+ [plugin's README](../../../plugins/rigor-actionpack/README.md). To
79
+ write a plugin, see [`examples/`](../../../examples/README.md) and
80
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,58 @@
1
+ # rigor-activejob
2
+
3
+ Validates `Job.perform_later(...)` / `.perform_now(...)` /
4
+ `.perform(...)` argument arity against the discovered `#perform`
5
+ definition. No Rails runtime dependency — the plugin reads project
6
+ source via Prism only.
7
+
8
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
9
+
10
+ ```yaml
11
+ plugins:
12
+ - rigor-activejob
13
+ ```
14
+
15
+ ## What it checks
16
+
17
+ Given a job whose `#perform` takes one required and one optional
18
+ argument (arity `1..2`):
19
+
20
+ ```text
21
+ demo.rb:6:1: info: `WelcomeEmailJob.perform_later` matches `#perform` (arity 1..2)
22
+ demo.rb:9:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 0
23
+ demo.rb:12:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 3
24
+ ```
25
+
26
+ A `*rest` parameter yields an unbounded upper bound (`arity 0+`).
27
+ All three entry points — `perform_later` (async), `perform_now`
28
+ (sync), and bare `perform` — are validated against the same
29
+ `#perform` envelope.
30
+
31
+ ## Configuration
32
+
33
+ ```yaml
34
+ plugins:
35
+ - gem: rigor-activejob
36
+ config:
37
+ job_search_paths: ["app/jobs"] # default
38
+ job_base_classes: ["ApplicationJob", "ActiveJob::Base"] # default
39
+ ```
40
+
41
+ ## Limitations
42
+
43
+ - **Direct-superclass match only.** `class WelcomeJob < BaseJob`
44
+ where `BaseJob < ApplicationJob` is not discovered unless you
45
+ add `BaseJob` to `job_base_classes`.
46
+ - **Syntactic arity.** `#perform` arity is read from the parameter
47
+ list; a `#perform` built with `define_method` is out of scope.
48
+ - **Positional arity only.** Required keyword arguments are
49
+ recorded by the discoverer but not yet validated at the call
50
+ site.
51
+
52
+ ## Plugin internals
53
+
54
+ The job discoverer / index, the cached `:job_index` producer, the
55
+ demo, and the contract surfaces this plugin exercises are in the
56
+ [plugin's README](../../../plugins/rigor-activejob/README.md). To
57
+ write a plugin, see [`examples/`](../../../examples/README.md) and
58
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,102 @@
1
+ # rigor-activerecord
2
+
3
+ Types ActiveRecord finder and relation calls against your
4
+ project's `db/schema.rb` and discovered model classes — so
5
+ `User.find(1)` is `User`, `User.where(emial: …)` is flagged as
6
+ an unknown column, and `user.posts` carries its element type
7
+ through the chain. The plugin reads source only; it never loads
8
+ `active_record`, so Rigor stays decoupled from Rails.
9
+
10
+ It ships bundled in `rigortype` — no separate install. Activate
11
+ it under `plugins:` in your config file:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-activerecord
16
+ ```
17
+
18
+ ## What it checks
19
+
20
+ ```text
21
+ demo.rb:20:1: info: `User.find` returns User (table: `users`) [plugin.activerecord.model-call]
22
+ demo.rb:23:1: info: `User.where` (:admin) on table `users` [plugin.activerecord.model-call]
23
+
24
+ errors_demo.rb:13:1: error: `User.where(emial: ...)` references unknown column `emial` on table `users` (did you mean `:email`?) [plugin.activerecord.unknown-column]
25
+ errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plugin.activerecord.wrong-arity]
26
+ ```
27
+
28
+ | Diagnostic | Severity | Rule |
29
+ | --- | --- | --- |
30
+ | Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
31
+ | `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
32
+ | `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
33
+ | `db/schema.rb` not readable | `:warning` | `plugin.activerecord.load-error` |
34
+
35
+ Did-you-mean suggestions use Levenshtein distance ≤ 3 against
36
+ the resolved table's column names.
37
+
38
+ ## Configuration
39
+
40
+ ```yaml
41
+ plugins:
42
+ - gem: rigor-activerecord
43
+ config:
44
+ schema_file: "db/schema.rb" # default
45
+ model_search_paths: ["app/models"] # default
46
+ model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"] # default
47
+ ```
48
+
49
+ All three keys are optional. Tweak them when:
50
+
51
+ - the schema lives elsewhere (`schema_file: "shared/db/schema.rb"`);
52
+ - models are in a non-standard directory
53
+ (`model_search_paths: ["domain/models", "engines/billing/app/models"]`);
54
+ - the base class is custom
55
+ (`model_base_classes: ["DbRecord", "ApplicationRecord"]`).
56
+
57
+ ## What it infers
58
+
59
+ The plugin contributes call-site types as well as diagnostics.
60
+ Class-side: `User.find(1)` → `User`, `User.find_by(...)` →
61
+ `User | nil`, `User.find_by!(...)` → non-nullable `User`.
62
+ Instance-side: a column read (`user.name`) narrows to the
63
+ column's value type, `user.admin?` to `bool`, and a singular
64
+ association (`post.user`) to the target model.
65
+
66
+ Relation-returning call sites — `User.where(...)`, `User.all`,
67
+ `User.order(...)`, a `has_many` / `has_and_belongs_to_many`
68
+ accessor (`user.posts`), and user-declared `scope`s
69
+ (`Post.published`) — narrow to `ActiveRecord::Relation[Model]`.
70
+ Chained query methods keep the element type, and iteration
71
+ (`user.posts.each { |p| ... }`) yields the model. A user-defined
72
+ scope invoked on a typed relation (`User.where(...).published`)
73
+ never surfaces a false `call.undefined-method`.
74
+
75
+ ## Limitations
76
+
77
+ - **Direct-superclass match only.** `class Admin < User` where
78
+ `User < ApplicationRecord` is not discovered. Either add `User`
79
+ to `model_base_classes`, or list every concrete model
80
+ explicitly.
81
+ - **`db/schema.rb` only.** `db/structure.sql` (raw SQL dumps) is
82
+ not supported in this iteration.
83
+ - **Column reads, not setters.** The plugin types instance-side
84
+ column *reads* (`user.name`, `user.admin?`) and singular
85
+ associations, but not the `name=` setter or the dirty-tracking
86
+ family (`name_changed?`, `name_was`, …).
87
+ - **Project-custom inflections aren't read yet.** Model↔table
88
+ pluralization goes through the real ActiveSupport inflector
89
+ (so `Person → people`, `Mouse → mice` resolve), but rules you
90
+ declare in `config/initializers/inflections.rb` are not yet
91
+ ingested — a model relying on one needs `self.table_name`
92
+ (ADR-39 slice 3).
93
+
94
+ ## Plugin internals
95
+
96
+ Architecture (the cached schema-parser → model-index → analyzer
97
+ chain), the source layout, how to run the demo, and the plugin
98
+ contract surfaces this plugin exercises are documented in the
99
+ [plugin's README](../../../plugins/rigor-activerecord/README.md).
100
+ To write a plugin of your own, see the
101
+ [`examples/`](../../../examples/README.md) walkthroughs and the
102
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,74 @@
1
+ # rigor-activestorage
2
+
3
+ Walks ActiveRecord model files for `has_one_attached` /
4
+ `has_many_attached` macros and types the attachment accessors they
5
+ generate, so navigating an attachment resolves through
6
+ ActiveStorage's own RBS surface instead of falling to the untyped
7
+ envelope. It reads source only — no Rails runtime dependency.
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
10
+
11
+ ```yaml
12
+ plugins:
13
+ - rigor-activestorage
14
+ ```
15
+
16
+ ## What it infers
17
+
18
+ ```ruby
19
+ class User < ApplicationRecord
20
+ has_one_attached :avatar
21
+ has_many_attached :photos
22
+ end
23
+
24
+ user = User.find(1)
25
+ user.avatar # Nominal[ActiveStorage::Attached::One]
26
+ user.avatar.attached? # resolves through ActiveStorage's RBS
27
+ user.photos # Nominal[ActiveStorage::Attached::Many]
28
+ ```
29
+
30
+ | Macro | Accessor | Contributed type |
31
+ | --- | --- | --- |
32
+ | `has_one_attached :avatar` | `user.avatar` | `Nominal[ActiveStorage::Attached::One]` |
33
+ | `has_many_attached :photos` | `user.photos` | `Nominal[ActiveStorage::Attached::Many]` |
34
+
35
+ Setters (`user.avatar = …`) and attachment-name calls with
36
+ arguments decline — those are covered by ActiveStorage's own RBS.
37
+
38
+ ## Diagnostics
39
+
40
+ | Rule | Severity | When |
41
+ | --- | --- | --- |
42
+ | `plugin.activestorage.attachment-call` | info | a recognised `model.attachment_name` call surfaces; confirms the model → attachment mapping |
43
+ | `plugin.activestorage.load-error` | warning | discovery failed (e.g. the model directory is inaccessible under the IoBoundary trust policy) |
44
+
45
+ No `:error` diagnostics in this slice — the value is the
46
+ return-type contribution; an "unknown attachment name" rule is a
47
+ future slice.
48
+
49
+ ## Configuration
50
+
51
+ ```yaml
52
+ plugins:
53
+ - gem: rigor-activestorage
54
+ config:
55
+ model_search_paths: ["app/models"] # default
56
+ ```
57
+
58
+ ## With or without rigor-activerecord
59
+
60
+ The plugin discovers model files independently, so it works
61
+ stand-alone. When [`rigor-activerecord`](rigor-activerecord.md) is
62
+ also active the two coexist (each contributes its own per-call
63
+ return type and the contribution merger reconciles); the
64
+ `:model_index` dependency is declared `optional`, reserved for a
65
+ future slice that would restrict attachment recognition to
66
+ discovered AR classes.
67
+
68
+ ## Plugin internals
69
+
70
+ The discovery pass, the `AttachmentIndex`, and the contract
71
+ surfaces this plugin exercises are in the
72
+ [plugin's README](../../../plugins/rigor-activestorage/README.md).
73
+ To write a plugin, see [`examples/`](../../../examples/README.md)
74
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,86 @@
1
+ # rigor-activesupport-core-ext
2
+
3
+ An opt-in **RBS bundle** for the ActiveSupport `core_ext` extensions
4
+ that real Rails code uses most — `Time.current`, `3.days`, `Array.wrap`,
5
+ `"x".squish`, `obj.blank?`, and the rest. It ships no analyzer and no
6
+ diagnostics: its whole job is to hand Rigor signatures for these
7
+ methods so they stop showing up as `call.undefined-method` false
8
+ positives. A four-project Rails survey found **64–90% of every
9
+ project's diagnostics** came from ActiveSupport extensions missing from
10
+ stdlib RBS — making this the single largest false-positive suppressor
11
+ for Rails apps, and the one to reach for first when Rigor floods a Rails
12
+ codebase with undefined-method noise.
13
+
14
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
15
+
16
+ ```yaml
17
+ plugins:
18
+ - rigor-activesupport-core-ext
19
+ ```
20
+
21
+ That is the whole setup — Rigor resolves the bundled `sig/`
22
+ automatically ([ADR-25](../../adr/25-plugin-contributed-rbs.md)); no
23
+ path, no vendoring, no `signature_paths:` wiring.
24
+
25
+ > **You may not need this plugin.** As of
26
+ > [ADR-72](../../adr/72-gemfile-lock-gated-rbs-overlays.md), Rigor
27
+ > auto-loads a bundled core-ext RBS overlay whenever `activesupport` is
28
+ > in your `Gemfile.lock` but ships no RBS — so the most common
29
+ > ActiveSupport false positives are already suppressed with zero config.
30
+ > This plugin is the **opt-in, fuller twin** of that overlay (and the
31
+ > authoring home for the signatures); load it when you want the complete
32
+ > surface. When it is loaded, the auto overlay stands down so the two
33
+ > never double-declare.
34
+
35
+ ## What it covers
36
+
37
+ Roughly the top ~40 selectors plus their close neighbours, across:
38
+
39
+ - **Object (universal)** — `#blank?`, `#present?`, `#presence`, `#try`,
40
+ `#try!`, `#acts_like?` (+ `NilClass` / `TrueClass` / `FalseClass`).
41
+ - **Integer / Float** — Duration multipliers (`#days`, `#hours`,
42
+ `#minutes`, …) and Bytes multipliers (`#megabytes`, `#gigabytes`, …).
43
+ - **String** — inflections (`#underscore`, `#camelize`, `#classify`,
44
+ `#constantize`, `#pluralize`, …), filters (`#squish`, `#truncate`),
45
+ `#html_safe`, `#starts_with?` / `#ends_with?`, conversions.
46
+ - **Time / Date / DateTime** — `.current`, `.zone`, `#yesterday`,
47
+ `#tomorrow`, `#beginning_of_*` / `#end_of_*`, `#ago`, `#since`.
48
+ - **Array** — `.wrap`, `#to_sentence`, `#in_groups_of`, `#second` …
49
+ `#fifth`, `#compact_blank`, `#exclude?`.
50
+ - **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang),
51
+ `#deep_merge`, `#with_indifferent_access`, `#except!`.
52
+ - **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
53
+
54
+ ```ruby
55
+ 3.days # without the bundle: call.undefined-method Integer#days
56
+ " x ".squish # without the bundle: call.undefined-method String#squish
57
+ Time.current # without the bundle: call.undefined-method Time.current
58
+ ```
59
+
60
+ ## No diagnostics, no config
61
+
62
+ The plugin is RBS-only — it emits no diagnostics and has no
63
+ configuration knobs. It contributes its signatures unconditionally when
64
+ listed under `plugins:`.
65
+
66
+ ## Limitations
67
+
68
+ - **Conservative return types.** `Integer#days` really returns
69
+ `ActiveSupport::Duration`, but the bundle types it `untyped` because
70
+ the analysis environment usually lacks the Duration class — the goal
71
+ is to silence undefined-method, not to give precise returns. Likewise
72
+ `#html_safe` is typed `String` (not `SafeBuffer`) and `#try` / `#try!`
73
+ return `untyped`.
74
+ - **Project-private monkey-patches are not covered** — only real
75
+ ActiveSupport extensions. For your own core-class patches see the
76
+ `pre_eval:` mechanism ([ADR-17](../../adr/17-monkey-patch-pre-evaluation.md)).
77
+ - **Top ~40 selectors, not exhaustive.** ActiveSupport ships hundreds of
78
+ extensions; this covers the head of the real-world distribution.
79
+
80
+ ## Plugin internals
81
+
82
+ The RBS layout, the per-class coverage, and the survey that picked the
83
+ selectors are in the
84
+ [plugin's README](../../../plugins/rigor-activesupport-core-ext/README.md).
85
+ To write a plugin, see [`examples/`](../../../examples/README.md) and the
86
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,70 @@
1
+ # rigor-devise
2
+
3
+ Teaches Rigor about the methods Devise mixes into a model from a
4
+ `devise :strategy, …` declaration, so cross-file calls to those
5
+ methods (`user.valid_password?("pw")`,
6
+ `user.send_reset_password_instructions`) resolve instead of
7
+ surfacing a false `call.undefined-method` — and resolve with their
8
+ real return types. It reads source only; no Devise runtime
9
+ dependency.
10
+
11
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-devise
16
+ ```
17
+
18
+ ## What it does — no diagnostics, no config
19
+
20
+ `rigor-devise` is a macro-expansion plugin (ADR-16 Tier B): it
21
+ emits no diagnostics and has no configuration. From a declaration
22
+ like
23
+
24
+ ```ruby
25
+ class User < ApplicationRecord
26
+ devise :database_authenticatable, :recoverable
27
+ end
28
+ ```
29
+
30
+ it synthesises the instance methods each named strategy module
31
+ contributes, attaching them to the declaring class so a call in
32
+ another file type-checks:
33
+
34
+ ```ruby
35
+ user.valid_password?("pw") # bool
36
+ user.send_reset_password_instructions # (the module's RBS return)
37
+ ```
38
+
39
+ Return types are the strategy module's **authored RBS return**
40
+ (via `origin_module:` provenance), not a widened `Dynamic[T]`.
41
+ Eleven strategies are recognised — `database_authenticatable`,
42
+ `recoverable`, `rememberable`, `registerable`, `trackable`,
43
+ `validatable`, `confirmable`, `lockable`, `timeoutable`,
44
+ `omniauthable`, `authenticatable` — plus the always-included
45
+ `Devise::Models::Authenticatable`. Strategies declared inside an
46
+ `ActiveSupport::Concern`'s `included do … end` are re-targeted onto
47
+ whatever class includes the concern.
48
+
49
+ ## Limitations
50
+
51
+ - **Instance methods only.** Per-module `ClassMethods` (e.g.
52
+ `User.reset_password_by_token`) are not yet synthesised.
53
+ - **Controller helpers deferred.** `current_user` /
54
+ `authenticate_user!` / `user_signed_in?` come from
55
+ `devise_for :users` in the routes file (Tier C work), not the
56
+ model declaration, so they are not contributed yet.
57
+ - **Third-party strategies aren't scanned.** A strategy registered
58
+ via `Devise.add_module :foo` in an initializer is unknown to the
59
+ bundled strategy table.
60
+
61
+ ## Plugin internals
62
+
63
+ The macro manifest (the trait registry mapping each strategy to its
64
+ module), the concern re-targeting walk, the demo, and the
65
+ contract surfaces this plugin exercises are in the
66
+ [plugin's README](../../../plugins/rigor-devise/README.md);
67
+ [handbook chapter 9](../../handbook/09-plugins.md) covers the Tier B
68
+ macro substrate generally. To write a plugin, see
69
+ [`examples/`](../../../examples/README.md) and the
70
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,56 @@
1
+ # rigor-dry-schema
2
+
3
+ Recognises dry-schema declarations and publishes a per-schema
4
+ typed-key table as a cross-plugin fact (`:dry_schema_table`) that
5
+ `rigor-dry-validation` consumes for typed-payload synthesis. Like
6
+ [`rigor-dry-types`](rigor-dry-types.md), it is a foundation plugin:
7
+ no diagnostics of its own, no config keys.
8
+
9
+ It ships bundled in `rigortype`. Activate it (pair with
10
+ `rigor-dry-types` to resolve `Types::*` aliases inside predicate
11
+ arguments):
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-dry-types # optional: resolves Types::Email etc.
16
+ - rigor-dry-schema
17
+ ```
18
+
19
+ ## What it recognises
20
+
21
+ ```ruby
22
+ NewUserSchema = Dry::Schema.Params do
23
+ required(:email).filled(:string)
24
+ required(:age).value(:integer)
25
+ required(:tags).each(:string)
26
+ optional(:nickname).maybe(:string)
27
+ end
28
+ ```
29
+
30
+ - `required` / `optional` keys, with the predicate's type symbol
31
+ mapped to a Ruby class (`:string` → `String`, `:integer` →
32
+ `Integer`, `:decimal` → `BigDecimal`, `:bool` → `TrueClass`, …).
33
+ - `each(:T)` marks the key as a **list** (`list: true`);
34
+ `filled` / `value` / `maybe` are scalar (`list: false`).
35
+ - `value(Types::Email)` resolves through the `:dry_type_aliases`
36
+ fact when `rigor-dry-types` is loaded; without it (or for an
37
+ unknown reference) the row drops from the table rather than
38
+ mislead downstream consumers.
39
+ - Top-level (`Foo = Dry::Schema.Params { … }`) and class-level
40
+ (`class Bar; SCHEMA = …; end` → `"Bar::SCHEMA"`) declarations,
41
+ across `.Params` / `.JSON` / `.define`.
42
+
43
+ ## No diagnostics, no config
44
+
45
+ The plugin only publishes the schema table for other plugins to
46
+ consume; it surfaces no diagnostics and takes no config keys. (A
47
+ future slice adds `dry-schema.unknown-predicate` / `unknown-type`
48
+ info diagnostics and typed `result.to_h` synthesis.)
49
+
50
+ ## Plugin internals
51
+
52
+ The `prepare(services)` scan, the published `:dry_schema_table`
53
+ shape, and the slice floor/ceiling are documented in the
54
+ [plugin's README](../../../plugins/rigor-dry-schema/README.md). To
55
+ write a plugin, see [`examples/`](../../../examples/README.md) and
56
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,60 @@
1
+ # rigor-dry-struct
2
+
3
+ Recognises dry-struct's class-level `attribute :name, Type` /
4
+ `attribute? :name, Type` DSL on `Dry::Struct` subclasses and
5
+ synthesises the reader methods it generates — so a bare
6
+ `address.city` in another file resolves instead of falling through
7
+ to `call.undefined-method`.
8
+
9
+ It ships bundled in `rigortype`. Activate it — and pair it with
10
+ [`rigor-dry-types`](rigor-dry-types.md) for precise reader types:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-dry-struct
15
+ - rigor-dry-types # optional: resolves Types::String → String on the readers
16
+ ```
17
+
18
+ ## What it does
19
+
20
+ ```ruby
21
+ class Address < Dry::Struct
22
+ attribute :city, Types::String
23
+ attribute? :postcode, Types::String
24
+ end
25
+
26
+ address.city # resolves (synthesised reader)
27
+ address.postcode # resolves
28
+ ```
29
+
30
+ When [`rigor-dry-types`](rigor-dry-types.md) is also active and the
31
+ project declares `module Types; include Dry.Types(); end`, the
32
+ reader's return type resolves through the attribute's type argument
33
+ (`attribute :city, Types::String` → `city` returns `String`). When
34
+ dry-types isn't loaded, or for a shape it can't resolve (a
35
+ `.constrained(...)` chain, an inline composition), the reader falls
36
+ back to `Dynamic[top]` — silently, no diagnostic.
37
+
38
+ ## No diagnostics, no config
39
+
40
+ The plugin contributes synthesised methods, not diagnostics, and
41
+ has no config keys. It handles any class inheriting from
42
+ `Dry::Struct` (lexically, transitively, or through the RBS env when
43
+ the chain terminates upstream). Its visible effect is that
44
+ attribute-reader calls type-check.
45
+
46
+ ## Limitations
47
+
48
+ - **Readers only.** The `schema` / `to_h` / `[:key]` / keyword-arg
49
+ `.new(name:)` shapes are not synthesised yet.
50
+ - **No nested-block form.** `attribute :details do … end` (which
51
+ mints a sibling `Dry::Struct` subclass) is deferred.
52
+
53
+ ## Plugin internals
54
+
55
+ The declarative `HeredocTemplate` manifest, the
56
+ `returns_from_arg` precision path (ADR-18), and the
57
+ synthetic-method substrate it rides on are documented in the
58
+ [plugin's README](../../../plugins/rigor-dry-struct/README.md). To
59
+ write a plugin, see [`examples/`](../../../examples/README.md) and
60
+ the [`rigor-plugin-author`](../08-skills.md) skill.