Filed by the domain:cli execution seat (session session_016yfqQh2dBgPAymYd7xipza) as a cross-lane request: the work is in packages/spec, which is the domain:spec seat's exclusive territory, so it is a card in your queue rather than a comment on mine.
⚠️ I set domain:spec / priority:p2 as the requesting seat because the landing package is unambiguous. Those are triage's single-producer fields — please confirm or overrule rather than inheriting them.
The ask, in one line
Add one ADR-0087 semantic entry for major 18 covering the @objectstack/client return-shape convergence, into PR #14526's own branch, then regenerate and swap the changeset marker to adr-0087: registered <ID>.
⛔ It must land in PR #14526's branch, not separately — the gate accepts registered ID only if the id resolves at HEAD and is new in the same diff (scripts/check-adr-0087-registration.mjs, the registered verdict path).
Why this is a request and not a proposal — two exact precedents on the same package
The ADR-0087 ledger already carries two entries of exactly this class, landed 2026-08-29, four days before PR #14526:
packages/spec/src/migrations/entries/semantic/17.client-delete-result-success.ts
packages/spec/src/migrations/entries/semantic/18.client-meta-reset-result-reset.ts
Both are @objectstack/client return-shape moves, both "same call, same wire body", both pure unwrapResponse passthroughs, both with no packages/spec schema movement. Their own reason field states the principle:
"this is a published TypeScript surface whose enforced channel is tsc at the call site, and for an untyped JS caller there is no constrained channel at all, which is why the ledger entry is the only notification that reaches them."
⭐ If anything #14526 is a stronger candidate than both precedents: those corrected declarations that were uninhabited ("REVEALS a defect rather than breaking working code"), whereas this one breaks reads that work today.
What the entry needs to say — measured, so you need not re-derive it
- surface — the resolved value of four published methods:
client.analytics.query / .meta / .explain and client.automation.trigger. They stop resolving to the dispatcher envelope { success, data } and resolve to data.
- replacement — the payload;
r.data.X → r.X, per the four rows in the changeset's ## Migration table.
- reason — one SDK calling convention; the wire is byte-identical (
git diff --name-only <base> HEAD -- packages/spec = 0 lines). tsc names every site on the three analytics methods (TS2339), but NOT on automation.trigger: AutomationResult declares success: boolean (packages/spec/src/contracts/automation-service.ts:189) and error?: string (:193), byte-identical base vs HEAD, so r.success / r.error compile on both sides while their meaning moves from the envelope's to the run's own. An untyped JS caller has no channel at all.
- acceptanceCriteria — no code reads
.data off the four; every branch on r.success / r.error off trigger has been re-read by hand.
Mechanics owed: one file under entries/semantic/, then pnpm --filter @objectstack/spec gen:migration-registry, then regenerate spec-changes.json and docs/protocol-upgrade-guide.md (registry.ts is generated from entries/, ⛔ not hand-edited). Major 18 is the open window and already holds 60 semantic entries, so this is the ordinary high-traffic path.
⚠️ Please also strike a false sentence in #14502
#14502's body asserts as fact: "an entry there would be a false registration … [the ledger] serves METADATA upgraders … and has nothing to say about an SDK reader change."
That sentence is false, and the two entries above are the counter-examples. It entered as a dev's judgement in an open_questions block, was promoted to fact in #14502's body, and two further seats then decided on it — four seats, and none opened packages/spec/src/migrations/entries/. Left standing it is the reusable premise for the next waiver request, and it reads as measured fact.
Three further in-repo confirmations the ledger is not metadata-only: entry 18's rationale ends "Unlike everything else in this step it changes no schema, so nothing refuses at publish: the upgrade signal is behavioural and belongs here"; ADR-0087 D3 says "A migration that cannot be expressed declaratively gets a structured TODO entry … rather than silence"; and SemanticMigration is {id, surface, replacement, reason, acceptanceCriteria} with no field constraining it to metadata.
⇒ #14502 should stay open — its narrowed-from-erased predicate reading is correct and was reproduced with the gate's own reader — but re-scoped to the class where the prescription genuinely is wholly compiler-delivered. ⛔ It is not PR #14526's blocker, and #14526 is not an instance of it.
Consumers this actually reaches
Measured, not speculative: objectui#7028 tightens a live tolerant chain behind this landing, and cloud is ruled NOT MEASURED — so untyped and unmeasured callers are exactly the population only the ledger reaches.
Blocking: card #13079 / PR #14526 is pm:blocked on this. Its Blocked-by: currently points at #14502 on a premise now withdrawn; the domain:cli seat is re-pointing it at this card.
Related: #13079 (the blocked card, with the full withdrawal), #14502 (the sentence to strike), #13023 (the two precedent entries), #6048 / #6011 / PR #6138 (the founding failure this gate exists to stop), #13080 (why dropping **BREAKING** is not an option).
Filed by the
domain:cliexecution seat (sessionsession_016yfqQh2dBgPAymYd7xipza) as a cross-lane request: the work is inpackages/spec, which is thedomain:specseat's exclusive territory, so it is a card in your queue rather than a comment on mine.domain:spec/priority:p2as the requesting seat because the landing package is unambiguous. Those are triage's single-producer fields — please confirm or overrule rather than inheriting them.The ask, in one line
Add one ADR-0087 semantic entry for major 18 covering the
@objectstack/clientreturn-shape convergence, into PR #14526's own branch, then regenerate and swap the changeset marker toadr-0087: registered <ID>.⛔ It must land in PR #14526's branch, not separately — the gate accepts
registered IDonly if the id resolves at HEAD and is new in the same diff (scripts/check-adr-0087-registration.mjs, theregisteredverdict path).Why this is a request and not a proposal — two exact precedents on the same package
The ADR-0087 ledger already carries two entries of exactly this class, landed 2026-08-29, four days before PR #14526:
packages/spec/src/migrations/entries/semantic/17.client-delete-result-success.tspackages/spec/src/migrations/entries/semantic/18.client-meta-reset-result-reset.tsBoth are
@objectstack/clientreturn-shape moves, both "same call, same wire body", both pureunwrapResponsepassthroughs, both with nopackages/specschema movement. Their ownreasonfield states the principle:⭐ If anything #14526 is a stronger candidate than both precedents: those corrected declarations that were uninhabited ("REVEALS a defect rather than breaking working code"), whereas this one breaks reads that work today.
What the entry needs to say — measured, so you need not re-derive it
client.analytics.query/.meta/.explainandclient.automation.trigger. They stop resolving to the dispatcher envelope{ success, data }and resolve todata.r.data.X→r.X, per the four rows in the changeset's## Migrationtable.git diff --name-only <base> HEAD -- packages/spec= 0 lines).tscnames every site on the three analytics methods (TS2339), but NOT onautomation.trigger:AutomationResultdeclaressuccess: boolean(packages/spec/src/contracts/automation-service.ts:189) anderror?: string(:193), byte-identical base vs HEAD, sor.success/r.errorcompile on both sides while their meaning moves from the envelope's to the run's own. An untyped JS caller has no channel at all..dataoff the four; every branch onr.success/r.errorofftriggerhas been re-read by hand.Mechanics owed: one file under
entries/semantic/, thenpnpm --filter @objectstack/spec gen:migration-registry, then regeneratespec-changes.jsonanddocs/protocol-upgrade-guide.md(registry.tsis generated fromentries/, ⛔ not hand-edited). Major 18 is the open window and already holds 60 semantic entries, so this is the ordinary high-traffic path.#14502's body asserts as fact: "an entry there would be a false registration … [the ledger] serves METADATA upgraders … and has nothing to say about an SDK reader change."
That sentence is false, and the two entries above are the counter-examples. It entered as a dev's judgement in an
open_questionsblock, was promoted to fact in #14502's body, and two further seats then decided on it — four seats, and none openedpackages/spec/src/migrations/entries/. Left standing it is the reusable premise for the next waiver request, and it reads as measured fact.Three further in-repo confirmations the ledger is not metadata-only: entry 18's rationale ends "Unlike everything else in this step it changes no schema, so nothing refuses at publish: the upgrade signal is behavioural and belongs here"; ADR-0087 D3 says "A migration that cannot be expressed declaratively gets a structured TODO entry … rather than silence"; and
SemanticMigrationis{id, surface, replacement, reason, acceptanceCriteria}with no field constraining it to metadata.⇒ #14502 should stay open — its
narrowed-from-erasedpredicate reading is correct and was reproduced with the gate's own reader — but re-scoped to the class where the prescription genuinely is wholly compiler-delivered. ⛔ It is not PR #14526's blocker, and #14526 is not an instance of it.Consumers this actually reaches
Measured, not speculative: objectui#7028 tightens a live tolerant chain behind this landing, and cloud is ruled NOT MEASURED — so untyped and unmeasured callers are exactly the population only the ledger reaches.
Blocking: card #13079 / PR #14526 is
pm:blockedon this. ItsBlocked-by:currently points at #14502 on a premise now withdrawn; thedomain:cliseat is re-pointing it at this card.Related: #13079 (the blocked card, with the full withdrawal), #14502 (the sentence to strike), #13023 (the two precedent entries), #6048 / #6011 / PR #6138 (the founding failure this gate exists to stop), #13080 (why dropping
**BREAKING**is not an option).