test(spec): wire the top-level zod-only direction of the metadata-form reconciliation gate (#19333, item 2) - #20520
Conversation
…m reconciliation gate The per-type top level asserted only form-only and retired, so a key the schema declares and no form row offers had no reader there. Every object-rooted type now reconciles its root the way nested lists already did: an authorable key is offered, or a root ledger row records why it is not, and any other key fails the gate by name. The ADR-0010 overlay and tombstones need no row. `view` stays outside the direction by name, with its reason, until its per-arm forms exist, and a pin holds that deferral equal to the union-rooted registered types. Re-derived first with the gate's own helper block: the object-rooted residue is 0 on this base, so the ledger is unchanged. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…p-level-zodonly-wiring
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: Inputs, and nothing else: card #19333 (body and every comment through the dev report ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (
Dev open question — should #19188 close when #20520 lands? The seat's answer is A (close it Out-of-scope findings: Check-runs on the head, read once (36 runs): 13 success, 7 skipped, 0 failure, 16 not concluded. Concluded success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · source gates, filter. Skipped: Build Docs, Console Pin Gate, both Packed-tarball smoke runs, and the second Auto Label / Check Changeset / Check PR Size runs. NOT concluded at the read (in progress; not presumed green, not polled): Build Core; Dogfood Regression Gate 1/3, 2/3 and 3/3; Dogfood Verify CLI; Lint and Repo Gates; Temporal Conformance (live PG + MySQL); Test Core 1/6 through 6/6; Type Check · consumer gates, · debt ledger and · workspace. Test Core is the run that executes this gate in CI; the owning seat reads it green before enqueue. There is no failure to attribute to this diff. This record was written and posted at 2026-09-28T22:12Z. PR form: line one Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19333
Clause-②: no
What this does
Item 2 of #19333, its last remaining item (landing record 5860224378): the top-level
zodOnlydirection of the metadata-form reconciliation gate,packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, is now wired.Before this PR, the per-type top level asserted only form-only and retired. So "the schema declares this key and no form row offers it" had no reader at the root, while the nested lists already had one. Now every object-rooted type reconciles its root the same way:
reconcileRootis the nested predicate'szodOnlyatROOT_PATH, built from the sameresolveCoordinate/offerableKeysAt/omittedAt/isSubsethelpers the resolve test uses. The ADR-0010 overlay andretiredKey()tombstones need no row.it.each(TOP_LEVEL_TYPES)fails a type by name when a key the author may write at the top level is neither offered by the form nor excused by a root ledger row. Failure text:TYPE.(root): accepted by the Zod but unauthorable in the form — offer it, or add a root ledger entry that records why it is not offered, with the offending keys in the diff.viewis deferred by name, with its reason, inTOP_LEVEL_DEFERRED. Its root is a union, and it is reconciled per arm once an arm form exists (the [Decision] isviewreconciled per metadata type or per union arm? 36 keys of the #19188 census cannot be filed until this is settled #19330 ruling, letter A). A pin holds the deferred set equal to the union-rooted registered types, so the map cannot excuse an object-rooted type, and a new union-rooted type cannot slip into the direction unexcused. The direction judges 16 of 17 types.reconcileRootover the file's existing root-coordinate fixture:omitor rootsubsetrow excuses it.The reason ledger is unchanged: 37 rows, 26 at the root. No schema, form,
describe(), liveness row or generated artefact changes. One file, +114 / −14.Verification record
1. The residue, re-derived first on
main4a1df19656, with the gate's own helper blockInstrument. The gate file's bytes 0 up to the first line-start
describe((0..48202, sha25606ccb54e052ad2b2…), copied verbatim into a throwaway probe beside it. The prefix was checked byte-identical, and the probe was deleted after the run.736c63a85reproduces sha2567b97432d8408f12e…, the instrument recorded in 5825062779.resolveCoordinate(form, root, ROOT_PATH),authorableKeysOf,offerableKeysAt(…, ROOT_PATH),omittedAt(LEDGER, type, ROOT_PATH)andisSubset.os-verify-lock: VERDICT command-exit 0, 2 files / 58 tests.Controls, asserted inside the probe:
nameis offered by 17 of 17 forms and declared by 17 of 17 schemas.field.formatrow from a ledger copy surfacesformat.formatstays out.Reading.
All 44 residue keys are
view's, which is union-rooted and outside the direction (ruling A). ⇒ the claim's branch "it reads 0" holds, and the direction was wired.Against the previous round (5859927065 at
096a8dbab: 230 / 132 / 83, object-rooted 39): the 39 object-rooted keys were resolved by #19188 split: 39 top-level zod-only keys are structured controls needing a designed widget, not a row #19332's flights. 11 got root rows (root rows 15 → 26); the other 28 left the not-offered set through form rows or theaction.ariaretirement (230 − 28 = 202).viewstayed at 44.app._unpublishedis not inFRAMEWORK_FIELDS, and today's ledger answers it with its own platform-written root row. The census counts it as excused, not as residue, so the wiring does not fail on it.Re-read after merging
main(9449512a31): the gate's new direction, green at the merged head, IS the same census, at 0 object-rooted residue.mainhas since moved to9e9bb46417, touching no form, registered root schema or registry path.2. Ablation, from the committed state (
47ecd08a9f), one lock hold (VERDICT command-exit 0)Every mutation went through
scripts/ablation-replace.mjsin WRAP mode: anchor hit x1 → x0, blob changed, on-diskgrep -cof the planted and removed text printed inside the wrapped child.zzPlanted19333inPositionSchema, no reasonposition.(root): accepted by the Zod but unauthorable in the form …expected[ 'zzPlanted19333' ]it.each(TOP_LEVEL_TYPES)block deletedomitrow recording its reasonfield.formatroot row deletedfield.(root): …expected[ 'format' ]1aa1b108e284andposition.zod.ts's blob989e07cae48eeach equal HEAD,git diff HEADis empty, andgit status --porcelainshows 0 lines. The tool's own proof after each leg and a final script-level hash check agree.srcby relative path.3. Tests, typecheck, lint, gates
47ecd08a9f: 1 file / 76 tests, VERDICT command-exit 0. That is 57 before, plus 16 per-type root cases, 1 deferral pin and 2 synthetic controls.turbo run build --concurrency=2 --filter='./packages/*' --filter='./packages/*/*', 71 of 71 successful (VERDICT command-exit 0). The tree was clean afterwards.eeb01c7143(the merge; the diff vsmainis this one file):pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: exit 0, 573 files, 16820 passed + 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0 (tsc --noEmit,check:scripts-typecheck, andcheck:test-typecheckholding 53 files / 251 errors / 138 pinned signatures).tsc --noEmit -p tsconfig.test.json --listFileslists this file (1 hit; controlsrc/identity/position.zod.ts1 hit) with 0 errors in it. The program's 251 errors equal the pinned count, and its exit 2 is that debt.eslint --no-inline-config --format jsonon the one file gives 1 file, 0 errors, 0 warnings.eslint --print-configresolves a config for it, so it is linted, not ignored.parserOptionsholds onlyecmaVersion/sourceType(noproject, noprojectService), with 4 rules, none type-aware. So this edit cannot move any other file's verdict.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsateeb01c7143derived 78 commands. Each was run with its exit code captured before any pipe, and all 78 exit 0.--ran:78 derived famil(ies) accounted for — 78 run, 0 NOT-MEASURED (a DERIVED zero …).npm pack --dry-run --ignore-scriptsof@objectstack/speclists 2028 files with 0*.test.ts. The changed path is absent; the positive controlsrc/identity/position.zod.tsis present. The new symbols (reconcileRoot,TOP_LEVEL_DEFERRED) hit 0 files indist/, against the controlMetadataProtectionFieldsindist/identity/index.js. ⇒skip-changeset.Acceptance notes
whyis over 20 characters, which is the same discipline the nested ledger has always had. The wiring does not change that. It is noted here because "explained" at the root is now exactly as strong as that discipline.view's 44 residue keys stay recorded, not asserted, until the first arm form is registered (ruling A). Among them is_isOverride, the console-stamped wire discriminant: underscore-prefixed, but not in the ADR-0010 envelope. Whoever registers an arm form meets it.viewblock still saysowner/hiddenare no longer writable at all. The earlier round routed that to PR feat(spec)!: retire the flattened view overlay's owner and hidden keys (ADR-0049) #20286; it is untouched here.zodOnly方向**根本没接线**(只有嵌套列表有),这就是两个已声明键在全门禁绿的情况下缺席表单的原因 —— 本树实测 276 个 top-level zod-only 键 #19188 is the card that named this defect. It remains open for the seat's own disposition; itsBlocked-bylines name this card and #19188 split: 39 top-level zod-only keys are structured controls needing a designed widget, not a row #19332.Generated by Claude Code