rigortype 0.2.0 → 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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -20
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/docs/handbook/01-getting-started.md +311 -0
  27. data/docs/handbook/02-everyday-types.md +337 -0
  28. data/docs/handbook/03-narrowing.md +359 -0
  29. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  30. data/docs/handbook/05-methods-and-blocks.md +339 -0
  31. data/docs/handbook/06-classes.md +305 -0
  32. data/docs/handbook/07-rbs-and-extended.md +427 -0
  33. data/docs/handbook/08-understanding-errors.md +373 -0
  34. data/docs/handbook/09-plugins.md +241 -0
  35. data/docs/handbook/10-sorbet.md +347 -0
  36. data/docs/handbook/11-sig-gen.md +312 -0
  37. data/docs/handbook/12-lightweight-hkt.md +333 -0
  38. data/docs/handbook/README.md +275 -0
  39. data/docs/handbook/appendix-elixir.md +370 -0
  40. data/docs/handbook/appendix-go.md +399 -0
  41. data/docs/handbook/appendix-java-csharp.md +470 -0
  42. data/docs/handbook/appendix-liskov.md +580 -0
  43. data/docs/handbook/appendix-mypy.md +370 -0
  44. data/docs/handbook/appendix-phpstan.md +338 -0
  45. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  46. data/docs/handbook/appendix-rust.md +446 -0
  47. data/docs/handbook/appendix-steep.md +336 -0
  48. data/docs/handbook/appendix-type-theory.md +1662 -0
  49. data/docs/handbook/appendix-typeprof.md +416 -0
  50. data/docs/handbook/appendix-typescript.md +332 -0
  51. data/docs/install.md +189 -0
  52. data/docs/llms.txt +72 -0
  53. data/docs/manual/01-installation.md +342 -0
  54. data/docs/manual/02-cli-reference.md +557 -0
  55. data/docs/manual/03-configuration.md +152 -0
  56. data/docs/manual/04-diagnostics.md +206 -0
  57. data/docs/manual/05-inspecting-types.md +109 -0
  58. data/docs/manual/06-baseline.md +104 -0
  59. data/docs/manual/07-plugins.md +92 -0
  60. data/docs/manual/08-skills.md +143 -0
  61. data/docs/manual/09-editor-integration.md +245 -0
  62. data/docs/manual/10-mcp-server.md +532 -0
  63. data/docs/manual/11-ci.md +274 -0
  64. data/docs/manual/12-caching.md +116 -0
  65. data/docs/manual/13-troubleshooting.md +120 -0
  66. data/docs/manual/14-rails-quickstart.md +332 -0
  67. data/docs/manual/15-type-protection-coverage.md +204 -0
  68. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  69. data/docs/manual/17-driving-improvement.md +160 -0
  70. data/docs/manual/README.md +87 -0
  71. data/docs/manual/ci-templates/README.md +58 -0
  72. data/docs/manual/plugins/README.md +86 -0
  73. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  74. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  75. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  76. data/docs/manual/plugins/rigor-activejob.md +58 -0
  77. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  78. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  79. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  80. data/docs/manual/plugins/rigor-devise.md +70 -0
  81. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  82. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  83. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  84. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  85. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  86. data/docs/manual/plugins/rigor-graphql.md +89 -0
  87. data/docs/manual/plugins/rigor-hanami.md +83 -0
  88. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  89. data/docs/manual/plugins/rigor-minitest.md +86 -0
  90. data/docs/manual/plugins/rigor-pundit.md +72 -0
  91. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  92. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  93. data/docs/manual/plugins/rigor-rails.md +44 -0
  94. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  95. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  96. data/docs/manual/plugins/rigor-rspec.md +86 -0
  97. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  98. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  99. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  100. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  101. data/docs/manual/plugins/rigor-statesman.md +75 -0
  102. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  103. data/exe/rigor +1 -1
  104. data/lib/rigor/analysis/incremental_session.rb +4 -2
  105. data/lib/rigor/analysis/run_stats.rb +13 -1
  106. data/lib/rigor/analysis/runner.rb +54 -12
  107. data/lib/rigor/cli/check_command.rb +26 -3
  108. data/lib/rigor/cli/coverage_command.rb +67 -92
  109. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  110. data/lib/rigor/cli/docs_command.rb +248 -0
  111. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  112. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  113. data/lib/rigor/cli/skill_command.rb +103 -41
  114. data/lib/rigor/cli/skill_describe.rb +346 -0
  115. data/lib/rigor/cli.rb +25 -3
  116. data/lib/rigor/config_audit.rb +152 -0
  117. data/lib/rigor/configuration.rb +12 -0
  118. data/lib/rigor/environment/rbs_loader.rb +27 -0
  119. data/lib/rigor/environment.rb +49 -1
  120. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
  121. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  122. data/lib/rigor/inference/scope_indexer.rb +87 -89
  123. data/lib/rigor/inference/statement_evaluator.rb +27 -0
  124. data/lib/rigor/plugin/isolation.rb +5 -5
  125. data/lib/rigor/plugin/loader.rb +4 -2
  126. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  127. data/lib/rigor/protection/mutation_scanner.rb +98 -38
  128. data/lib/rigor/protection/mutator.rb +21 -0
  129. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  130. data/lib/rigor/signature_path_audit.rb +92 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/skills/rigor-ask/SKILL.md +172 -0
  133. data/skills/rigor-doctor/SKILL.md +87 -0
  134. data/skills/rigor-editor-setup/SKILL.md +114 -0
  135. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  136. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  137. data/skills/rigor-next-steps/SKILL.md +113 -0
  138. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  139. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  140. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  141. data/skills/rigor-upgrade/SKILL.md +79 -0
  142. metadata +120 -1
@@ -0,0 +1,92 @@
1
+ # rigor-rails-i18n
2
+
3
+ Validates `t('key.path')` / `I18n.t(...)` / `I18n.translate(...)`
4
+ calls against `config/locales/*.yml`: missing keys (with
5
+ did-you-mean suggestions), per-locale coverage gaps, and
6
+ interpolation-variable mismatches. No Rails runtime dependency —
7
+ locale files are read through Prism and `YAML.safe_load` only.
8
+
9
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
10
+
11
+ ```yaml
12
+ plugins:
13
+ - rigor-rails-i18n
14
+ ```
15
+
16
+ ## What it checks
17
+
18
+ Against a locale catalogue, every statically-resolvable call site
19
+ is validated:
20
+
21
+ ```text
22
+ demo.rb:14:1: info: `t('users.welcome')` resolves in en, ja
23
+ errors_demo.rb:12:1: error: missing translation key `users.welcom` in any locale (did you mean `users.welcome`?)
24
+ errors_demo.rb:16:1: error: `t('users.welcome')` expects interpolation `name`, got (none)
25
+ errors_demo.rb:20:1: warning: `t('users.welcome')` does not use interpolation `extra` (known placeholders: `name`)
26
+ errors_demo.rb:25:1: warning: `t('errors.messages.blank')` is missing from locale(s) ja
27
+ ```
28
+
29
+ 1. **Key existence** — a key absent from every locale is flagged,
30
+ with `DidYouMean` near-matches.
31
+ 2. **Per-locale coverage** — a key present in some
32
+ `configured_locales` but not others emits a `missing-locale`
33
+ warning (suppressed when the call passes `default:`).
34
+ 3. **Interpolation variables** — the leaf string's `%{var}`
35
+ placeholders must match the call's keyword arguments. Missing
36
+ required placeholders are errors; extras are warnings. Reserved
37
+ I18n option keys (`default:` / `scope:` / `locale:` / `count:`
38
+ / `raise:` / …) are excluded.
39
+
40
+ ### Recognised call shapes
41
+
42
+ `t(...)` (implicit self), `I18n.t(...)`, and `I18n.translate(...)`
43
+ with a literal first argument. **Lazy keys** — `t('.title')` in a
44
+ controller — are expanded to `<controller_scope>.<action>.<key>`
45
+ from the file path and the innermost enclosing `def`, matching
46
+ Rails' convention; lazy keys in non-controller files are skipped
47
+ (the scope can't be determined statically). Calls with a
48
+ non-literal key (`t(some_variable)`) pass through unchecked.
49
+
50
+ Keys under the prefixes Rails and the `rails-i18n` gem ship
51
+ themselves (`date.` / `time.` / `datetime.` / `number.` /
52
+ `errors.messages.` / `errors.format` / `support.array.` /
53
+ `helpers.{select,submit,label}.` / `i18n.transliterate.` /
54
+ `activerecord.errors.{messages,models}.`) are not flagged as
55
+ unknown, since the framework provides them.
56
+
57
+ ## Configuration
58
+
59
+ ```yaml
60
+ plugins:
61
+ - gem: rigor-rails-i18n
62
+ config:
63
+ locale_search_paths: ["config/locales"] # default
64
+ configured_locales: ["en"] # default
65
+ ```
66
+
67
+ `configured_locales` is the set of locales the project ships;
68
+ setting it to `["en", "ja"]` turns on `missing-locale` warnings
69
+ whenever a key resolves in one but not the other.
70
+
71
+ ## Limitations
72
+
73
+ - **Literal-string keys only** — a variable key passes through.
74
+ - **Lazy keys outside controllers are skipped** — the
75
+ controller/action scope `t('.x')` depends on isn't derivable in
76
+ a model / helper / mailer.
77
+ - **Pluralization is recognised but not validated** — `count:` is
78
+ treated as a reserved option; whether the locale defines
79
+ `:zero` / `:one` / `:other` is not checked.
80
+ - **Per-locale interpolation differences are merged** into one
81
+ placeholder set (if `en` uses `%{name}` and `ja` uses
82
+ `%{user_name}`, both are treated as required).
83
+ - **`safe_load` only** — YAML aliases / merges are accepted;
84
+ custom Ruby classes in the YAML are not.
85
+
86
+ ## Plugin internals
87
+
88
+ The locale loader / index, the cached `:locale_index` producer,
89
+ the demo, and the contract surfaces this plugin exercises are in
90
+ the [plugin's README](../../../plugins/rigor-rails-i18n/README.md).
91
+ To write a plugin, see [`examples/`](../../../examples/README.md)
92
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,94 @@
1
+ # rigor-rails-routes
2
+
3
+ Statically interprets `config/routes.rb` with Prism (no Rails
4
+ runtime dependency), builds the route-helper table Rails would
5
+ generate, and validates every `*_path` / `*_url` call site against
6
+ it: an unknown helper is flagged (with a did-you-mean suggestion),
7
+ as is a wrong argument count. Model↔route inflection uses the real
8
+ `ActiveSupport::Inflector`, so irregular names resolve the way Rails
9
+ resolves them.
10
+
11
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-rails-routes
16
+ ```
17
+
18
+ ## What it checks
19
+
20
+ Given a `config/routes.rb`, the plugin recognises every helper
21
+ Rails would generate and flags typos / arity mismatches at the call
22
+ site:
23
+
24
+ ```text
25
+ file:line:col: info: `users_path` → GET /users
26
+ file:line:col: info: `admin_widgets_path` → GET /admin/widgets
27
+
28
+ file:line:col: error: no route helper `widgts_path` (did you mean `users_path`?)
29
+ file:line:col: error: `user_path` expects 1 argument(s), got 3
30
+ ```
31
+
32
+ Both `_path` and `_url` forms are recognised.
33
+
34
+ ### Recognised routing DSL
35
+
36
+ The parser covers the routing DSL real apps use (expanded across
37
+ the v0.1.11 / v0.1.12 OSS surveys against Mastodon / Redmine /
38
+ GitLab FOSS):
39
+
40
+ - `Rails.application.routes.draw do … end`, plus `draw :name` /
41
+ `draw_all :name` partial route files.
42
+ - `resources` / `resource` (with `only:` / `except:`), nested
43
+ resources, and `member do … end` / `collection do … end`.
44
+ - `namespace :admin do … end` and `scope` — both the positional
45
+ form and keyword `scope(path:, as:, module:)`, with the `as:`
46
+ prefix and dynamic path segments counted into helper arity.
47
+ - `root`, and explicit `get`/`post`/`patch`/`put`/`delete`
48
+ routes (named via `as:`, including anonymous static routes).
49
+ - `devise_for`, `mount`, `use_doorkeeper`, `with_options`,
50
+ `direct`, and `concern :name do … end` (definition recorded;
51
+ the body is skipped to avoid wrong-arity false positives).
52
+
53
+ ## Configuration
54
+
55
+ ```yaml
56
+ plugins:
57
+ - gem: rigor-rails-routes
58
+ config:
59
+ routes_file: "config/routes.rb" # default
60
+ helper_paths: ["app"] # default; dirs scanned for
61
+ # project-defined *_path / *_url methods
62
+ ```
63
+
64
+ `helper_paths` lets the plugin also register URL builders you
65
+ define yourself (e.g. a private `def callback_url` under
66
+ `app/controllers` or `app/lib`), so calls to them are not flagged
67
+ as unknown helpers.
68
+
69
+ ## What it provides
70
+
71
+ The parsed helper table is published as the `:helper_table`
72
+ cross-plugin fact (ADR-9), which `rigor-actionpack` consumes to
73
+ validate helper calls inside controllers.
74
+
75
+ ## Limitations
76
+
77
+ - **Statically unfoldable route definitions.** Helpers produced by
78
+ metaprogramming the parser can't unfold (routes built in a loop
79
+ over runtime data, helpers injected by an engine the parser
80
+ doesn't model) may not register, which can surface a false
81
+ `unknown-helper`. Record those in a baseline, or
82
+ `# rigor:disable` the line.
83
+ - **Project-custom inflections** declared in
84
+ `config/initializers/inflections.rb` are not yet ingested
85
+ (ADR-39 slice 3); the standard ActiveSupport inflections are
86
+ covered.
87
+
88
+ ## Plugin internals
89
+
90
+ The Prism routes-parser, the cached `:helper_table` producer, the
91
+ demo, and the contract surfaces this plugin exercises are in the
92
+ [plugin's README](../../../plugins/rigor-rails-routes/README.md).
93
+ To write a plugin, see [`examples/`](../../../examples/README.md)
94
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,44 @@
1
+ # rigor-rails
2
+
3
+ A convenience grouping of the seven Tier 1+2 Rails ecosystem
4
+ plugins. It is **not itself a checker** — it runs no analysis of
5
+ its own — and there is nothing to install: every plugin ships
6
+ bundled in `rigortype`.
7
+
8
+ You enable the Rails plugins by listing the ones you want under
9
+ `plugins:`. `rigor-rails` does **not** one-line-activate the set
10
+ (that keeps each plugin's opt-in under your control — a
11
+ route-helper-light app skips `rigor-rails-routes`, a
12
+ fixture-light app skips `rigor-factorybot`, and so on):
13
+
14
+ ```yaml
15
+ plugins:
16
+ - rigor-rails-routes
17
+ - rigor-rails-i18n
18
+ - rigor-actionmailer
19
+ - rigor-activejob
20
+ - rigor-activerecord
21
+ - rigor-actionpack
22
+ - rigor-factorybot
23
+ ```
24
+
25
+ | Plugin | Scope |
26
+ | --- | --- |
27
+ | [rigor-rails-routes](rigor-rails-routes.md) | `*_path` / `*_url` helper validation |
28
+ | [rigor-rails-i18n](rigor-rails-i18n.md) | `t('key')` translation-key validation |
29
+ | [rigor-actionmailer](rigor-actionmailer.md) | mailer action existence / arity / view templates |
30
+ | [rigor-activejob](rigor-activejob.md) | `Job.perform_*` argument arity |
31
+ | [rigor-activerecord](rigor-activerecord.md) | finder / relation typing, schema-checked columns |
32
+ | [rigor-actionpack](rigor-actionpack.md) | controller helpers / filters / renders / strong-params |
33
+ | [rigor-factorybot](rigor-factorybot.md) | factory + attribute (+ AR column) validation |
34
+
35
+ Historically `rigor-rails` was a Gemfile meta-gem whose
36
+ `require "rigor-rails"` pulled in all seven entry points at once.
37
+ Under Rigor's single-bundled-gem distribution model that `require`
38
+ is redundant — the plugins are already loadable by id from
39
+ `rigortype` — so the `plugins:` list above is the canonical setup.
40
+
41
+ **Tier 3 plugins** (`rigor-pundit`, `rigor-sidekiq`, `rigor-rspec`,
42
+ `rigor-actioncable`, `rigor-activestorage`, `rigor-graphql`) are not
43
+ part of this grouping; enable them individually as your project
44
+ needs them.
@@ -0,0 +1,83 @@
1
+ # rigor-rbs-inline
2
+
3
+ Ingests [rbs-inline](https://github.com/soutaro/rbs-inline)-shaped
4
+ comments (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`, attribute
5
+ `#:` casts, `# @rbs!` raw RBS, …) in your Ruby source and feeds the
6
+ synthesised RBS into the analysis environment — so a `# @rbs`
7
+ annotation Rigor would otherwise ignore becomes an enforced contract
8
+ that fires the same `argument-type-mismatch` diagnostics as a
9
+ hand-written `.rbs` file. The design is recorded in
10
+ [ADR-32](../../adr/32-rbs-inline-comment-ingestion.md).
11
+
12
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
13
+
14
+ ```yaml
15
+ plugins:
16
+ - rigor-rbs-inline
17
+ ```
18
+
19
+ > **Full guide.** The worked walkthrough — every supported annotation
20
+ > form, the magic-comment opt-in, the top-level-`def` caveat, and parse-
21
+ > failure handling — is
22
+ > [handbook chapter 7 — RBS and Extended](../../handbook/07-rbs-and-extended.md),
23
+ > § "Inline RBS in Ruby source". This page is the operational quick
24
+ > reference.
25
+
26
+ ## What it does
27
+
28
+ Per file, opt in with the upstream magic comment:
29
+
30
+ ```ruby
31
+ # rbs_inline: enabled
32
+
33
+ class AscDesc
34
+ # @rbs asc_or_desc: :asc | :desc
35
+ def ascdesc(asc_or_desc) = asc_or_desc
36
+ end
37
+
38
+ AscDesc.new.ascdesc(:bad) # error: argument type mismatch — expected :asc | :desc, got :bad
39
+ ```
40
+
41
+ Files without `# rbs_inline: enabled` are untouched (a top-of-file scan
42
+ only). The synthesised RBS is cached per file (keyed on content SHA +
43
+ plugin id/version + config), so an unchanged second run skips the parse.
44
+
45
+ | Rule | Severity | Fires when |
46
+ | --- | --- | --- |
47
+ | `plugin.rbs-inline.source-rbs-synthesis-failed` | info | rbs-inline could not parse a file; analysis falls back to no inline-RBS contribution and the diagnostic carries the upstream error |
48
+
49
+ ## Configuration
50
+
51
+ ```yaml
52
+ plugins:
53
+ - gem: rigor-rbs-inline
54
+ config:
55
+ require_magic_comment: true # default
56
+ ```
57
+
58
+ - **`require_magic_comment`** (default `true`) — when `true`, only files
59
+ carrying `# rbs_inline: enabled` are processed. Set `false` to treat
60
+ every file as if it carried the magic comment — useful only when you
61
+ own the whole analysis scope (a single-file CI run or the hosted
62
+ [browser playground](../../adr/29-browser-playground.md), which sets
63
+ it so pasted snippets analyse without the magic line).
64
+
65
+ ## Limitations
66
+
67
+ - **Top-level `def` produces no RBS.** Upstream rbs-inline emits nothing
68
+ for a bare top-level `def` (verified against rbs-inline 0.14.0) —
69
+ wrap the method in a `class` / `module`. This is an inherited upstream
70
+ behaviour, not a Rigor limitation.
71
+ - **Parse failures fail soft.** A file rbs-inline can't parse is
72
+ analysed as if it had no inline RBS (the `:info` diagnostic above
73
+ records it); re-stamp the severity via `severity_profile:` to escalate.
74
+ - **Runtime dependency.** The plugin pulls in the `rbs-inline` gem; core
75
+ `rigortype` stays zero-runtime-dep, so only projects that opt in pay it.
76
+
77
+ ## Plugin internals
78
+
79
+ The synthesizer, the `source_rbs_synthesizer:` manifest hook, and the
80
+ caching wiring are in the
81
+ [plugin's README](../../../plugins/rigor-rbs-inline/README.md). To write
82
+ a plugin, see [`examples/`](../../../examples/README.md) and the
83
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,72 @@
1
+ # rigor-rspec-rails
2
+
3
+ Validates [rspec-rails](https://github.com/rspec/rspec-rails)
4
+ **behavioral** matchers whose arguments are statically checkable —
5
+ currently `have_http_status(int_or_symbol)`, flagging out-of-range
6
+ status codes and typo'd status symbols. It is the behavioral sibling
7
+ of [rigor-rspec](rigor-rspec.md): where rigor-rspec *narrows a local's
8
+ type* through matchers like `be_a` / `be_nil` / `eq(literal)`,
9
+ rigor-rspec-rails emits *domain diagnostics* on matcher arguments
10
+ without narrowing anything. The two activate independently and compose.
11
+
12
+ It ships bundled in `rigortype`. Activate it under `plugins:` (usually
13
+ alongside rigor-rspec):
14
+
15
+ ```yaml
16
+ plugins:
17
+ - rigor-rspec
18
+ - rigor-rspec-rails
19
+ ```
20
+
21
+ ## What it checks
22
+
23
+ ```ruby
24
+ RSpec.describe HomeController do
25
+ it "returns 200" do
26
+ expect(response).to have_http_status(200) # OK
27
+ expect(response).to have_http_status(:ok) # OK
28
+ expect(response).to have_http_status(:success) # OK (Rails 2xx group alias)
29
+ expect(response).to have_http_status(99) # warning: out-of-range
30
+ expect(response).to have_http_status(:succes) # warning: unknown symbol (typo)
31
+ end
32
+ end
33
+ ```
34
+
35
+ | Rule | Severity | Fires when |
36
+ | --- | --- | --- |
37
+ | `plugin.rspec-rails.have_http_status.out-of-range` | warning | an integer argument is outside `100..599` |
38
+ | `plugin.rspec-rails.have_http_status.unknown-symbol` | warning | a symbol argument is not a known Rack status code nor a Rails status-group alias (with a did-you-mean) |
39
+
40
+ The accepted status symbols come from the **real**
41
+ `Rack::Utils::SYMBOL_TO_STATUS_CODE` read at analysis time (the same
42
+ authority `have_http_status` itself uses), not a vendored snapshot — so
43
+ a newly-added Rack symbol is never mistaken for a typo
44
+ ([ADR-39](../../adr/39-plugin-target-library-invocation.md)). When Rack
45
+ can't be loaded the plugin **declines** to flag any symbol (reduced
46
+ coverage, never a false positive). The eight Rails status-group aliases
47
+ (`:success`, `:successful`, `:missing`, `:redirect`, `:error`,
48
+ `:client_error`, `:server_error`, `:informational`) are a small stable
49
+ constant set. Only literal integer / symbol arguments are checked;
50
+ strings, variables, and computed expressions pass through.
51
+
52
+ ## No configuration
53
+
54
+ The plugin has no configuration knobs.
55
+
56
+ ## Limitations
57
+
58
+ - **Only `have_http_status`.** Other rspec-rails matchers are queued
59
+ behind cross-plugin coordination: `render_template` (overlaps
60
+ rigor-actionpack render-target validation), `route_to` / `redirect_to`
61
+ / `be_routable` (need the rigor-rails-routes table), `have_enqueued_job`
62
+ / `have_received` (overlap engine constant / undefined-method rules).
63
+ - **Literal arguments only** — a status passed via a variable or method
64
+ call is not statically checkable, so it's accepted silently.
65
+
66
+ ## Plugin internals
67
+
68
+ The matcher recogniser, the Rack-table lookup, and the contract
69
+ surfaces this plugin exercises are in the
70
+ [plugin's README](../../../plugins/rigor-rspec-rails/README.md). To
71
+ write a plugin, see [`examples/`](../../../examples/README.md) and the
72
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,86 @@
1
+ # rigor-rspec
2
+
3
+ Validates RSpec `let` / `subject` declarations within each
4
+ `describe` / `context` scope. It is deliberately small: the two
5
+ checks it ships have the lowest false-positive risk of the
6
+ proposed RSpec surface, run in pure syntactic-walk mode, and
7
+ catch real bugs that `rspec` / `rubocop-rspec` don't always
8
+ surface clearly. No RSpec runtime dependency.
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-rspec
15
+ ```
16
+
17
+ ## What it checks
18
+
19
+ ```ruby
20
+ RSpec.describe "User" do
21
+ let(:user) { :alice }
22
+ let(:user) { :bob } # ← warning: duplicate `let(:user)`
23
+
24
+ let(:tags) { tags.map(&:up) } # ← error: self-referencing let
25
+
26
+ context "when admin" do
27
+ let(:user) { :admin } # ← OK: different scope
28
+ end
29
+ end
30
+ ```
31
+
32
+ ```text
33
+ spec/user_spec.rb:5:3: warning: duplicate `let(:user)` in this scope (first declared at line 4); the last declaration wins at runtime
34
+ spec/user_spec.rb:7:3: error: `let(:tags)` references its own name `tags` — this will infinite-loop at runtime
35
+ ```
36
+
37
+ 1. **Duplicate `let` / `subject` declarations** within the same
38
+ scope — `warning`. RSpec's runtime lets the last declaration
39
+ win, so the first is silently shadowed; the message names the
40
+ line of the first declaration.
41
+ 2. **Self-referencing `let` / `subject`** — calling the declared
42
+ name from inside its own block body — `error`. At runtime this
43
+ infinite-loops.
44
+
45
+ The walker recognises `RSpec.describe … do` (root), nested
46
+ `describe` / `context … do`, `let(:name)` / `let!(:name)`, and
47
+ `subject(:name)` / bare `subject` (the implicit `:subject`).
48
+
49
+ ## Configuration
50
+
51
+ No configuration knobs. The plugin walks every file on the
52
+ project's `paths:` for `RSpec.describe … do` blocks; files with
53
+ no recognised describe block are silently skipped, so it is safe
54
+ to enable project-wide alongside non-spec files.
55
+
56
+ ## Limitations
57
+
58
+ - **No let-typo detection in `it` bodies.** Flagging a misspelled
59
+ `let` name inside an `it` block needs a much heavier walker
60
+ (matcher DSL, helper methods, the let scope chain) — queued.
61
+ - **No mock-target validation.** `expect(x).to receive(:nme)`
62
+ against `x`'s methods is a separate slice.
63
+ - **No shared-context resolution.** `include_context`,
64
+ `shared_context`, and `it_behaves_like` are ignored.
65
+ - **Self-reference detection is intra-block only.** An indirect
66
+ loop (`let(:user) { foo }` where `foo` calls back to `user`) is
67
+ not flagged.
68
+ - Constant validation (`RSpec.describe SomeClass`) is the
69
+ engine's job, not this plugin's.
70
+
71
+ ## Related plugins
72
+
73
+ `rspec-rails` and `shoulda-matchers` matchers are mostly
74
+ *behavioral* (they assert runtime state, not a static type), so
75
+ they are out of scope here; the queued `rigor-rspec-rails` and
76
+ `rigor-shoulda-matchers` plugins would emit domain-specific
77
+ diagnostics for them. The README covers that boundary in detail.
78
+
79
+ ## Plugin internals
80
+
81
+ The scope-walker / analyzer layout, how to run the demo, the
82
+ contract surfaces this plugin exercises, and the future-direction
83
+ slices are in the
84
+ [plugin's README](../../../plugins/rigor-rspec/README.md). To
85
+ write a plugin, see [`examples/`](../../../examples/README.md)
86
+ and the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,78 @@
1
+ # rigor-shoulda-matchers
2
+
3
+ Validates [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers)
4
+ calls inside `RSpec.describe <Model> do … end` blocks against the
5
+ model's real schema: column matchers (`validate_presence_of(:col)`,
6
+ `have_db_column(:col)`, …) must name a real column, and association
7
+ matchers (`belong_to(:assoc)`, `have_many(:assoc)`, …) must name a real
8
+ association of the matching kind. It cross-checks against the
9
+ `:model_index` fact published by [rigor-activerecord](rigor-activerecord.md)
10
+ (ADR-9) — so it complements, rather than overlaps, rigor-rspec (which
11
+ handles RSpec's own `let` / `subject` DSL).
12
+
13
+ It ships bundled in `rigortype`. Activate it under `plugins:` together
14
+ with rigor-activerecord (which publishes the model index it consumes):
15
+
16
+ ```yaml
17
+ plugins:
18
+ - rigor-activerecord # publishes :model_index
19
+ - rigor-shoulda-matchers # consumes it
20
+ ```
21
+
22
+ ## What it checks
23
+
24
+ ```ruby
25
+ RSpec.describe User do
26
+ it { should validate_presence_of(:email) } # OK if `email` is a column
27
+ it { should validate_presence_of(:nme) } # warning: unknown column
28
+ it { should belong_to(:author) } # OK if `author` is singular
29
+ it { should belong_to(:posts) } # warning: kind mismatch (posts is a collection)
30
+ it { should have_many(:comments) } # OK if `comments` is a collection
31
+ it { should have_many(:nonexistent) } # warning: unknown association
32
+ end
33
+ ```
34
+
35
+ | Rule | Severity | Fires when |
36
+ | --- | --- | --- |
37
+ | `plugin.shoulda-matchers.unknown-column` | warning | a column matcher names a column absent from the model |
38
+ | `plugin.shoulda-matchers.unknown-association` | warning | an association matcher names an association absent from the model |
39
+ | `plugin.shoulda-matchers.association-kind-mismatch` | warning | the matcher's expected kind (singular / collection) disagrees with the association's actual kind |
40
+
41
+ Column matchers: `validate_presence_of` / `_uniqueness_of` /
42
+ `_length_of` / `_numericality_of` / `_acceptance_of` / `_inclusion_of`
43
+ / `_exclusion_of` / `_absence_of` / `_format_of` / `_confirmation_of`,
44
+ plus `have_db_column` / `have_db_index`. Association matchers and their
45
+ expected kind: `belong_to` / `have_one` (singular), `have_many` /
46
+ `have_and_belong_to_many` (collection). The enclosing
47
+ `describe <Constant>` (innermost wins) anchors which model is checked.
48
+
49
+ Suppress with the qualified rule, e.g.
50
+ `# rigor:disable plugin.shoulda-matchers.unknown-column`, or silence
51
+ the whole family with `# rigor:disable plugin.shoulda-matchers`.
52
+
53
+ ## No configuration
54
+
55
+ The plugin has no configuration knobs. When rigor-activerecord is not
56
+ loaded — or hasn't published an index for the analysed model — the
57
+ plugin falls silent; the cross-check is opt-in.
58
+
59
+ ## Limitations
60
+
61
+ - **No chained-matcher argument validation** — the chain terminals on
62
+ `validate_length_of(:col).is_at_most(50)`,
63
+ `validate_inclusion_of(:col).in_array([...])`, etc. are runtime-only.
64
+ - **No polymorphic / `through:` validation** — only the named
65
+ association is checked; the chain modifiers are ignored.
66
+ - **No nested-attribute or callback matchers**
67
+ (`accept_nested_attributes_for`, `callback(...)`).
68
+ - **No route / routing matchers** — those are the rigor-rspec-rails
69
+ domain.
70
+
71
+ ## Plugin internals
72
+
73
+ The describe-walker / matcher recogniser and the contract surfaces this
74
+ plugin exercises (the optional `:model_index` consume, `NodeContext`
75
+ ancestor resolution) are in the
76
+ [plugin's README](../../../plugins/rigor-shoulda-matchers/README.md). To
77
+ write a plugin, see [`examples/`](../../../examples/README.md) and the
78
+ [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,78 @@
1
+ # rigor-sidekiq
2
+
3
+ Validates Sidekiq enqueue calls — `Worker.perform_async(...)`,
4
+ `.perform_inline(...)`, `.perform_in(t, ...)`, `.perform_at(t, ...)` —
5
+ against the arity of the worker's discovered `#perform`. It discovers
6
+ workers by walking the configured search paths and matching classes
7
+ that `include Sidekiq::Job` (or the legacy `Sidekiq::Worker`); it reads
8
+ source only, with no `sidekiq` runtime dependency.
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-sidekiq
15
+ ```
16
+
17
+ ## What it checks
18
+
19
+ ```ruby
20
+ # app/workers/welcome_email_worker.rb
21
+ class WelcomeEmailWorker
22
+ include Sidekiq::Job
23
+ def perform(user_id, locale = "en") # arity 1..2
24
+ end
25
+ end
26
+
27
+ WelcomeEmailWorker.perform_async(123) # info: matches #perform (arity 1..2)
28
+ WelcomeEmailWorker.perform_in(60, 123, "ja") # info: schedule carved out, 123/"ja" forwarded
29
+ WelcomeEmailWorker.perform_async # error: expects 1..2 argument(s), got 0
30
+ WelcomeEmailWorker.perform_in # error: requires a schedule as its first argument
31
+ ```
32
+
33
+ `perform_in` / `perform_at` consume their first argument as the
34
+ schedule (interval / Time); the remaining arguments are validated
35
+ against `#perform`. `perform_async` / `perform_inline` forward all
36
+ arguments.
37
+
38
+ | Rule | Severity | Fires when |
39
+ | --- | --- | --- |
40
+ | `plugin.sidekiq.worker-call` | info | a `Worker.perform_*` call matched a discovered worker's `#perform` arity |
41
+ | `plugin.sidekiq.wrong-arity` | error | the forwarded argument count falls outside `#perform`'s arity envelope (message names the schedule carve-out for `perform_in` / `perform_at`) |
42
+ | `plugin.sidekiq.missing-schedule` | error | `perform_in()` / `perform_at()` called with zero arguments (the schedule is required even when `#perform` takes none) |
43
+ | `plugin.sidekiq.load-error` | warning | worker discovery failed (parse/read error) — once per file |
44
+
45
+ ## Configuration
46
+
47
+ ```yaml
48
+ plugins:
49
+ - gem: rigor-sidekiq
50
+ config:
51
+ worker_search_paths: ["app/workers", "app/sidekiq"] # default
52
+ worker_marker_modules: ["Sidekiq::Job", "Sidekiq::Worker"] # default
53
+ ```
54
+
55
+ The default `worker_marker_modules` covers both modern Sidekiq
56
+ (`Sidekiq::Job`, since 6.3) and the legacy `Sidekiq::Worker`.
57
+
58
+ ## Limitations
59
+
60
+ - **Direct `include` only.** A worker that mixes in a custom concern
61
+ which re-includes `Sidekiq::Job` is not discovered — add the
62
+ intermediate module to `worker_marker_modules`.
63
+ - **Syntactic arity only.** `#perform` arity is read from the parameter
64
+ list; methods built with `define_method` are out of scope.
65
+ - **No keyword-argument validation.** Sidekiq serialises arguments to
66
+ JSON, so positional args are the standard shape.
67
+ - **Schedule type is not validated.** The first slot of `perform_in` /
68
+ `perform_at` is consumed as the schedule regardless of its type.
69
+ - **Chained `set(...)`** (`Worker.set(queue: "low").perform_async(...)`)
70
+ is validated as a normal call; `set`'s own options are not checked.
71
+
72
+ ## Plugin internals
73
+
74
+ The worker discoverer / index and the contract surfaces this plugin
75
+ exercises are in the
76
+ [plugin's README](../../../plugins/rigor-sidekiq/README.md). To write a
77
+ plugin, see [`examples/`](../../../examples/README.md) and the
78
+ [`rigor-plugin-author`](../08-skills.md) skill.