You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while implementing #9232 (narrowing the flat REST door's thrown code). Filed unassigned, not fixed there: #9232 narrows doors, and registering or renaming a code is the packages/spec lane's call — dispatcher-error-vocabulary.ts's own scope note says "⛔ Nothing here edits the ledger."
The measurement
packages/plugins/plugin-security/src/object-posture-gate.ts:119 throws a real, live refusal carrying a lowercase code:
throw postureError(
'owd_widening_forbidden',
`object/${ctx.name}: an environment overlay may only TIGHTEN a packaged object's OWD, never widen it ...`,
)
It reaches an HTTP error envelope — packages/rest/src/meta-object-owd-gate.test.ts drives it end to end through PUT /api/v1/meta/object/:name and reads it off the wire.
It is not an ADR-0112 member on two counts at once:
D1 rules the value space ^[A-Z][A-Z0-9_]*$. A lowercase code cannot be one.
ERROR_CODE_LEDGER does not carry it in any casing. Verified against the live union: ErrorCode.safeParse('owd_widening_forbidden') and ErrorCode.safeParse('OWD_WIDENING_FORBIDDEN') both fail. Control: ErrorCode.safeParse('PERMISSION_DENIED') succeeds, so the probe works.
Why the gate never reported it
pnpm check:dispatcher-error-vocabulary passes clean, and reports its scope as "1838 non-test source files under packages/; 290 registered codes; 12 unregistered code-stamping site(s) found; 12 classified." This site is not among the 12.
The reason is structural, not a missing table row: the scan's shapes match SCREAMING_SNAKE literals. A lowercase code stamps no site it can see, so it is not reported as unclassified either — it is invisible, which is the same class of blind spot #9223 fixed for objlitconst / objlittemplate (a shape that "matched NOTHING and was not even reported as unresolved").
So the gate's guarantee is narrower than it reads: it sweeps unregistered SCREAMING_SNAKE codes, not codes outside the vocabulary. Lowercase is precisely the ADR-0112 batch-2 sweep residue the gate would be most useful for catching.
Not an outage, and #9232 already improved the wire answer: the flat door now demotes it, so the body is { code: 'PERMISSION_DENIED', declaredCode: 'owd_widening_forbidden' } — a legal member with the gate's own spelling preserved beside it. Before #9232 it was { code: 'owd_widening_forbidden' }, a body that could never satisfy the ApiErrorSchema it claimed to.
Consumer check: nothing branches on it. Grep across packages/, examples/, the objectui checkout and cloud returned 7 hits, all of them the producer itself or prose (control: the same grep finds the producer, so the search works).
(1) and (2) are separable; (1) is the one that stops the next one.
Related: #9232 (where this was found) · #9106 / ADR-0112 (the demote rule) · #8087 / #9223 (the gate and its previous blind spot) · #8846 (the registration hand-off).
Found while implementing #9232 (narrowing the flat REST door's thrown
code). Filed unassigned, not fixed there: #9232 narrows doors, and registering or renaming a code is thepackages/speclane's call —dispatcher-error-vocabulary.ts's own scope note says "⛔ Nothing here edits the ledger."The measurement
packages/plugins/plugin-security/src/object-posture-gate.ts:119throws a real, live refusal carrying a lowercase code:It reaches an HTTP error envelope —
packages/rest/src/meta-object-owd-gate.test.tsdrives it end to end throughPUT /api/v1/meta/object/:nameand reads it off the wire.It is not an ADR-0112 member on two counts at once:
^[A-Z][A-Z0-9_]*$. A lowercase code cannot be one.ERROR_CODE_LEDGERdoes not carry it in any casing. Verified against the live union:ErrorCode.safeParse('owd_widening_forbidden')andErrorCode.safeParse('OWD_WIDENING_FORBIDDEN')both fail. Control:ErrorCode.safeParse('PERMISSION_DENIED')succeeds, so the probe works.Why the gate never reported it
pnpm check:dispatcher-error-vocabularypasses clean, and reports its scope as "1838 non-test source files under packages/; 290 registered codes; 12 unregistered code-stamping site(s) found; 12 classified." This site is not among the 12.The reason is structural, not a missing table row: the scan's shapes match SCREAMING_SNAKE literals. A lowercase code stamps no site it can see, so it is not reported as unclassified either — it is invisible, which is the same class of blind spot #9223 fixed for
objlitconst/objlittemplate(a shape that "matched NOTHING and was not even reported as unresolved").So the gate's guarantee is narrower than it reads: it sweeps unregistered SCREAMING_SNAKE codes, not codes outside the vocabulary. Lowercase is precisely the ADR-0112 batch-2 sweep residue the gate would be most useful for catching.
Current state after #9232
Not an outage, and #9232 already improved the wire answer: the flat door now demotes it, so the body is
{ code: 'PERMISSION_DENIED', declaredCode: 'owd_widening_forbidden' }— a legal member with the gate's own spelling preserved beside it. Before #9232 it was{ code: 'owd_widening_forbidden' }, a body that could never satisfy theApiErrorSchemait claimed to.Consumer check: nothing branches on it. Grep across
packages/,examples/, the objectui checkout andcloudreturned 7 hits, all of them the producer itself or prose (control: the same grep finds the producer, so the search works).What needs deciding
code:in an object literal — a constant or template produces no finding, silently #9223 used.packages/speclane), or accept the demote as the permanent answer and letdeclaredCodecarry the app-specific spelling — which is what the [Decision] The dispatcher'serror.codehas a limb authored by TENANTS at runtime — registration cannot close it, and ADR-0112 does not say what should happen there #9106 / finding:packages/rest's flatsendThrownErrorstill puts a thrown error'scodeon the wire un-narrowed — ADR-0112's closure does not reach that door #9232 rule already does for every unregistered spelling.(1) and (2) are separable; (1) is the one that stops the next one.
Related: #9232 (where this was found) · #9106 / ADR-0112 (the demote rule) · #8087 / #9223 (the gate and its previous blind spot) · #8846 (the registration hand-off).