⛔ Recorded for triage; no severity asserted, no domain:*, no type — routing and grading are triage's. Found while binding oauth.applications.delete under #15451; filed rather than ridden along, because settling it is a decision about a governed surface (docs/adr/0087-metadata-protocol-upgrade-contract.md and the gate that enforces its vocabulary).
The shape
ADR-0087's sixth category, added 2026-08-30 under #13080, is described in the ADR and in the gate header as
a published SDK method whose declared return moves off any onto the contract it always answered
and D8 justifies the exemption with "the only thing that moved is a type annotation".
That description fits a narrowing where the body is untouched. It does not name the case where the body has to move for any annotation to be true at all — and that case is not exotic: it is what happens when the reason a return is erased is that the method is broken.
Worked example, measured end to end on #15451. ObjectStackClient.oauth.applications.delete ended return res.json() on a route that answers HTTP 200 with a zero-byte body, so it rejected with a SyntaxError on every successful delete. Its own filing card states the consequence exactly: no declared return type can be true while that call stands. void becomes the wire fact only after the body changes. So binding it is:
- a type-surface narrowing off an erased
any — predicates 1 through 4 of type-surface-only all hold on the real diff; and
- a runtime behaviour change from always-rejecting to resolving.
Both halves are BREAKING, and both reach the same audience through the same two channels — the compiler and the caller's own runtime. Neither half is anything objectstack migrate meta can rewrite, which is the question the disposition actually asks: no Zod schema, no packages/spec path, no stored representation moves.
Why this is not already covered
Why it matters rather than being a wording nit
The erosion #13080 was filed about is a class of genuinely breaking change being nudged away from the **BREAKING** token because the token routes it into a ledger it does not belong in. This is the same pressure one notch further along, and the exits are the ones that ADR already closed:
⇒ Three exits, each closed by something already written down. That is the finding.
Options a decision could take (not a recommendation to any of them)
- A. Widen the sixth category's text to "a published surface bound off an erased type, including the body change required to make the binding true", leaving predicates 1 through 4 exactly as they are — predicate 4 already carries the safety and is untouched either way.
- B. A seventh category for a behaviour-carrying binding, with its own predicate (e.g. the base-side member is erased AND its base-side body is provably unable to produce any value, as here).
- C. Rule that the sixth category may be asserted as-is whenever predicates 1 through 4 hold, and amend D8 so the text stops promising more than the predicates check.
⚠️ Any of these edits docs/adr/**, a governed surface — maintainer's, by hand.
Reproduce
gh pr diff <the #15451 PR> # the diff both halves are read off
node scripts/check-adr-0087-registration.mjs --base origin/main
sed -n '/D8 — the affected party/,/^####/p' docs/adr/0087-metadata-protocol-upgrade-contract.md
Refs: #15451 (where it was measured) · #13080 (the sixth category) · #14502 (the mirror shape) · #15627 (the mechanical half) · PR #15445.
⛔ Recorded for triage; no severity asserted, no
domain:*, no type — routing and grading are triage's. Found while bindingoauth.applications.deleteunder #15451; filed rather than ridden along, because settling it is a decision about a governed surface (docs/adr/0087-metadata-protocol-upgrade-contract.mdand the gate that enforces its vocabulary).The shape
ADR-0087's sixth category, added 2026-08-30 under #13080, is described in the ADR and in the gate header as
and D8 justifies the exemption with "the only thing that moved is a type annotation".
That description fits a narrowing where the body is untouched. It does not name the case where the body has to move for any annotation to be true at all — and that case is not exotic: it is what happens when the reason a return is erased is that the method is broken.
Worked example, measured end to end on #15451.
ObjectStackClient.oauth.applications.deleteendedreturn res.json()on a route that answers HTTP 200 with a zero-byte body, so it rejected with aSyntaxErroron every successful delete. Its own filing card states the consequence exactly: no declared return type can be true while that call stands.voidbecomes the wire fact only after the body changes. So binding it is:any— predicates 1 through 4 oftype-surface-onlyall hold on the real diff; andBoth halves are BREAKING, and both reach the same audience through the same two channels — the compiler and the caller's own runtime. Neither half is anything
objectstack migrate metacan rewrite, which is the question the disposition actually asks: no Zod schema, nopackages/specpath, no stored representation moves.Why this is not already covered
**BREAKING**token instead #13080 / the sixth category — created for the type-only shape, and its own text excludes this one.needs-user-decision) — the mirror case: concrete at base plus a shipped prescription. This one is erased at base, which is the reading that separates them, so predicate 4 answers them oppositely.type-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 — the mechanical half of the same wall: a reference is a bare identifier, so a nested SDK member with a common name cannot be addressed at all. That one blocks the claim before this one is even reached. Fixing check-adr-0087-registration: atype-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 does not answer this card: it would make the claim spellable, and then the author has to decide whether a category whose text says "the only thing that moved is a type annotation" may be asserted over a diff that also moved the body.Why it matters rather than being a wording nit
The erosion #13080 was filed about is a class of genuinely breaking change being nudged away from the
**BREAKING**token because the token routes it into a ledger it does not belong in. This is the same pressure one notch further along, and the exits are the ones that ADR already closed:no-migration-prescription— mechanically accepted here, and it is exactly the detector miss D7 records against fix(service-package): classify a publish driver fault as 5xx and stop returning driver text as caller data (#8131) #8277 and names as the pattern the sixth category exists to stop;type-surface-onlyanyway — an exemption whose written description does not describe the diff.⇒ Three exits, each closed by something already written down. That is the finding.
Options a decision could take (not a recommendation to any of them)
docs/adr/**, a governed surface — maintainer's, by hand.Reproduce
Refs: #15451 (where it was measured) · #13080 (the sixth category) · #14502 (the mirror shape) · #15627 (the mechanical half) · PR #15445.