feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) - #11125
Conversation
…Script twin declared (objectui#6152 round 1) `UnmirroredDeclared['objectql.zod.ts#ObjectFormSchema']` recorded 21 keys the published `ObjectFormSchema` interface invites and the zod mirror had never heard of. The tolerant face kept any value there unexamined, and the strict authoring face refused the keys although tsc accepted them — objectui#5250's M3 class (iii). Nineteen are measured authored and read by `ObjectForm` (or relayed into it by `ObjectView` / `RecordFormPage` / `ScreenView`), so each is now declared on the mirror, shaped as the twin declares it; `nextText` / `prevText` by reference to the spec's `I18nLabelSchema`. The entry keeps `open` and `submitHandler`, whose routes are open on the card. Ledger 12 entries / 84 keys -> 12 / 65, every figure the file pins moved with it. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…edger row objectui#6152 round 1 closed The row pointed at objectui#6152's `UnmirroredDeclared` entry: the TS `ObjectFormSchema` and the spec both declared `formType`, PageBlockCanvas read it, and the zod mirror omitted it. The mirror now declares it, so the "every ledger row still applies" ratchet turned red as written; the row goes, and a live control re-measures the retired verdict so an empty node-face ledger stays a reading rather than a silence. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Director: contract review deferred — this head does not land · 2026-09-30T03:48Z Director seat (objectstack#12708, |
origin/main carries objectui#11068 (#11130), which closed three keys of `UnmirroredDeclared['objectql.zod.ts#ObjectGridSchema']` (emptyState mirrored, rowSpecActions / bulkSpecActions tombstoned). The only conflict was the ledger's docblock figures in zod-mirror-parity.test.ts: main's rows are kept, this branch's ObjectFormSchema edits are re-applied, and every pinned figure is re-derived from the merged ledger — 12 entries / 62 keys, 3 / 15 spec-derived, 9 / 47 local — with both cards' movements in the chains. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…Text / prevText stay unmirrored That sentence publishes verbatim at the next release, and objectui#6152 round 1 (this branch) mirrors both members by the same `I18nLabelSchema` reference, so the two entries would have published contradicting each other. The sentence now says what #10993 did and what #6152 does — a prose correction to a pending entry, no package name or bump level touched. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Director seat's at-tier review (objectstack#12708, on the maintainer's 「项目总监契约复审」, 2026-09-30) — the review this PR was drafted for, after the base merge Check-runs on the head, read 2026-09-30T04:3xZ: 43 runs, 0 failure, 0 in progress (the rest success or skipped by matrix). Base condition, stated once: ① Derived judgments
② Semver levelClause-②: yes — as declared: mirroring a declared key widens the strict face; the tolerant face narrows on wrong-typed values (stated as the break). ③ Boundary flags
Implemented-by: VERDICT: PASS State: |
objectui#11070 (PR #11115) landed on main and moved the same UnmirroredDeclared split and totals line this branch moves. Both conflict hunks sit in packages/types/src/__tests__/zod-mirror-parity.test.ts: - the SPEC-DERIVED split bullet: both histories kept, in landing order (objectui#11068, then objectui#11070, then objectui#6152 round 1); - the totals line: both sentences kept, figures re-derived from the merged ledger and SPEC_DERIVED_PAIRS. Merged ledger, measured: 12 entries / 62 keys, 4 / 16 spec-derived, 8 / 46 local. The file-header line and both ledger docstrings (62 keys) and the LOCAL split (8 / 46, from main) merged cleanly and already agree. The pending .changeset/10993-object-form-i18nlabel.md takes main's bytes back; its correction becomes an appended, dated note in the next commit. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…993 entry instead of rewriting its line The base-merge round rewrote one sentence of this pending changeset. The maintainer's ruling keeps pending changesets append-only: the merge commit put the line back to main's bytes, and this commit appends one dated note saying nextText / prevText are now mirrored (objectui#6152, PR #11125), by the same I18nLabelSchema reference. Frontmatter and every existing line are byte-identical to main. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ion-list / repeater) validate by their spec ComponentPropsMap rows (objectui#10872, batch 4) (#11180) Part of #10872 Clause-②: yes (widening: six namespaced component types move from refused-at-type to accepted by their spec rows) Batch 4 of objectui#10872. The six ADR-0080 public blocks held back until `@objectstack/spec` carried a `ComponentPropsMap` row for each (`action:button`, `action:icon`, `action:group`, `action:menu`, `element:definition-list`, `element:repeater`) are armed **by reference** to their 17.5.0 rows, the batch-1 method. The card stays open for `record:line_items` (no spec row yet) and the flat-props / envelope-key batch.⚠️ **Draft with one known red, deliberately:** `packages/types/src/__tests__/zod-mirror-parity.test.ts` census (`every exported const in ../zod/ is either a registered pair or an excluded one`) names exactly the six new exports. The claim puts that file off limits (draft PRs objectui#11115 and objectui#11125 hold it) and says to stop and report the shape, so the six rows are **not** in this diff. They are in the os-dev report on the card. Measured: with those six rows added, the census passes (37/37, in a scratch copy of the test), and `git merge-file` of the rows against the heads of objectui#11115 (`8628df2fd9`) and objectui#11125 (`a5884b4030`) reports 0 conflicts. ## What changed - `packages/types/src/zod/public-blocks.zod.ts`: six arms, `BaseSchema` + the `type` literal + `properties`, which is the block's `ComponentPropsMap` row passed straight through `stripImportedDefaults` (no member restated). All six join `PublicBlockComponentSchema`; the barrel re-exports them. - `action:button` / `action:icon` also declare the two `on*` keys their renderers read off the node, because `check:handler-key-reads` requires every such read to be an arm member: - `onClick`: `handlerKeyRefusal(..., 'runtime-slot', ...)`. The renderer calls it only when it is a function (a code-composed schema), and the row does not declare it. This is the `ButtonSchema.onClick` precedent. - flat `onSuccess`: `aliasKeyRefusal` naming `properties.onSuccess`, where the row declares the post-success `{ navigate, openIn }` block. This is the `record:alert` flat-`body` precedent (batch 3). The spec's own `PageComponentSchema` refuses the flat key too, as mis-layered. - Ratchet: `NAMESPACED_REFUSED_AT_TYPE` falls 397 to 391, and `ARMED_PUBLIC_BLOCKS_10872_BATCH_4` names the six. The ratchet's validator import and `refusedAtType` (objectui#11069's hunks) are untouched. - Page pin: `objectui validate` passes a page whose `action:button` is the quick-start's "Add Actions" node, read from `content/docs/guide/quick-start.md` (the node AGENTS.md #4 teaches). It also passes a page of all six in the spec's `properties` spelling. The control is an undeclared bag prop, which is refused and named. - Arm-list consumer sweep, the edits it required: - `imported-defaults-8317.test.ts` measures the six rows at the import boundary. They are placed away from objectui#11115's insertion point, and none carries a default or a `z.lazy`, so the lazy count stays 4. - `public-block-arms-10872.test.ts` gets `VALID_BAG` rows for the six. Its identity leg now compares by definition, because 17.5.0 publishes these rows as lazy proxies whose methods run on the real schema, so `.optional()` wraps the object behind the proxy. - The handler-key census: `action:button` and `action:icon` leave the objectui#9573 alias population, which is now `view:form`, `view:grid`, `view:list`. Their four reads are judged on their own arms. Both the gate docblock and the gate test are amended. - Changeset `10872-held-public-block-arms.md` (`'@object-ui/types': minor`). A dated note on the pending `10872-public-block-zod-arms.md`, whose "Not armed in this release" bullet this falsifies. The zod README lists the six. ## The spec round's registration notes, re-measured at `main` `0ffc423b1` The arm follows the row, and the row follows the read points. Items 1–3 and 5 are pinned at the validator in `held-public-block-arms-10872.test.ts`; item 4 is a renderer reading. 1. `action:group` publishes `name`, which is read nowhere. Its `size` enum carries `md`, which inline mode hands raw to the primitive. **Still true.** The arm refuses both, with the row's prescription. 2. `element:definition-list` publishes `columns` as the strings `'1'` / `'2'`, and the renderer compares the number `2`. **Still true.** The arm refuses `'2'` and accepts `2`. 3. `element:repeater` advertises `fields[].label` (its TS interface and its registration description), which is never rendered. **Still true.** The arm refuses it. 4. `action:menu` spreads `...rest` after `disabled={loading}`. **Reproduced** with a probe through the real `SchemaRenderer` and registry: an in-flight action (an `autoTrigger` member whose handler never settles) shows the spinner on both mount channels, but the trigger is `disabled: true` only on a direct registry mount and `disabled: false` through `SchemaRenderer`, whose forwarded `disabled: undefined` wins. It is reported on the card as a class-(a) finding; it is not fixed here. 5. `objectName`: the 17.5.0 rows declare it on `action:button` and `action:icon`. On `action:group` / `action:menu` it rides each member (`actions[]` members are open records) and is refused at container level. Pinned. **The registrations did not move in this PR.** Items 1–3 sit in `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`'s objectui#11111 ledger, which the maintainer's decision 3 = B routes to objectui#11168 ("nothing else may own an entry"). objectui#11168's slice 1 (the `action:*` family, including `renderers/action/`) was claimed at 2026-09-30T05:39Z, before this card's claim. So the registration moves, and the item-4 renderer fix, would duplicate in-flight work. They are reported on the card instead. This PR does not touch `registry-inputs-spec-parity.test.ts` or any renderer. ## Tests and gates (all after the final commit, `c1ea153ad`) - `pnpm exec vitest run packages/types/ packages/cli/src/__tests__/ packages/components/src/renderers/action/ packages/components/src/renderers/basic/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: 351 files, **7678 passed, 1 failed**. The failure is the zod-mirror-parity census above, and nothing else. - Arm-list consumer sweep: 54 files, 2878 passed, 0 failed. It covered: - app-shell `block-config-schema-parity-8216`, `block-config`, `definition-list-item-keys-8279` and `PageBlockInspector.retiredBlockProps`; - `examples/schema-catalog/test/`; - console `public-contract`, `html-tier-manifest`, `component-input-union-specimens` and `unfulfilled-chart-stubs-8760`; - plugin-dashboard `metricCardRegisteredInputsStrictFace-11022` and plugin-map `bareMapKeyRetired-10393`; - the scripts tests that read the zod directory. - Every test that names `check-handler-key-read-sites`: 5 files, 132 passed. - `type-check` for `@object-ui/types` (three programs; `tsconfig.test.json` lists the three changed test files) and `@object-ui/cli`, after `pnpm --filter '@object-ui/cli^...' build`: exit 0. `@object-ui/components` is not in the diff. - eslint on the 8 changed code files: 0 errors, 0 warnings, 0 ignored (`--format json`). `eslint.config.js` configures no type-aware parsing, so this diff cannot move a verdict on an untouched file. - These gates exit 0: `check-changeset-presence`, `check:control-bytes`, `check:new-line-citations` (0 new), `check:spec-symbols`, `check:handler-key-reads`, `check:changeset-claims` (report-only), `check:pending-changeset-literals`, `check-changeset-no-major`. - NOT MEASURED: `check:readme-exports`. Reason: it needs every package's `dist` and reported "the population COLLAPSED" on this unbuilt tree. CI runs it. - Ablation, both legs through `ablation-replace.mjs`: the anchor hit once, the write was verified on disk, and the restore was proven (blob equals HEAD, `git diff HEAD` empty). - Leg A drops the six from the union: 60 of 179 tests go red across the three pin files. - Leg B deletes `action:button`'s `onClick` / `onSuccess` members: `check:handler-key-reads` exits 1 naming both reads, and 2 held-arm tests go red. ## Overlap `git merge-tree` of this head with objectui#11069, #11115 and #11125: my files merge cleanly with all three. objectui#11069 conflicts in `packages/cli/src/commands/check.ts`, a file this PR does not touch; the same conflict appears against the base alone. ## Acceptance notes - **The taught `action:button` node is flat, and the strict face refuses it.** AGENTS.md #4, the quick-start, the layout guide and the other guides write `label` / `actionType` / `target` on the node. The tolerant face `objectui validate` runs today passes that node (which is what the page pin reads). `StrictAnyComponentSchema` refuses exactly `actionType` and `target`, as `PageComponentSchema` does, and this is pinned as a reading. objectui#11069 moves `objectui validate` onto the strict face. When it next merges `main`, this PR's taught-node page pin turns red there. That is the flat-props question for `action:*`, which this card holds, and it is raised on the card. - **Content channels are not narrowed.** The six arms carry no objectui#9256 `children` / `body` refusal: that measurement never covered them. They keep `BaseSchema`'s channels, and the module docblock and README say so. - **Inference, not reproduced, not filed:** inline `action:group` also spreads `...rest` onto its wrapping div. This is the item-4 shape on a div. --- _Generated by [Claude Code](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…00 entry The pending objectui#7200 changeset gives, as its reason for not using a never-typed tombstone, that ObjectFormSchema's zod mirror does not declare `sections`. objectui#6152 round 1 (this PR) mirrors `sections` with a closed section entry, so that parenthetical goes false when this lands. The changeset-claim re-read on PR #11125 asked for exactly this read. Per the maintainer's append-only ruling for pending changesets, one dated note is appended; the frontmatter and every existing line are byte-identical to main. Measured with a scratch probe at 4ed5adf: the strict authoring face refuses a section `className` as unrecognized_keys at sections.0, and the tolerant face accepts the document and drops the key. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Director seat (objectstack#12708, summon #30 续 2). The head moved past this seat's PASS 5904218639 ( ① Derived judgmentsThe PR's own change is what the previous record judged. The net diff against The three changeset faces at this head:
Nothing else in the net diff is a review face. ② Semver levelUnchanged: ③ Boundary flags
Check-runs on the head, read 2026-09-30T09:28Z — none Implemented-by: VERDICT: PASS |
|
Correction to record 5908321649 · director seat 2026-09-30T09:29Z One sentence in ① is wrong and is withdrawn: "the base merge introduced no |
Refs #6152
Clause-②: yes — mirroring a declared key widens both zod faces; removing an unread declared key narrows a published TypeScript type (stated as a break in a
minorchangeset). The PR waits as a draft for the director seat's contract review.What changed
UnmirroredDeclared['objectql.zod.ts#ObjectFormSchema'](inpackages/types/src/__tests__/zod-mirror-parity.test.ts) recorded 21 keys. The publishedObjectFormSchemaTypeScript type invites these keys, but the zod mirror had never heard of them. So the two faces gave different answers:BaseSchemais.passthrough()) kept any value at those keys without judging it;StrictAnyComponentSchema) refused the keys, althoughtscaccepts them. That is objectui#5250's M3 class (iii).This round measured every key in the entry and routed each one:
19 keys MIRRORED (route a) in
packages/types/src/zod/objectql.zod.ts. Each is shaped as the TypeScript twin declares it.nextText/prevTextare the spec'sI18nLabelSchemaby reference, because the twin takesI18nLabelfrom the spec. objectui#10993 bound the form's other five label members the same way.sectionsgets a module-private entry schema,ObjectFormSectionEntrySchema, member for member withObjectFormSection. One exception: afieldsentry staysz.any(), as the mirror's existingcustomFieldsdoes.FormFieldSchemacarries its ownKnownDrift/UnmirroredDeclaredrows, and binding it here would import that drift.formslot (ObjectFormSchema.omit(...)) inherits all 19 members.2 keys LEFT in the entry, with their routes open:
openandsubmitHandler. See the open questions.Ledger: 12 entries / 84 keys → 12 entries / 65 keys. The split moves from 3 / 37 spec-derived to 3 / 18. Every docblock figure the file pins moved with it, in the prose convention the file uses: the file header, both ledger docstrings, the split, and the totals line.
object-form-i18n-label-members-10993.test.ts: its row "nextTextandprevTextare not mirrored" asked to move together with the ledger. It now holds all seven label members.New pin file,
packages/types/src/__tests__/object-form-unmirrored-members-6152.test.ts. For each mirrored key it pins:The file also pins:
formslot;open/submitHandlerstay out of the shape.One file outside the claimed surface:
packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config-schema-parity-8216.test.ts. Its ledger rowobject-form::formType@nodepointed at 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #6152. It went stale the momentformTypewas mirrored, and its own ratchet turned red: "every ledger row still applies". The row is deleted, with a tombstone comment like the one above it.EXEMPTblock, a separate hunk); same gate family.Changeset
.changeset/6152-object-form-unmirrored-members.md:@object-ui/typesminor. It names every mirrored key and states the tolerant face's new refusal of wrongly typed values as the breaking half.Per-key table:
objectql.zod.ts#ObjectFormSchemaTwo instruments, both scratch-only (not committed):
.json, the JSON fences inmd/mdx, and TS object literals carryingtype: 'object-form'(plus the object-viewformslot).objectName/modeon the same nodes (catalog 2, fences 3, in-code 12 src).incheck and destructure whose receiver resolves toObjectFormSchema, including plugin-form's mappedLocalizedObjectFormSchema.objectName32 reads,mode28.mode= 0 and was discarded as a broken instrument: it could not see through the mapped type. The fix resolves property symbols to their declaration."spec" is
@objectstack/spec17.4.0:CPM=ComponentPropsMap['object-form'],FV=FormViewSchema.formTypeplugin-form/object-form-tabbed-sections.json; fences inapi/schema-reference.md(also its object-viewformslot) andplugins/plugin-form.mdx· 9 hostsObjectForm·schema.formType(the variant fork)sectionsObjectForm·schema.sections;ObjectView·schema.form?.sectionsdefaultTabObjectForm·defaultTab: schema.defaultTabtabPositionObjectForm·tabPosition: schema.tabPositionallowSkipObjectForm·allowSkip: schema.allowSkipshowStepIndicatorplugins/plugin-form.mdxObjectForm·showStepIndicator: schema.showStepIndicatornextTextObjectForm·nextText: schema.nextTextprevTextObjectForm·prevText: schema.prevTextsplitDirectionObjectForm·splitDirection: schema.splitDirectionsplitSizeObjectForm·splitSize: schema.splitSizesplitResizableObjectForm·splitResizable: schema.splitResizabledrawerSideapi/schema-reference.md(object-viewformslot) ·FieldDesignerObjectForm·drawerSide: schema.drawerSidedrawerWidthFieldDesignerObjectForm·drawerWidth: schema.drawerWidthmodalSizeuseActionModal,ObjectManagerObjectForm·modalSize: schema.modalSizemodalCloseButtonObjectForm·modalCloseButton: schema.modalCloseButtonmobileObjectForm·const mobileOpts = schema.mobilebuttonsRecordFormPage(formDef.buttons),ObjectView(schema.form?.buttons)ObjectForm·foldFormButtons:(schema as { buttons?: … }).buttons(a cast read)defaultsformDef.defaults,schema.form?.defaults)ObjectForm·foldFormButtons:(schema as { defaults?: … }).defaults(a cast read)subformsRecordFormPage/ScreenView(objectDef.form?.subforms),ObjectView(schema.form?.subforms)ObjectForm·(schema as any).subforms?.length(a cast read)openAppContent,useActionModal,ObjectManager,FieldDesigner, the BYO example · 0 documentsObjectForm·open: schema.open(drawer and modal arms)submitHandlerMasterDetailForm(submitHandler: submitViaBatch) · 0 documentsObjectForm·await schema.submitHandler(writePayload)"protocol-authorable" is stated because it is a judgment call the director may overrule. Eleven of the mirrored keys have zero document occurrences in this repository (
allowSkipappears in tests only;drawerWidthandmodalSizeare written only by in-code hosts). They are read, and the spec publishes each as an author-facing member ofComponentPropsMap['object-form'].So the #6170 family rule, "the exported type aligns to the measured authored+read set", keeps them declared, and mirroring is what closes declared ≠ enforced for them.
Scope: stopped at the
ObjectFormSchemapair boundaryThe dispatch allowed this: "If round 1 becomes too large to review as one PR, stop at a pair boundary, with
ObjectFormSchemafirst". The two remaining pairs were measured in full and are handed to the next round as a worklist. They were not implemented, for three reasons:ObjectGridSchema's mirrors includebulkActionDefs(a nestedBulkActionDef/BulkActionParamshape whosevisibleindexes the spec's slot type) andconditionalFormatting(the two-arm rule union theListViewSchemamirror spells inline).p1-spec-alignment.test.ts, which writes all three keys.objectql.zod.ts#ObjectGridSchema: measured, not implementedMeasured with the same two instruments. "spec" is
ComponentPropsMap['object-grid'], a strict object.aggregationsObjectGrid·aggregations: schema.aggregationsbulkActionDefsObjectView·tableRelay.bulkActionDefsObjectGrid·Array.isArray(schema.bulkActionDefs) ? schema.bulkActionDefs : []conditionalFormattingtableRelay.conditionalFormatting)ObjectGrid·conditionalFormatting: schema.conditionalFormatting as unknown[] | undefinedgroupingtableRelay.grouping)ObjectGrid·schema.grouping?.fieldsGroupingConfigfrom the spec)navigationObjectGrid·navigation: schema.navigationViewNavigationConfigis the spec'sNavigationConfig)operationsREADME.mdandapi/schema-reference.mdObjectGrid·'operations' in schema ? schema.operations : undefined;ObjectView·schema.table?.operationsreorderableColumnsObjectGrid·reorderableColumns: schema.reorderableColumns ?? falserowColortableRelay.rowColor)ObjectGrid·useRowColor(schema.rowColor)rowHeighttableRelay.rowHeight)ObjectGrid·resolveRowHeightMode(schema.rowHeight)singleClickEditplugins/plugin-grid.mdxObjectGrid·singleClickEdit: schema.singleClickEdit ?? trueresizableColumnsapi/schema-reference.mdObjectGrid·schema.resizable ?? schema.resizableColumns ?? trueresizable"emptyStatep1-spec-alignment.test.ts,object-view-table-slot-10976.test.ts)emptyStatereads inListView/ObjectVieware on other types)?: never+retirementTombstone)rowSpecActionsrowActionsbulkSpecActionsbulkActionsSchemaRendererand registry, varying only that key.navigation.zod.ts#PaginationSchema: measured, not implementedcurrentPagecomponents-basic-pagination/basic-pagination.jsonandwith-item-count.json;apps/siteplaygroundpaginationrenderer ·schema.currentPage || schema.page || 1page(the twin callspagethe "Legacy page property"); open question 3Acceptance: objectui#5250 M3 class (iii), re-run
This used a scratch port of PR #11069's
findUndeclaredKeys(⛔ not committed; none of #11069's wiring). It ran over the SHIPPED face:StrictAnyComponentSchemafrom a freshly builtpackages/types/dist. The corpora were those ofscripts/measure-strict-authoring-face.mjs(catalog, docs JSON fences, apps pluspackages/*/examples), plusexamples/**/*.json. Class (iii) = membership of theUnmirroredDeclaredledger in the same tree, which tsc reconciles against the compiler measurement.fe41dc76a, base ledger)The before reading reproduces M3's published class (iii) exactly: 10 pairs, 15 occurrences, 13 strict-only. It is the positive control for the port.
object-form·formType,sections,defaultTab,showStepIndicator;object-view·form.formType,form.drawerSide.object-grid·operations,singleClickEdit(route a) andresizableColumns(route d);pagination·currentPage(route d).sectionssurfaced no nested refusal in the corpora.Ablations
Predictions were written to a scratch file before each run. Each mutation went through objectstack's
scripts/ablation-replace.mjsin wrap mode, on committed code (HEAD78b4eee67):git diff HEAD;The subject resolves to
src(relative imports; the root vitest alias maps@object-ui/types/zodtopackages/types/src/zod/index.zod.ts), so nodistleg exists.formTypefrom the mirror; ledger untouched (route a)TS2322atassertionUnmirroredMatchesLedgernaming the pair; 5 red in the 6152 pin file; 2 red in block-config 8216; parity runtime half greenType '"objectql.zod.ts#ObjectFormSchema"' is not assignable to type 'never'atassertionUnmirroredMatchesLedger; vitest 7 failed / 107 passed. The 7 are the member, both-faces, wrong-typed, object-view slot and catalog rows, plus 8216's "no control writes a name its oracle refuses" and its new node-face control. The parity runtime file stayed green.formTypeback into the ledger while the mirror keeps it, a stale ledger key (the ledger half)UnmirroredDeclared's key total. It defers that figure to objectui#7279's pin by design, and #7279's pin reads the file-header bullet too.Gates (at HEAD
78b4eee67, exit codes captured before any pipe)pnpm --filter @object-ui/types type-check(tsc --noEmit && … tsconfig.examples.json && … tsconfig.test.json)pnpm exec vitest run packages/types/ …/block-config-schema-parity-8216.test.ts(root form)Test Files 283 passed (283)·Tests 6555 passed (6555)packages/plugin-form/+ theObjectViewform-slot tests +RecordFormPagetestsTest Files 149 passed (149)·Tests 1765 passed | 1 skippedexamples/schema-catalog/test/, app-shellpreviews/__tests__/,packages/cli/src/__tests__/, fourapps/consolecontract testsTests 15 passed (15)pnpm --filter @object-ui/types lint·pnpm --filter @object-ui/app-shell lint0 errors; no finding on a changed line (the 7 warnings in touched files sit on untouched lines)check-changeset-presence·-no-major·-fixed·-overwrite.changeset/6152-object-form-unmirrored-members.md· no majorcheck:changeset-claims·check:pending-changeset-literalscheck:control-bytes·check:new-line-citationsOK·0 new citation(s)check:spec-symbols·check:test-path-roots·check-type-check-coverage0 untriaged collisions·OK·43/43 packages compile their testscheck:handler-key-reads·check:element-data-source-declaration·check:unreferenced-sourcescheck-governed-queue-guard --test(the 6 changed paths) ·--self-testNOT GOVERNED· self-test OKNOT MEASURED locally:
tsconfig.test.jsontype-check. Reason: its dependency closure'sdistis not built in this worktree. The one edit there calls the file's ownjudge(schema: unknown, name: string)with a string. Declared to CI.pnpm lint. CI's.pnpm check, the CLI self-check inlint.yml. It is advisory (exit 0 unless a JSON file is unreadable), and the M3 re-run shows tolerant verdicts unchanged across the corpora. Declared to CI.check:doc-snippets. No docs fence is touched.No TypeScript type was narrowed, so no downstream type-check is owed.
Open questions (in the round report on #6152, with options)
submitHandlerandopen.submitHandler: objectui#6182 rules it runtime-only (function arm). ButRuntimeOnlyDeclared's shape pin admits/^on[A-Z]/spellings only.open: a host-driven boolean. It is not callback-shaped, so it is not that ledger's either.ObjectGridSchema.resizableColumns: canonicalresizable(the spec's own description, the renderer's first read, the twin's@deprecated). The spec half forks to objectstack.PaginationSchema.currentPage: canonicalcurrentPage, andpageretires. Evidence: the renderer's first read, the twin's primary member, and the only authored spelling.Remaining rounds (not this PR)
objectql.zod.ts#ObjectGridSchema(14; worklist above);navigation.zod.ts#PaginationSchema(1; route d);objectql.zod.ts#ObjectFormSchema(open,submitHandler; open question 1).data-display.zod.ts#DataTableSchema(17)views.zod.ts#DetailViewSchema(11)form.zod.ts#FormSchema(8)complex.zod.ts#ChatbotSchema(3)reports.zod.ts#ReportComponentSchema(3)complex.zod.ts#ChatbotFloatingSchema(2)form.zod.ts#FormFieldSchema(1; objectui#11070 in flight re-derives its half)form.zod.ts#LabelSchema(1)complex.zod.ts#DashboardWidgetSchema(2).Acceptance notes
@objectstack/spec17.4.0'sComponentPropsMap['object-form']refusesbuttons,defaultsandsubformswithunrecognized_keys(measured). ItsFormViewSchemadeclares all three.object-formreads them:RecordFormPagerelays a form view's values onto the node.sections[].fieldsentry is judged nowhere on this key (z.any(), thecustomFieldsprecedent). It becomes judgeable whenFormFieldSchema's own ledger rows close.objectql.zod.tsandzod-mirror-parity.test.ts.UnmirroredDeclaredtotals line (**12 entries / 84 keys** — …), so whichever PR lands second mergesmainand re-derives that line.FormFieldSchema's one key into the spec-derived half.origin/mainmoved three commits pastfe41dc76aduring this round, none touching this diff's files. No merge was needed.Round 2:
mainmerged after PR #11115 (objectui#11070), figures re-derivedWritten 2026-09-30T08:17Z by the dispatched dev, session
https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm.Two things opened this round. PR #11115 landed on
mainasb0a05dda1, and the maintainer ruled on this PR's pending-changeset edit: 「改成追加说明 (Recommended)」 (seat comment5904913872on objectui#6152). The director's PASS (5904218639) was given ata5884b403. This round moves the head, so that PASS is spent andneeds:contract-reviewis back on this PR for a new record. The director's earlier deferral (5903664978) was answered by the base-merge commitf78dc3602, the head5904218639reviewed.No rebase and no force-push: three commits on top of
a5884b403, each pushed fast-forward.f3e9d830fmainb0a05dda1(parentsa5884b403,b0a05dda1)4ed5adf6f.changeset/10993-object-form-i18nlabel.md8f9bb76f6.changeset/7200-object-form-section-style-keys-undeclared.md(see The pending-changeset re-read below)The merge: two conflict hunks, one file
Merging
b0a05dda1conflicted only inpackages/types/src/__tests__/zod-mirror-parity.test.ts, in two docblock hunks of theUnmirroredDeclaredsplit section. The ledger itself (interface UnmirroredDeclared) andSPEC_DERIVED_PAIRSmerged without a conflict:form.zod.ts#FormFieldSchemaandobjectql.zod.ts#ObjectKanbanSchematoSPEC_DERIVED_PAIRSand changed no ledger key;ObjectFormSchemaentry.mainread "(4 entries, 35 keys) — it was 3 / 34 until objectui#11070 …".mainread "… and objectui#11070 movedFormFieldSchema's entry (one key) the same way … 12 entries / 81 keys — 4 / 35 spec-derived, 8 / 46 local."ObjectFormSchema's", becausemain's sentence now stands between it and its antecedent. The line reads 12 entries / 62 keys — 4 / 16 spec-derived, 8 / 46 local.The figure sites that merged without a conflict were each checked against the same measurement:
UnmirroredDeclaredbullet (12 entries / 62 keys, from this branch);main's objectui#11070 history sentence).The two other auto-merged files move separate hunks:
objectql.zod.ts:main's hunks there (among themdataSourceonObjectFormSchemaand its siblings) do not overlap this branch's;block-config-schema-parity-8216.test.ts:mainempties theEXEMPTtable, and this branch's ledger-row hunk is untouched.The figures, measured on the merged ledger
A scratch reader (⛔ not committed) parsed
interface UnmirroredDeclaredandSPEC_DERIVED_PAIRSat four trees. The file's own pin, objectui#7279's "the split figures and the totals the header writes down equal the ledger", re-derives the merged row on every run and is green at4ed5adf6f.c2a8d23c6a5884b403main,b0a05dda14ed5adf6fObjectGridSchemakeys (84 → 81 onmain). So the measured total and the measured spec-derived half are each three keys lower. The paragraph above is left as written; this section supersedes its figures.The pending objectui#10993 changeset: restored, then appended
Per the ruling, the line this PR rewrote is back to
main's bytes, and the correction is an appended, dated note:f3e9d830ftakesmain's blob for.changeset/10993-object-form-i18nlabel.md(its diff againstb0a05dda1on that path is empty);4ed5adf6fappends one blank line and one paragraph, "Correction, 2026-09-30 (objectui#6152). …". It saysnextTextandprevTextare now mirrored (PR feat(types): the object-form zod mirror declares the members its TypeScript twin declared (objectui#6152, round 1) #11125), by the same reference to the spec'sI18nLabelSchema;git diff b0a05dda1 --numstaton the path reads 2 added, 0 deleted. The frontmatter is byte-identical, andmain's blob is an exact byte prefix of the new one (cmp).This PR's own changeset,
.changeset/6152-object-form-unmirrored-members.md, states no ledger figure, so nothing in it moved with the merge. It is not edited.The pending-changeset re-read (the
changeset-claim-re-readrequest on this PR,5894373256)That comment asked this PR to re-read the 31 pending changesets that name a file it touches, against its diff. This round did, on the net diff against
main. Each body was searched for theObjectFormSchema/object-formmembers this PR moves (the nineteen mirrored keys,open,submitHandler,sections) and forUnmirroredDeclaredfigures, and every hit was read in its paragraph.One claim goes false when this PR lands:
.changeset/7200-object-form-section-style-keys-undeclared.md. It gives, as its reason for not using a never-typed tombstone, that "ObjectFormSchemainzod/objectql.zod.tsdoes not declaresections", "so there is no parse door to refuse at". This PR mirrorssectionswith a closed entry.4ed5adf6f: the strict face refuses{ sections: [{ label, className, fields }] }asunrecognized_keysatsections.0namingclassName. The tolerant face accepts it and drops the key. The control withoutclassNameparses on both.main,ObjectFormSchema's mirror has nosectionsmember; itsobjectNamemember is found by the same read, as the control.The remedy follows the same ruling as step 2:
8f9bb76f6appends one dated note ("Correction, 2026-09-30 (objectui#6152). …"), 2 added / 0 deleted againstb0a05dda1. The frontmatter and every existing line are byte-identical:HEAD's blob before the append is an exact byte prefix of the new one. Card objectui#7200 is closed (completed 2026-09-02), so no open claim holds that file.The other 30 hold. 22 carry none of those tokens. The 8 that do describe one of three things:
ObjectGanttSchema,ObjectViewSchema,ObjectGridSchema.title, the tree-view and chatbot slots;ObjectFormSchema.recordIdoronStepChange, members this PR does not move;objectui#6150's "
RuntimeOnlyDeclaredis now a SUBSET ofUnmirroredDeclared" still holds at pair level: its three pairs (DataTableSchema,FormSchema,DetailViewSchema) are allUnmirroredDeclaredentries on the merged ledger.Ablation A3: a wrong total in the docblock turns the figure pin red
Predictions were written to a scratch file before the run. The mutation went through objectstack's
scripts/ablation-replace.mjsin wrap mode, on committed code (HEAD4ed5adf6f). It put the predicted-but-wrong spec-derived figure back into the totals line: "4 / 16 spec-derived" → "4 / 19 spec-derived".ed320c3c755d→1e6b780b577ftsc -p packages/types/tsconfig.test.jsonvitestonzod-mirror-parity.test.ts1 failed | 36 passed (37), that test. Its diff:totalsLine.specDerived.keysis 16 in the ledger and 19 in the headergit diff HEADemptyed320c3c755dequals HEAD's;git diff HEAD0 bytes;git status --porcelain0 linesNo
distleg exists: the parity file reads its own source off disk and compiles againstsrc.Gates on the merged tree (exit codes captured before any pipe)
The head is
8f9bb76f6. Every row was read there except the readers' suites, which were read at4ed5adf6f. The one commit between the two only appends a paragraph to.changeset/7200-object-form-section-style-keys-undeclared.md, and no test names that file (git grepoutside.changeset/: 0 hits;check:pending-changeset-literalsgreen).pnpm --filter @object-ui/types type-check(tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)pnpm exec vitest run packages/types/Test Files 285 passed (285)·Tests 6602 passed (6602)block-config-schema-parity-8216.test.tsTests 15 passed (15)4ed5adf6f):packages/plugin-form/, the threeObjectViewform-slot tests, the twoRecordFormPagetestsTest Files 158 passed (158)·Tests 1817 passed | 1 skipped (1818)check-changeset-presence·-no-major·-fixed.changeset/6152-object-form-unmirrored-members.md· no major · fixed group OKcheck-changeset-overwrite10993-object-form-i18nlabel.mdand7200-object-form-section-style-keys-undeclared.md; each declares the same packages at the same levels at base and nowcheck:changeset-claims·check:pending-changeset-literalsNo test source names a pending changesetcheck:control-bytes·check:new-line-citationsOK·0 new citation(s)check-governed-queue-guard --test(the 8 paths of the diff againstmain)NOT GOVERNEDNOT MEASURED locally:
lintruns and the repo-widepnpm lint. This round edits two docblock comments and appends two changeset paragraphs. CI's.tsconfig.test.jsontype-check. CI's, as in round 1.The director's other answers in
5904218639③ go to later rounds on their own PRs, not this one: theRuntimeOnlyDeclarednamed allow-list,resizableColumns→resizable, andcurrentPagecanonical.Generated by Claude Code