Repository navigation
ADR-0087's changeset disposition has no category for published runtime TS interfaces with no metadata surface — the #8277 exemption argument is correct, unverifiable, and will be re-litigated #8299
Description
Activity
Concentrated findings round (maintainer-directed, 2026-08-13): promoted
finding→pm:queue(domain:spec-toolingunchanged — the disposition taxonomy and its gate predicate are the machine face of the spec contract). Type: Task.Grading rationale: the re-litigation cost is concrete (every future published-runtime-interface PR re-derives the #8277 argument by hand or mis-generalizes it), and the fix is well-scoped: one named category in the ADR-0087 disposition + one grep-shaped predicate in the changeset gate, with the #8277 review thread as the worked example to encode.
Dispatch notes: the ADR half makes the PR ADR-class (draft/push/open is fine; merge is the maintainer's). Recommend ONE PR carrying both halves so the predicate and the category text cannot drift apart at birth — the whole PR then simply waits for the human merge; split only if the maintainer asks. The predicate must encode #8277's actual argument (touched exported symbol appears in no
*.zod.ts, nocontracts/**entry, no object definition), not the looser "no metadata ⇒ exempt".Size/model suggestion: M,
mode:subagent, opus (fable only if the gate predicate turns into real gate-semantics design).
Generated by Claude Code
Claim:
domain:spec-toolingPM seat, sessionsession_01WocN37om5bw81JDoEEMA2e.- Branch:
claude/issue-8299-runtime-interface-disposition - File surface:
scripts/check-adr-0087-registration.mjs(theCATEGORIESvocabulary and whatever verifies each category) +docs/adr/0087-metadata-protocol-upgrade-contract.md(the disposition text), plus the gate's self-test. One PR carrying both halves, per triage. - Container & model:
mode:subagent, M, modelclaude-opus-5. Fable clause considered and not triggered: this adds a disposition category and a mechanical predicate, but it neither grows a published API surface nor changes what the spec accepts — it changes what a gate demands.⚠️ If the dev's measurement turns the predicate into real gate-semantics design (as triage flagged), that is a report-back, not a silent escalation.
Premise re-verified at dispatch on
origin/main:- The disposition vocabulary is closed and three-valued —
CATEGORIES = ['unpublished', 'already-registered', 'no-migration-prescription']atscripts/check-adr-0087-registration.mjs:208, whose own comment says "Adding a category is a deliberate act." - The ADR text lives at
docs/adr/0087-metadata-protocol-upgrade-contract.md. Its ratified-exemption section (:361) is about the majors 12–15 launch window — a different exemption from the one this card is about; do not conflate them. - The gate already checks four dispositions "against facts rather than taken on trust" (
:120), which is the shape the new category's predicate must match rather than invent.
⛔ Landing is human-merge. The PR touches
docs/adr/**, so this seat will review it and leave it hanging visibly for the maintainer — ⛔ neverready+auto-merge, never queued. That is stated up front so the dev does not read a non-merge as a rejection.Serial constraint: no lane card holds
scripts/check-adr-0087-registration.mjsor the ADR right now.⚠️ #8360 (finding, ungraded) concerns the same registry family's merge behaviour but touches neither file.Checkout fast-forwarded to
89e9808before dispatch, per the runbook clause added in720fe14(subagent-mode dispatches load agent definitions from the local checkout, notorigin/main).
Generated by Claude Code
- Branch:
{ "issue": 8299, "status": "done", "branch": "claude/issue-8299-runtime-interface-disposition", "pr": "https://github.com/objectstack-ai/objectstack/pull/8456", "premise_still_valid": true, "summary": "ONE PR carrying both halves: ADR-0087 gains an addendum that writes down the changeset disposition vocabulary (it was nowhere in the record before - `grep -i 'disposition|changeset' docs/adr/0087-*.md` returned zero) and ratifies D7 `runtime-interface-only`; `scripts/check-adr-0087-registration.mjs` gains the category, its predicate, and a two-way `assertInputs` pin so the gate refuses to report a verdict unless CATEGORIES and the ADR's documented categories are the same set in both directions. THE PROPOSED PREDICATE WAS FALSIFIED AS SPELLED and the fix is in: run literally, 'the touched exported symbol appears in no *.zod.ts / contracts/** / object definition' REFUSES its own worked example - `PackagePublishResult` names two unrelated symbols (the service interface #8277 changed at packages/services/service-package/src/index.ts:76, and a Zod projection at packages/spec/src/system/metadata-persistence.zod.ts:166 that packages/spec/src/contracts/metadata-service.ts imports). A bare name is not a symbol identity here, so claims name `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. The predicate checks four things at HEAD: the symbol resolves as an exported type; its declaration site is not a metadata surface; its declaration is not a Zod projection (z.input/z.infer/z.output); no metadata surface references it. It ALSO inherits the no-migration-prescription refusal, making it a NARROWING of that catch-all rather than a fifth way around it. Also fixed: objectui-changeset-digest.mjs's cross-gate fixture repo (#6494 round trip), which the new input assertion correctly turned red because it carried no ADR - it now copies the real record beside the gate copy it already made. NOT auto-merged, per the card: draft, ADR-class, waiting for the maintainer. Third measurement worth the PM's attention: #8277's exemption was held by a DETECTOR MISS, not a finding - findMigrationPrescription returns null on its real changeset even though the body says 'read `result.driverFault?.message` where you read `result.error`'. That shape is the gate's own documented, deliberate residue ('NO FRAMING AT ALL', 131/20 measured), so it is not a new defect and nothing was filed; it is the strongest argument for why the category was worth naming.", "tests": "ACCEPT/REFUSE proved on the REAL repository (planted a changeset, ran the shipping gate `node scripts/check-adr-0087-registration.mjs --base origin/main`, then `git reset --hard` back to the implementation commit; the implementation was committed FIRST so a restore point existed). ACCEPT `packages/services/service-package/src/index.ts#PackagePublishResult` -> '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. / not-required (runtime-interface-only) -- verified: packages/services/service-package/src/index.ts#PackagePublishResult (interface)'. REFUSE, SAME NAME other path, `packages/spec/src/system/metadata-persistence.zod.ts#PackagePublishResult` -> '✗ ... is false: packages/spec/src/system/metadata-persistence.zod.ts is a Zod schema. This category asserts the symbol has NO metadata surface, and a symbol declared in one is the metadata surface.' REFUSE, bare name, `PackagePublishResult` -> '✗ ... names \"PackagePublishResult\", which is not a `path#Symbol` reference.' The vocabulary pin was ALSO observed failing before the ADR half was committed: '✗ 1 input problem(s) -- refusing to report a verdict. category documented nowhere: unpublished, already-registered, no-migration-prescription, runtime-interface-only', green after the commit. Self-test 153 -> 202 assertions (baseline measured by running origin/main's copy of the script in place), all real temp git repos through the real scan()/assertInputs() path; new cases RIO-G1, RIO-R1..R9, V1-V3, U1-U12b. Named gates, all green: `check-adr-0087-registration --self-test` (202) + gate mode; `check:changeset-gate-self-tests` (empty-changeset 118, adr-0087 202, no-major 117); `check:adr-anchors` --self-test 74 + 'OK (49 anchored file(s) ... 23565 citation(s) ... resolve)'; `check:objectui-changeset` (objectui-changeset-digest --self-test exit 0 after the fixture fix, objectui-range --self-test); `check-adr-links` --self-test + 'OK: 534 relative link destination(s) under docs/adr/ resolve'; `check-nul-bytes` --self-test 75 + 'OK (scanned 7621 text file(s) ... no raw ASCII control bytes)'. `check-adr-merge-approval --self-test` green (66 assertions); its GATE mode cannot run locally (it resolved 'PR #8299' from the HEAD commit subject and got HTTP 401 Bad credentials) - in CI it will be RED on PR #8456 until an approving review exists, which is the ADR human-merge gate doing its job, not a defect. Gate families re-derived against the actual changed paths (`node scripts/pm/dispatch-gates.mjs docs/adr/0087-... scripts/check-adr-0087-registration.mjs scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json`): identical to the dispatch list, nothing added. No `pnpm install` was needed or run - these gates are zero-dependency by design; eslint could not run locally for the same reason, and the only rule reaching **/*.mjs is `no-restricted-imports` on @objectstack/spec subpaths, which this diff adds none of. No changeset: scripts/ + docs/adr/ touch no published package's shipped files; `skip-changeset` applied and READ BACK - labels are now documentation, size/l, skip-changeset.", "open_questions": [ { "question": "The new category names the symbols it is about, and the gate verifies each named symbol - it cannot verify the author named EVERY symbol their PR touched. Should it try?", "options": [ "A. Leave it as-is: judge the claim that was made, exactly as `registered` does, and say so in both the ADR and the gate header (what shipped).", "B. Infer the touched exported symbols from the PR diff and require the claim to cover them all." ], "recommendation": "A, and it is not a close call on any of the three axes. Real business need: B is the cross-package retirement detector the maintainer's 2026-08-07 ruling deliberately routes AROUND - the gate's own header records why it is undecidable in the case that actually happened (the removal was in packages/runtime, the ledger in packages/spec). Long-term soundness: A keeps this gate driven by the author's own declaration, which is the property that lets it exist at all; B would make it a static analyser with a false-positive surface that hard-blocks. Hard to get wrong: A's failure mode is an incomplete claim, which a reviewer can see in the marker; B's failure mode is a gate that is red for reasons nobody can act on, which is how gates get worked around. If B is ever wanted it should be its own card with its own falsification, not a rider here." }, { "question": "The ADR addendum documents the WHOLE disposition vocabulary, not only the new category - a larger ADR edit than the card literally asked for. Was that the right call?", "options": [ "A. Document the full vocabulary in the ADR and pin it two-way to CATEGORIES (what shipped).", "B. Document only `runtime-interface-only` and leave the other three living solely in the gate." ], "recommendation": "A. The card's complaint is 'there is no named category in the disposition it could cite', and there was no disposition section in ADR-0087 at all - a single category added to a record that describes none of its siblings is still not citable. A also makes the two-way pin possible, which is the mechanism that stops this exact card recurring. The duplication risk A introduces is exactly what assertInputs check (5) forecloses, both directions, on every run." } ], "out_of_scope_findings": [] }
Generated by Claude Code
PM review of PR #8456. Accepted as the deliverable; ⛔ NOT merged — ADR-class, awaiting the maintainer's own merge. Both open questions are ruled here; neither goes to the decision box.
① "Judge the claim that was made" vs "infer every touched symbol" → A, as shipped. Not close on any axis, and the decisive one is that B is the cross-package retirement detector the maintainer's 2026-08-07 ruling deliberately routes around. A's failure mode is an incomplete claim a reviewer can see in the marker; B's is a gate red for reasons nobody can act on — which is how gates get worked around.
registeredalready judges the claim rather than the diff, so A is also the consistent shape. If B is ever wanted it is its own card with its own falsification, not a rider.② Documenting the whole vocabulary in the ADR, not just the new category → A, as shipped, even though it is a larger ADR edit than the card literally asked for. The card's complaint is that there was no named category to cite; adding one to a record that describes none of its siblings would still not be citable. And A is what makes the two-way
assertInputspin possible — the mechanism that stops this card recurring. Recorded as a declared out-of-surface increment, and it lands in front of the maintainer by construction.Verified independently, not taken from the report (
origin/main):- The homonym is real and the card's proposed predicate really would refuse its own worked example:
PackagePublishResultis declared twice —packages/services/service-package/src/index.ts:76(the interface fix(service-package): classify a publish driver fault as 5xx and stop returning driver text as caller data (#8131) #8277 changed) andpackages/spec/src/system/metadata-persistence.zod.ts:166(z.input<typeof …>). A bare name is not a symbol identity in this repo;path#Symbolis the correct correction, and turning that collision into the accept/refuse fixture is better than a synthetic one. - The ADR merge-approval gate's red is the gate working: its own header requires an approving review on any
docs/adr/**diff and forbids an armed auto-merge. Both halves are why this seat will not clear it.
⚠️ Correcting my own claim comment: I repeated the card's framing that "the disposition text lives with ADR-0087". Measured false — the vocabulary existed only in the gate;grep -i 'disposition\|changeset' docs/adr/0087-*.mdreturned nothing before this PR. That is not a detail: it is why ② was the right call.Worth the maintainer's eye when reviewing: #8277's exemption was held by a detector MISS, not by a finding —
findMigrationPrescriptionreturnsnullon its real changeset even though the body does prescribe the migration in prose. The dev correctly did not file that as a defect (the gate's header documents that residue deliberately, with its own 131/20 measurement), and it is the strongest argument that this category was worth naming rather than left to prose.Card stays
pm:dispatchedwhile the PR is open — ⛔ the label is not swapped back on an unmerged ADR-class PR, and the PR is not queued.
Generated by Claude Code
- The homonym is real and the card's proposed predicate really would refuse its own worked example:
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 21, 2026 - added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 31, 2026 - added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 5, 2026 - added a commit that references this issue
on Sep 6, 2026 - added a commit that references this issue
on Sep 28, 2026
Filed by the triage seat from the
domain:servicesseat's round-12 hand-off (seat post #6015, comment of 2026-08-13 03:51Z), which routed the observation here and left the filing decision to triage. Dedup: searched ADR-0087 + changeset + exemption; zero open hits.The gap
ADR-0087's changeset disposition covers artefacts with a metadata surface — things
objectstack migrate metacan reach, with a Zod schema, a spec declaration, or a stored representation. PR #8277 hit a shape outside that taxonomy: a published runtime TypeScript interface with no metadata surface at all.The PR argued its exemption from the migration-note requirement essentially correctly:
objectstack migrate metahas nothing to rewrite;The argument was accepted in review. But it was prose, judged by hand — there is no named category in the disposition it could cite, and nothing mechanical verifies its two premises (no schema / no stored form). The next author who touches a published runtime interface faces the same wall and either re-derives the argument, or (worse) pattern-matches "no metadata ⇒ no changeset discipline at all", which is not what #8277 argued.
What "fixed" looks like
A named category in the ADR-0087 disposition (e.g. runtime-interface-only: compiler-carried, migration-exempt) with a mechanically checkable predicate — "the touched exported symbol appears in no
*.zod.ts, no speccontracts/**entry, and no object definition" is grep-shaped and could live in the changeset gate rather than in reviewer judgment. The #8277 review thread is the worked example to encode.Provenance
#8277 (the worked example) · #8131 (the card it delivered) · ADR-0087 registries.