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>
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