Skip to content

feat(gates): name the runtime-interface-only ADR-0087 disposition and check it (#8299) - #8456

Draft
os-zhuang wants to merge 2 commits into
mainfrom
claude/issue-8299-runtime-interface-disposition
Draft

feat(gates): name the runtime-interface-only ADR-0087 disposition and check it (#8299)#8456
os-zhuang wants to merge 2 commits into
mainfrom
claude/issue-8299-runtime-interface-disposition

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8299

ADR-class: this PR is for the maintainer to merge by hand. It touches docs/adr/**, so it is pushed, opened as a draft and left visibly waiting — never queued, never auto-merged. check-adr-merge-approval will be RED until an approving review exists; that is the gate working, not a defect in this branch.

The gap, and what closes it

ADR-0087's changeset disposition had a closed, three-valued vocabulary — unpublished, already-registered, no-migration-prescription — and every member reasons about metadata. PR #8277 (out of scope: not addressed here, already merged) hit a shape outside that taxonomy: a published runtime TypeScript interface with no metadata surface at all. Its exemption argument was correct and was accepted, but it was prose with no category to cite and nothing mechanical behind it.

This PR carries both halves in one commit series so the text and the predicate cannot drift apart at birth:

  • docs/adr/0087-metadata-protocol-upgrade-contract.md — a new addendum recording the disposition vocabulary itself (it had never been written down anywhere but the gate) and ratifying D7, runtime-interface-only.
  • scripts/check-adr-0087-registration.mjs — the category, its predicate, and a two-way pin between the two files.
  • scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json — PD [WIP] Add Chinese version of the documentation #13 anchor, so the next author of this gate is told which decision they are standing on.

Three measurements, one of which changed the design

1. The proposed predicate, run literally, REFUSES its own worked example. The card proposed "the touched exported symbol appears in no *.zod.ts, no spec contracts/** entry, and no object definition". Measured on origin/main:

git grep -l 'PackagePublishResult' -- '*.zod.ts'
packages/spec/src/system/metadata-persistence.zod.ts
git grep -l 'PackagePublishResult' -- 'packages/spec/src/contracts/**'
packages/spec/src/contracts/metadata-service.ts

Both hits are a homonym: metadata-persistence.zod.ts:166 declares its own unrelated PackagePublishResult, and the contracts file merely imports that one. Neither is the service interface #8277 changed (packages/services/service-package/src/index.ts:76). A bare name is not a symbol identity in this repo — so a claim names path#Symbol, the notation packages/spec/export-origins/*.json already uses, and the reference scan clears a hit file that declares the name itself or imports it from another module.

That collision is now the predicate's own accept/refuse fixture, and it is real rather than synthetic: the same name under two paths must come out two different ways.

2. #8277's exemption was held by a detector MISS, not by a finding. Running the shipped detector on its real changeset (.changeset/lucky-schools-smash.md, merged as fc71b84): findMigrationPrescription returns null, even though the body does tell consumers "read result.driverFault?.message where you read result.error" — no arrow, no framing, so branches 1–4 all miss it. Two consequences, both encoded: the category was worth naming, and it can inherit the prescription refusal at zero cost to the worked example.

3. The disposition vocabulary was not in the ADR at all.grep -i 'disposition\|changeset' docs/adr/0087-*.md returned nothing before this PR. The card's "the disposition text lives with ADR-0087" was aspiration, not fact — so the addendum writes the whole vocabulary down, which is what makes a category citable.

What the category checks

For each named path#Symbol, at HEAD:

  1. it resolves — an exported interface / type / class / enum really exists there;
  2. its declaration site is not a metadata surface — not a *.zod.ts, not under packages/spec/src/contracts/, not an object definition;
  3. its declaration is not a Zod projectionz.input< typeof X > and family (spaced here only so GitHub's sanitizer does not eat it), which live in ordinary .ts files too;
  4. no metadata surface references it — steps 2–3 only say where a symbol was born.

And it inherits the no-migration-prescription refusal, which is what makes it a narrowing of that catch-all rather than a fifth way around it. Nothing refused today becomes claimable by renaming the category (pinned: RIO-R9).

Deliberately not checked: that the author named every symbol their PR touched. Like registered, the gate judges the claim that was made — inferring the touched surface is the cross-package retirement detector the 2026-08-07 ruling routes around.

The two halves cannot drift

assertInputs gains a fifth rot assertion: the categories CATEGORIES accepts and the categories ADR-0087 documents must be the same set, both directions. 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. Both are red, and the assertion was observed failing before the ADR half was committed.

Evidence

Accept/refuse proved on the real repository, planting a changeset and running the shipping gate (--base origin/main), then reset away:

ACCEPT packages/services/service-package/src/index.ts#PackagePublishResult
✓ check-adr-0087-registration: 1 declared-breaking changeset(s) ...
not-required (runtime-interface-only) -- verified:
packages/services/service-package/src/index.ts#PackagePublishResult (interface)
REFUSE packages/spec/src/system/metadata-persistence.zod.ts#PackagePublishResult (same NAME)
✗ ... is false: packages/spec/src/system/metadata-persistence.zod.ts is a Zod schema.
REFUSE PackagePublishResult (bare name)
✗ ... names "PackagePublishResult", which is not a `path#Symbol` reference.

Self-test: 153 → 202 assertions, all over real temp git repos through the real scan() / assertInputs() path. New cases RIO-G1, RIO-R1RIO-R9, V1V3, U1U12b.

Gates run locally, all green except the one that is supposed to be red: check:adr-0087-registration (+ --self-test), check:changeset-gate-self-tests, check:adr-anchors, check:objectui-changeset, check-adr-links (534 links resolve), check:nul-bytes (7621 files), check-adr-merge-approval --self-test. Gate families re-derived against the actual changed paths with scripts/pm/dispatch-gates.mjs: identical to the dispatch list, nothing added.

Why check:objectui-changeset is in the diff

objectui-changeset-digest.mjs builds a throwaway repo and runs this gate as a subprocess (#6494 round trip). The new input assertion made that fixture red, correctly — it had no ADR. It now copies the real record next to the gate copy it already makes, the one spelling that cannot drift when the vocabulary next changes.

No changeset

scripts/ + docs/adr/ touch no published package's shipped files, so nothing releases; skip-changeset applied.


Generated by Claude Code

os-zhuangand others added 2 commits August 13, 2026 13:58
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 2:03pm

Request Review

@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation labels Aug 13, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM note on the red ADR maintainer approval check — no fix is owed and none will be pushed.

That gate's own header states the rule: a PR whose diff touches docs/adr/** must not be mergeable without an approving review, and must not sit on an armed auto-merge. This PR touches docs/adr/0087-metadata-protocol-upgrade-contract.md, so the red is the gate doing its job on a PR that has not been reviewed by a human yet — not a defect in the branch. Read the gate's semantics on origin/main rather than taking the PR body's word for it; it says the pass condition is the presence of an approving review.

⛔ This seat will not clear it. The standing guardrail for docs/adr/** is that the domain:spec-tooling seat reviews and then leaves the PR visibly hanging for the maintainer's own merge — never ready + auto-merge, never queued. Arming auto-merge would itself trip the second half of this same gate.

Awaiting a human merge. Everything else on the branch is green and the substance is in the review comment on #8299.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Second CI note — the ESLint job's red is a different thing from the ADR gate's, and it is not an eslint finding.

The job dies inside an unrelated gate's fixture self-test:

at fixtureSelfTest (scripts/check-regen-pending.mjs:455)
Error: Command failed: git merge --abort
error: Entry 'package.json' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD'.

Attribution, measured rather than assumed — this branch changes exactly four files:

docs/adr/0087-metadata-protocol-upgrade-contract.md
scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json
scripts/check-adr-0087-registration.mjs
scripts/objectui-changeset-digest.mjs

Neither package.json nor scripts/check-regen-pending.mjs is among them, and the failure is that script's own temp-repo fixture ending up with a dirty index — an environment/state failure in a gate this diff does not touch.

Next step is a re-run of the failed job to separate flake from real: rerun-failed-jobs is currently refused with "This workflow is already running" (TypeScript Type Check is still going), so it will be re-issued once the run completes. If it reproduces on a clean re-run, it is not a flake and this seat will chase it as a real blocker rather than land around it — and if it reproduces on main too, that is a pre-existing break and will be said so here plainly.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

ESLint resolved: flake, not a defect — and stated with both readings rather than as a verdict.

Same commit 06542a4, two attempts:

attemptconclusionevidence
1 (14:03Z)failuredied in scripts/check-regen-pending.mjsfixtureSelfTestgit merge --abortEntry 'package.json' not uptodate
2 (14:16Z, re-run of failed jobs)successall 51 steps green, including the step that hosted the failure (Changeset-family gate self-tests)

A red that does not reproduce on the identical tree is an environment/state failure in that gate's temp-repo fixture, not a finding about this branch — which is also what the attribution said before the re-run: the diff touches four files, none of them package.json or check-regen-pending.mjs.

⚠️ Not claimed: that the fixture is sound. A self-test whose temp repo can reach a dirty index under load is a flake generator, and it will bite someone else. That is a separate surface from this PR and this seat is not filing it as a rider here — if it recurs it deserves its own card against that script.

Current state of this PR: everything green exceptADR maintainer approval, which stays red by design until an approving human review exists. Awaiting a human merge — ⛔ this seat will not mark it ready, arm auto-merge, or queue it.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant

@os-zhuang