…(objectui#11070) (objectstack-ai#11115)
Refs objectstack-ai#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
- **Declared on the TypeScript face and the zod mirror.**
- `form.showSubmit` on `FormSchema`: a boolean, default `true`.
- `form.fields[]` on `FormField` / `FormFieldSchema`: `multiple`,
`rows`, `accept`, `dimensions`, `reference`, `min`, `max`, `minLength`,
`maxLength` and `pattern`. 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 || field` at the one
`renderFieldComponent` call in `renderers/form/form.tsx`). The built-in
`input` / `textarea` branches spread the entry onto the native control.
All but `pattern` are the spec's `FieldSchema` members by reference, on
both faces: `SpecField['KEY']` on the TypeScript face, and
`stripImportedDefaults(SpecFieldSchema).shape.KEY` on the zod face.
`pattern` is a string: the spec does not declare it, and JSON has no
`RegExp`. It is also the field-level spelling that the
`validation.pattern` refusal already directs JSON authors to. `multiple`
is read by the `file`, `image`, `lookup` and `user` widgets; the
built-in `select` branch still ignores it (objectui#11116). `reference`
is round 2: it is the spec spelling of a lookup's target, which the
`lookup` and `user` widgets read beside the legacy `reference_to`, and
`reference_to` stays refused by the strict face.
- `dataSource` on `object-grid`, `object-form`, `object-kanban` and
`list-view`, and (round 2, the seat folding in the same family under
objectui#6678) on `object-gantt`, `object-map` and `object-calendar`.
Each is the spec's `ElementDataSourceSchema`, by reference, as
`element:number` already declares it in `public-blocks.zod.ts`. Every
one of these registrations is gate-wrapped (`elementDataSourceBlock`),
so `ElementDataSourceGate` reads the binding off the node and lands its
`object` on `objectName`. The tolerant face still refuses the documented
bindings on the nodes that require their own `objectName` / `mode` /
record source, and nothing here changes that (objectui#11117).
- **`object-chart`'s `dataSource` is withdrawn, and pinned as still
refused (round 2).** `buildComponentScope` in
`renderers/layout/react-page.tsx` builds every public data block's node
as `{ dataSource, ...props, type: tag }`, with the host's ADAPTER (or
`null`) under `dataSource`. `object-chart-react-tier-node-10770.test.ts`
pins that node, with `dataSource: null`, as valid on the mirror and
through `safeValidateSchema`. Declaring the binding refused it: 7 red.
See "Remaining on objectui#11070".
- **`list-view`'s `dataSource` in app-shell's relay census (round 2; the
claim widened to this one line).**
`ObjectView.relayRungCensus-7559.test.ts` gains one `ABSENCES` entry,
`dataSource`, of kind `unread`. The reason is measured: `ListView` has
no read of `schema.dataSource`. The binding is resolved one layer up, by
the registered `list-view` renderer `ListViewBlock` through
`ElementDataSourceGate`, and `renderListView` bypasses that layer: it
renders `ListView` directly with `schema={fullSchema}` and takes the
adapter as its separate `dataSource` argument (`ds`). The census's own
`unread` check re-derives the first half on every run.
- **`object-view` slots.** `ObjectViewSchema.table` withholds
`dataSource` on both faces, as a record source the view owns. It joins
`data` / `staticData` / `bind` under the objectui#10976 rule, and the
zod twin refuses it by name. `ObjectViewSchema.form` carries it, because
that slot withholds nothing but the identity keys.
- **Documents corrected (round 2; the claim widened to exactly these).**
- `fields-lookup/basic-lookup.json` and `multi-select-lookup.json`:
`reference_to` becomes `reference`.
- `fields-password/with-minimum-length.json`: `min_length` becomes
`minLength`.
- `fields-auto-number/date-based-ticket-id.json` and
`invoice-number-format.json`: `format` is DROPPED, not renamed.
`FormField` declares no `autonumberFormat`, and nothing on the form path
reads an auto-number format: `AutoNumberField` renders the value only.
- `guide/schema-playground.md`: the flat `validation: { pattern, message
}`, which objectui#5186 removed, now reads as the field-level `pattern`
string. The dialect objectstack-ai#5186 kept cannot carry this rule in JSON, because
its `pattern.value` must be a compiled `RegExp`. The message has no
field-level spelling, so it is gone.
- `api/schema-reference.md`: the page region's `children` becomes
`components`, which is what the page renderer reads.
- `plugins/plugin-detail.mdx`: the `page:tabs` `items` move under
`properties`, the spelling the platform's producers write. ⛔ This is no
ruling on whether the flat spelling is an authoring channel for `page:`
/ `record:` blocks; that stays objectui#10872's open question.
- Round 1 also corrected `guide/objectos-integration.mdx`, whose snippet
put the adapter into `ObjectGridSchema.dataSource` (TS2741 under
`check:doc-snippets` once the binding was typed).
- **Ledger rows the declarations move, and only those.**
- `form-field-zod-coverage` gained 10 keys.
- `zod-mirror-parity`: `SPEC_DERIVED_PAIRS` gained `FormFieldSchema` and
`ObjectKanbanSchema`. `FormFieldSchema`'s one-key `UnmirroredDeclared`
entry (`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`: `IMPORTED` gained `FieldSchema`, whose walk
reaches a fifth `z.lazy`.
- `object-view-slot-key-lists`: the source member counts moved, and
`dataSource` is classified in both slots.
- `list-view-spec-parity`: `dataSource` goes in a new
`PAGE_COMPONENT_ENVELOPE` category, checked to behave as the spec
binding.
- **Changeset:** `.changeset/11070-strict-face-read-keys.md`,
`@object-ui/types` **minor**. It names every declared key and what now
refuses. Among those refusals: a node whose `dataSource` holds an
adapter or `null` (the react-page wrapper's in-memory nodes, which are
rendered, not validated) is refused by `safeValidateSchema` on the
declared blocks.
- ⛔ Not touched: `strict-authoring-face.ts`, the `.passthrough()` of the
rendering face, the class (iii) pairs (objectui#6152), PR objectstack-ai#11069, and
the widgets' dual reads.
- Merged `origin/main` once (`6fe4b631e`, no rebase), because main had
moved `api/schema-reference.md`. At `d1e683fa1` main has no further
change on this PR's paths.
## M3, before and after
My port of PR objectstack-ai#11069's M3 classifier (a scratch script, never committed)
is `scripts/measure-strict-authoring-face.mjs`'s corpus loaders plus
objectstack-ai#11069's `findUndeclaredKeys` (at `a11f73347`). It runs over the SHIPPED
`StrictAnyComponentSchema` from a freshly built `packages/types/dist`.
At base it read exactly the card's figures.
| reading | base `88fbd793d` | head `42feeeb25` |
| --- | --: | --: |
| node documents | 588 | 588 |
| refused by the strict face | 158 | 96 |
| of them strict-only | 120 | 58 |
| refused by the tolerant face | 38 | 38 (the same documents) |
| **class (ii)** pairs / occurrences / strict-only | **32 / 149 / 141**
| **12 / 38 / 38** |
| class (iii) | 10 / 15 / 13 | 10 / 15 / 13 |
| class (i), M1 corpora | 47 / 69 / 49 | 47 / 69 / 49 |
The 12 left are exactly the seat's expectation: the 9 dashboard pairs,
`return_type`, `summary_type` and `columns`. 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 for `pageType: "detail"`, and
`schema-playground`'s form for its string `options`.
## Per pair
Read sites are cited by symbol and quoted expression, not by line
(AGENTS.md objectstack-ai#11). "Probe" means a scratch test, never committed. It
rendered each corpus document through the real `SchemaRenderer` and
registry, with `@object-ui/fields` registered, once with the key and
once without it, and compared the settled markup.
| # | pair | read site | docs | route | ruling / evidence |
| --: | --- | --- | --: | --- | --- |
| 1 | `form` · `showSubmit` | form renderer: `showSubmit = true` in the
schema destructure; submit button under `{showSubmit && (` | 76 | **(a)
declared** | probe: the submit button appears only when the key is
removed |
| 2 | `form` · `fields[].multiple` | `FileField` / `ImageField`
(`?.multiple`), `LookupField` (`fieldMeta?.multiple`), `UserField`
(delegates) | 6 | **(a) declared** | probe: `input multiple` on file and
image |
| 3 | `form` · `fields[].rows` | built-in `textarea` branch (spread);
`RichTextField` (`richField?.rows \|\| 8`, markdown) | 5 | **(a)
declared** | probe: `rows="6"` against `rows="8"` or none |
| 4 | `form` · `fields[].accept` | `FileField`
(`fileField.accept.join(',')`) | 1 | **(a) declared** | probe:
`accept="application/pdf"` |
| 5 | `form` · `fields[].dimensions` | `VectorField`
(`vectorField?.dimensions \|\| value.length`) | 2 | **(a) declared** |
probe: `(768D)` against `(5D)` |
| 6 | `form` · `fields[].min` | `NumberField` (`numberField?.min`);
built-in `input` spread | 2 | **(a) declared** | probe: native `min`.
The form sets no `noValidate` |
| 7 | `form` · `fields[].max` | as `min` | 2 | **(a) declared** | probe:
native `max` |
| 8 | `form` · `fields[].minLength` | built-in `input` / `textarea`
spread | 1 | **(a) declared** | probe: native `minlength` |
| 9 | `form` · `fields[].maxLength` | built-in `input` branch
(`maxLength ?? max_length`), `textarea` branch and counter | 1 | **(a)
declared** | probe: native `maxlength` |
| 10 | `form` · `fields[].pattern` | built-in `input` spread | 1 | **(a)
declared** | probe: native `pattern`. A string |
| 11 | `form` · `fields[].return_type` | `FormulaField`
(`formulaField?.return_type \|\| 'text'`), snake only | 3 | **remaining
on objectui#11070** | spec `returnType`, which no widget reads |
| 12 | `form` · `fields[].summary_type` | `SummaryField`
(`summaryField?.summary_type \|\| 'count'`), snake only | 3 |
**remaining on objectui#11070** | spec `summaryOperations` (an object) |
| 13 | `form` · `fields[].reference_to` | `LookupField` (`reference_to
\|\| reference`), `UserField` (`reference \|\| reference_to`) | 2 |
**declared the spec's `reference`; fixtures corrected** | seat's answer
A. `reference_to` stays refused by the strict face |
| 14 | `form` · `fields[].min_length` | `buildValidationRules`
(`minLength ?? min_length`), object-bound paths only | 1 | **fixture
corrected to `minLength`** | seat's answer A. `min_length` stays refused
|
| 15 | `form` · `fields[].columns` (grid field) | `GridField`
(`cfg.columns`) | 3 | **remaining on objectui#11070** |
`GridColumnDefinition` is not the shape `GridField` reads |
| 16 | `form` · `fields[].format` (auto-number) | none on this path | 2
| **documents corrected (dropped)** | probe: identical markup. No
`autonumberFormat` is declared or read |
| 17 | `form` · `fields[].validation.message` | the form renderer only
REPORTS it as a rule that validates nothing | 1 | **document corrected**
| the regex is now the field-level `pattern` |
| 18–23 | `dashboard` · `widgets[].options.` `data` / `xField` /
`yField` / `value` / `description` / `trend` | `DashboardGridLayout` /
`DashboardRenderer` (`options.data`, `options.xField \|\| 'name'`,
`options.yField \|\| 'value'`, `options.value ?? …`; `{ ...widget,
...options }`) | 7 / 6 / 6 / 5 / 1 / 1 | **remaining on objectui#11070**
(to a decision) | the spec's widget `options` is an OPEN object the
strict face closes; TS types `options` as `unknown` |
| 24–26 | `dashboard` · `widgets[].component.` `chartType` / `xAxisKey`
/ `series` | `DashboardRenderer` renders `widget.component` as a node |
1 / 1 / 1 | **remaining on objectui#11070** | the slot is `BaseSchema`
by the objectui#8344 / objectstack#8593 routing |
| 27 | `object-grid` · `dataSource` | `ElementDataSourceGate` (binding
read off the node; `object` lands on `objectName`) | 2 | **(a)
declared** | objectui#6678 |
| 28 | `object-form` · `dataSource` | same gate (`ObjectFormRenderer`) |
1 | **(a) declared** | same |
| 29 | `object-kanban` · `dataSource` | same gate
(`ObjectKanbanRenderer`) | 1 | **(a) declared** | same |
| 30 | `list-view` · `dataSource` | same gate (`ListViewBlock`) | 2 |
**(a) declared** (round 2) | census entry `unread`, reason measured
above |
| 31 | `page` · `regions[].children` | none: the page renderer reads
`region.components` | 1 | **document corrected** | now `components` |
| 32 | `page:tabs` · `items` (flat) | `PageTabsRenderer`
(`schema?.items`, the hoisted key) | 1 | **document corrected** | now
`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-map` and `object-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_type` and `fields[].summary_type`: snake_case
spellings with no read of the spec's spelling to declare instead.
- The grid field's `columns`: the element shape is undecided.
- The 9 dashboard pairs: to a decision.
- `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.
| gate | commit | exit | verdict |
| --- | --- | --: | --- |
| `pnpm --filter @object-ui/types type-check` (including `tsc -p
tsconfig.test.json`, which judges the parity ledgers) | `42feeeb25` | 0
| clean |
| `pnpm exec vitest run packages/types/` | `42feeeb25` | 0 | `Test Files
282 passed (282)` · `Tests 6506 passed (6506)` |
| `pnpm --filter @object-ui/types build` | `863d71e87` source | 0 |
`dist completeness: 1 package(s) complete` |
| downstream consumers `type-check`, against the rebuilt `dist`: `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-charts` | `184d3c165` | 0 | 20 of 20 `type-check:
Done` |
| app-shell's census plus every `elementDataSource` reader test,
`ListViewBlock`, `ObjectView.tableSlotRelay-10976`, `gridNonAuthorKeys`,
and every test outside `packages/types` that imports
`@object-ui/types/zod` (81 files) | `184d3c165` | 0 | `Test Files 81
passed (81)` · `Tests 1032 passed (1032)` |
| `examples/schema-catalog/`, `packages/cli/`, `packages/core/` |
`184d3c165` | 0 | `Test Files 242 passed (242)` · `Tests 6184 passed /
27 skipped` |
| `packages/components/src/renderers/form/` | `184d3c165` | 0 | `Test
Files 67 passed (67)` · `Tests 459 passed / 17 skipped` |
| `packages/fields/` | `184d3c165` | 0 | `Test Files 219 passed / 1
skipped (220)` · `Tests 3497 passed / 7 skipped` |
| `pnpm check:doc-snippets` (Doc Snippet Type Check), after building its
closure | `42feeeb25` | 0 | `681 of 681 block(s) judged, 0 failed` |
| `pnpm --filter @object-ui/types lint`, and eslint on the census file |
`42feeeb25` | 0 | 0 errors; no finding on an added line (eslint JSON
over the 11 touched types files and the census file) |
| `node scripts/check-changeset-presence.mjs` · `pnpm changeset:check` |
`42feeeb25` | 0 · 0 | declares
`.changeset/11070-strict-face-read-keys.md` · `No changeset declares a
major bump` |
| `pnpm check:changeset-claims` · `check:pending-changeset-literals` |
`42feeeb25` | 0 · 0 | report-only |
| `pnpm check:control-bytes` · `check:new-line-citations` | `42feeeb25`
| 0 · 0 | `OK (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-ids` | `42feeeb25` | all 0 | OK |
| `node scripts/check-governed-queue-guard.mjs --test` over the 22
changed paths | `42feeeb25` | 0 | `NOT GOVERNED` |
NOT MEASURED locally, left to CI: the Spec Main Shape Gate (it builds
`@objectstack/spec` main) 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 with `git diff HEAD` empty.
| ablation | predicted | measured |
| --- | --- | --- |
| A1 zod `FormFieldSchema.multiple` deleted (round 1) | 2 red + parity
`tsc` red | 2 red / 34 green; `tsc -p tsconfig.test.json` exit 2 |
| A2 zod `FormSchema.showSubmit` deleted (round 1) | 2 red + `tsc` red |
2 red / 34 green; `tsc` exit 2 |
| A3 zod `ObjectKanbanSchema.dataSource` renamed away (round 1) | 1 red
+ `tsc` red | 1 red / 35 green; `tsc` exit 2 |
| A4 zod `FormFieldSchema.reference` deleted | 2 red + `tsc` red | 2 red
/ 60 green; `tsc` exit 2 |
| A5 zod `ObjectGanttSchema.dataSource` renamed away | 1 red + `tsc` red
| 1 red / 61 green; `tsc` exit 2 |
| A6 zod `ListViewSchema.dataSource` renamed away | 3 red (the pin,
sanctioned-local, envelope) + `tsc` red | 3 red / 59 green, **but `tsc`
exit 0** |
| A6b the same, after the fix below | `tsc` red | `tsc` exit 2 (TS2344,
TS2559 on the binding pin) |
| A7 the census `dataSource` line deleted | 1 red | 1 red / 24 green
(`leaves NO member both unrelayed and undeclared`) |
A6 showed that the type-level pin was blind for `list-view`.
`ListViewSchema` is derived from its mirror, so a member that leaves the
mirror resolves to the index signature's `unknown`, and an `IsAny` check
passes `unknown`. The pin now asserts that each binding member IS the
spec's `ElementDataSource` (`42feeeb25`).
- **Consumers read the rebuilt declarations** (round 1). A temporary
`plugin-form` file assigned `rows: 'four'` and an adapter-shaped
`dataSource`; `tsc --noEmit` exited 2 (TS2322, TS2353). The file was
removed.
## Acceptance notes
- **`object-chart` and the react-page wrapper.** `buildComponentScope`
in `renderers/layout/react-page.tsx` still writes the adapter (or
`null`) under `dataSource` on every public data block's node. It
comments that data blocks read it "from props", but `SchemaRenderer`
strips that key from the props it spreads, and `ElementDataSourceGate`
ignores a value that is not a binding. So on the blocks declared here,
such a node now fails `safeValidateSchema`; it is rendered, never
validated. Only `object-chart` has a pin that validates one.
- **Docs pages still teach the legacy spellings, out of this PR's
surface.** `fields/lookup.mdx` still teaches `reference_to` and
`fields/password.mdx` teaches `min_length`, in TypeScript fences. The
`fields/auto-number.mdx` headings ("Custom Format", "Date-Based Format")
now show only the seeded values.
## Queue fix (17.5.0 `filter` shape)
Round 3, after the merge queue dequeued this PR on `CI_FAILURE` (merge
group head `7c3a0340`, CI run `36674156292`; the seat's diagnosis is
comment `5905013609`). Written by the dev dispatched from session
`https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm`, 2026-09-30.
**Cause.** `main` resolves `@objectstack/spec` 17.5.0, whose
`ElementDataSourceSchema.filter` takes the ViewFilterRule array (`[{
field, operator, value }]`, migration
`element-data-source-and-object-block-filter-rule-array`). This PR
declares `dataSource` by 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 of `origin/main` at `0ffc423b1` (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 seven `BOUND_NODES` pins use, moves to `filter: [{
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 objectstack-ai#7774's two EXCLUDED readings"): the fragment moves to the same
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 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 gave `Tests 8 failed | 39 passed (47)`.
The seven `BOUND_NODES` pins got `invalid_type` at `dataSource.filter`
("`filter` on this element data source takes the ViewFilterRule ARRAY
form"), and the 7780 pin got `expected [ undefined ] to include
'RECORD_SOURCE_REQUIRED'`. These are the queue's two failures.
### Census of `filter` under a `dataSource`
The 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-tree` and read with `git cat-file` from
that same ref. It parsed 637 `.json` files, 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 a `filter` member of a
`dataSource` object literal, or of a `const` that a `dataSource` names.
Each hit's value is judged by the installed 17.5.0
`ElementDataSourceSchema.shape.filter`, not by its spelling. Control: a
text scan for a `dataSource` object whose `filter` comes 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 `e2c19f95d` it read 37 `filter` literals under a
`dataSource`. 12 of them sit on one of the seven declared blocks by
literal `type`, and `BINDING` reaches all seven through `it.each`. The
record form on the seven blocks: 7 literals, counting `BINDING` once.
Fixed here: the 3 that went red or that a pin mirrors. Left as they are,
not edited:
| where | block | literal | why it is left |
| --- | --- | --- | --- |
| `data-objectstack.mdx`, the fence under "Narrowing a saved view" |
`object-grid` | `{ "total": { "$gt": 100 } }` | No pin mirrors it and
nothing goes red. The tolerant face already refuses that fence on
`main`, for `objectName` (objectui#11117); on this head it reports
`dataSource.filter` as well. The page still teaches a `filter` spelling
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' }` | A render test through the gate, never validated. Green. |
| `ListView.sharedGate.test.tsx`, "AND-combines the component's own
filter with the view's and the binding's" | `list-view` | `{ owner: 'me'
}` | The same. |
| `strict-face-read-keys-11070.test.ts`, the refusal case "a binding
that names no `object`" | `object-kanban` | `{ a: 1 }` | Still refused,
so green. On 17.5.0 it is refused at `dataSource.object` AND at
`dataSource.filter`, so the case no longer isolates the reason it is
named for. Measured: with a rule-array `filter` it is refused at
`dataSource.object` alone. |
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 tests `ObjectGrid.elementDataSource.test.tsx` (2),
`ObjectKanban.elementDataSource.test.tsx` (2),
`ObjectGantt.elementDataSource.test.tsx` (1) and
`ObjectMap.elementDataSource.test.tsx` (1). All green: none of them
validates the node. On `bdd950ec6` the same census reads 4 record-form
literals on the seven blocks, the four in the table.
### Changeset
`.changeset/11070-strict-face-read-keys.md` is 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 of
`main` `0ffc423b1` and of this head, both on 17.5.0: 179 docs json
fences, 432 schema-catalog documents, and the two pre-fix
`data-objectstack.mdx` fences. No verdict differs; each tree refuses 58.
The apps' authored documents were not measured again; the census found
no `dataSource` `filter` among them.
### Gates, at `bdd950ec6`
Exit codes were captured before any pipe.
| gate | exit | verdict |
| --- | --: | --- |
| `pnpm exec vitest run packages/types/` | 0 | `Test Files 284 passed
(284)` · `Tests 6532 passed (6532)` |
| `pnpm --filter @object-ui/types type-check` (`tsc`,
`tsconfig.examples.json`, `tsconfig.test.json`) | 0 | clean |
| 166 test files outside `packages/types`: each one that imports
`@object-ui/types/zod`, names `ElementDataSourceSchema`,
`ElementDataSourceGate`, `elementDataSource` or a `dataSource: { object`
literal, or holds a census hit, plus all of `examples/schema-catalog` |
0 · 0 · 0 | three batches: 59, 51 and 56 files passed; 2975, 748 and 896
tests passed |
| closure build of the packages the doc gates compile against (turbo,
`--concurrency=2`) | 0 | `35 successful, 35 total` |
| `pnpm check:doc-snippets` | 0 | `679 of 679 block(s) judged, 0 failed`
|
| `pnpm check:doc-examples` | 0 | every covered example compiles, or
fails exactly as its ledger row declares |
| `pnpm check:doc-fences` · `check:doc-types` | 0 · 0 | OK · every
documented component type is registered |
| `pnpm check:control-bytes` · `check:new-line-citations` | 0 · 0 | `OK
(scanned 9536 tracked text file(s); skipped 85 binary)` · `0 new
citation(s)` |
| `node scripts/check-changeset-presence.mjs` · `pnpm changeset:check` |
0 · 0 | declares `.changeset/11070-strict-face-read-keys.md` · `No
changeset declares a major bump` |
| `pnpm check:changeset-claims` · `check:pending-changeset-literals` | 0
· 0 | report-only · `No test source names a pending changeset` |
| eslint on the two edited test files | 0 | 0 errors, 0 warnings |
NOT MEASURED locally, left to CI: the repository-wide lint, the Spec
Main Shape Gate, and the test shards outside the files named above.
### Ablation
`BINDING` was put back to the record form through objectstack's
`scripts/ablation-replace.mjs`. The anchor hit once and the blob moved
from `11d08d26d` to `c8a32682b`. Predicted: the 7 `BOUND_NODES` pins go
red. Measured: `Tests 7 failed | 23 passed (30)`, and the 7 failures are
those pins. Restored: the blob equals HEAD's (`11d08d26d`) and `git diff
HEAD` is empty.
### State
The PR stays a draft. `needs:contract-review` is back on it for the
director's review of the new head; the seat re-enqueues only after that
record.
---
_Generated by [Claude
Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Refs #7322 (item ①). Item ② —
ObjectKanbanComponentProps.schemainpackages/plugin-kanban— is not addressed here; it lands indomain:uiafter this is onmain, and #7322 stays open for it.What this does
ObjectKanbanSchemanow declares the two keys theobject-kanbanrenderer reads and retires the one it never read, on BOTH published faces:packages/types/src/objectql.ts)packages/types/src/zod/objectql.zod.ts)groupBygroupBy: string— REQUIREDz.string()limitlimit?: numberz.number().int().positive().optional()groupFieldgroupField?: never—@deprecated RETIRED (objectui#7322) — author groupBy(theTimelineSchema.timeScaleshape, #6355)retirementTombstone('RETIRED (objectui#7322) — groupField is not read by the object-kanban renderer; author groupBy. (The view-level kanban.groupField alias is unaffected.)')Every other member and its docblock is unchanged; member order is the same on both faces. The tombstone stays a MEMBER of the mirror shape, so the parity ratchet's key sets stay equal and an authored
groupFieldis refused BY NAME at its own path (not stripped, not admitted).Plus: the row-cap block on
content/docs/plugins/plugin-kanban.mdxis annotatedconst board: ObjectKanbanSchema = { … }withimport type { ObjectKanbanSchema } from '@object-ui/types'(the page's semicolon-free import style) — triage's completion signal; the one in-repo fixture that authoredgroupFieldon this node now authorsgroupBy; a new pinpackages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts; a changeset (@object-ui/types: minor, "Breaking for authored metadata").Measured on the branch base (
53ded82b; the two faces are byte-identical on the merge-base tip2760075f)Read sites (
packages/plugin-kanban/src/ObjectKanban.tsx):schema.groupByat 13 sites — lane materialisation:601,:602,:613,:625,:626,:631,:640,:641, deps:649, card moves:747,:837,:865,:895;schema.limitat:264($top: schema.limit ?? DEFAULT_KANBAN_LIMIT,DEFAULT_KANBAN_LIMIT = 100at:71) and:291(deps).groupField: 0 hits underpackages/plugin-kanban/(control: the 13groupByhits in the same query).Before → after, from source, both entry paths (
ObjectKanbanSchema.safeParseandsafeValidateSchema):{ type, objectName, groupBy: 'status', limit: 20 }— the documented shapegroupField(required, missing)groupByandlimitsurvive{ …, groupField: 'status' }— the old declared shapegroupField, message namesgroupBy; and atgroupBy(required){ …, groupField, groupBy }groupField{ …, groupBy: 42 }/{ …, limit: 'twenty' }groupFieldrefusal; under.passthrough()the wrong-typed value was admitted unexaminedgroupBy/ atlimit{ type, objectName }— lane-lessgroupFieldgroupBy{ …, laneField: 42 }— control, a key the renderer does not readBaseSchemauntouched, #5155)groupByrequired-ness measurement (the dispatch's open question): declared REQUIRED. Evidence: the retired contract required a lane field; the renderer'sif (!schema.groupBy)branches at:601/:613are defensive early-returns; the docs page has exactly 1object-kanbannode (:159) and it authorsgroupBy;packages/plugin-kanbantests carry 20object-kanbannode literals, 19 authorgroupBy, and the one that does not (registration.test.tsx:32,{ type: 'object-kanban' }) mocks./ObjectKanbanwholesale and tests only that the renderer wrapper forwardsdataSource— not a lane-less board;ElementDataSourceGate(packages/react) maps a bounddataSource.objectontoobjectNameand never derives a lane key, and its ownobject-kanbancase authorsgroupBy: 'status'next to the binding. Two readings considered and NOT counted as a lane-less mode:content/docs/utilities/data-objectstack.mdx:322is ajsonfragment (not compiled, not validated) that omitsgroupByANDobjectName— an illustration of the binding keys, never a complete node under the old contract either; andListView.tsx:2411can emit a runtime-generated node withgroupBy: undefineduntilobjectDefloads (ADR-0085 then fills it from the object's lifecycle field) — a typed-anyadapter output, not authored metadata. If the PM reads either as a documented lane-less mode,groupBy?: stringis a one-line change on each face plus the two required-ness pins.Corpus rewrite count. Named docs / app corpus (
plugin-kanban.mdx,guide/data-source.md,guide/react-pages.md,utilities/data-objectstack.mdx,apps/console/src/register-plugins.tsand its tests): 2object-kanbannode literals, 0 authorgroupField, 0 rewritten.packages/**test files: 24object-kanbannode literals across 16 files, 1 authorsgroupField(kanban-conditional-formatting.test.ts:22) → rewritten togroupBy; the other 23 untouched.skills/objectui/guides/*: 2 prose mentions, 0 node literals.Node-local. The view-level
groupFieldalias is live and untouched:packages/core/src/utils/normalize-list-view.ts:191,packages/plugin-list/src/ListView.tsx:1462/:1816/:2091,packages/plugin-view/src/ObjectView.tsx:1234, and theKanbanConfig.groupFielddeclaration in the same zod file (objectql.zod.ts:352). The pin reads all four sites plus that declaration off disk as a control.Runtime reach of the tombstone.
SchemaRendererruns@object-ui/core's structuralvalidateSchema(no zod); the zod validator is reached by the CLIvalidate/checkcommands, the VS Code extension and tests. Boards render exactly as before.Parity
Key sets on both faces move together (
groupBy,limitadded;groupFieldkept as a tombstone).WiderThanDeclared(the fifth ledger, #7758) /NarrowerThanDeclared/UnmirroredDeclared:undefinedvsundefined,stringvsstring,number | undefinedvsnumber | undefined— no ledger row needed, none added. Measured on the tip that already carries #7758.Gates (exit captured before any pipe; verdict lines quoted)
pnpm --filter @object-ui/types build(verify lock)d7250015✓ dist completeness: 1 package(s) complete (124 emitted files verified)pnpm exec vitest run --maxWorkers=2 packages/types/(verify lock)d7250015Test Files 122 passed (122)·Tests 2116 passed (2116)(onb99b8ad2, before the merge: 122 / 2109)pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/(verify lock; the plugin's tests author the node shape)d7250015Test Files 22 passed (22)·Tests 112 passed (112)pnpm --filter @object-ui/types type-check(tsc --noEmit,tsconfig.examples.json,tsconfig.test.json— the last compiles the pin's type-level legs and its two@ts-expect-errorlines)d7250015error TSlinepnpm --filter @object-ui/plugin-kanban type-check, read-only, against the dependency-closure dists (pnpm exec turbo run build --filter='@object-ui/plugin-kanban^...' --concurrency=2under the lock:Tasks: 12 successful, 12 total, 1m15s shared-box)d7250015error TSline — the plugin imports noObjectKanbanSchemaand resolves@object-ui/typesthrough the root tsconfigpathsto SOURCE, so this gate is structurally independent of the diffpnpm --filter @object-ui/types lintd7250015✖ 268 problems (0 errors, 268 warnings)— 0 findings on the two touched test filesnode scripts/check-changeset-presence.mjs·check-changeset-fixed.mjs·check-changeset-no-major.mjsd7250015✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)·✅ All workspace packages are in the changeset fixed group.·✅ No changeset declares a major bump.node scripts/check-control-bytes.mjsd7250015✅ check-control-bytes: OK (scanned 6329 tracked text file(s); skipped 85 binary)pnpm check:spec-symbolsd7250015✅ spec member citations: no comment cites a key its spec symbol does not declare.pnpm check:doc-types·pnpm check:doc-fences·node scripts/check-doc-links.mjsd7250015✅ Every documented component type is registered.·✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript …·Links are valid across 17 scan roots.node scripts/pm/check-governed-merges.mjs --test …(objectstack, the six paths)0 of 6 path(s) hit the register— NOT governedNOT MEASURED locally, declared:
pnpm check:doc-snippets— its--build-filterclosure is 26 packages, which the shared box cannot build inside a foreground turn; CI'sDoc Snippet Type Checkis the measurement. Narrowed measurement in its place: the annotated block, extracted from the page and compiled with the gate's ownCOMPILER_OPTIONS(ES2020 / ESNext / bundler / react-jsx / strict / skipLibCheck /types: []) and itspathsderivation (@object-ui/types→packages/types/dist/index.d.ts, rebuilt from this branch):tsc exit=0; the same block against a dist rebuilt from the BASE faces:TS2741: Property 'groupField' is missing in type … but required in type 'ObjectKanbanSchema'— the card's own diagnostic, reproduced.pnpm --filter @object-ui/console type-check: skipped —apps/consoledoes not author the node (register-plugins.ts:120registers it; the two tests list the type name in string arrays).Ablation (red-first, both faces reverted to
53ded82b, restore proven)Run 1's docs leg was VOID — the probe tsconfig used
baseUrl, which TypeScript 6 refuses (TS5101) before compiling anything — and was re-run; run 2 is the reading. Mutation on disk:objectql.tsblob2714449218…(= BASE, ≠ HEAD1db955cf26…),objectql.zod.tsblob70fec93c61…(= BASE, ≠ HEAD45c5facc52…); anchors:groupField: string;x1,groupField?: neverx0,retirementTombstone('RETIRED (objectui#7322)x0,groupByin the zod block x0. Legs: the pin + fixture + parity from source →vitest exit=1,Test Files 2 failed | 1 passed (3),Tests 18 failed | 33 passed (51)(parity stays green: the reverted faces are a consistent pair);tsc -p tsconfig.test.json→ exit 2, 8 errors — sixTS2344on the type-level pins,TS2741on the documented literal,TS2578unused@ts-expect-error; a dist rebuilt from the reverted faces (marker x0,groupField: stringx1) → the docs block failsTS2741. Restore bygit checkout HEAD -- …undertrap … EXIT INT TERM: blobs equal HEAD (1db955cf26…,45c5facc52…),git diff HEADempty, dist rebuilt from HEAD (markerobjectui#7322x4 inobjectql.d.ts, x2 inzod/objectql.zod.js,groupField?: neverx1).Serial constraints / overlap
2760075f; this branch was fast-forwarded onto it before any commit and the parity suite ran there.exportOptionsreconciliation never landed — zod mirror still takes'pdf'and declares nostreaming, and bothas anyreads survive #6956, theexportOptionsregion ofobjectql.zod.ts): landed at4dfdcc3cwhile this branch was open; merged into the branch asd7250015(a merge commit — no rebase of a pushed branch) before opening, and every gate above was re-run on that head. Different region of the same file; no textual overlap with theObjectKanbanSchemaconst.domain:uiseat's finding(types/plugin-kanban): an authoredkanbandocument can PASSsafeValidateSchemaand still render empty — the validator and the renderer honour different faces #7664 holdspackages/plugin-kanban/**; this PR does not touch that package (read-only: gates only).Out of scope — filed, not touched here
groupField— a key the renderer never reads and, after objectui#7322, the validator refuses by name; nogroupBy/limitcontrol #7772 — app-shell: the page-block designer'sobject-kanbaninspector (block-config.ts:222) writesgroupField(label "Group by field"), a key the renderer never reads and, after this PR, the validator refuses by name; it offers nogroupBy/limitcontrol. Not mechanical (i18n keys en / zh + their pin + a possible strip-on-load for saved pages);domain:ui/ app-shell.groupFieldalongsidegroupByonto the generated object-kanban node — an inert duplicate write held in place by two pins #7773 — plugin-view / plugin-list:ObjectView.tsx:1283andListView.tsx:2416writegroupField: groupByonto the generatedobject-kanbannode (inert — the renderer readsgroupBy; the node isany-typed and never reaches the zod validator at runtime), held in place by two pins. Unblocked; the alias READS stay untouched.Not in this PR by dispatch: item ② (
packages/plugin-kanban/**), the view-level alias sites,BaseSchema's index signature (#5155), the declarativeKanbanSchema.Angle-bracket shapes are spelled out in words throughout this body because GitHub's sanitizer strips tag-shaped fragments (see AGENTS.md).
Generated by Claude Code
Generated by Claude Code