Filed unassigned as a finding — recording only, not graded. Surfaced while implementing #11222 (PR #11677) from the domain:engine seat. ⛔ Nothing here blocks that PR, and the specific instance #11222 recorded is fixed by it. What survives is the general gap, which #11222's card named in passing and which nothing now tracks once that card closes.
The gap
check:slot-lookup (the #4251 ratchet, scripts/check-slot-lookup-ratchet.mjs) is the only gate covering the service-lookup slot. It bans erasing a service lookup to any — ctx.getService('settings') with no type argument, or one typed any. It says nothing about whether the type you do name describes the object you actually call.
Both halves of the erasure are already gone in the case just fixed, and the gate was green throughout:
Green gate, swept file, and a declaration that under-stated its consumer by a whole method for the entire interval.
Why nothing else catches it either
The type is erased three times between the declaration and the call, so no tsc program ever compares them:
ctx.getService<SettingsReadSurface>('settings') is a cast, not a structural check — nothing in the repo implements the interface.rest-server.ts widens the provider's return to a bare promise.resolveLocalizationContext receives settings?: any.
And TS erasure means a narrow declaration cannot strip the method off the real object, so the runtime feature-detect finds getMany regardless. The declaration is inert in both directions: it cannot break anything, and nothing can notice it is wrong.
The blast radius is correspondingly bounded, which is what keeps this a finding: a host author reading the interface to decide what their occupant must implement is told a method is not consumed. Believing it is safe — the consumer feature-detects and degrades. The cost is a false declaration plus an unrealized optimization, never a wrong answer.
Scope, honestly
This shape exists wherever a SettingsReadSurface-style per-consumer interface names a slot occupant. Known instances today: packages/rest/src/rest-api-plugin.ts (fixed in #11677), packages/plugins/plugin-auth/src/auth-plugin.ts:77, and a reference in packages/plugins/plugin-email/src/email-plugin.ts:159. The latter two were checked during #11677 and are accurate — neither calls getMany (grep: zero hits) — so there is no second live instance right now. The gap is that this was established by hand, and there is no way to establish it mechanically.
Not obviously fixable, which is part of the finding
A completeness check has no obvious ground truth: these interfaces are deliberately per-consumer and deliberately narrow (that is the #4251 B4 decision for an OPTIONAL slot — the REST layer must not acquire a runtime dependency on service-settings, and SettingsService declares no implements). "Complete" cannot mean "matches the service's full API"; it would have to mean "covers every member the consuming package actually calls on this value" — a reachability question across three erasure boundaries.
Possible angles, none costed:
- Compare members called on the lookup result within the consuming package against the named interface's members. Defeated by boundary 2 above (the value leaves the package as a bare promise), which may itself be the more tractable thing to fix.
- Require slot interfaces to be declared once and imported, rather than re-declared per consumer — a bigger architectural change that trades away the B4 property on purpose.
- Accept it as uncoverable and rely on the pattern that caught it here: a card that changes what a consumer calls also re-reads the declarations.
⛔ Not graded, no domain:* set, no pm:queue — triage's to route, including "won't fix, recorded".
Generated by Claude Code
Filed unassigned as a
finding— recording only, not graded. Surfaced while implementing #11222 (PR #11677) from thedomain:engineseat. ⛔ Nothing here blocks that PR, and the specific instance #11222 recorded is fixed by it. What survives is the general gap, which #11222's card named in passing and which nothing now tracks once that card closes.The gap
check:slot-lookup(the #4251 ratchet,scripts/check-slot-lookup-ratchet.mjs) is the only gate covering the service-lookup slot. It bans erasing a service lookup toany—ctx.getService('settings')with no type argument, or one typedany. It says nothing about whether the type you do name describes the object you actually call.Both halves of the erasure are already gone in the case just fixed, and the gate was green throughout:
packages/rest/src/rest-api-plugin.tsnamesSettingsReadSurface— a real, hand-written interface — so the lookup is typed and the file is not inscripts/slot-lookup-baseline.json, i.e. already swept.get. The platform had been callinggetManythrough it since resolveLocalizationContext reads the same sys_setting namespace three times per request #10826.Green gate, swept file, and a declaration that under-stated its consumer by a whole method for the entire interval.
Why nothing else catches it either
The type is erased three times between the declaration and the call, so no
tscprogram ever compares them:ctx.getService<SettingsReadSurface>('settings')is a cast, not a structural check — nothing in the repoimplementsthe interface.rest-server.tswidens the provider's return to a bare promise.resolveLocalizationContextreceivessettings?: any.And TS erasure means a narrow declaration cannot strip the method off the real object, so the runtime feature-detect finds
getManyregardless. The declaration is inert in both directions: it cannot break anything, and nothing can notice it is wrong.The blast radius is correspondingly bounded, which is what keeps this a finding: a host author reading the interface to decide what their occupant must implement is told a method is not consumed. Believing it is safe — the consumer feature-detects and degrades. The cost is a false declaration plus an unrealized optimization, never a wrong answer.
Scope, honestly
This shape exists wherever a
SettingsReadSurface-style per-consumer interface names a slot occupant. Known instances today:packages/rest/src/rest-api-plugin.ts(fixed in #11677),packages/plugins/plugin-auth/src/auth-plugin.ts:77, and a reference inpackages/plugins/plugin-email/src/email-plugin.ts:159. The latter two were checked during #11677 and are accurate — neither callsgetMany(grep: zero hits) — so there is no second live instance right now. The gap is that this was established by hand, and there is no way to establish it mechanically.Not obviously fixable, which is part of the finding
A completeness check has no obvious ground truth: these interfaces are deliberately per-consumer and deliberately narrow (that is the #4251 B4 decision for an OPTIONAL slot — the REST layer must not acquire a runtime dependency on
service-settings, andSettingsServicedeclares noimplements). "Complete" cannot mean "matches the service's full API"; it would have to mean "covers every member the consuming package actually calls on this value" — a reachability question across three erasure boundaries.Possible angles, none costed:
⛔ Not graded, no
domain:*set, nopm:queue— triage's to route, including "won't fix, recorded".Generated by Claude Code