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.
- checksums.yaml +4 -4
- data/README.md +41 -14
- 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 +569 -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 +539 -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 +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli/triage_command.rb +8 -2
- data/lib/rigor/cli/triage_renderer.rb +4 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/triage/catalogue.rb +16 -1
- data/lib/rigor/triage.rb +30 -7
- 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 +90 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Appendix — Coming from PHPStan
|
|
2
|
+
|
|
3
|
+
PHPStan is the closest spiritual peer Rigor has in another
|
|
4
|
+
language. Both tools share the same priorities: stay silent on
|
|
5
|
+
code the analyzer cannot characterise, lean on inference rather
|
|
6
|
+
than mandatory annotations, and surface a small catalogue of
|
|
7
|
+
high-confidence diagnostics. Many of Rigor's design choices —
|
|
8
|
+
config-file shape, baseline diffing, severity profiles, the
|
|
9
|
+
`assert` family of directives — were directly informed by
|
|
10
|
+
PHPStan.
|
|
11
|
+
|
|
12
|
+
If you have used PHPStan, the mental model carries over almost
|
|
13
|
+
unchanged. This appendix maps the vocabulary.
|
|
14
|
+
|
|
15
|
+
## The five-second pitch
|
|
16
|
+
|
|
17
|
+
| Question | PHPStan | Rigor |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Where do annotations live? | PHPDoc `/** ... */` blocks | `.rbs` files alongside `.rb` |
|
|
20
|
+
| Default behaviour | Inference, fall back silent | Inference, fall back silent |
|
|
21
|
+
| "Levels" | 0 – 10 (numeric) | `lenient` / `balanced` / `strict` (named) |
|
|
22
|
+
| Per-rule control | `ignoreErrors:` regex, level demotion | `disable:`, `severity_overrides:` |
|
|
23
|
+
| Baseline | `phpstan-baseline.neon` | `.rigor-baseline.yml` (managed) / `rigor.baseline.json` (ad-hoc) |
|
|
24
|
+
| Stub format | PHP stub files | RBS files |
|
|
25
|
+
| Custom narrowing | Type-Specifying Extensions | Plugins (Chapter 9) |
|
|
26
|
+
| Custom return shape | Dynamic Return Type Extensions | Plugin `dynamic_return` |
|
|
27
|
+
|
|
28
|
+
The two tools agree on most of the foundational decisions. The
|
|
29
|
+
biggest differences are surface (Ruby's syntax and runtime
|
|
30
|
+
shape), not philosophy.
|
|
31
|
+
|
|
32
|
+
## Type vocabulary mapping
|
|
33
|
+
|
|
34
|
+
PHPStan and Rigor have an overlapping refinement vocabulary; this
|
|
35
|
+
is the closest match of any peer.
|
|
36
|
+
|
|
37
|
+
| PHPStan PHPDoc | Rigor representation | Notes |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `string` | `String` | |
|
|
40
|
+
| `int` | `Integer` | |
|
|
41
|
+
| `float` | `Float` | |
|
|
42
|
+
| `bool` | `bool` (`Constant<true> \| Constant<false>`) | |
|
|
43
|
+
| `null` | `Constant<nil>` | Ruby has only `nil`. |
|
|
44
|
+
| `mixed` | `Top` | The "anything" carrier. |
|
|
45
|
+
| `never` | `Bot` | Empty type. |
|
|
46
|
+
| `void` | `void` | Same. |
|
|
47
|
+
| `array<T>` / `T[]` | `Array[T]` | |
|
|
48
|
+
| `array<K, V>` | `Hash[K, V]` | Ruby splits by container kind. |
|
|
49
|
+
| `array{name: string, age: int}` | `HashShape{name: String, age: Integer}` | Same per-key model. |
|
|
50
|
+
| `array{0: int, 1: string}` (list shape) | `Tuple[Integer, String]` | Same per-position model. |
|
|
51
|
+
| `non-empty-string` | `non-empty-string` | **Identical name and meaning.** |
|
|
52
|
+
| `non-falsy-string` | `non-empty-string` | Rigor does not split out the falsy-but-nonempty case. |
|
|
53
|
+
| `numeric-string` | `numeric-string` | Identical. |
|
|
54
|
+
| `lowercase-string` | `lowercase-string` | Identical. |
|
|
55
|
+
| `class-string` | `Singleton[T]` | Equivalent shape. |
|
|
56
|
+
| `int<1, 9>` | `int<1, 9>` | **Identical syntax.** |
|
|
57
|
+
| `positive-int` | `positive-int` | Identical. |
|
|
58
|
+
| `negative-int` | `negative-int` | Identical. |
|
|
59
|
+
| `non-zero-int` | `non-zero-int` | Identical. |
|
|
60
|
+
| `non-empty-array<T>` | `non-empty-array[T]` | Identical. |
|
|
61
|
+
| `non-empty-list<T>` | (no separate carrier — `non-empty-array[T]` covers it) | Ruby has no list/dict split. |
|
|
62
|
+
| `T \| U` | `T \| U` | |
|
|
63
|
+
| `T & U` | `Intersection[T, U]` | |
|
|
64
|
+
| `literal-string` | `literal-string` | **Identical concept.** Provably built from source-code literals. |
|
|
65
|
+
| `'hello'` (literal type) | `Constant<"hello">` | |
|
|
66
|
+
| `42` (literal type) | `Constant<42>` | |
|
|
67
|
+
|
|
68
|
+
This table is the densest in the appendix because the overlap
|
|
69
|
+
is so close. If you are reading PHPStan's "PHPDoc Types" page
|
|
70
|
+
in another tab, almost every advanced refinement transfers.
|
|
71
|
+
|
|
72
|
+
## The `@phpstan-assert` family
|
|
73
|
+
|
|
74
|
+
PHPStan's assertion-narrowing PHPDoc tags map directly onto
|
|
75
|
+
Rigor's `RBS::Extended` directive grammar. Chapter 7 covers
|
|
76
|
+
the table in depth; here it is again for reference:
|
|
77
|
+
|
|
78
|
+
| PHPStan PHPDoc | Rigor RBS::Extended | Effect |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `@phpstan-assert T $x` | `%a{rigor:v1:assert x is T}` | After return, caller's `x` is `T`. |
|
|
81
|
+
| `@phpstan-assert-if-true T $x` | `%a{rigor:v1:predicate-if-true x is T}` | If method returns truthy, caller's `x` is `T`. |
|
|
82
|
+
| `@phpstan-assert-if-false T $x` | `%a{rigor:v1:predicate-if-false x is T}` | If method returns falsey, caller's `x` is `T`. |
|
|
83
|
+
| `@phpstan-assert !T $x` | `%a{rigor:v1:assert x is ~T}` | After return, caller's `x` is **not** `T`. |
|
|
84
|
+
| `@phpstan-assert =T $x` (assert-and-narrow) | (covered by `assert:`) | Same effect. |
|
|
85
|
+
| `@phpstan-self-out T` | `%a{rigor:v1:assert self is T}` | `self` narrows in caller scope. |
|
|
86
|
+
| `@phpstan-impure` | (no analogue) | Rigor does not yet model purity for fold-through-method-call. |
|
|
87
|
+
|
|
88
|
+
Every directive Rigor's grammar ships has a PHPStan PHPDoc
|
|
89
|
+
analogue. If you have a PHPStan-shaped mental model for "what
|
|
90
|
+
narrows what after this method returns," it transfers
|
|
91
|
+
unchanged.
|
|
92
|
+
|
|
93
|
+
## Type-Specifying Extensions ↔ Plugins
|
|
94
|
+
|
|
95
|
+
When the assertion is recognised by **call shape** rather than
|
|
96
|
+
by signature — PHPStan's `TypeSpecifyingExtension` interface,
|
|
97
|
+
where you write a class that the framework instantiates and
|
|
98
|
+
asks "given this call, what narrowings does it produce?" —
|
|
99
|
+
Rigor's analogue is a plugin's `type_specifier` / `dynamic_return` and
|
|
100
|
+
`#diagnostics_for_file` hooks plus the engine's
|
|
101
|
+
`post_return_facts` substrate.
|
|
102
|
+
|
|
103
|
+
| PHPStan extension type | Rigor analogue |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `MethodTypeSpecifyingExtension` | Plugin's `Fact(target_kind: :parameter)` returned from `type_specifier` |
|
|
106
|
+
| `StaticMethodTypeSpecifyingExtension` | Same, with `Fact(target_kind: :receiver-class)` |
|
|
107
|
+
| `FunctionTypeSpecifyingExtension` | Same, with `Fact(target_kind: :argument)` |
|
|
108
|
+
| `DynamicMethodReturnTypeExtension` | Plugin's `dynamic_return(methods:) { |call_node, scope, ...| ... }` |
|
|
109
|
+
| `DynamicStaticMethodReturnTypeExtension` | Same, varying by receiver-class branch in plugin code |
|
|
110
|
+
| `DynamicFunctionReturnTypeExtension` | Same, for module-level methods |
|
|
111
|
+
|
|
112
|
+
The plugin contract pinned at
|
|
113
|
+
[`docs/internal-spec/plugin.md`](../internal-spec/plugin.md)
|
|
114
|
+
gives every shape PHPStan's extension API covers, with
|
|
115
|
+
analogous lifecycle (manifest declaration, per-call dispatch,
|
|
116
|
+
fact emission). Chapter 9 has the high-level orientation; the
|
|
117
|
+
internal spec is the binding contract.
|
|
118
|
+
|
|
119
|
+
The `rigor-sorbet` adapter in Chapter 10 is itself a worked
|
|
120
|
+
example of a "Type-Specifying Extension at scale" — every
|
|
121
|
+
`T.must`, `T.cast`, `T.bind`, `T.assert_type!` call is
|
|
122
|
+
recognised by call shape, not by sig.
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
PHPStan's `phpstan.neon` and Rigor's `.rigor.yml` /
|
|
127
|
+
`.rigor.dist.yml` use the same shape: a single config file at
|
|
128
|
+
the project root, autoloaded if present, with `paths:`,
|
|
129
|
+
severity controls, and includes.
|
|
130
|
+
|
|
131
|
+
| PHPStan | Rigor |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `phpstan.neon` | `.rigor.yml` |
|
|
134
|
+
| `phpstan.neon.dist` | `.rigor.dist.yml` |
|
|
135
|
+
| `paths:` | `paths:` |
|
|
136
|
+
| `level:` | `severity_profile:` |
|
|
137
|
+
| `excludePaths:` | (no analogue today — paths are explicitly listed) |
|
|
138
|
+
| `ignoreErrors:` (regex / pattern) | `disable:` (rule identifier or wildcard) |
|
|
139
|
+
| `parameters: ignoreErrors:` per-path | `# rigor:disable-file <rule>` at the file head |
|
|
140
|
+
| `includes:` | `includes:` |
|
|
141
|
+
| `phpstan-baseline.neon` | `.rigor-baseline.yml` (managed) — or `rigor.baseline.json` (ad-hoc) |
|
|
142
|
+
| `phpstan analyse --generate-baseline` | `rigor baseline generate` (managed) — or `rigor check --format=json > rigor.baseline.json` (ad-hoc) |
|
|
143
|
+
| `phpstan analyse` | `rigor check` |
|
|
144
|
+
| `phpstan analyse --baseline` | `rigor check` with `baseline: .rigor-baseline.yml` configured (managed) — or `rigor diff rigor.baseline.json` (ad-hoc) |
|
|
145
|
+
| Path resolution: relative to declaring file | Path resolution: relative to declaring file (same rule). |
|
|
146
|
+
|
|
147
|
+
Rigor has two baseline mechanisms: a **managed** baseline
|
|
148
|
+
(`rigor baseline generate` → `.rigor-baseline.yml`, read on
|
|
149
|
+
the next `rigor check` via the `baseline:` config key — the
|
|
150
|
+
closest match to PHPStan's `--baseline`), and a **lightweight**
|
|
151
|
+
ad-hoc snapshot (`rigor diff` over a `--format=json` dump).
|
|
152
|
+
Chapter 8 has the walkthrough.
|
|
153
|
+
|
|
154
|
+
The `includes:` semantics also match PHPStan's: declaration
|
|
155
|
+
order, later overrides earlier, the current file's keys win
|
|
156
|
+
over included files. Rigor's `.rigor.yml` does NOT auto-merge
|
|
157
|
+
with `.rigor.dist.yml` — the override must list the dist file
|
|
158
|
+
explicitly under `includes:`. PHPStan has the same behaviour
|
|
159
|
+
when you have both `phpstan.neon` and `phpstan.neon.dist` in
|
|
160
|
+
play.
|
|
161
|
+
|
|
162
|
+
## Stubs ↔ RBS
|
|
163
|
+
|
|
164
|
+
PHPStan reads PHP stub files (`.stub`) for libraries that ship
|
|
165
|
+
no PHPDoc. Rigor reads `.rbs` files for the same purpose. The
|
|
166
|
+
dispatch is similar: both tools layer "stub-declared
|
|
167
|
+
contract beats inferred-from-body," and both use the stub
|
|
168
|
+
files as the canonical place to attach refinements via
|
|
169
|
+
PHPDoc / `RBS::Extended` annotations.
|
|
170
|
+
|
|
171
|
+
| PHPStan | Rigor |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `*.stub` files | `.rbs` files in `sig/` (project) and `rbs_collection.lock.yaml` (third-party) |
|
|
174
|
+
| PHPDoc on stubs | `RBS::Extended` `%a{rigor:v1:...}` annotations |
|
|
175
|
+
| `#[Override]` / `#[\Deprecated]` attributes | RBS `attr_*` and `def` declarations |
|
|
176
|
+
| `phpstan/extension-installer` | Bundler + `Gemfile` for plugin gems |
|
|
177
|
+
|
|
178
|
+
A practical pattern that works in both worlds: keep the stub /
|
|
179
|
+
RBS file authoritative for the public contract, then layer
|
|
180
|
+
project-specific tightenings under
|
|
181
|
+
`@phpstan-*` / `RBS::Extended` directives that ship alongside
|
|
182
|
+
the stub.
|
|
183
|
+
|
|
184
|
+
## Severity profiles vs PHPStan levels
|
|
185
|
+
|
|
186
|
+
PHPStan's levels are a numeric ladder (0 = "shapes only," 10 =
|
|
187
|
+
"strictest"). Rigor's profiles are named (`lenient`,
|
|
188
|
+
`balanced`, `strict`).
|
|
189
|
+
|
|
190
|
+
| PHPStan level | Rigor profile (rough) | Notes |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| 0 – 2 | `lenient` | Most rules → `:warning`; uncertain rules drop to `:info`. |
|
|
193
|
+
| 3 – 6 | `balanced` (default) | Most rules → `:error`. |
|
|
194
|
+
| 7 – 10 | `strict` | Everything → `:error`, including `:warning` rules in `balanced`. |
|
|
195
|
+
|
|
196
|
+
The mapping is approximate (the rule sets are not 1:1), but
|
|
197
|
+
the practical advice is the same: start with the default,
|
|
198
|
+
tighten over time. Chapter 8's "helpful workflow" matches the
|
|
199
|
+
PHPStan onboarding pattern.
|
|
200
|
+
|
|
201
|
+
## "No annotations needed" — yes, but with stubs
|
|
202
|
+
|
|
203
|
+
PHPStan and Rigor share the philosophy that **inference does
|
|
204
|
+
the heavy lifting**. You do not annotate every variable; you
|
|
205
|
+
annotate the boundary (function signatures, library stubs)
|
|
206
|
+
and inference propagates inward.
|
|
207
|
+
|
|
208
|
+
PHPStan's catch is that PHPDoc lives in the same file as the
|
|
209
|
+
PHP source. Rigor's catch is that RBS lives in `sig/`, a
|
|
210
|
+
parallel tree. The trade-offs are well-known:
|
|
211
|
+
|
|
212
|
+
- **Same-file PHPDoc** keeps the docs adjacent to the code
|
|
213
|
+
they describe — easier to update, harder to forget.
|
|
214
|
+
- **Parallel `.rbs`** keeps the runtime source clean for
|
|
215
|
+
developers who do not care about types — no PHPDoc clutter
|
|
216
|
+
on production methods.
|
|
217
|
+
|
|
218
|
+
Rigor leans toward the parallel-file model for cultural reasons
|
|
219
|
+
(Ruby's tradition of compact source), but `RBS::Inline`
|
|
220
|
+
provides an in-file alternative for projects that want
|
|
221
|
+
PHPDoc-style adjacency. See ADR-1 for the rationale.
|
|
222
|
+
|
|
223
|
+
## What PHPStan has and Rigor does not
|
|
224
|
+
|
|
225
|
+
- **Generics with bounded constraints across the stub library.**
|
|
226
|
+
PHPStan's generics ecosystem is more mature; RBS generics
|
|
227
|
+
exist but the standard library's coverage is patchier.
|
|
228
|
+
- **`@phpstan-impure` and pure-by-default modelling.** Rigor
|
|
229
|
+
catalogues per-method purity inside its built-in
|
|
230
|
+
`data/builtins/ruby_core/` YAML, but does not yet expose a
|
|
231
|
+
user-facing way to declare a method pure for fold-through.
|
|
232
|
+
- **Custom rules.** PHPStan's `Rule` interface lets you write a
|
|
233
|
+
rule in PHP that fires on AST patterns; Rigor's plugin
|
|
234
|
+
surface covers diagnostics emission via
|
|
235
|
+
`#diagnostics_for_file`, but the rule shape is less polished
|
|
236
|
+
than PHPStan's framework.
|
|
237
|
+
- **`treatPhpDocTypesAsCertain`.** PHPStan's "trust PHPDoc"
|
|
238
|
+
knob has no Rigor equivalent — Rigor always trusts RBS
|
|
239
|
+
declarations as authoritative.
|
|
240
|
+
|
|
241
|
+
## What Rigor has and PHPStan does not
|
|
242
|
+
|
|
243
|
+
- **Constant folding through method calls.** PHPStan does some
|
|
244
|
+
constant propagation; Rigor folds aggressively through
|
|
245
|
+
catalogued built-ins (`Numeric`, `String`, `Symbol`, `Array`,
|
|
246
|
+
`Hash`).
|
|
247
|
+
- **First-class flow-sensitive narrowing on Ruby's predicate
|
|
248
|
+
methods.** `s.empty?` / `n.zero?` / `n.positive?` etc. are
|
|
249
|
+
recognised by name and narrow accordingly. PHPStan has the
|
|
250
|
+
same idea via Type-Specifying Extensions, but Rigor ships
|
|
251
|
+
the catalogue out of the box.
|
|
252
|
+
- **`literal-string` carrier.** Both tools have the concept,
|
|
253
|
+
but Rigor's carrier composes through interpolation —
|
|
254
|
+
`"#{a}#{b}"` is `literal-string` if both `a` and `b` are.
|
|
255
|
+
PHPStan has `literal-string` for "literal at this position"
|
|
256
|
+
but the propagation rules are different.
|
|
257
|
+
- **Sorbet-input adapter.** If your project happens to be
|
|
258
|
+
partially-Sorbet (you migrated some files to RBS but kept
|
|
259
|
+
the rest), Rigor reads both sources concurrently. PHPStan
|
|
260
|
+
has nothing analogous — there is no parallel "Sorbet of
|
|
261
|
+
PHP."
|
|
262
|
+
|
|
263
|
+
## A migration vignette
|
|
264
|
+
|
|
265
|
+
You are porting a PHPStan-tightened library to Ruby. The
|
|
266
|
+
original PHP:
|
|
267
|
+
|
|
268
|
+
```php
|
|
269
|
+
class Slug {
|
|
270
|
+
/**
|
|
271
|
+
* @phpstan-param non-empty-string $name
|
|
272
|
+
* @phpstan-return non-empty-lowercase-string
|
|
273
|
+
*/
|
|
274
|
+
public function normalise(string $name): string {
|
|
275
|
+
return strtolower(preg_replace('/\s+/', '-', $name));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @phpstan-assert non-empty-string $value
|
|
280
|
+
*/
|
|
281
|
+
public function assertNotEmpty(string $value): void {
|
|
282
|
+
if ($value === '') throw new InvalidArgumentException();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The Rigor port — Ruby source unchanged from idiomatic, RBS at
|
|
288
|
+
the boundary:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# lib/slug.rb
|
|
292
|
+
class Slug
|
|
293
|
+
def normalise(name)
|
|
294
|
+
name.downcase.gsub(/\s+/, "-")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def assert_not_empty(value)
|
|
298
|
+
raise ArgumentError if value.empty?
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```rbs
|
|
304
|
+
# sig/slug.rbs
|
|
305
|
+
class Slug
|
|
306
|
+
%a{rigor:v1:param: name is non-empty-string}
|
|
307
|
+
%a{rigor:v1:return: non-empty-lowercase-string}
|
|
308
|
+
def normalise: (String name) -> String
|
|
309
|
+
|
|
310
|
+
%a{rigor:v1:assert value is non-empty-string}
|
|
311
|
+
def assert_not_empty: (String value) -> void
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The directive grammar is structurally a translation: every
|
|
316
|
+
PHPStan `@phpstan-*` becomes a `%a{rigor:v1:...}` annotation on
|
|
317
|
+
the matching `def` line in the `.rbs` file.
|
|
318
|
+
|
|
319
|
+
## What's next
|
|
320
|
+
|
|
321
|
+
You probably do not need to read the rest of this appendix
|
|
322
|
+
section sequentially. Three useful pointers:
|
|
323
|
+
|
|
324
|
+
- [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
|
|
325
|
+
has the full directive grammar, including the PHPStan-mapping
|
|
326
|
+
table that this page summarises.
|
|
327
|
+
- [Chapter 8 — Understanding errors](08-understanding-errors.md)
|
|
328
|
+
covers the rule catalogue, severity profiles, baseline
|
|
329
|
+
diffing — every PHPStan onboarding analogue.
|
|
330
|
+
- [Chapter 9 — Plugins](09-plugins.md) for the
|
|
331
|
+
Type-Specifying / Dynamic-Return analogues.
|
|
332
|
+
|
|
333
|
+
If you want to compare against another tool, the sibling
|
|
334
|
+
appendix pages cover [TypeScript](appendix-typescript.md),
|
|
335
|
+
[mypy](appendix-mypy.md), [Steep](appendix-steep.md),
|
|
336
|
+
[TypeProf](appendix-typeprof.md),
|
|
337
|
+
[Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
|
|
338
|
+
[Go](appendix-go.md), and [Elixir](appendix-elixir.md).
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Appendix — Protocols, interfaces, and structural typing
|
|
2
|
+
|
|
3
|
+
If you arrived from Python, "protocol" means one specific thing:
|
|
4
|
+
`typing.Protocol`, PEP 544's **structural typing** — a class
|
|
5
|
+
satisfies a protocol by *having the right methods*, no inheritance
|
|
6
|
+
required. It is "static duck typing," and it is one of the first
|
|
7
|
+
things a Python typist reaches for.
|
|
8
|
+
|
|
9
|
+
That instinct is right, but the *word* is a trap in Rigor. Rigor's
|
|
10
|
+
structural-typing feature is not called "protocol" — it is the RBS
|
|
11
|
+
**`interface`**. Meanwhile "protocol" *does* appear in Rigor, naming
|
|
12
|
+
a **different** feature: a framework's path-scoped *behavioural
|
|
13
|
+
contract* ([ADR-28](../adr/28-path-scoped-protocol-contracts.md)).
|
|
14
|
+
This appendix untangles the two so you reach for the right one.
|
|
15
|
+
|
|
16
|
+
> **The one-line version.** Rigor's `interface` is **structural** —
|
|
17
|
+
> like Go's `interface` and Python's `Protocol`. A class fits by
|
|
18
|
+
> *having the methods*; there is **no `implements`** clause (Ruby has
|
|
19
|
+
> none). It is *not* the Java / PHP nominal `interface`, where a class
|
|
20
|
+
> declares conformance by name. Because the bare word "interface"
|
|
21
|
+
> reads as the Java / PHP kind to many Ruby developers (Ruby has no
|
|
22
|
+
> `interface` keyword, so the intuition is imported), Rigor's docs
|
|
23
|
+
> qualify it on first use — **"structural interface"** or **"RBS
|
|
24
|
+
> interface"** — and so should you when writing about it.
|
|
25
|
+
|
|
26
|
+
## Two things called "protocol"
|
|
27
|
+
|
|
28
|
+
| You mean… | The Rigor word | What it is | Lives where |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| "static duck typing" — a class fits if it has the methods (Python `Protocol`) | **interface** | A structural *type* in the type lattice. | RBS `interface _Foo`, read from `sig/` |
|
|
31
|
+
| "every action under `app/actions/` must define `#handle`" — a framework convention enforced by tooling | **protocol contract** | A *behavioural contract* bound to classes by file path, declared by a plugin. | A plugin's `protocol_contracts:` manifest field (ADR-28) |
|
|
32
|
+
|
|
33
|
+
The two are different axes, not two flavours of one
|
|
34
|
+
thing, and the rest of this page is mostly about keeping them
|
|
35
|
+
apart. The one-line discriminator:
|
|
36
|
+
|
|
37
|
+
- An **interface** is a *type* you name in a signature. The check
|
|
38
|
+
happens *where the interface is mentioned* (`def f: (_Closable)
|
|
39
|
+
-> void` checks `f`'s argument).
|
|
40
|
+
- A **protocol contract** is *never mentioned by the conforming
|
|
41
|
+
class or any signature*. A plugin says "the classes under this
|
|
42
|
+
directory carry this contract," and the engine provisions and
|
|
43
|
+
checks them implicitly.
|
|
44
|
+
|
|
45
|
+
## Structural typing: RBS interfaces
|
|
46
|
+
|
|
47
|
+
This is the direct analogue of Python's `Protocol`. RBS has had
|
|
48
|
+
structural `interface _Foo` since its first release; Rigor reads
|
|
49
|
+
them from `sig/` and checks conformance structurally.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# Python (PEP 544)
|
|
53
|
+
class SupportsClose(Protocol):
|
|
54
|
+
def close(self) -> None: ...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```rbs
|
|
58
|
+
# RBS — the same idea
|
|
59
|
+
interface _SupportsClose
|
|
60
|
+
def close: () -> void
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A class that defines `close` with a compatible signature satisfies
|
|
65
|
+
the interface **with no `include`, no superclass, and no runtime
|
|
66
|
+
marker** — the match is structural. From the normative spec
|
|
67
|
+
([`structural-interfaces-and-object-shapes.md`](../type-specification/structural-interfaces-and-object-shapes.md)):
|
|
68
|
+
|
|
69
|
+
> An RBS interface type … is a *named structural contract*. A
|
|
70
|
+
> nominal type or object shape is assignable to an interface when
|
|
71
|
+
> Rigor can prove that it provides all required members with
|
|
72
|
+
> compatible types.
|
|
73
|
+
|
|
74
|
+
Where does Rigor apply structural matching? Deliberately at the
|
|
75
|
+
**boundaries**, not class-to-class by default:
|
|
76
|
+
|
|
77
|
+
- assigning or passing a value where an RBS interface is expected;
|
|
78
|
+
- checking whether an inferred object shape satisfies an interface;
|
|
79
|
+
- a direct method send against a known shape;
|
|
80
|
+
- plugin-provided dynamic reflection adding members to a shape.
|
|
81
|
+
|
|
82
|
+
Ordinary `Foo`/`Bar` class compatibility stays **nominal** — Ruby's
|
|
83
|
+
own `is_a?` / `kind_of?` depend on the class hierarchy, and RBS uses
|
|
84
|
+
class names as declarations about Ruby constants, so Rigor does
|
|
85
|
+
*not* go fully TypeScript-style-structural for classes. Structural
|
|
86
|
+
typing is a tool you opt into by naming an interface, exactly as in
|
|
87
|
+
Python you opt in by annotating against a `Protocol`. (The
|
|
88
|
+
[mypy / Pyright appendix](appendix-mypy.md#protocols--rbs-interfaces)
|
|
89
|
+
covers the same mapping from the Python side.)
|
|
90
|
+
|
|
91
|
+
## Object shapes and capability roles
|
|
92
|
+
|
|
93
|
+
RBS interfaces are the *named* structural types. Rigor also infers
|
|
94
|
+
**anonymous** structural types — *object shapes* — from what a value
|
|
95
|
+
demonstrably responds to, and ships a curated catalogue of
|
|
96
|
+
**capability roles** for the common IO-like contracts:
|
|
97
|
+
|
|
98
|
+
- `_Reader`, `_Writer` — the read/write halves of a stream;
|
|
99
|
+
- `_RewindableStream`, `_ClosableStream` — `rewind` / `close`
|
|
100
|
+
capabilities;
|
|
101
|
+
- `_Callable` — responds to `call`.
|
|
102
|
+
|
|
103
|
+
These keep `IO` and `StringIO` as separate *nominal* types while
|
|
104
|
+
letting each satisfy the smaller structural *roles* a method
|
|
105
|
+
actually needs — the structural-typing payoff (write against the
|
|
106
|
+
capability, not the concrete class) without giving up nominal
|
|
107
|
+
identity where Ruby's runtime relies on it.
|
|
108
|
+
|
|
109
|
+
If you came looking for "Rigor's Protocol," **this section is it**:
|
|
110
|
+
RBS interfaces + object shapes + capability roles are Rigor's
|
|
111
|
+
structural-typing surface. None of it is spelled "protocol."
|
|
112
|
+
|
|
113
|
+
## The word vs the semantics, across languages
|
|
114
|
+
|
|
115
|
+
Why does Rigor spell its structural type `interface` and reserve
|
|
116
|
+
`protocol` for something else? Because the *word* "protocol" and the
|
|
117
|
+
*semantics* "structural typing" have drifted apart across languages,
|
|
118
|
+
and Rigor picks the spelling that least surprises a Ruby reader (RBS
|
|
119
|
+
already says `interface`).
|
|
120
|
+
|
|
121
|
+
The term itself is old. **Smalltalk** (1970s) called the set of
|
|
122
|
+
messages an object understands its *protocol* — grouped into "message
|
|
123
|
+
protocols" in the class browser. It was never a static-checking
|
|
124
|
+
construct; it named "what you can send this object," the direct
|
|
125
|
+
ancestor of Ruby's duck typing. Every later use inherits that core
|
|
126
|
+
idea — "a set of methods a conforming object provides" — and then
|
|
127
|
+
adds its own rules about *how conformance is established*.
|
|
128
|
+
|
|
129
|
+
Two such rules matter, and they are independent of the spelling:
|
|
130
|
+
|
|
131
|
+
- **Structural / implicit** — you conform by *having the methods*. No
|
|
132
|
+
declaration. (Python `Protocol`, Go `interface`, **RBS `interface`**,
|
|
133
|
+
Smalltalk's original sense.)
|
|
134
|
+
- **Nominal / explicit** — you conform by *declaring that you do*.
|
|
135
|
+
(Java / PHP `interface ... implements`, Swift / Objective-C
|
|
136
|
+
`protocol` with explicit adoption.)
|
|
137
|
+
|
|
138
|
+
The spelling does **not** track the rule; the two crossed long ago:
|
|
139
|
+
|
|
140
|
+
| Language | Spelling | Conformance | Same as Rigor? |
|
|
141
|
+
| --- | --- | --- | --- |
|
|
142
|
+
| Smalltalk | "protocol" | (dynamic; duck typing) | ancestor of the idea |
|
|
143
|
+
| **Rigor / RBS** | **`interface`** | **structural / implicit** | — |
|
|
144
|
+
| Python (PEP 544) | `Protocol` | structural / implicit | ✅ same model, different word |
|
|
145
|
+
| Go | `interface` | structural / implicit | ✅ same model, same word |
|
|
146
|
+
| Java / PHP | `interface` | nominal / explicit `implements` | ❌ same word, opposite model |
|
|
147
|
+
| Swift / Objective-C | `protocol` | nominal / explicit adoption | ❌ different word *and* model |
|
|
148
|
+
|
|
149
|
+
So a reader's intuition depends entirely on where they came from:
|
|
150
|
+
|
|
151
|
+
- **From Swift / Objective-C:** "protocol" means a type you *declare*
|
|
152
|
+
conformance to (`struct Resource: Closable`). Rigor's `interface`
|
|
153
|
+
needs no such declaration — just define `close`. (Objective-C's
|
|
154
|
+
`respondsToSelector:` and informal protocols are the runtime
|
|
155
|
+
duck-typing escape hatch; Swift allows *retroactive* conformance via
|
|
156
|
+
extensions, but adoption is still explicit.) And the one Rigor
|
|
157
|
+
surface that reuses the *word* — the **protocol contract** below — is
|
|
158
|
+
not Swift's protocol either: it binds classes by file path, not by an
|
|
159
|
+
adoption clause.
|
|
160
|
+
- **From Java / PHP:** "interface" means a contract a class must
|
|
161
|
+
`implement` by name. Rigor reuses the *word* but not the rule: a
|
|
162
|
+
Ruby class satisfies an RBS `interface` structurally, never by an
|
|
163
|
+
`implements` clause (Ruby has none).
|
|
164
|
+
- **From Python or Go:** you are already home. RBS `interface` is your
|
|
165
|
+
`Protocol` / `interface` — structural, implicit, checked where it is
|
|
166
|
+
named.
|
|
167
|
+
|
|
168
|
+
And the Smalltalk sense — "a named set of messages a conforming type
|
|
169
|
+
must provide" — is exactly what Rigor revives under the name **protocol
|
|
170
|
+
contract** in the next section. Fittingly, that is the one Rigor *does*
|
|
171
|
+
spell "protocol."
|
|
172
|
+
|
|
173
|
+
## Protocol contracts (ADR-28)
|
|
174
|
+
|
|
175
|
+
Now the other axis. A Rack-shaped web framework expects a
|
|
176
|
+
controller action to take a request and return a response; a job
|
|
177
|
+
framework expects `#perform`; a serializer expects `#call`. The
|
|
178
|
+
convention is real, but it lives in the framework's prose — *no
|
|
179
|
+
class declaration records it*, and nothing checks it, so a
|
|
180
|
+
violation is a runtime surprise.
|
|
181
|
+
|
|
182
|
+
An RBS interface cannot express this. RBS (like Python) has **no
|
|
183
|
+
"every class under this directory implements interface I" form** —
|
|
184
|
+
an interface only bites where a signature names it, and these
|
|
185
|
+
controllers name nothing. That gap is what ADR-28's **protocol
|
|
186
|
+
contracts** fill.
|
|
187
|
+
|
|
188
|
+
A plugin that knows the framework declares a contract in its
|
|
189
|
+
manifest (`param_types` is an array of positional
|
|
190
|
+
`{ index:, type_name: }` provisions):
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# inside a framework plugin's manifest — an illustrative serializer contract
|
|
194
|
+
protocol_contracts: [
|
|
195
|
+
Rigor::Plugin::ProtocolContract.new(
|
|
196
|
+
path_glob: "app/serializers/**/*.rb", # which files
|
|
197
|
+
method_name: :call, # the method every class must define
|
|
198
|
+
param_types: [{ index: 0, type_name: "ActiveRecord::Base" }],
|
|
199
|
+
return_type_name: "String",
|
|
200
|
+
severity: :error
|
|
201
|
+
)
|
|
202
|
+
]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The engine then does **provide-and-check**:
|
|
206
|
+
|
|
207
|
+
- **Provide** (engine-side). When binding a `def call(record)` in a
|
|
208
|
+
matching file, the contract's `param_types` *replace* the usual
|
|
209
|
+
`Dynamic[top]` the un-annotated parameter would get. The body is
|
|
210
|
+
then analysed as if `record` carried its real type — so a misuse
|
|
211
|
+
inside the body (`record.no_such_column`) surfaces as an ordinary
|
|
212
|
+
`call.undefined-method`, and the inferred return type is precise.
|
|
213
|
+
- **Check** (plugin-side). The plugin confirms every class in a
|
|
214
|
+
matching file defines the method (else `missing-protocol-method`)
|
|
215
|
+
and that its inferred return type conforms to `return_type_name`
|
|
216
|
+
(else `protocol-return-mismatch`).
|
|
217
|
+
|
|
218
|
+
The provision half is the load-bearing one: without it, `request`
|
|
219
|
+
is `Dynamic[top]`, which answers every method, so any return built
|
|
220
|
+
from it is also `Dynamic[top]` and the return check is vacuous.
|
|
221
|
+
|
|
222
|
+
Two things to note as an *application* developer (not a plugin
|
|
223
|
+
author):
|
|
224
|
+
|
|
225
|
+
- You **never write** `protocol_contracts:` — the framework's
|
|
226
|
+
plugin does. You write a plain `def handle(request)`, and it gets
|
|
227
|
+
checked for free.
|
|
228
|
+
- The `missing-protocol-method` / `protocol-return-mismatch`
|
|
229
|
+
diagnostics are **plugin diagnostics**, emitted under the
|
|
230
|
+
plugin's `plugin.<id>.` provenance — not core Rigor rules. The
|
|
231
|
+
worked references are [`examples/rigor-web/`](../../examples/rigor-web/)
|
|
232
|
+
(the minimal tutorial) and [`plugins/rigor-hanami/`](../../plugins/rigor-hanami/)
|
|
233
|
+
(production Hanami 2 actions).
|
|
234
|
+
|
|
235
|
+
## Interface vs protocol contract
|
|
236
|
+
|
|
237
|
+
| | RBS `interface` (structural type) | ADR-28 protocol contract |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| **What it is** | A type in the lattice | A tooling-enforced convention; *not* a type |
|
|
240
|
+
| **Cross-language analogue** | Python `Protocol`, Go `interface` | the Smalltalk "required message set" sense — but bound by **file path**, a mechanism no mainstream `protocol`/`interface` has |
|
|
241
|
+
| **How a class opts in** | Structurally — just have the methods | Implicitly — be defined under the path glob |
|
|
242
|
+
| **Where it's referenced** | Named in a signature (`(_Closable) -> void`) | Named *nowhere*; bound by file path |
|
|
243
|
+
| **Where the check fires** | At the use site that names the interface | At the contracted `def` (provide) + per class (check) |
|
|
244
|
+
| **Who declares it** | Whoever writes the `.rbs` | A framework plugin's manifest |
|
|
245
|
+
| **Provides parameter types?** | No (it's a type, used where named) | **Yes** — into an un-annotated `def` |
|
|
246
|
+
| **Diagnostics** | Core type errors at the use site | `missing-protocol-method`, `protocol-return-mismatch` (plugin) |
|
|
247
|
+
|
|
248
|
+
The naming overlap is historical (see [the cross-language
|
|
249
|
+
detour](#the-word-vs-the-semantics-across-languages) above): the
|
|
250
|
+
Smalltalk "set of required messages" sense survives in the *protocol
|
|
251
|
+
contract*, while Python's `typing.Protocol` reused the word for the
|
|
252
|
+
*structural-type* idea that Ruby/RBS spell `interface`. So in Rigor,
|
|
253
|
+
"protocol" never means the structural type.
|
|
254
|
+
|
|
255
|
+
## Which one do I want?
|
|
256
|
+
|
|
257
|
+
- You want to **write a method that accepts anything with a
|
|
258
|
+
`#close`** → that is a structural type. Declare an RBS
|
|
259
|
+
**`interface _Closable`** and annotate against it (or rely on
|
|
260
|
+
inferred object shapes for the anonymous case). See
|
|
261
|
+
[Chapter 7 — RBS and RBS::Extended](07-rbs-and-extended.md).
|
|
262
|
+
- You want to **enforce that every class in a directory implements
|
|
263
|
+
a framework method with given parameter/return types** → that is
|
|
264
|
+
a **protocol contract**, and it is a *plugin-authoring* feature.
|
|
265
|
+
See [ADR-28](../adr/28-path-scoped-protocol-contracts.md) and the
|
|
266
|
+
[`examples/`](../../examples/README.md) walkthroughs.
|
|
267
|
+
- You are an **application developer** using such a framework → you
|
|
268
|
+
do neither explicitly. Write idiomatic Ruby; the framework's
|
|
269
|
+
plugin supplies the contract, and Rigor checks your actions
|
|
270
|
+
against it. When you see `missing-protocol-method`, you forgot
|
|
271
|
+
the method the framework requires; when you see
|
|
272
|
+
`protocol-return-mismatch`, your action returns the wrong shape.
|
|
273
|
+
|
|
274
|
+
## What's next
|
|
275
|
+
|
|
276
|
+
- [Chapter 7 — RBS and RBS::Extended](07-rbs-and-extended.md) — how
|
|
277
|
+
to write the `.rbs` an interface lives in.
|
|
278
|
+
- [Chapter 9 — Plugins](09-plugins.md) and the
|
|
279
|
+
[examples/ landing page](../../examples/README.md) — where
|
|
280
|
+
protocol contracts are authored.
|
|
281
|
+
- [`docs/type-specification/structural-interfaces-and-object-shapes.md`](../type-specification/structural-interfaces-and-object-shapes.md)
|
|
282
|
+
— the normative spec for interfaces, object shapes, and
|
|
283
|
+
capability roles.
|
|
284
|
+
- [ADR-28](../adr/28-path-scoped-protocol-contracts.md) — the
|
|
285
|
+
protocol-contract design decision and its rejected alternatives
|
|
286
|
+
(including *why* an RBS interface bound by directory was not the
|
|
287
|
+
route).
|
|
288
|
+
- Coming from another checker? The
|
|
289
|
+
[mypy / Pyright appendix](appendix-mypy.md#protocols--rbs-interfaces)
|
|
290
|
+
maps `Protocol` ↔ RBS `interface` from the Python side, and the
|
|
291
|
+
[type-theory appendix](appendix-type-theory.md) places nominal vs
|
|
292
|
+
structural typing in the broader landscape.
|