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.
- checksums.yaml +4 -4
- data/README.md +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- 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.
|