Met while writing the changeset for the #12104 shipping half (a @objectstack/client
return-type narrowing). Recorded rather than worked around in place: the workaround the gate
leaves available is the one that makes its own trigger less meaningful.
The dead end, measured
The changeset declared **BREAKING** — truthfully: narrowing a published Promise< any > to a
concrete type can stop a consumer compiling. check-adr-0087-registration then demands one of
its five dispositions, and every one is unavailable:
| disposition |
why it does not apply |
registered <id> |
The ADR-0087 registry feeds objectstack migrate meta, spec-changes.json and the generated upgrade guide. Nothing metadata-shaped moved — no Zod schema, no packages/spec declaration, no stored representation — so an entry would carry no prescription any of those three can project. It would be false data in the one ledger the gate exists to keep true. |
not-required (unpublished) |
False. @objectstack/client publishes to npm. |
not-required (already-registered <id>) |
False. No entry covers it. |
not-required (no-migration-prescription) |
Refused by the gate, because the changeset carries a FROM/TO block: "A changeset that ships instructions for rewriting a consumer's code cannot also claim that no consumer has to rewrite anything." |
not-required (runtime-interface-only …) |
The gate says outright it "inherits this same refusal (#8299)". |
The refusal text names the intended exit — "if the prescription is genuinely for someone the
ledger does not serve, use a category that can be verified (unpublished,
already-registered)" — and both of those are factually false here. So the exit the gate
offers is closed for exactly the case it describes.
What the class actually is
A published TYPE-surface narrowing: a consumer-facing prescription exists (rewrite
(await client.analytics.query(q)).rows as .data.rows), the affected party is a TypeScript
consumer, and the delivery channel is the compiler at their own call site. The ADR-0087 ledger
serves metadata upgraders and cannot reach them; the compile error already does, more precisely
than a release note.
The gate's no-migration-prescription refusal is not wrong in general — #6048 is exactly the
shape it was built to catch — it just cannot tell "a prescription for a METADATA upgrader" from
"a prescription for a source-code consumer", and only the first is the ledger's business.
Why this is worth a card rather than a shrug
The available move is to drop the **BREAKING** token, which is what the three prior cards of
this same class did (#8140, #11925, #12034 — all @objectstack/client return-type narrowings,
all minor, all recording the no-entry disposition in prose, none using the token). #12104
followed that precedent for consistency, and the consumer impact is stated in full either way.
But the cumulative effect is the concern: a whole recurring class of genuinely
compile-breaking change is nudged away from the repo's breaking-change marker, purely because
the marker routes it into a ledger it does not belong in. That erodes the token this gate
depends on, and the erosion is invisible — every gate stays green.
Two shapes worth weighing (not prescribed)
- A sixth category — e.g.
not-required (type-surface-only <package>), verified the way
unpublished is: the named package publishes, nothing under packages/spec changed in the
diff, and no ADR-0087-shaped surface moved. Mechanically checkable from the same diff the
gate already reads.
- Narrow the
no-migration-prescription refusal to prescriptions aimed at the ledger's
own audience, rather than firing on any FROM/TO block.
Either way, the class should be able to say **BREAKING** and still answer the gate honestly.
Generated by Claude Code
Met while writing the changeset for the #12104 shipping half (a
@objectstack/clientreturn-type narrowing). Recorded rather than worked around in place: the workaround the gate
leaves available is the one that makes its own trigger less meaningful.
The dead end, measured
The changeset declared
**BREAKING**— truthfully: narrowing a publishedPromise< any >to aconcrete type can stop a consumer compiling.
check-adr-0087-registrationthen demands one ofits five dispositions, and every one is unavailable:
registered <id>objectstack migrate meta,spec-changes.jsonand the generated upgrade guide. Nothing metadata-shaped moved — no Zod schema, nopackages/specdeclaration, no stored representation — so an entry would carry no prescription any of those three can project. It would be false data in the one ledger the gate exists to keep true.not-required (unpublished)@objectstack/clientpublishes to npm.not-required (already-registered <id>)not-required (no-migration-prescription)not-required (runtime-interface-only …)The refusal text names the intended exit — "if the prescription is genuinely for someone the
ledger does not serve, use a category that can be verified (
unpublished,already-registered)" — and both of those are factually false here. So the exit the gateoffers is closed for exactly the case it describes.
What the class actually is
A published TYPE-surface narrowing: a consumer-facing prescription exists (rewrite
(await client.analytics.query(q)).rowsas.data.rows), the affected party is a TypeScriptconsumer, and the delivery channel is the compiler at their own call site. The ADR-0087 ledger
serves metadata upgraders and cannot reach them; the compile error already does, more precisely
than a release note.
The gate's
no-migration-prescriptionrefusal is not wrong in general — #6048 is exactly theshape it was built to catch — it just cannot tell "a prescription for a METADATA upgrader" from
"a prescription for a source-code consumer", and only the first is the ledger's business.
Why this is worth a card rather than a shrug
The available move is to drop the
**BREAKING**token, which is what the three prior cards ofthis same class did (#8140, #11925, #12034 — all
@objectstack/clientreturn-type narrowings,all
minor, all recording the no-entry disposition in prose, none using the token). #12104followed that precedent for consistency, and the consumer impact is stated in full either way.
But the cumulative effect is the concern: a whole recurring class of genuinely
compile-breaking change is nudged away from the repo's breaking-change marker, purely because
the marker routes it into a ledger it does not belong in. That erodes the token this gate
depends on, and the erosion is invisible — every gate stays green.
Two shapes worth weighing (not prescribed)
not-required (type-surface-only <package>), verified the wayunpublishedis: the named package publishes, nothing underpackages/specchanged in thediff, and no ADR-0087-shaped surface moved. Mechanically checkable from the same diff the
gate already reads.
no-migration-prescriptionrefusal to prescriptions aimed at the ledger'sown audience, rather than firing on any FROM/TO block.
Either way, the class should be able to say
**BREAKING**and still answer the gate honestly.Generated by Claude Code