Skip to content

Commit 9cfdddd

Browse files
os-zhuangos-support-aiclaude
authored
feat(gates): name the runtime-interface-only ADR-0087 disposition and check it (#8299) (#8456)
* feat(gates): name the runtime-interface-only ADR-0087 disposition and check it (#8299) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WocN37om5bw81JDoEEMA2e * test(gates): give the objectui digest fixture the ADR the 0087 gate now reads (#8299) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WocN37om5bw81JDoEEMA2e --------- Co-authored-by: os-zhuang <support@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 478f1fd commit 9cfdddd

4 files changed

Lines changed: 797 additions & 10 deletions

File tree

docs/adr/0087-metadata-protocol-upgrade-contract.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,3 +563,107 @@ re-entering `saveMetaItem` with already-canonical bodies pay nothing.
563563

564564
Reads still skip flows, and now the loop is closed from the other side: a
565565
served legacy body is healed the moment it is saved back.
566+
567+
## Addendum (2026-08-13) — the changeset disposition vocabulary, and its fifth category (#8299)
568+
569+
Everything above is about the ledger itself. This addendum records the **question
570+
asked of every declared-breaking changeset***what did you do about the ADR-0087
571+
migration ledger?* — and the closed vocabulary of answers, because until now that
572+
vocabulary lived only in the gate that enforces it, and an author who disagreed with
573+
it had nothing to cite.
574+
575+
The question is answered in the changeset body, in one HTML comment. It is a
576+
comment rather than a visible line because a changeset body is copied verbatim into
577+
`CHANGELOG.md` and shipped to end users; the marker is for this repo's authors and
578+
reviewers, and stays fully visible where they read — the PR diff, `git grep`, the
579+
gate's log and its `--list` output.
580+
581+
```text
582+
<!-- adr-0087: registered <id>[, <id>...] -->
583+
<!-- adr-0087: not-required (unpublished) <why> -->
584+
<!-- adr-0087: not-required (already-registered <id>[, <id>...]) <why> -->
585+
<!-- adr-0087: not-required (no-migration-prescription) <why> -->
586+
<!-- adr-0087: not-required (runtime-interface-only <path>#<Symbol>[, ...]) <why> -->
587+
```
588+
589+
**The vocabulary is closed, and every exemption is re-verified on every run** — an
590+
allow-list nobody re-checks is the failure mode this whole mechanism exists to
591+
avoid. `registered` must name ids that resolve *and* are new in the diff;
592+
`unpublished` requires every bumped package to be `private: true`;
593+
`already-registered` requires the named ids to pre-date the merge base;
594+
`no-migration-prescription` is refused by a body that carries a migration
595+
prescription; and `runtime-interface-only` is the subject of the rest of this
596+
addendum. The checks live in
597+
[`scripts/check-adr-0087-registration.mjs`](../../scripts/check-adr-0087-registration.mjs),
598+
which is also where each one's measured history is written down.
599+
600+
### D7 — a published runtime TS interface with no metadata surface is compiler-carried, and needs no ledger entry
601+
602+
Every other category reasons about **metadata**: a Zod schema, a spec declaration, a
603+
stored row — something `objectstack migrate meta` can reach. A published **runtime
604+
TypeScript interface** with none of those is outside that taxonomy, and it kept
605+
arriving anyway. The worked example is PR #8277, which removed the `error` member of
606+
`PackagePublishResult` in `packages/services/service-package/src/index.ts`. Its
607+
argument, in the changeset's own words: no Zod schema, no `packages/spec`
608+
declaration, no stored representation, so nothing exists for `objectstack migrate
609+
meta` to rewrite — and the channel that actually reaches every affected consumer is
610+
the **compiler** (`error TS2339: Property 'error' does not exist on type
611+
'PackagePublishResult'`), which is strictly more precise than a ledger line.
612+
613+
**That argument is accepted, and it is now a named category.** It was accepted once
614+
before as prose, judged by hand, riding on `no-migration-prescription` — an
615+
exemption whose one mechanical check is that the body carries no prescription.
616+
Measured on #8277's real changeset, the prescription detector returns *nothing*, so
617+
the exemption was held by a detector **miss** rather than by a positive finding.
618+
That is why it was worth naming: the next author either re-derives the paragraph, or
619+
pattern-matches the much looser *"no metadata surface ⇒ no changeset discipline"*,
620+
which is **not** what #8277 argued and is not what this decision ratifies.
621+
622+
An author may claim it when, for each symbol they name, all four hold at HEAD — and
623+
the gate checks all four:
624+
625+
1. **it resolves**`<path>#<Symbol>` names an exported `interface` / `type` /
626+
`class` / `enum` that really exists. An unverifiable claim is refused, never
627+
assumed true.
628+
2. **its declaration site is not a metadata surface** — not a `*.zod.ts`, not a file
629+
under `packages/spec/src/contracts/`, not an object definition.
630+
3. **its declaration is not a projection of a Zod schema**`z.input<typeof X>`
631+
and its family. Under Prime Directive #1 that is the house spelling of *"this
632+
type IS metadata"*, and it appears in ordinary `.ts` files too.
633+
4. **no metadata surface references it** — steps 2–3 only say where a symbol was
634+
born; a runtime interface pulled into a schema or an object definition has a
635+
metadata surface wherever it was declared.
636+
637+
**It also inherits the `no-migration-prescription` refusal.** A body that prescribes
638+
a rewrite cannot claim this category either, so it is a **narrowing** of that
639+
catch-all and never a fifth way around it: nothing refused today becomes claimable
640+
by renaming the category. What the author gains is an exemption that rests on a
641+
positive, re-runnable finding.
642+
643+
#### Why the symbol is path-qualified
644+
645+
The obvious spelling — *"the touched symbol appears in no `*.zod.ts`, no spec
646+
`contracts/**` entry, and no object definition"* — is a bare-name grep, and run
647+
literally it **refuses its own worked example**. `PackagePublishResult` names two
648+
unrelated symbols in this repo: the service interface #8277 changed, and a Zod
649+
projection in `packages/spec/src/system/metadata-persistence.zod.ts` that
650+
`packages/spec/src/contracts/metadata-service.ts` imports. A bare name is not a
651+
symbol identity here, so a claim names `<path>#<Symbol>` — the notation
652+
`packages/spec/export-origins/*.json` already uses — and the reference scan clears a
653+
hit file that either declares the name itself or imports it from somewhere other
654+
than the declaring module. The pair is the gate's own accept/refuse fixture: the
655+
same name under two paths must come out two different ways.
656+
657+
#### What this does not decide
658+
659+
That the author named **every** symbol their PR touched. Like `registered`, the gate
660+
judges the claim that was made, not its completeness — inferring the touched surface
661+
is the cross-package retirement detector the 2026-08-07 ruling deliberately routes
662+
around, and it is no more decidable here. The gain is that the claim is a checkable
663+
sentence a reviewer can re-run, instead of a paragraph they must re-derive.
664+
665+
**The text and the predicate are pinned to each other.** The gate refuses to report
666+
a verdict unless the categories listed above and the categories it accepts are the
667+
same set, checked in both directions. A category added to the gate and described
668+
nowhere is an exemption an author cannot look up; a category described here that the
669+
gate rejects is an exemption nobody can claim. Both are red.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"file": "scripts/check-adr-0087-registration.mjs",
3+
"adrs": [
4+
"ADR-0087"
5+
],
6+
"invariant": "This gate enforces ADR-0087's changeset disposition: every changeset a PR newly declares breaking must carry exactly one `<!-- adr-0087: ... -->` marker, and every `not-required` category is RE-VERIFIED on every run — an allow-list nobody re-checks is the failure mode the whole mechanism exists to avoid. `CATEGORIES` is closed and is pinned to ADR-0087's addendum of 2026-08-13 in BOTH directions by `assertInputs`: a category the gate accepts and the ADR never describes is an exemption an author cannot look up; a category the ADR describes and the gate rejects is one nobody can claim (#8299). `runtime-interface-only` is a NARROWING of `no-migration-prescription`, never a fifth way around it — it inherits that refusal, and it names symbols as `<path>#<Symbol>` because a bare name is not a symbol identity here (`PackagePublishResult` is two unrelated symbols: the service interface PR #8277 changed, and a Zod projection in `packages/spec`). Never widen the exemptions here without the ADR half in the same PR, and never let this gate infer WHICH symbols a PR touched — that is the cross-package retirement detector the maintainer ruling of 2026-08-07 deliberately routes around."
7+
}

0 commit comments

Comments
 (0)