Repository navigation
feat(types): declare the read keys the strict authoring face refused (objectui#11070) - #11115
Conversation
…(objectui#11070) `StrictAnyComponentSchema` refused keys a registered renderer reads, so the strict `objectui validate` would tell an author to delete a key that works. Declare, on the TypeScript face and the zod mirror, the ones whose read, spelling and value shape are settled: - `form.showSubmit` (the form renderer's submit-button switch, default true); - `form.fields[]`: `multiple`, `rows`, `accept`, `dimensions`, `min`, `max`, `minLength`, `maxLength` (the spec's `FieldSchema` members, by reference) and `pattern` (a string), the field metadata a hand-authored form writes on the entry the renderer hands each field widget as its metadata carrier; - `dataSource` on `object-grid`, `list-view`, `object-form` and `object-kanban`: the spec's `ElementDataSourceSchema`, by reference, read off the node through `ElementDataSourceGate`. The object-view `table` slot withholds `dataSource` as a record source the view owns (the objectui#10976 rule); the `form` slot carries it, as it carries `bind` and `data`. Ledger rows the declarations move are updated. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
…-census entry (objectui#11070) Declaring `dataSource` on the `list-view` mirror turns `@object-ui/app-shell`'s ListViewSchema relay census (objectui#7559) red: a new member owes a rung or a declared absence there, and that file is outside this card's claimed surface. Withdraw the list-view declaration and pin the key as still refused; the entry the census needs is reported to the seat. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
…eys (objectui#11070) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
… not as a schema key (objectui#11070) The "Using ObjectQL for Queries" snippet put the adapter into `ObjectGridSchema.dataSource`. That key is the per-element BINDING, and `SchemaRenderer` strips it from the props it spreads (objectstack#5576), so an adapter written there never reached the grid. Now that objectui#11070 declares the binding's type, `check:doc-snippets` refused the snippet (TS2741: property `object` is missing). The snippet now leaves the key out and says where the adapter goes. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
|
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
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
… gantt, map and calendar (objectui#11070) Round 2, on the seat's answers: - `FormField.reference` — the spec's `FieldSchema` member by reference, the spelling the `lookup` and `user` widgets read beside the legacy `reference_to`, which stays refused by the strict face. - `dataSource` on `list-view` (re-added from the first round), and on `object-gantt`, `object-map` and `object-calendar`: the spec's `ElementDataSourceSchema` by reference; each registration is gate-wrapped. The list-view member's absence from app-shell's relay census is declared there (`unread`: ListView has no read of `schema.dataSource`; the binding is resolved by `ListViewBlock` through `ElementDataSourceGate`, which `renderListView` bypasses). - `object-chart` is withdrawn and pinned as still refused: the react-page wrapper writes the host adapter (or null) under `dataSource`, and objectui#10770 pins that node as valid on the tolerant face. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
…11070)
- fields-lookup: `reference_to` -> `reference` (both widgets read it).
- fields-password: `min_length` -> `minLength`.
- fields-auto-number: drop `format` — `FormField` declares no auto-number
format key and nothing on the form path reads one (`AutoNumberField`
renders the value only).
- guide/schema-playground: the flat `validation: { pattern, message }` is the
dialect objectui#5186 removed; the object dialect's `pattern.value` must be
a compiled RegExp, which JSON cannot carry, so the regex moves to the
field-level `pattern` string.
- api/schema-reference: a page region's children are `components`.
- plugins/plugin-detail: `page:tabs` items under `properties`, the spelling
the platform's producers write (no ruling on the flat position,
objectui#10872).
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
✅ 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
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
…aSource (objectui#11070) An ablation showed the `IsAny` check could not see `ListViewSchema` lose its member: the derived type answers the index signature's `unknown`, not `any`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
✅ 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, |
…keys Brings in objectui#11068's first key group (#11130): on ObjectGridSchema, name / placeholder / rowSpecActions / bulkSpecActions are retired as tombstones, and emptyState / description are honoured. Conflicts resolved with both sides kept: - objectql.ts: ObjectGridSchema keeps `dataSource` right after `type`, followed by main's `name` / `placeholder` tombstones. The table-slot docblock keeps main's unread / not-relayed split, and `dataSource` is added to the record sources the view owns. - objectql.zod.ts: main's four retired-key strings, then the binding description string. In the table-slot withheld map, the `dataSource` refusal sits beside main's `TABLE_KEY_NOT_RELAYED` description refusal. - zod-mirror-parity.test.ts: the header figures were recomputed from the merged ledger (the objectui#7279 pin derives them). SPEC-DERIVED is 4 / 35 (3 / 34 on main, plus FormFieldSchema's one key moved in by membership). LOCAL is 8 / 46. The total is 12 entries / 81 keys. The auto-merged slot pin keeps main's notRelayed group and the 63 / 69 member counts, which the merged interface still measures. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
✅ 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. Base condition, stated once: ① Derived judgments
② Semver levelClause-②: yes — as declared: declaring a read key widens the strict accept set of the published ③ Boundary flags
Implemented-by: VERDICT: PASS State: |
|
Dequeued from the merge queue on The merge group
So the review's reading that "the PR's own pins survive" the 17.5.0 |
…keys Brings in the @objectstack/spec 17.5.0 resolution (objectui#11073, #11086), whose ElementDataSourceSchema.filter takes the ViewFilterRule array, so this branch's pins can be measured on the tree the merge queue builds. No conflicts; six files auto-merged. Refs objectui#11070 Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…array (objectui#11070)
`@objectstack/spec` 17.5.0 types `ElementDataSourceSchema.filter` as the
ViewFilterRule array (`[{ field, operator, value }]`, migration
`element-data-source-and-object-block-filter-rule-array`), and this branch
declares `dataSource` by reference to that schema. Three literals still wrote
the record form, which the merge queue's run on 17.5.0 refused:
- `strict-face-read-keys-11070.test.ts`: `BINDING`, used by the seven
`BOUND_NODES` pins, moves to the rule array. The pins still measure only
that the key is declared.
- `object-kanban-record-source-7780.test.ts` ("PR #7774's two EXCLUDED
readings"): the fragment moves to the rule array, so the refusal it asserts
is again the record-source rule's `RECORD_SOURCE_REQUIRED`, not the filter
shape.
- `content/docs/utilities/data-objectstack.mdx`: the Kanban fence that pin
mirrors moves to the same form, so the two do not diverge.
Refs objectui#11070
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
|
|
Correction to the director's record 5904239785 (head Director seat (objectstack#12708, |
Contract reviewServed-tier: Director seat's at-tier review of the new head (objectstack#12708, ① Derived judgments
② Semver levelClause-②: yes — unchanged from 5904239785: ③ 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>
…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>
…Script twin declared (objectui#6152, round 1) (objectstack-ai#11125) Refs objectstack-ai#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 `minor` changeset). The PR waits as a draft for the director seat's contract review. > This PR uses only the mirroring half of the Clause-② line above. It narrows no TypeScript declaration. The removals this round measured belong to the `ObjectGridSchema` pair, which moved to the next round (see **Scope** below). ⛔ Draft for the director seat's contract review. Do not mark it ready, and do not enable auto-merge. ## What changed `UnmirroredDeclared['objectql.zod.ts#ObjectFormSchema']` (in `packages/types/src/__tests__/zod-mirror-parity.test.ts`) recorded 21 keys. The published `ObjectFormSchema` TypeScript type invites these keys, but the zod mirror had never heard of them. So the two faces gave different answers: - the tolerant face (`BaseSchema` is `.passthrough()`) kept any value at those keys without judging it; - the strict authoring face (`StrictAnyComponentSchema`) refused the keys, although `tsc` accepts 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` / `prevText` are the spec's `I18nLabelSchema` by reference, because the twin takes `I18nLabel` from the spec. objectui#10993 bound the form's other five label members the same way. - `sections` gets a module-private entry schema, `ObjectFormSectionEntrySchema`, member for member with `ObjectFormSection`. One exception: a `fields` entry stays `z.any()`, as the mirror's existing `customFields` does. `FormFieldSchema` carries its own `KnownDrift` / `UnmirroredDeclared` rows, and binding it here would import that drift. - The object-view `form` slot (`ObjectFormSchema.omit(...)`) inherits all 19 members. - **2 keys LEFT** in the entry, with their routes open: `open` and `submitHandler`. 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 "`nextText` and `prevText` are 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 key is a mirror member; - an authored value parses on both the strict face and the tolerant face; - a wrong-typed value is refused at the key on the tolerant face. The file also pins: - the object-view `form` slot; - a section is closed on the strict face; - the catalog document M3 charged now parses strict; - `open` / `submitHandler` stay 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 row `object-form::formType@node` pointed at objectstack-ai#6152. It went stale the moment `formType` was 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. - A live control re-measures the retired verdict. The node-face ledger is now empty, and an empty ledger reads exactly like a broken oracle. - This is an in-place repair. All four conditions hold: same defect class; a mechanical change the ratchet pinned; no other claim holds the file (the only in-flight PR touching it, objectstack-ai#11086, edits the `EXEMPT` block, a separate hunk); same gate family.⚠️ The claim's file surface needs this path added. - Changeset `.changeset/6152-object-form-unmirrored-members.md`: `@object-ui/types` **minor**. 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#ObjectFormSchema` Two instruments, both scratch-only (not committed): - **Authored:** an AST/JSON census over every tracked `.json`, the JSON fences in `md`/`mdx`, and TS object literals carrying `type: 'object-form'` (plus the object-view `form` slot). - Positive control: the census sees `objectName` / `mode` on the same nodes (catalog 2, fences 3, in-code 12 src). - **Read:** a TypeScript type-checker census. It finds every property read, `in` check and destructure whose receiver resolves to `ObjectFormSchema`, including plugin-form's mapped `LocalizedObjectFormSchema`. - Positive controls: `objectName` 32 reads, `mode` 28. - Its first run read `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. - A cast read is invisible to the checker, so the three cast reads were found by hand and are quoted as such. "spec" is `@objectstack/spec` 17.4.0: `CPM` = `ComponentPropsMap['object-form']`, `FV` = `FormViewSchema`. | key | authored (documents · in-code src) | read site (symbol · quoted expression) | spec | route | | --- | --- | --- | --- | --- | | `formType` | catalog `plugin-form/object-form-tabbed-sections.json`; fences in `api/schema-reference.md` (also its object-view `form` slot) and `plugins/plugin-form.mdx` · 9 hosts | `ObjectForm` · `schema.formType` (the variant fork) | CPM | a · mirrored | | `sections` | same catalog doc + 2 fences · 5 hosts | `ObjectForm` · `schema.sections`; `ObjectView` · `schema.form?.sections` | CPM | a · mirrored | | `defaultTab` | catalog tabbed-sections doc | `ObjectForm` · `defaultTab: schema.defaultTab` | CPM | a · mirrored | | `tabPosition` | none | `ObjectForm` · `tabPosition: schema.tabPosition` | CPM | a · mirrored (read; protocol-authorable) | | `allowSkip` | tests only | `ObjectForm` · `allowSkip: schema.allowSkip` | CPM | a · mirrored (read; protocol-authorable) | | `showStepIndicator` | fence in `plugins/plugin-form.mdx` | `ObjectForm` · `showStepIndicator: schema.showStepIndicator` | CPM | a · mirrored | | `nextText` | none | `ObjectForm` · `nextText: schema.nextText` | CPM (I18nLabel) | a · mirrored by reference | | `prevText` | none | `ObjectForm` · `prevText: schema.prevText` | CPM (I18nLabel) | a · mirrored by reference | | `splitDirection` | none | `ObjectForm` · `splitDirection: schema.splitDirection` | CPM | a · mirrored (read; protocol-authorable) | | `splitSize` | none | `ObjectForm` · `splitSize: schema.splitSize` | CPM | a · mirrored (read; protocol-authorable) | | `splitResizable` | none | `ObjectForm` · `splitResizable: schema.splitResizable` | CPM | a · mirrored (read; protocol-authorable) | | `drawerSide` | fence in `api/schema-reference.md` (object-view `form` slot) · `FieldDesigner` | `ObjectForm` · `drawerSide: schema.drawerSide` | CPM | a · mirrored | | `drawerWidth` | 0 documents · `FieldDesigner` | `ObjectForm` · `drawerWidth: schema.drawerWidth` | CPM | a · mirrored (read; protocol-authorable) | | `modalSize` | 0 documents · `useActionModal`, `ObjectManager` | `ObjectForm` · `modalSize: schema.modalSize` | CPM | a · mirrored (read; protocol-authorable) | | `modalCloseButton` | none | `ObjectForm` · `modalCloseButton: schema.modalCloseButton` | CPM | a · mirrored (read; protocol-authorable) | | `mobile` | none | `ObjectForm` · `const mobileOpts = schema.mobile` | CPM | a · mirrored (read; protocol-authorable) | | `buttons` | form-view metadata relayed: `RecordFormPage` (`formDef.buttons`), `ObjectView` (`schema.form?.buttons`) | `ObjectForm` · `foldFormButtons`: `(schema as { buttons?: … }).buttons` (a cast read) | FV only | a · mirrored; spec half reported below | | `defaults` | relayed the same way (`formDef.defaults`, `schema.form?.defaults`) | `ObjectForm` · `foldFormButtons`: `(schema as { defaults?: … }).defaults` (a cast read) | FV only | a · mirrored; spec half reported below | | `subforms` | relayed: `RecordFormPage` / `ScreenView` (`objectDef.form?.subforms`), `ObjectView` (`schema.form?.subforms`) | `ObjectForm` · `(schema as any).subforms?.length` (a cast read) | FV only | a · mirrored; spec half reported below | | `open` | in-code only: `AppContent`, `useActionModal`, `ObjectManager`, `FieldDesigner`, the BYO example · 0 documents | `ObjectForm` · `open: schema.open` (drawer and modal arms) | neither (CPM refuses it) | b · **not mirrored**; open question 1 | | `submitHandler` | in-code only: `MasterDetailForm` (`submitHandler: submitViaBatch`) · 0 documents | `ObjectForm` · `await schema.submitHandler(writePayload)` | neither (CPM refuses it) | handler · **not mirrored**; open question 1 | "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 (`allowSkip` appears in tests only; `drawerWidth` and `modalSize` are written only by in-code hosts). They are read, and the spec publishes each as an author-facing member of `ComponentPropsMap['object-form']`. - They are not route (b): the protocol models each as authored metadata, and nine of them have no writer at all (not even a synthesising one). - They are not route (c): they are read. - They are not route (d): none is a second spelling. So the objectstack-ai#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 `ObjectFormSchema` pair boundary The dispatch allowed this: "If round 1 becomes too large to review as one PR, stop at a pair boundary, with `ObjectFormSchema` first". 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 include `bulkActionDefs` (a nested `BulkActionDef` / `BulkActionParam` shape whose `visible` indexes the spec's slot type) and `conditionalFormatting` (the two-arm rule union the `ListViewSchema` mirror spells inline). - Its removal half narrows a published TypeScript type. That needs a runtime inertness reading AGENTS.md requires, and a rewrite of `p1-spec-alignment.test.ts`, which writes all three keys. - That is a second contract-review question, not more of this one. ### `objectql.zod.ts#ObjectGridSchema`: measured, not implemented Measured with the same two instruments. "spec" is `ComponentPropsMap['object-grid']`, a strict object. | key | authored | read site | spec | route (proposed) | | --- | --- | --- | --- | --- | | `aggregations` | none | `ObjectGrid` · `aggregations: schema.aggregations` | yes | a | | `bulkActionDefs` | relayed from named views: `ObjectView` · `tableRelay.bulkActionDefs` | `ObjectGrid` · `Array.isArray(schema.bulkActionDefs) ? schema.bulkActionDefs : []` | yes | a (nested mirror) | | `conditionalFormatting` | relayed (`tableRelay.conditionalFormatting`) | `ObjectGrid` · `conditionalFormatting: schema.conditionalFormatting as unknown[] \| undefined` | yes | a | | `grouping` | relayed (`tableRelay.grouping`) | `ObjectGrid` · `schema.grouping?.fields` | yes | a, by reference (the twin takes `GroupingConfig` from the spec) | | `navigation` | tests only | `ObjectGrid` · `navigation: schema.navigation` | yes | a, by reference (`ViewNavigationConfig` is the spec's `NavigationConfig`) | | `operations` | fences in `README.md` and `api/schema-reference.md` | `ObjectGrid` · `'operations' in schema ? schema.operations : undefined`; `ObjectView` · `schema.table?.operations` | yes | a | | `reorderableColumns` | none | `ObjectGrid` · `reorderableColumns: schema.reorderableColumns ?? false` | yes | a | | `rowColor` | relayed (`tableRelay.rowColor`) | `ObjectGrid` · `useRowColor(schema.rowColor)` | yes | a, by reference | | `rowHeight` | relayed (`tableRelay.rowHeight`) | `ObjectGrid` · `resolveRowHeightMode(schema.rowHeight)` | yes | a | | `singleClickEdit` | fence in `plugins/plugin-grid.mdx` | `ObjectGrid` · `singleClickEdit: schema.singleClickEdit ?? true` | yes | a | | `resizableColumns` | fence in `api/schema-reference.md` | `ObjectGrid` · `schema.resizable ?? schema.resizableColumns ?? true` | yes, as "Alternate spelling of `resizable`" | **d** · alias retirement; spec-modelled, so the spec half forks; open question 2 | | `emptyState` | tests only (`p1-spec-alignment.test.ts`, `object-view-table-slot-10976.test.ts`) | **none** on the grid (checker census 0; the `emptyState` reads in `ListView` / `ObjectView` are on other types) | no (strict refusal) | **c** · retire (`?: never` + `retirementTombstone`) | | `rowSpecActions` | tests only | **none** (checker 0, source grep 0) | no | **c** · retire, naming `rowActions` | | `bulkSpecActions` | tests only | **none** (checker 0, source grep 0) | no | **c** · retire, naming `bulkActions` |⚠️ The three route-(c) rows are source/checker readings. AGENTS.md says a zero from a source read does not answer "no renderer reads this key". The round that removes them owes a runtime probe first: a real `SchemaRenderer` and registry, varying only that key. ### `navigation.zod.ts#PaginationSchema`: measured, not implemented | key | authored | read site | spec | route | | --- | --- | --- | --- | --- | | `currentPage` | catalog `components-basic-pagination/basic-pagination.json` and `with-item-count.json`; `apps/site` playground | `pagination` renderer · `schema.currentPage \|\| schema.page \|\| 1` | no pagination component in the spec | **d** · a second spelling of the mirrored `page` (the twin calls `page` the "Legacy page property"); open question 3 | ## Acceptance: objectui#5250 M3 class (iii), re-run This used a scratch port of PR objectstack-ai#11069's `findUndeclaredKeys` (⛔ not committed; none of objectstack-ai#11069's wiring). It ran over the SHIPPED face: `StrictAnyComponentSchema` from a freshly built `packages/types/dist`. The corpora were those of `scripts/measure-strict-authoring-face.mjs` (catalog, docs JSON fences, apps plus `packages/*/examples`), plus `examples/**/*.json`. Class (iii) = membership of the `UnmirroredDeclared` ledger in the same tree, which tsc reconciles against the compiler measurement. | reading | documents judged | strict-refused | strict-only | refusal pairs | class (iii) pairs · occurrences · strict-only | | --- | --: | --: | --: | --: | --- | | before (`fe41dc76a`, base ledger) | 620 | 190 | 121 | 109 | **10 · 15 · 13** | | after (this branch) | 620 | 186 | 117 | 103 | **4 · 5 · 3** | 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. - **The six pairs that left:** `object-form` · `formType`, `sections`, `defaultTab`, `showStepIndicator`; `object-view` · `form.formType`, `form.drawerSide`. - **The four that remain** are the next round's: `object-grid` · `operations`, `singleClickEdit` (route a) and `resizableColumns` (route d); `pagination` · `currentPage` (route d). - **No refusal pair appeared.** The before/after diff of all refusal pairs has deletions only. So closing `sections` surfaced no nested refusal in the corpora. - **Tolerant-face verdicts are unchanged:** 69 tolerant-refused documents on both readings. ## Ablations Predictions were written to a scratch file before each run. Each mutation went through objectstack's `scripts/ablation-replace.mjs` in wrap mode, on committed code (HEAD `78b4eee67`): - the anchor had to hit exactly once; - the blob change is proven on disk; - the restore is proven by blob equality with HEAD and an empty `git diff HEAD`; - the tree was porcelain-clean afterwards. The subject resolves to `src` (relative imports; the root vitest alias maps `@object-ui/types/zod` to `packages/types/src/zod/index.zod.ts`), so no `dist` leg exists. | ablation (route) | predicted | observed | | --- | --- | --- | | **A1**: delete `formType` from the mirror; ledger untouched (route a) | tsc exit 2, `TS2322` at `assertionUnmirroredMatchesLedger` naming the pair; 5 red in the 6152 pin file; 2 red in block-config 8216; parity runtime half green | **as predicted**: tsc exit 2 with `Type '"objectql.zod.ts#ObjectFormSchema"' is not assignable to type 'never'` at `assertionUnmirroredMatchesLedger`; 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. | | **A2**: put `formType` back into the ledger while the mirror keeps it, a stale ledger key (the ledger half) | tsc exit 2 at the same assertion; parity runtime red on the objectui#8222 / objectstack-ai#8243 / objectstack-ai#7279 figure pins | tsc **as predicted**. Runtime: 2 red, "the split figures and the totals the header writes down equal the ledger" (objectui#7279) and "both ledger docstrings state the key total" (objectui#8243).⚠️ **One pin fewer than predicted:** objectui#8222's pin does not read `UnmirroredDeclared`'s key total. It defers that figure to objectui#7279's pin by design, and objectstack-ai#7279's pin reads the file-header bullet too. | ## Gates (at HEAD `78b4eee67`, exit codes captured before any pipe) | gate | exit | verdict line | | --- | --: | --- | | `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && … tsconfig.examples.json && … tsconfig.test.json`) | 0 | no diagnostics | | `pnpm exec vitest run packages/types/ …/block-config-schema-parity-8216.test.ts` (root form) | 0 | `Test Files 283 passed (283)` · `Tests 6555 passed (6555)` | | readers' suites: `packages/plugin-form/` + the `ObjectView` form-slot tests + `RecordFormPage` tests | 0 | `Test Files 149 passed (149)` · `Tests 1765 passed \| 1 skipped` | | zod-face consumers: `examples/schema-catalog/test/`, app-shell `previews/__tests__/`, `packages/cli/src/__tests__/`, four `apps/console` contract tests | 1 → 0 | first run red only on the stale 8216 row (fixed above); 8216 re-run `Tests 15 passed (15)` | | `pnpm --filter @object-ui/types lint` · `pnpm --filter @object-ui/app-shell lint` | 0 · 0 | `0 errors`; no finding on a changed line (the 7 warnings in touched files sit on untouched lines) | | `check-changeset-presence` · `-no-major` · `-fixed` · `-overwrite` | 0 ×4 | declares `.changeset/6152-object-form-unmirrored-members.md` · no major | | `check:changeset-claims` · `check:pending-changeset-literals` | 0 · 0 | report-only; the self-contradiction reading passes | | `check:control-bytes` · `check:new-line-citations` | 0 · 0 | `OK` · `0 new citation(s)` | | `check:spec-symbols` · `check:test-path-roots` · `check-type-check-coverage` | 0 ×3 | `0 untriaged collisions` · `OK` · `43/43 packages compile their tests` | | `check:handler-key-reads` · `check:element-data-source-declaration` · `check:unreferenced-sources` | 0 ×3 | OK | | `check-governed-queue-guard --test` (the 6 changed paths) · `--self-test` | 0 · 0 | `NOT GOVERNED` · self-test OK | **NOT MEASURED locally:** - **app-shell's `tsconfig.test.json` type-check.** Reason: its dependency closure's `dist` is not built in this worktree. The one edit there calls the file's own `judge(schema: unknown, name: string)` with a string. Declared to CI. - **The repo-wide `pnpm lint`.** CI's. - **`pnpm check`,** the CLI self-check in `lint.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 objectstack-ai#6152, with options) 1. **`submitHandler` and `open`.** - `submitHandler`: objectui#6182 rules it runtime-only (function arm). But `RuntimeOnlyDeclared`'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. - Recommended: widen the shape pin to a named allow-list by ruling. 2. **`ObjectGridSchema.resizableColumns`**: canonical `resizable` (the spec's own description, the renderer's first read, the twin's `@deprecated`). The spec half forks to objectstack. 3. **`PaginationSchema.currentPage`**: canonical `currentPage`, and `page` retires. Evidence: the renderer's first read, the twin's primary member, and the only authored spelling. ## Remaining rounds (not this PR) - **This card's leftovers:** - `objectql.zod.ts#ObjectGridSchema` (14; worklist above); - `navigation.zod.ts#PaginationSchema` (1; route d); - `objectql.zod.ts#ObjectFormSchema` (`open`, `submitHandler`; open question 1). - **The other pairs:** - `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) - **Spec-derived, routed to objectstack-ai#2231 and ⛔ not a local edit:** `complex.zod.ts#DashboardWidgetSchema` (2). ## Acceptance notes - **Producer boundary, per objectui#6170's binding check.** - `@objectstack/spec` 17.4.0's `ComponentPropsMap['object-form']` refuses `buttons`, `defaults` and `subforms` with `unrecognized_keys` (measured). Its `FormViewSchema` declares all three. - objectui's `object-form` reads them: `RecordFormPage` relays a form view's values onto the node. - The local half is mirrored here. The spec half forks to the spec lane and is reported to the seat, ⛔ not edited here. - **A `sections[].fields` entry is judged nowhere on this key** (`z.any()`, the `customFields` precedent). It becomes judgeable when `FormFieldSchema`'s own ledger rows close. - **This PR overlaps two in-flight PRs,** objectui#11070 (PR objectstack-ai#11115) and objectui#11073 (PR objectstack-ai#11086), in `objectql.zod.ts` and `zod-mirror-parity.test.ts`. - No hunk touches their keys or ledger rows. - objectstack-ai#11115's docstring edits rewrite the same `UnmirroredDeclared` totals line (`**12 entries / 84 keys** — …`), so whichever PR lands second merges `main` and re-derives that line. - With both applied it reads 12 / 65 with the split 4 / 19 spec-derived, 8 / 46 local. objectstack-ai#11115 moves `FormFieldSchema`'s one key into the spec-derived half. - `origin/main` moved three commits past `fe41dc76a` during this round, none touching this diff's files. No merge was needed. ## Round 2: `main` merged after PR objectstack-ai#11115 (objectui#11070), figures re-derived Written 2026-09-30T08:17Z by the dispatched dev, session `https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm`. Two things opened this round. PR objectstack-ai#11115 landed on `main` as `b0a05dda1`, and the maintainer ruled on this PR's pending-changeset edit: 「改成追加说明 (Recommended)」 (seat comment `5904913872` on objectui#6152). The director's PASS (`5904218639`) was given at `a5884b403`. This round moves the head, so that PASS is spent and `needs:contract-review` is back on this PR for a new record. The director's earlier deferral (`5903664978`) was answered by the base-merge commit `f78dc3602`, the head `5904218639` reviewed. No rebase and no force-push: three commits on top of `a5884b403`, each pushed fast-forward. | commit | what | | --- | --- | | `f3e9d830f` | merge of `main` `b0a05dda1` (parents `a5884b403`, `b0a05dda1`) | | `4ed5adf6f` | the dated note appended to `.changeset/10993-object-form-i18nlabel.md` | | `8f9bb76f6` | the dated note appended to `.changeset/7200-object-form-section-style-keys-undeclared.md` (see **The pending-changeset re-read** below) | ### The merge: two conflict hunks, one file Merging `b0a05dda1` conflicted only in `packages/types/src/__tests__/zod-mirror-parity.test.ts`, in two docblock hunks of the `UnmirroredDeclared` split section. The ledger itself (`interface UnmirroredDeclared`) and `SPEC_DERIVED_PAIRS` merged without a conflict: - PR objectstack-ai#11115 added `form.zod.ts#FormFieldSchema` and `objectql.zod.ts#ObjectKanbanSchema` to `SPEC_DERIVED_PAIRS` and changed no ledger key; - this branch took nineteen keys off the `ObjectFormSchema` entry. 1. **The SPEC-DERIVED split bullet.** - This branch read "(3 entries, 15 keys) — it was 3 / 34 until objectui#6152 round 1 …". - `main` read "(4 entries, 35 keys) — it was 3 / 34 until objectui#11070 …". - Merged: both history sentences kept, newest first, in landing order (objectui#11068, then objectui#11070, then this PR). It now reads "(4 entries, 16 keys) — it was 4 / 35 until objectui#6152 round 1 … It was 3 / 34 until objectui#11070 … It was 3 / 37 until objectui#11068 …". 2. **The totals line and the sentence before it.** - This branch read "… objectui#6152 round 1 then MIRRORED nineteen of that entry's keys … **12 entries / 62 keys** — 3 / 15 spec-derived, 9 / 47 local." - `main` read "… and objectui#11070 moved `FormFieldSchema`'s entry (one key) the same way … **12 entries / 81 keys** — 4 / 35 spec-derived, 8 / 46 local." - Merged: both sentences kept, in landing order. "that entry's" became "`ObjectFormSchema`'s", because `main`'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: - the file-header `UnmirroredDeclared` bullet (12 entries / 62 keys, from this branch); - both ledger docstrings (62 keys, from this branch); - the LOCAL split bullet (8 entries, 46 keys, with `main`'s objectui#11070 history sentence). The two other auto-merged files move separate hunks: - `objectql.zod.ts`: `main`'s hunks there (among them `dataSource` on `ObjectFormSchema` and its siblings) do not overlap this branch's; - `block-config-schema-parity-8216.test.ts`: `main` empties the `EXEMPT` table, and this branch's ledger-row hunk is untouched. ### The figures, measured on the merged ledger A scratch reader (⛔ not committed) parsed `interface UnmirroredDeclared` and `SPEC_DERIVED_PAIRS` at 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 at `4ed5adf6f`. | tree | ledger | spec-derived | local | | --- | --- | --- | --- | | merge base `c2a8d23c6` | 12 / 81 | 3 / 34 | 9 / 47 | | this branch before the merge, `a5884b403` | 12 / 62 | 3 / 15 | 9 / 47 | | `main`, `b0a05dda1` | 12 / 81 | 4 / 35 | 8 / 46 | | **merged, `4ed5adf6f`** | **12 / 62** | **4 / 16** | **8 / 46** |⚠️ This corrects the prediction in **Acceptance notes** above: "12 / 65 with the split 4 / 19 spec-derived, 8 / 46 local". That prediction was written before the base-merge round brought in objectui#11068, which closed three `ObjectGridSchema` keys (84 → 81 on `main`). 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: - the merge commit `f3e9d830f` takes `main`'s blob for `.changeset/10993-object-form-i18nlabel.md` (its diff against `b0a05dda1` on that path is empty); - `4ed5adf6f` appends one blank line and one paragraph, "**Correction, 2026-09-30 (objectui#6152).** …". It says `nextText` and `prevText` are now mirrored (PR objectstack-ai#11125), by the same reference to the spec's `I18nLabelSchema`; - `git diff b0a05dd --numstat` on the path reads 2 added, 0 deleted. The frontmatter is byte-identical, and `main`'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-read` request 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 the `ObjectFormSchema` / `object-form` members this PR moves (the nineteen mirrored keys, `open`, `submitHandler`, `sections`) and for `UnmirroredDeclared` figures, 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 "`ObjectFormSchema` in `zod/objectql.zod.ts` does not declare `sections`", "so there is no parse door to refuse at". This PR mirrors `sections` with a closed entry. - Measured with a scratch probe (⛔ not committed) at `4ed5adf6f`: the strict face refuses `{ sections: [{ label, className, fields }] }` as `unrecognized_keys` at `sections.0` naming `className`. The tolerant face accepts it and drops the key. The control without `className` parses on both. - On `main`, `ObjectFormSchema`'s mirror has no `sections` member; its `objectName` member is found by the same read, as the control. - **The remedy follows the same ruling as step 2:** `8f9bb76f6` appends one dated note ("**Correction, 2026-09-30 (objectui#6152).** …"), 2 added / 0 deleted against `b0a05dda1`. 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. -⚠️ This path is outside this card's claimed file surface. It is an in-place repair: same defect class as step 2; a mechanical shape the maintainer's append-only ruling pins; no other claim on the file; the same changeset gate family. The claim's file surface needs this path added. - **The other 30 hold.** 22 carry none of those tokens. The 8 that do describe one of three things: - other pairs: `ObjectGanttSchema`, `ObjectViewSchema`, `ObjectGridSchema.title`, the tree-view and chatbot slots; - `ObjectFormSchema.recordId` or `onStepChange`, members this PR does not move; - a ledger figure as a dated "X to Y" move. objectui#6150's "`RuntimeOnlyDeclared` is now a SUBSET of `UnmirroredDeclared`" still holds at pair level: its three pairs (`DataTableSchema`, `FormSchema`, `DetailViewSchema`) are all `UnmirroredDeclared` entries 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.mjs` in wrap mode, on committed code (HEAD `4ed5adf6f`). It put the predicted-but-wrong spec-derived figure back into the totals line: "4 / 16 spec-derived" → "4 / 19 spec-derived". | leg | predicted | observed | | --- | --- | --- | | mutation on disk | anchor 1 → 0, replacement 0 → 1, the blob changes | as predicted; blob `ed320c3c755d` → `1e6b780b577f` | | `tsc -p packages/types/tsconfig.test.json` | exit 0: the type-level assertions read key sets, not prose | exit 0, no diagnostics | | `vitest` on `zod-mirror-parity.test.ts` | exit 1, exactly one red: objectui#7279's "the split figures and the totals the header writes down equal the ledger" | exit 1, `1 failed \| 36 passed (37)`, that test. Its diff: `totalsLine.specDerived.keys` is 16 in the ledger and 19 in the header | | restore, by state | blob equals the HEAD blob; `git diff HEAD` empty | blob `ed320c3c755d` equals HEAD's; `git diff HEAD` 0 bytes; `git status --porcelain` 0 lines | No `dist` leg exists: the parity file reads its own source off disk and compiles against `src`. ### 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 at `4ed5adf6f`. 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 grep` outside `.changeset/`: 0 hits; `check:pending-changeset-literals` green). | gate | exit | verdict line | | --- | --: | --- | | `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`) | 0 | no diagnostics | | `pnpm exec vitest run packages/types/` | 0 | `Test Files 285 passed (285)` · `Tests 6602 passed (6602)` | | app-shell `block-config-schema-parity-8216.test.ts` | 0 | `Tests 15 passed (15)` | | readers' suites (at `4ed5adf6f`): `packages/plugin-form/`, the three `ObjectView` form-slot tests, the two `RecordFormPage` tests | 0 | `Test Files 158 passed (158)` · `Tests 1817 passed \| 1 skipped (1818)` | | `check-changeset-presence` · `-no-major` · `-fixed` | 0 ×3 | declares `.changeset/6152-object-form-unmirrored-members.md` · no major · fixed group OK | | `check-changeset-overwrite` | 0 | report-only. It names the two pending changesets this round appends to, `10993-object-form-i18nlabel.md` and `7200-object-form-section-style-keys-undeclared.md`; each declares the same packages at the same levels at base and now | | `check:changeset-claims` · `check:pending-changeset-literals` | 0 · 0 | report-only · `No test source names a pending changeset` | | `check:control-bytes` · `check:new-line-citations` | 0 · 0 | `OK` · `0 new citation(s)` | | `check-governed-queue-guard --test` (the 8 paths of the diff against `main`) | 0 | `NOT GOVERNED` | **NOT MEASURED locally:** - the package `lint` runs and the repo-wide `pnpm lint`. This round edits two docblock comments and appends two changeset paragraphs. CI's. - app-shell's `tsconfig.test.json` type-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: the `RuntimeOnlyDeclared` named allow-list, `resizableColumns` → `resizable`, and `currentPage` canonical. --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ational-target spelling objectui writes or reads; reference_to retires (objectui#11070, round 4) (objectstack-ai#11264) Refs objectstack-ai#11070 Clause-②: yes (narrowing). `LookupFieldMetadata`, `MasterDetailFieldMetadata`, and `DetailViewField` retire `reference_to` and declare `reference`, and the ingestion pass no longer stamps `reference_to` onto served defs, under the startup no-gradualism rule. It is priced in the changeset. Round 4 of objectui#11070, the `reference_to` round: the seat's answer A to Q1 (`5915992755`), under the maintainer ruling on objectui#6837 (`5475017957`, 2026-08-31, verbatim: 「objectui不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀」, with the earlier question 「按 spec 拒绝面直接删不是更合理吗」). Claim `5916640555`. `reference` is now the only spelling ObjectUI writes or reads for a relational field's target object. The one fold that remains is the ruling's choke point: `normalizeFieldReferenceKeys` folds a foreign `reference_to` / `referenceTo` onto `reference` when `reference` is absent, keeps its dev warning, and never drops a key. ## What changed - **Types (`@object-ui/types`).** `LookupFieldMetadata` and `MasterDetailFieldMetadata` declare `reference` typed by reference to `@objectstack/spec`'s `FieldSchema.reference`. `DetailViewField` and its zod mirror `DetailViewFieldSchema` move from `reference_to` to `reference` in the same commit, so the two faces keep equal key sets. The `form.ts` comments and the form-field mirror's comment now say that the lookup and user widgets read `reference` only. `strict-face-read-keys-11070.test.ts` moves `reference_to` from the pending list to the retired list, and gains a type-level pin: both field metadata types carry the spec member, and none of the three contracts has a `reference_to` key. - **Ingestion (`@object-ui/core`, `reference-keys.ts`).** The pass folds onto `reference` and no longer stamps `reference_to`. The header prose that described a stamp of "BOTH snake_case keys" now describes the fold, and it records why the stamp protected nothing once both ends moved. The leave-arm rule stands. - **Readers read `reference` only:** `LookupField`, `UserField`, `LookupCellRenderer` and `UserCellRenderer` (only those two reads in `fields/src/index.tsx`), `InlineFieldInput`'s reference fallback, `schemaDefaults`' `current_user` seed, `sectionFields`' view override, and `MetadataProvider`'s inline-subform parent. - **Emitters write `reference`:** `paramToField`, `RecordDetailView`, `FlowReferenceField` (its one target line), `RecordDetailPanel`, `RecordMetaFooter`, `RelatedList`, `fieldEnrichment`, `buildDefaultPageSchema`, `sectionFields`, `bulkParamToField` and `ObjectGallery`. - **Copy sets and ledgers.** `plugin-grid`'s `RELATIONAL_META_READ_SET` loses its `reference_to` row and the `adapter-stamped` verdict that row alone held. `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` and `FieldMeta` drop `reference_to`. `resolveActionParams`' unread `RuntimeField.reference_to` member goes. `FieldDefaultsSchemaLike` pins three members. - **Docs and fixtures.** `lookup.mdx` (typed fences, plaintext fences, prose, plus one paragraph on the retirement), `record-edit-modes.md`, `plugin-form.mdx`'s field-slot row and `plugin-form`'s README. The tests that fed `reference_to` to a widget, a cell or a served-def stub now feed `reference`. The pins whose job is the fold, or a refusal, keep `reference_to` as their input and assert the fold to `reference` with no stamp back, or the refusal. - **One changeset**, `.changeset/11070-reference-to-round4.md`, minor with the break stated (AGENTS.md §9: never `major`). Fifteen pending changesets that this round made false get an appended dated note, in the append-only shape: `6837-reference-to-arm-deletion`, `6837-gantt-tree-referenceto-arms`, `6837-recorddrawer-invented-target-arms`, `6694-dashboard-lookup-reference-meta`, `6597-retire-fieldmeta-referenceto`, `7166-retire-inert-fieldmeta-copies`, `paramtofield-reference-rule-derives-from-core-5312`, `6711-retire-reference-to-field`, `6874-retire-titleformat`, `7155-converge-lookup-dialect-camelcase`, `6528-resolve-reference-to-census`, `7324-plugin-form-nameable-parameter-types`, `11070-field-metadata-spec-spellings`, `10535-person-name-read-gate` and `6875-grid-relational-meta-derive`. ### Pins that flipped, and why These pins asserted the stamp. Each now asserts that no stamp happens, and that the fold still does: - `reference-keys.test.ts`: a `reference` def gains no `reference_to`; `referenceTo` folds to `reference` only; idempotence; the map and array containers. - `reference-keys.legacyWarning-6837.test.ts`, case 5: "the STAMP is unchanged" becomes "the FOLD still runs". The `referenceTo` case asserts no `reference_to`; the `reference_to` case keeps the input key, because the pass never drops one. - `MetadataProvider.itemReferenceKeys-7650.test.tsx`: the first pin asserted the stamp and now asserts its absence. The rest feed a legacy spelling, because a `reference`-only def comes back unchanged and cannot show that the pass ran. - `getObjectSchema.test.ts` (data-objectstack). - The emit-side pins that read `reference_to` off an emitted bag: `referenceArms-6837.divergent.test.ts` (plugin-detail), `RecordDetailDrawer.referenceArms-6837.test.tsx`, `HeaderHighlight.editable.test.tsx`, `relationalMetaCopySet-6711` / `-6874`, `lookupRelationalMeta-6694`, `sectionFields*`, `paramToField.test.ts`, `resolveActionParams.test.ts`, `bulkParamToField.test.ts`. - New refusal pins: the lookup cell, the user cell and `LookupField` each read no target from a `reference_to`-only def. Pins that assert something else were not "fixed green". Three copy-set floors, which required more than two keys in `RELATIONAL_META_KEYS`, now require more than one, because the set is now `reference` and `displayField`, and each floor now also names `reference`. ### Beyond the claim's file surface, stated - `ObjectDataTable.tsx` gains `ObjectDataTableRetiredReferenceToSnakeTombstone`, and both ObjectDataTable test files gain a directive and a counter-control for it. Retiring `FieldMeta.reference_to` removed the key from the derived refusal bands' pool, and `recordFields.tsx`'s own `FieldMeta` docblock requires that a retired member be re-refused by hand at that seam. This is the third tombstone, beside `decimals` and `referenceTo`. - Comment-only corrections to statements this round made false: `auditHistoryDisplay.ts`, `deriveRelatedLists.ts`, `useDatasetFields.ts`, `object-fields-io.ts`, `components/custom/field.tsx`, core `ActionRunner.ts` / `chart-series.ts` / `expand-fields.ts`, `data-objectstack/src/index.ts`, `RecordPickerDialog.tsx`, `lookupColumnDisplay.tsx`, `HeaderHighlight.tsx` and `ObjectGantt.tsx`, plus the stamp prose in the gantt / tree / drawer 6837 pins. - The named tail is folded. PR objectstack-ai#11243 landed, so the three `ObjectGrid.tsx` relational-metadata comments now name `reference` and `displayField` (seat message, 18:53Z). `ListView.tsx`'s two lines were left as they are, because both are true after this round: one says `FieldSchema` refuses `reference_to`, the other that a list column declares neither spelling. PR objectstack-ai#11233 landed, so the `form.zod.ts` comment, the `FormPage.tsx` comment, the `plugin-form.mdx` row and the `FormPage.sharedFieldResolver-10179` pin were folded as well (seat message, 19:54Z; the comment only in `form.zod.ts`, with no new declaration). `FormPage.sharedWidgets-10179`'s lookup fixture moved first, because that test went red on the change. ## Census, before and after Base: merge base `615346d` for the round's own diff. Code lines are non-comment lines in `packages/*/src`, `apps/console/src` and `examples/*/src`, excluding tests. The typed half comes from a TypeScript LanguageService `findReferences` over 5568 source files, with workspace packages resolved to source. | kind | before | after | | --- | --: | --: | | emitter sites writing `reference_to` onto a def or widget `field` | 11 | 0 | | ingestion stamp of `reference_to` | 1 | 0 | | copy sets carrying `reference_to` (grid ledger row, dashboard cell keys) | 2 | 0 | | reader lines reading `reference_to` (widgets, cells, inline editor, seeding, section override, subform parent) | 10 | 0 | | the ingestion fold's read of the legacy key (the ruling's choke point) | 1 | 1 | | declared `reference_to` members (field-types ×2, views, views.zod, RuntimeField, FieldMeta, schemaDefaults ×2) | 8 | 0 | | `reference_to?: never` refusal (tombstone) | 0 | 1 | | typed references to a `reference_to` member: production writes / test writes | 2 / 7 | no member left to reference | The eleven emitters are the PM census's eleven. The type-checker census found two of them typed (`FlowReferenceField`, `RelatedList`); the other nine write through `any` or `Record` bags. It found no typed reader: every reader was an untyped read. ## Runtime probe — the user-visible proof A scratch vitest file, never committed, ran against the source at merge base `615346d` and at head (`1972d01`, re-run at `338b19d` with identical readings). `ObjectForm` in create mode opens its lookup picker; `ObjectGrid` renders a lookup column with a primitive id; the referenced record is `{ id: 'a1', name: 'Acme Corp' }`. | case | base | head | | --- | --- | --- | | (a) served def with `reference` only, through `ObjectStackAdapter` | picker queries `probe_account`; cell shows `Acme Corp`; served keys `label, reference, reference_to, type` | picker queries `probe_account`; cell shows `Acme Corp`; served keys `label, reference, type` | | (a) the same def from a BYO `DataSource` | works | works | | (b) action param `referenceTo` through `paramToField` into `LookupField` | field carries `reference_to`; picker queries `probe_account` | field carries `reference`; picker queries `probe_account` | | (c1) `reference_to`-only def served through `ObjectStackAdapter` | folded (`reference` added); dev warning fires; picker and cell work | the same: folded, warned, picker and cell work | | (c2) `reference_to`-only def from a BYO `DataSource` | picker queries `probe_account`; cell shows `Acme Corp` | **no `find` call at all; the cell shows the raw id `a1`** | (c2) is the break, and the changeset's BREAKING paragraph names that host. ## Hypotheses - **H1 holds.** Read at objectstack `origin/main` `165c1d49e`. The example apps write `reference_to` zero times, against a control of 30 `Field.lookup` / `Field.masterDetail` calls in 19 files; both helpers write `reference`. `FieldSchema` refuses `reference_to` by name (installed 17.5.0: `unrecognized_keys`, "Did you mean `reference_to` → `reference`?"), and so does the spec's form-field schema. The `field-reference-to-alias` conversion (`packages/spec/src/conversions/registry.ts`) rewrites stored rows on rehydration and in `os migrate meta`. The only other hits are refusals (the SQL and Mongo doors, verify, lint), a reader of three spellings in `plugin-security`, and a comment in `plugin-approvals` that describes this repo's widget key (noted below). - **H2 holds.** Every production write is one of the eleven emitters, the ingestion stamp, or one of the two copy sets the brief listed as readers. No twelfth writer turned up, typed or untyped. - **H3 holds.** On the ObjectStack path the cells receive folded defs, as probe cases (a) and (c1) show. `RELATIONAL_META_KEYS` is derived to `reference, displayField`, and the grid's derivation gate re-extracts the cell's read set with `reference` in it. - **H4:** **false as posed.** The `reference_to` pair was not in M3's class (ii) at this base. Round 1 (PR objectstack-ai#11115) moved both `fields-lookup` fixtures to `reference`, and round 3 reported class (ii) without it. Measured with `scripts/measure-strict-authoring-face.mjs --json` at merge base `af9e957` and at `338b19d`: every refusal figure is identical (2224 nodes, 105 strict-refused, 66 strict-only, 39 red today, 599 documents, 98 refused whole-tree), and `reference_to` occurs in zero refusals at both. The only differences are the renderer-source word-mention counts, which the comment edits moved, and the error text of one authored module that fails to load under either build state. So this round moves M3 by zero in every class. The absolute class (ii) figure at this base is NOT MEASURED: round 3's shipped-face port of PR objectstack-ai#11069's `findUndeclaredKeys` was a scratch script and was never committed. ## Checks Heavy runs went through `os-verify-lock` (slot `issue-11070`). Exit codes were captured before any pipe. - **Build closure** (`check-doc-snippet-types --build-filter`, turbo `--concurrency=2`): `35 successful, 35 total`, before and after the first merge. - **type-check** of 21 packages, the ones this diff touches plus the console and the schema catalog (the type-checker census found no other typed reader): `Tasks: 56 successful, 56 total`, before and after the first merge. After the second merge, `@object-ui/types` was rebuilt and type-checked again, exit 0. - **Tests, full affected set** (the 21 packages' paths, 3536 files), before the first merge: `4 failed | 3527 passed | 5 skipped (3536)`. The four were pins that asserted the old shape: three copy-set floors of more than two keys, and the console shared-widget lookup fixture. They were fixed in `c684de4` and re-run: `Test Files 4 passed (4)`, `Tests 30 passed (30)`. - **Tests after the first merge** (`338b19d`), narrowed to the files both sides touched plus this round's relational surface (types, fields tests and widgets, the Flow inspectors, console components, plugin-grid, plugin-detail and plugin-dashboard tests, plugin-form, core reference-keys, the adapter, and app-shell providers and utils): `Test Files 1137 passed | 3 skipped (1140)`, `Tests 16884 passed | 31 skipped (16915)`. After the second merge (`16b43a7`, no file overlap): `packages/types/` `295 passed (295)`, `7375 passed (7375)`, including `zod-mirror-parity.test.ts`, whose totals did not move. - **Doc gates** at `338b19d`: `check:doc-snippets` `696 of 696 block(s) judged, 0 failed`; `check:doc-examples`, `check:doc-types`, `check:spec-symbols`, `check:doc-example-readers` and `check:doc-example-ids` all exit 0. - **At `16b43a7`**, all exit 0: `check:doc-fences`, `docs:check-links`, `check:control-bytes`, `check:pending-changeset-literals`, `check-changeset-presence` (`174 source file(s) of 20 released package(s) changed, and this change declares 1 changeset(s)`), `changeset:check` (no major), `check:new-line-citations` (`0 new citation(s)`), `check:test-path-roots`, `check:changeset-claims` (report-only; every named changeset was read), and `check-governed-queue-guard --test` over the 194 paths (`NOT GOVERNED`). - **Lint, a measured narrowing.** The population is `eslint.config.js`'s `**/*.{ts,tsx}` files entry; there are no `parserOptions.project` or `projectService`, so type-aware linting is off and this diff cannot move an untouched file's verdict. The 174 changed `.ts` / `.tsx` files were linted at `16b43a7` and at merge base `54a7830`: 0 errors at both, 1909 warnings at both, and no file is worse than base. - **Ablations**, each run on a committed, clean tree through objectstack's `ablation-replace.mjs`. In every case the anchor hit once, the blob moved, the blob was restored equal to HEAD, and `git diff HEAD` was empty afterwards. - A1: `lookup.mdx`'s fence writes `reference_to`. `check:doc-snippets` exited 1 with `696 of 696 block(s) judged, 1 failed`, TS2561 "'reference_to' does not exist in type 'LookupFieldMetadata'. Did you mean to write 'reference'?", which proves the gate read the rebuilt `.d.ts`. - A2: the fold stamps `reference_to` again. 6 red / 83 green, against 5 predicted: the three core stamp pins, the adapter pin and the provider pin, plus the empty-target pin, because an empty `reference` also gained a stamp. - A3: `LookupCellRenderer` reads `reference || reference_to` again. 2 red / 14 green, as predicted: the new cell refusal pin, and the grid derivation gate's unclassified-spelling check. - **Type-checker census** at `338b19d`: the seven contracts carry no `reference_to` member. The control run of the same instrument on `reference` finds 7 declarations, the same two typed production writes that moved, 1 production read, 3 test reads and 10 test writes. - **Diff size:** 194 files, +1022 / −647 against merge base `54a7830`. That is under the 5000-line human-merge threshold: 62 non-test files (16 of them changesets) and 132 test files. NOT MEASURED: - The repository-wide lint and the test shards outside the sets above. CI runs those. - Tests after the second merge outside `packages/types/`. Its three incoming commits share no file with this branch, and none contains `reference_to`. - A browser dogfood against a live objectstack app. The probe used `ObjectForm` / `ObjectGrid` with a stub or adapter-wrapped `DataSource`. - The absolute M3 class (ii) figure (see H4). - CI convergence. It was not waited on, per the dispatch contract. ## Acceptance notes - `docs/adr/0059-action-params-shared-field-widgets.md`'s decision section still records `referenceTo` → `reference_to` as `paramToField`'s mapping. It was left as written: it is the accepted decision's record (2026-07-19), and the ruling kept the historical record. Editing `docs/adr/**` would also make this PR governed. The seat may want a dated amendment line in a docs-only PR. - `ROADMAP.md`'s two hits are completed items and changelog history, not current behaviour, so they were not changed. - `.changeset/audit-field-def-reference-narrow.md` says the choke point "stamps both snake_case spellings". It has empty front matter, so nothing publishes it, and no note was appended. - objectstack `plugin-approvals`' `sys-approval-request.object.ts` has a comment saying the console resolves a lookup config as `reference_to: sys_user`. That spelling is now `reference`. It is a comment in the sister repo, not a producer; noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Refs #11070
Clause-②: yes — declaring a read key widens the strict accept set of the published
StrictAnyComponentSchema(@object-ui/types/zod); a runtime-only ruling instead corrects corpus documents. The PR waits for the director seat's contract review.Refs, not a closing keyword, on purpose. Of the card's 32 pairs, 20 leave class (ii) here: 14 are declared, and 6 leave because their documents are corrected. The other 12 stay on objectui#11070 (see "Remaining on objectui#11070"), so the card stays open after this lands. Two rounds are in this PR; round 2 carries the seat's answers to round 1's questions. Draft, for the director seat's contract review; the dev does not mark it ready.What changed
form.showSubmitonFormSchema: a boolean, defaulttrue.form.fields[]onFormField/FormFieldSchema:multiple,rows,accept,dimensions,reference,min,max,minLength,maxLengthandpattern. A hand-authored form has no object schema behind it, so the renderer hands each field widget the field ENTRY ITSELF as its metadata carrier (field: field.field || fieldat the onerenderFieldComponentcall inrenderers/form/form.tsx). The built-ininput/textareabranches spread the entry onto the native control. All butpatternare the spec'sFieldSchemamembers by reference, on both faces:SpecField['KEY']on the TypeScript face, andstripImportedDefaults(SpecFieldSchema).shape.KEYon the zod face.patternis a string: the spec does not declare it, and JSON has noRegExp. It is also the field-level spelling that thevalidation.patternrefusal already directs JSON authors to.multipleis read by thefile,image,lookupanduserwidgets; the built-inselectbranch still ignores it (objectui#11116).referenceis round 2: it is the spec spelling of a lookup's target, which thelookupanduserwidgets read beside the legacyreference_to, andreference_tostays refused by the strict face.dataSourceonobject-grid,object-form,object-kanbanandlist-view, and (round 2, the seat folding in the same family under objectui#6678) onobject-gantt,object-mapandobject-calendar. Each is the spec'sElementDataSourceSchema, by reference, aselement:numberalready declares it inpublic-blocks.zod.ts. Every one of these registrations is gate-wrapped (elementDataSourceBlock), soElementDataSourceGatereads the binding off the node and lands itsobjectonobjectName. The tolerant face still refuses the documented bindings on the nodes that require their ownobjectName/mode/ record source, and nothing here changes that (objectui#11117).object-chart'sdataSourceis withdrawn, and pinned as still refused (round 2).buildComponentScopeinrenderers/layout/react-page.tsxbuilds every public data block's node as{ dataSource, ...props, type: tag }, with the host's ADAPTER (ornull) underdataSource.object-chart-react-tier-node-10770.test.tspins that node, withdataSource: null, as valid on the mirror and throughsafeValidateSchema. Declaring the binding refused it: 7 red. See "Remaining on objectui#11070".list-view'sdataSourcein app-shell's relay census (round 2; the claim widened to this one line).ObjectView.relayRungCensus-7559.test.tsgains oneABSENCESentry,dataSource, of kindunread. The reason is measured:ListViewhas no read ofschema.dataSource. The binding is resolved one layer up, by the registeredlist-viewrendererListViewBlockthroughElementDataSourceGate, andrenderListViewbypasses that layer: it rendersListViewdirectly withschema={fullSchema}and takes the adapter as its separatedataSourceargument (ds). The census's ownunreadcheck re-derives the first half on every run.object-viewslots.ObjectViewSchema.tablewithholdsdataSourceon both faces, as a record source the view owns. It joinsdata/staticData/bindunder the objectui#10976 rule, and the zod twin refuses it by name.ObjectViewSchema.formcarries it, because that slot withholds nothing but the identity keys.fields-lookup/basic-lookup.jsonandmulti-select-lookup.json:reference_tobecomesreference.fields-password/with-minimum-length.json:min_lengthbecomesminLength.fields-auto-number/date-based-ticket-id.jsonandinvoice-number-format.json:formatis DROPPED, not renamed.FormFielddeclares noautonumberFormat, and nothing on the form path reads an auto-number format:AutoNumberFieldrenders the value only.guide/schema-playground.md: the flatvalidation: { pattern, message }, which objectui#5186 removed, now reads as the field-levelpatternstring. The dialectFormFieldSchema.validation的 zod 镜像(FieldConstraintsSchema)与FieldValidationRules形状完全不符:拒绝 TS 契约合法的对象形、放行渲染器从不读的扁平形 #5186 kept cannot carry this rule in JSON, because itspattern.valuemust be a compiledRegExp. The message has no field-level spelling, so it is gone.api/schema-reference.md: the page region'schildrenbecomescomponents, which is what the page renderer reads.plugins/plugin-detail.mdx: thepage:tabsitemsmove underproperties, the spelling the platform's producers write. ⛔ This is no ruling on whether the flat spelling is an authoring channel forpage:/record:blocks; that stays objectui#10872's open question.guide/objectos-integration.mdx, whose snippet put the adapter intoObjectGridSchema.dataSource(TS2741 undercheck:doc-snippetsonce the binding was typed).form-field-zod-coveragegained 10 keys.zod-mirror-parity:SPEC_DERIVED_PAIRSgainedFormFieldSchemaandObjectKanbanSchema.FormFieldSchema's one-keyUnmirroredDeclaredentry (field) is now SPEC-DERIVED by membership, so the header's split figures and history moved with it. No ledger KEY set changed.imported-defaults-8317:IMPORTEDgainedFieldSchema, whose walk reaches a fifthz.lazy.object-view-slot-key-lists: the source member counts moved, anddataSourceis classified in both slots.list-view-spec-parity:dataSourcegoes in a newPAGE_COMPONENT_ENVELOPEcategory, checked to behave as the spec binding..changeset/11070-strict-face-read-keys.md,@object-ui/typesminor. It names every declared key and what now refuses. Among those refusals: a node whosedataSourceholds an adapter ornull(the react-page wrapper's in-memory nodes, which are rendered, not validated) is refused bysafeValidateSchemaon the declared blocks.strict-authoring-face.ts, the.passthrough()of the rendering face, the class (iii) pairs (objectui#6152), PR feat(cli):objectui validateandobjectui checkjudge through the strict authoring face (objectui#5250, slice A) #11069, and the widgets' dual reads.origin/mainonce (6fe4b631e, no rebase), because main had movedapi/schema-reference.md. Atd1e683fa1main has no further change on this PR's paths.M3, before and after
My port of PR #11069's M3 classifier (a scratch script, never committed) is
scripts/measure-strict-authoring-face.mjs's corpus loaders plus #11069'sfindUndeclaredKeys(ata11f73347). It runs over the SHIPPEDStrictAnyComponentSchemafrom a freshly builtpackages/types/dist. At base it read exactly the card's figures.88fbd793d42feeeb25The 12 left are exactly the seat's expectation: the 9 dashboard pairs,
return_type,summary_typeandcolumns. No pair entered any class. The tolerant face refused the same 38 documents both times. The corrected documents that it refused before are still refused for OTHER reasons:schema-reference's page forpageType: "detail", andschema-playground's form for its stringoptions.Per pair
Read sites are cited by symbol and quoted expression, not by line (AGENTS.md #11). "Probe" means a scratch test, never committed. It rendered each corpus document through the real
SchemaRendererand registry, with@object-ui/fieldsregistered, once with the key and once without it, and compared the settled markup.form·showSubmitshowSubmit = truein the schema destructure; submit button under{showSubmit && (form·fields[].multipleFileField/ImageField(?.multiple),LookupField(fieldMeta?.multiple),UserField(delegates)input multipleon file and imageform·fields[].rowstextareabranch (spread);RichTextField(richField?.rows || 8, markdown)rows="6"againstrows="8"or noneform·fields[].acceptFileField(fileField.accept.join(','))accept="application/pdf"form·fields[].dimensionsVectorField(vectorField?.dimensions || value.length)(768D)against(5D)form·fields[].minNumberField(numberField?.min); built-ininputspreadmin. The form sets nonoValidateform·fields[].maxminmaxform·fields[].minLengthinput/textareaspreadminlengthform·fields[].maxLengthinputbranch (maxLength ?? max_length),textareabranch and countermaxlengthform·fields[].patterninputspreadpattern. A stringform·fields[].return_typeFormulaField(formulaField?.return_type || 'text'), snake onlyreturnType, which no widget readsform·fields[].summary_typeSummaryField(summaryField?.summary_type || 'count'), snake onlysummaryOperations(an object)form·fields[].reference_toLookupField(reference_to || reference),UserField(reference || reference_to)reference; fixtures correctedreference_tostays refused by the strict faceform·fields[].min_lengthbuildValidationRules(minLength ?? min_length), object-bound paths onlyminLengthmin_lengthstays refusedform·fields[].columns(grid field)GridField(cfg.columns)GridColumnDefinitionis not the shapeGridFieldreadsform·fields[].format(auto-number)autonumberFormatis declared or readform·fields[].validation.messagepatterndashboard·widgets[].options.data/xField/yField/value/description/trendDashboardGridLayout/DashboardRenderer(options.data,options.xField || 'name',options.yField || 'value',options.value ?? …;{ ...widget, ...options })optionsis an OPEN object the strict face closes; TS typesoptionsasunknowndashboard·widgets[].component.chartType/xAxisKey/seriesDashboardRendererrenderswidget.componentas a nodeBaseSchemaby the objectui#8344 / objectstack#8593 routingobject-grid·dataSourceElementDataSourceGate(binding read off the node;objectlands onobjectName)object-form·dataSourceObjectFormRenderer)object-kanban·dataSourceObjectKanbanRenderer)list-view·dataSourceListViewBlock)unread, reason measured abovepage·regions[].childrenregion.componentscomponentspage:tabs·items(flat)PageTabsRenderer(schema?.items, the hoisted key)properties.items. No ruling on the flat position (objectui#10872)Outside the 32-pair population, the same declaration (round 2, same family) also lands on
object-gantt,object-mapandobject-calendar·dataSource. The strict face refused it by name there, measured, while the tolerant face accepted the same documents.Remaining on objectui#11070
fields[].return_typeandfields[].summary_type: snake_case spellings with no read of the spec's spelling to declare instead.columns: the element shape is undecided.object-chart·dataSource: withdrawn in round 2 (see "What changed"). The react-page wrapper writes the adapter on the node; SchemaRenderer strips it, and the gate ignores a non-binding value.Tests and gates
Round 2 figures unless marked. Commits:
863d71e87(declarations),184d3c165(documents),e7afb7d96(changeset),42feeeb25(the exact-type pin; head). Exit codes were captured before any pipe.pnpm --filter @object-ui/types type-check(includingtsc -p tsconfig.test.json, which judges the parity ledgers)42feeeb25pnpm exec vitest run packages/types/42feeeb25Test Files 282 passed (282)·Tests 6506 passed (6506)pnpm --filter @object-ui/types build863d71e87sourcedist completeness: 1 package(s) completetype-check, against the rebuiltdist:core,components,fields,react,plugin-form,plugin-grid,plugin-kanban,plugin-list,plugin-view,app-shell,plugin-designer,plugin-detail,plugin-calendar,plugin-gantt,plugin-map,plugin-timeline,plugin-tree,plugin-dashboard,sdui-parser,plugin-charts184d3c165type-check: DoneelementDataSourcereader test,ListViewBlock,ObjectView.tableSlotRelay-10976,gridNonAuthorKeys, and every test outsidepackages/typesthat imports@object-ui/types/zod(81 files)184d3c165Test Files 81 passed (81)·Tests 1032 passed (1032)examples/schema-catalog/,packages/cli/,packages/core/184d3c165Test Files 242 passed (242)·Tests 6184 passed / 27 skippedpackages/components/src/renderers/form/184d3c165Test Files 67 passed (67)·Tests 459 passed / 17 skippedpackages/fields/184d3c165Test Files 219 passed / 1 skipped (220)·Tests 3497 passed / 7 skippedpnpm check:doc-snippets(Doc Snippet Type Check), after building its closure42feeeb25681 of 681 block(s) judged, 0 failedpnpm --filter @object-ui/types lint, and eslint on the census file42feeeb25node scripts/check-changeset-presence.mjs·pnpm changeset:check42feeeb25.changeset/11070-strict-face-read-keys.md·No changeset declares a major bumppnpm check:changeset-claims·check:pending-changeset-literals42feeeb25pnpm check:control-bytes·check:new-line-citations42feeeb25OK (scanned 9443 tracked text file(s))·0 new citation(s)pnpm check:spec-symbols·check:element-data-source-declaration·check:handler-key-reads·check:test-path-roots·check:doc-types·check:doc-fences·docs:check-links·check:doc-examples·check:doc-example-ids42feeeb25node scripts/check-governed-queue-guard.mjs --testover the 22 changed paths42feeeb25NOT GOVERNEDNOT MEASURED locally, left to CI: the Spec Main Shape Gate (it builds
@objectstack/specmain) and the repository-wide lint.Reverse verification
Ablations. Each was predicted before the run and applied through objectstack's
scripts/ablation-replace.mjs, which checks that the anchor hit, that the blob moved, and that the restore left the blob equal to HEAD withgit diff HEADempty.FormFieldSchema.multipledeleted (round 1)tscredtsc -p tsconfig.test.jsonexit 2FormSchema.showSubmitdeleted (round 1)tscredtscexit 2ObjectKanbanSchema.dataSourcerenamed away (round 1)tscredtscexit 2FormFieldSchema.referencedeletedtscredtscexit 2ObjectGanttSchema.dataSourcerenamed awaytscredtscexit 2ListViewSchema.dataSourcerenamed awaytscredtscexit 0tscredtscexit 2 (TS2344, TS2559 on the binding pin)dataSourceline deletedleaves NO member both unrelayed and undeclared)A6 showed that the type-level pin was blind for
list-view.ListViewSchemais derived from its mirror, so a member that leaves the mirror resolves to the index signature'sunknown, and anIsAnycheck passesunknown. The pin now asserts that each binding member IS the spec'sElementDataSource(42feeeb25).Consumers read the rebuilt declarations (round 1). A temporary
plugin-formfile assignedrows: 'four'and an adapter-shapeddataSource;tsc --noEmitexited 2 (TS2322, TS2353). The file was removed.Acceptance notes
object-chartand the react-page wrapper.buildComponentScopeinrenderers/layout/react-page.tsxstill writes the adapter (ornull) underdataSourceon every public data block's node. It comments that data blocks read it "from props", butSchemaRendererstrips that key from the props it spreads, andElementDataSourceGateignores a value that is not a binding. So on the blocks declared here, such a node now failssafeValidateSchema; it is rendered, never validated. Onlyobject-charthas a pin that validates one.fields/lookup.mdxstill teachesreference_toandfields/password.mdxteachesmin_length, in TypeScript fences. Thefields/auto-number.mdxheadings ("Custom Format", "Date-Based Format") now show only the seeded values.Queue fix (17.5.0
filtershape)Round 3, after the merge queue dequeued this PR on
CI_FAILURE(merge group head7c3a0340, CI run36674156292; the seat's diagnosis is comment5905013609). Written by the dev dispatched from sessionhttps://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30.Cause.
mainresolves@objectstack/spec17.5.0, whoseElementDataSourceSchema.filtertakes the ViewFilterRule array ([{ field, operator, value }], migrationelement-data-source-and-object-block-filter-rule-array). This PR declaresdataSourceby reference to that schema, and the previous head's CI ran on 17.4.0. Three literals on this PR's surface still wrote the record form.Commits.
e2c19f95d: merge oforigin/mainat0ffc423b1(a merge commit; no rebase, no force-push). No conflicts; six files auto-merged:imported-defaults-8317.test.ts,zod-mirror-parity.test.ts,objectql.ts,zod/form.zod.ts,zod/imported-defaults.ts,zod/objectql.zod.ts.bdd950ec6: the fix, and the head. Three paths:packages/types/src/__tests__/strict-face-read-keys-11070.test.ts:BINDING, which the sevenBOUND_NODESpins use, moves tofilter: [{ field: 'project', operator: 'equals', value: 'acme' }]. The pins still measure only that the key is declared.packages/types/src/__tests__/object-kanban-record-source-7780.test.ts("PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774's two EXCLUDED readings"): the fragment moves to the same rule array, so the refusal it asserts is again the record-source rule'sRECORD_SOURCE_REQUIRED, not the filter shape.content/docs/utilities/data-objectstack.mdx: the fence under "Kanban", which that pin mirrors, moves to the same form. No other fence on the page changed.Reproduced first, on the merged tree (
e2c19f95d, spec 17.5.0, before the fix): the two files gaveTests 8 failed | 39 passed (47). The sevenBOUND_NODESpins gotinvalid_typeatdataSource.filter("filteron this element data source takes the ViewFilterRule ARRAY form"), and the 7780 pin gotexpected [ undefined ] to include 'RECORD_SOURCE_REQUIRED'. These are the queue's two failures.Census of
filterunder adataSourceThe instrument is a scratch script, not committed; its method is stated here so the reading can be re-taken. Population: every tracked file of the ref, enumerated with
git ls-treeand read withgit cat-filefrom that same ref. It parsed 637.jsonfiles, 349 json fences and 1128 ts/tsx/js fences in.md/.mdx, and walked 5992 ts/tsx/js sources with the TypeScript compiler. A hit is afiltermember of adataSourceobject literal, or of aconstthat adataSourcenames. Each hit's value is judged by the installed 17.5.0ElementDataSourceSchema.shape.filter, not by its spelling. Control: a text scan for adataSourceobject whosefiltercomes before its first closing brace matched 34 files. Every one of them without a structural hit is CHANGELOG prose naming the binding's key list, or a code comment (RelatedList.listFilter.test.tsx).On the merged tree
e2c19f95dit read 37filterliterals under adataSource. 12 of them sit on one of the seven declared blocks by literaltype, andBINDINGreaches all seven throughit.each. The record form on the seven blocks: 7 literals, countingBINDINGonce. Fixed here: the 3 that went red or that a pin mirrors. Left as they are, not edited:data-objectstack.mdx, the fence under "Narrowing a saved view"object-grid{ "total": { "$gt": 100 } }main, forobjectName(objectui#11117); on this head it reportsdataSource.filteras well. The page still teaches afilterspelling the 17.5.0 spec refuses, and so does its TypeScript excerpt of the binding (filter?: FilterCondition).ListView.elementDataSource.test.tsx, "AND-combines the binding filter with the view's, never replacing it"list-view{ owner: 'me' }ListView.sharedGate.test.tsx, "AND-combines the component's own filter with the view's and the binding's"list-view{ owner: 'me' }strict-face-read-keys-11070.test.ts, the refusal case "a binding that names noobject"object-kanban{ a: 1 }dataSource.objectAND atdataSource.filter, so the case no longer isolates the reason it is named for. Measured: with a rule-arrayfilterit is refused atdataSource.objectalone.Also refused by the 17.5.0
filter, though not the record form: 6 legacy tuple arrays (such as[['owner', '=', 'me']]) on the seven blocks, in the render testsObjectGrid.elementDataSource.test.tsx(2),ObjectKanban.elementDataSource.test.tsx(2),ObjectGantt.elementDataSource.test.tsx(1) andObjectMap.elementDataSource.test.tsx(1). All green: none of them validates the node. Onbdd950ec6the same census reads 4 record-form literals on the seven blocks, the four in the table.Changeset
.changeset/11070-strict-face-read-keys.mdis unchanged, because no sentence in it is false on 17.5.0. The one sentence a 17.5.0 move could falsify, "the tolerant face refused the same documents both times", was measured again. 613 node documents were judged by the tolerant face ofmain0ffc423b1and of this head, both on 17.5.0: 179 docs json fences, 432 schema-catalog documents, and the two pre-fixdata-objectstack.mdxfences. No verdict differs; each tree refuses 58. The apps' authored documents were not measured again; the census found nodataSourcefilteramong them.Gates, at
bdd950ec6Exit codes were captured before any pipe.
pnpm exec vitest run packages/types/Test Files 284 passed (284)·Tests 6532 passed (6532)pnpm --filter @object-ui/types type-check(tsc,tsconfig.examples.json,tsconfig.test.json)packages/types: each one that imports@object-ui/types/zod, namesElementDataSourceSchema,ElementDataSourceGate,elementDataSourceor adataSource: { objectliteral, or holds a census hit, plus all ofexamples/schema-catalog--concurrency=2)35 successful, 35 totalpnpm check:doc-snippets679 of 679 block(s) judged, 0 failedpnpm check:doc-examplespnpm check:doc-fences·check:doc-typespnpm check:control-bytes·check:new-line-citationsOK (scanned 9536 tracked text file(s); skipped 85 binary)·0 new citation(s)node scripts/check-changeset-presence.mjs·pnpm changeset:check.changeset/11070-strict-face-read-keys.md·No changeset declares a major bumppnpm check:changeset-claims·check:pending-changeset-literalsNo test source names a pending changesetNOT MEASURED locally, left to CI: the repository-wide lint, the Spec Main Shape Gate, and the test shards outside the files named above.
Ablation
BINDINGwas put back to the record form through objectstack'sscripts/ablation-replace.mjs. The anchor hit once and the blob moved from11d08d26dtoc8a32682b. Predicted: the 7BOUND_NODESpins go red. Measured:Tests 7 failed | 23 passed (30), and the 7 failures are those pins. Restored: the blob equals HEAD's (11d08d26d) andgit diff HEADis empty.State
The PR stays a draft.
needs:contract-reviewis back on it for the director's review of the new head; the seat re-enqueues only after that record.Generated by Claude Code