Repository navigation
feat(plugin-grid): one display page size read from the spec, and a distinct fetch batch (objectui#9853) - #10278
Conversation
…stinct fetch batch
The grid fell back to three page-size literals: 10 rows (client-paged table),
10 groups (grouped view) and a 50-row "server window" that sized both the
fetch and the visible page of the server-paged table. Per ruling C-prime:
- the undeclared display page size on all three display paths is the default
@objectstack/spec declares for pagination.pageSize, read via
PaginationConfigSchema.parse({}) with no local fallback number;
- the server-window constant becomes DEFAULT_FETCH_BATCH_SIZE (value kept),
read only by the grouped view's fetch, never as a page size;
- the grouped pager's size selector merges in the active size, as the
DataTable pager already does, so it shows the size in force.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…y default groupedPagination.test.tsx pinned the undeclared page of groups as 10. The fixture is now sized off the spec's declared default (two full pages and a partial third), so the same seven rows keep their meaning. Adds the grouped size-selector row to the objectui#9853 pin. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…dd changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…-display-page-size
|
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
|
|
Contract review deferred at head
Generated by Claude Code |
✅ 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
|
Round 2 of objectui#9853 syncs the draft PR with main before flipping the objectui#7394 CONTROL row per ruling record 5909000462. The merge is clean: the five files both sides touched (ObjectGrid.tsx, plugin-grid README.md and plugin-grid.mdx, types objectql.ts and objectql.zod.ts) auto-merged with no conflict hunk. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…ne further query, per ruling record 5909000462 (objectui#9853) The row "CONTROL (objectui#7394): with nothing held, the switch to the dataset chart re-issues no query" read red on this branch: with no pageSize declared, the paged grid's window is the spec display default and the dataset chart's is the list's fetch batch, so the switch re-issues the fetch. The maintainer accepted that cost (ruling record 5909000462 on objectui#9853). - The row is renamed for what it now pins: exactly one further query, whose $top is the fetch batch, after a first query whose $top is the spec display default. Both numbers are read, never spelled: the default from PaginationConfigSchema, the batch from DEFAULT_LIST_FETCH_BATCH_SIZE, which ListView.tsx now exports (the package index does not re-export it). The further query equals the one on screen with only $top changed. - A new CONTROL row pins the no-query half on the same switch: with a pageSize declared, grid to dataset chart re-issues nothing (objectui#7394 section 1 pins grid to kanban). No other assertion in the file changed. 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
|
…equest on a view switch, and qualifies the objectui#10512 note it contradicts With no declared page size, a switch between ListView's grid view and an unpaged view issues one request whose $top is the fetch batch (ruling record 5909000462 on objectui#9853). The pending objectui#10512 changeset, which publishes in the same release, says a switch into a dataset chart with no user filter set issues no new request. That now holds only with a declared page size, so this card's own changeset says so. The objectui#10512 changeset itself is not edited. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read, and nothing else: card #9853 (its body and all 18 comments, the rulings ① Derived judgmentsEvery accept-set or public-surface change the diff implies, each named right or wrong:
Nothing in the diff widens or narrows what an author can write. ② Semver level
③ Boundary flagsEvery dev flag and every
Deferral Implemented-by: VERDICT: PASS Readings taken 2026-09-30T11:24Z. |
Dequeued by a required check that is red on every objectui merge group. ⛔ Not this PR's failure. Re-queued, unchanged, once objectui#11206's repair is on
|
… family D input slice) (objectstack-ai#10907) Part of objectstack-ai#9256 Clause-②: yes **Clause-② `yes`, as the claim declared:** `input` accepted an authored `children` on both published faces and rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it. It now refuses it by name. A published accept set narrows, so a contract review is owed before landing. This is the family-D slice that release `5864147075` on objectui#9256 lists, and nothing else: `input`, the family-D test header's superseded remarks, and `PartialSchema`'s docblock claim about generic `Omit` re-spellings. `br`, `hr` and `img` are not touched. objectui#9256 stays open. ## What changed - **`input` narrowed** in the family-D shape PRs objectui#9589 and objectui#10897 used: - TypeScript face (`InputSchema` in `packages/types/src/form.ts`): `body?: never` and `children?: never`, each with a docblock saying what the renderer reads and what it renders instead. - zod mirror (`InputSchema` in `zod/form.zod.ts`): two `retirementTombstone` members fed one string, `INPUT_NEITHER_CHANNEL`, built by `neitherContentChannelGuidance`. Both stay MEMBERS, so `zod-mirror-parity`'s key sets stay equal. - `body` was already refused on both faces by `BaseSchema` (objectui#6771). It is restated because that refusal names `children` as the remedy, and `input` does not read `children` either. - **`email` / `password` (`InputShorthandSchema`)**: no type or accept-set change. See "The shorthand's own pair" below. - **Texts (comments only):** - The family-D test header: 「family E, frozen」 (the `InputSchema` bullet), 「unattributable」 (the `DetailViewSchema` bullet) and the two `ui:calendar` remarks that still said plain `Omit` (the header bullet on the six bare-name hold-outs, and the comment above the slice-2 rows) now say what is true. No assertion in that file moved. - `PartialSchema`'s docblock in `packages/types/src/index.ts`: the sentence reading every generic re-spelling as collapsing is marked false, with `OmitDeclared` named as the counter-example. - **Bounded in-place fix, declared:** `partial-schema-collapse-pin.test.ts`'s header carried the same false claim, and even named the key-remapping spelling as one that collapses. It is corrected the same way, comment only. See "Texts" below for the four conditions and the evidence. - **New pin file** `packages/types/src/__tests__/content-channel-input-9256.test.ts`. - **One changeset** `.changeset/9256-input-content-channels.md`: `@object-ui/types` `minor` with an explicit BREAKING note and a migration line, the spelling PRs objectui#9589 and objectui#10897 used under this repo's no-major rule. - **Four pending changesets are corrected, prose only.** - Round 2 added a dated note to `6397-partial-schema-collapse-pin.md`, `9256-content-channel-family-d.md` and `8499-node-slot-registered-arms.md`. - Round 3 added a second note to the family-D entry and reworded the E3 entry's render claim in place. - Each carries a sentence a later change in this same release made false, or an overstatement, and each ships verbatim into the CHANGELOG. Every frontmatter block is byte-identical. See "Round 2" and "Round 3" below. ## Measurement, taken on `origin/main` `244d516df` before any edit **Instrument.** The TypeScript compiler API (6.0.3), ⛔ not grep. It builds one program per `tsconfig.json`: the 40 workspace packages, `apps/console` and the three examples, 44 programs over 2015 non-test source files. It ran on a BUILT tree (`turbo run build` 43/43, 0 cached) with 0 unresolved-module diagnostics. It files every `.body` / `.children` read (property access, string element access, object destructuring) under the declared type of its receiver: 520 channel reads were filed. - **`InputSchema`: 0 `body` / 0 `children` reads.** `InputShorthandSchema` is never a receiver: the `email` / `password` wrappers are `any`-typed and hand the node to the `input` renderer, which reads it as `InputSchema`. - **Lit controls fire in the same run:** `ButtonSchema` 1, `DivSchema` 1, `CardSchema` 1, `ContainerSchema` 1 (each `children`). - **Receiver reachability:** the same programs see `InputSchema` as the receiver of 23 reads over 17 keys: `defaultValue`, `description`, `error`, `id`, `inputType`, `label`, `max`, `maxLength`, `min`, `name`, `pattern`, `placeholder`, `readOnly`, `required`, `step`, `value`, `wrapperClass`. Neither channel is among them, so the zero is a reading, not blindness. **Every registration that claims `input`, bare and namespaced** (`check:registry-bare-names --json`, 1657 files, 424 claims, 0 contested): - `input`: sole claimant `ui:input`, a literal `ComponentRegistry.register('input', InputRenderer, …)` in `renderers/form/input.tsx`. The hop is typed (`InputRenderer` takes `schema: InputSchema`), not `any`. No `field:input` or other claimant exists; `ai:input` is a different key, a guarded protocol placeholder. - `email` / `password`: `ui:email` / `ui:password` own the bare keys; `field:email` / `field:password` stand down with `skipFallback: true`. **Does any path pass the node's content through? No.** - `SchemaRenderer` destructures `children` and `body` out of the props bag before the spread. - The only pass-through `InputRenderer` gives the native element is `toFormControlDomProps(inputProps)`, a whitelist pick of `SDUI_DOM_PASS_THROUGH_KEYS` plus `name` / `disabled`; `children` is not on it. - The `email` / `password` wrappers only spread `props` and `props.schema` into `InputRenderer`. - The registration declares no `children` slot input (objectui#9910). - No in-source host builds an `input` node by spreading another node. The literal `input` nodes in `form.tsx`, `cli`'s `init` scaffold and the dashboard / designer config panels are assembled key by key, and none carries a channel. - **The 75 `any`-typed channel reads were attributed file by file.** None can receive an `input` node's channel: - app-shell nav / search trees, and HTTP request, error or email `body` payloads; - React `children` destructured off component props (`view:simple`'s renderer, `react-page`'s wrapper), which `SchemaRenderer` never fills from a node; - the metadata-admin preview canvases, which read a page block's `properties.children` or a page draft's root `children` to list and append blocks for selection, not to render a node's channel; - the page-container and sectioning registrations in `containers.tsx`, `page.tsx` and `semantic.tsx`, and the html-elements factory, whose tag list excludes `input` by name (its own comment lists `input` among the tags it never registers); - `core`'s schema builder (card / grid / flex builders; `InputBuilder` has no children setter); - `record:alert`'s own `props.body`, the runner's nav items, and the vscode validator. - **Generic traversers** (`validateChildren` in core, `sdui-parser`'s parse / validate, `cli validate`) walk children to validate. They render nothing and are node-agnostic, as for every family-D row already landed. **Producers.** `pnpm census:body-dialect --keys input,email,password,ui:input,ui:email,ui:password,div,card,page,button` read 9016 files. The key population is printed by the tool itself, so objectui#9545's blindness does not apply. - Nodes on the six input keys: `input` 321, `email` 75, `password` 60, `ui:email` 1, `ui:password` 17, `ui:input` 0. **0 author `body`, 0 author `children`.** - The controls fire in the same pass: `children` on `div` 172, `card` 183, `page` 51, `button` 5. - Real producers: 0. Nothing was migrated. ## The shorthand's own pair: type-redundant now, kept on purpose `InputShorthandSchema` inherits `InputSchema` through `OmitDeclared` on the TypeScript face and `.omit()` on the mirror, so it now inherits the new pair. It already declared its own (PR objectui#10897). - **Redundant as a type:** on the built d.ts both are the same `?: never`, and the new `Eq` pin makes `tsc` hold that. - **Not redundant as text.** The repo's rule for a restated tombstone is the one `neitherContentChannelGuidance`'s docblock gives for restating `body` over `BaseSchema`: restate when the inherited message misdirects this node's author. The helper's contract spells the node and its route into the message. The inherited string names `input` and the `ui:input` route; the shorthand's names `email` / `password` and the wrapper route. - **Decision:** keep both members and rewrite their docblocks (TS) and comment (zod) to say they are type-redundant and why they stay. The shorthand's old `body` docblock gave a reason that became false with this change (that the inherited refusal names `children` as the remedy), and it is replaced. - **Pinned:** a CONTROL row asserts that an author of `email` / `password` reads the message naming those two nodes and not the `input` one, so deleting the restatement turns it red. - Counter-precedents, noted: `UiCalendarSchema` restates nothing and inherits `calendar`'s message, which names it. objectui#8072 deleted the shorthand's `wrapperClass` restatement once `InputSchema` carried the key; that was a live key with one description, not a refusal message naming a node. ## The `OmitDeclared` propagation, measured on the BUILT d.ts A compiler-API probe over `packages/types/dist/form.d.ts` reads the heritage of `InputShorthandSchema` on its own: the base type the checker gives for `OmitDeclared` of `InputSchema` minus `type` / `inputType`, before the interface's own members. | built d.ts | `InputSchema.children` | heritage `.children` | shorthand face `.children` | |---|---|---|---| | base `244d516df` | `SchemaNode \| SchemaNode[]` | `SchemaNode \| SchemaNode[]` | `undefined` (its own restated tombstone) | | this branch | `undefined` | `undefined` | `undefined` | A consumer-side probe resolves `@object-ui/types` the way `packages/components` does (`--traceResolution`: `packages/types/dist/index.d.ts`). There, `input` + `children`, `input` + `body` and `email` + `children` each answer their `@ts-expect-error`, and a lit control (`const n: number = 'x'`) fires. ## Red on base, then green — one-shot, the restore proven by state The predictions were written to a file BEFORE the mutation. The mutation was `git checkout 244d516` of the two declaration files (`form.ts`, `zod/form.zod.ts`), with every test at HEAD. It was proven by both blob hashes equal to BASE and by anchor counts: `INPUT_NEITHER_CHANNEL` 0, the new TS docblock marker 0. The run sat under `trap … EXIT INT TERM` with absolute paths. | reader | mutated (BASE declarations) | restored (HEAD `72dba894a`) | |---|---|---| | vitest, `content-channel-input-9256.test.ts` | **RED** exit 1: 6 failed / 13 passed | **GREEN** 19 / 19 | | `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2: TS2578 on the `inputChildren` pin, TS2322 on the `Eq` pin for `children`; 0 elsewhere (`zod-mirror-parity` included) | **GREEN** exit 0 | | built d.ts probe (types rebuilt; dist markers 0 then 2) | `InputSchema` and heritage `children` accepted | refused | - **The 6 red rows:** `input.children` refused at its path, its message, `.describe()` equality, every-value, and the root + nested `AnyComponentSchema` row; plus the `input.body` message row. The base message is `BaseSchema`'s `Did you mean` pointer. - **Green on base, by design:** `input.body` refused at its path, every-value and `AnyComponentSchema` (`BaseSchema` already refused `body`), and the shorthand message CONTROL (its own pair is unchanged). - **One prediction was recorded as uncertain and read, not assumed:** `input.body` `.describe()` equality is GREEN on base, because `BaseSchema`'s alias refusal also describes with its own message. -⚠️ `?: never` is erased before vitest runs: the TypeScript half is read by `tsc` alone. `tsc --listFiles` counts the new pin file 1 under `tsconfig.test.json` and 0 under `tsconfig.json`, so it is a real assertion under the `type-check` script. - **Restore:** proven by both blob hashes equal to HEAD, `git diff HEAD` empty and `git status` clean. ## Texts - **Family-D test header.** The `InputSchema` bullet now records the plain-`Omit` hold-out ground in the past tense, the E3 slice's `OmitDeclared` repair, and this slice's narrowing, pinned in the new file. The `DetailViewSchema` bullet records that the E3 slice attributed the `any` hop directly and narrowed it there. The two `ui:calendar` remarks name `OmitDeclared` for the TS face, `.extend()` for the mirror, and say which file pins which half. The third `ui:calendar` remark (the old TRIPWIRE note) was already in the past tense and is unchanged. - **`PartialSchema`'s docblock.** The triage's reading is kept as the triage's. The re-spelling claim is marked FALSE, with `OmitDeclared` as the counter-example. The docblock now says that re-spelling the alias would narrow a published type, a contract change not made in a comment correction, and that objectui#6397 is closed. The alias itself is unchanged. - **Bounded in-place fix: `partial-schema-collapse-pin.test.ts` header** (comment only; its assertions are about the alias as written and do not move). All four conditions hold: 1. It is the same defect as the carried item: the same sentence, one hop away. The corrected docblock points readers at this file. 2. The fix is mechanical, and its shape is pinned by the carried correction. 3. No open PR touches the file. 4. Same gate family: types `tsc` and vitest, no new verification surface. **Evidence, a one-shot probe against the built d.ts (not shipped):** the exact spelling the header named as collapsing, `{ type } & { [K in keyof T as K extends 'type' ? never : K]?: T[K] }`, declares 62 properties at `ObjectGridSchema` and 28 at `ButtonSchema`, where the shipped `PartialSchema` declares 1 at each. It refuses `label: 42` on `ButtonSchema` and `objectName: 42` on `ObjectGridSchema` (both `@ts-expect-error` used; tsc exit 0). ## Gates at HEAD `72dba894a` (round 1; round 2's are above) (exit codes captured by redirect-then-capture; heavy runs through the shared verify lock) | gate | result | |---|---| | `@object-ui/types` build (tsc + vite + dist completeness) | exit 0, 134 emitted files verified | | `@object-ui/types` `type-check` (`tsc --noEmit` + examples + `tsconfig.test.json`) | exit 0 | | `pnpm exec vitest run packages/types/` (parity, the family-D / E3 / input pins, the 8072 and 8762 shorthand pins) | 265 files / 5873 tests, exit 0 | | `pnpm exec vitest run scripts/` (every `scripts/__tests__` gate suite, a superset of those whose corpus holds a touched file) | 177 passed + 2 skipped of 179 files / 5322 tests, exit 0 | | downstream consumer type-check: the packages naming `InputSchema` (`components`, `core`) plus those authoring `input` nodes or configs (`cli`, `plugin-dashboard`, `plugin-designer`) and `fields`, `react`, `types`, against the rebuilt `dist` | 8 × `type-check: Done`, exit 0 | | `@object-ui/types` lint (`eslint .`, JSON) | 338 files, 0 errors; the 9 `form.ts` warnings sit outside every added hunk | | `check:handler-key-reads` | exit 0; 128 arms, 50 / 50 reads judged, 0 unjudged | | `check:control-bytes` · `check:new-line-citations` (0 new) · `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` | exit 0 each | | `check:spec-symbols` · `check:component-surface-parity` · `check:readme-exports` · `check:registry-bare-names` · `check:prompt-keys` · `check:doc-types` · `check:test-path-roots` · `check:esm-specifiers` · `check:vi-mock-*` · `type-check:coverage` | exit 0 each | | `check:doc-snippets` (678 / 678) · `check:doc-examples` · `check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` · `check:sdui-registration-pins` · `pnpm check` (CLI self-check) | exit 0 each | | governed guard `--test` over the 7 paths | NOT GOVERNED (lit control `AGENTS.md`: exit 3) | - **The gate list was derived by hand.** objectstack's `dispatch-gates.mjs` refuses an objectui answer by design (exit 2), so the list comes from objectui's `package.json` and `.github/workflows/`. - `check:changeset-claims` is report-only. It names 18 pending changesets that mention a touched file; each was read, and none is falsified as a record of its own change. The two that carry a claim this slice bears on are in the Acceptance notes. - **NOT MEASURED, left to CI:** the 8-way `pnpm test` shards beyond `packages/types` and `scripts/`, `test:dist`, and E2E. `check:sdui-registration-pins` read the console built at base; this diff touches no registration and no `sideEffects` array. ## Round 2: four prose edits, head `6eb3397b4` These come from contract review `5865087439` ③. The seat took them into this PR because each ships verbatim into the CHANGELOG. They are prose only: no code, no test logic, and every frontmatter block is byte-identical (the `name: bump` lines at base and head compare equal). 1. **`.changeset/9256-input-content-channels.md`, and the Clause-② sentence above.** - Old: "an authored child list on it rendered nothing: no error, no warning, no element." - New: "an authored child list on it rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it." - Measured with a one-shot probe, not committed. It built the manifest from the live registry the way `container-declaration-ratchet.test.tsx` does, then ran `validateTree` and `compile`. - `input`, `ui:input`, `email` and `password` carrying `children`: each draws exactly `not-a-container`. - The same four without `children`: none. - `div` carrying `children` (control): none. - The JSX `input` tag with a child: `not-a-container`, severity `warning`. 2. **`.changeset/6397-partial-schema-collapse-pin.md`.** A dated note follows the "Why a pin and not a repair or a retirement" paragraph: a generic re-spelling of this `Omit` does not collapse, objectui#9256. - Spelling: `OmitDeclared` is spelled exactly as quoted, in `packages/types/src/form.ts`. - Counts: the re-spelled alias declares 28 properties at `ButtonSchema` and 62 at `ObjectGridSchema` on this head and on `main` `733fd5ac6`. The shipped alias declares 1 at each. - "Not true when written": at that entry's own commit (`c8ea8af9c`), under the same TypeScript 6.0.3 its lockfile pins, the same spelling declares 27 and 61. Those are the source counts in that entry's own table. Both re-spellings refuse `label: 42` (the `@ts-expect-error` is used), and a lit control on the shipped alias admits it. - "Later in this same release": the E3 entry and the 6397 entry are both pending on `main` (`git cat-file -e` exit 0 for each; control on an absent path: exit 128). 3. **`.changeset/9256-content-channel-family-d.md`.** A dated note follows the AMENDED paragraph, in that file's own convention, naming the hold-outs this card later narrowed in this release. - Six of the nine names, in `nine-holdouts-six-narrow.md`. - `list` and `timeline`, in `9256-list-timeline-content-channels.md`. This is one more than the reviewer's draft, measured. - `DetailViewSchema` and the shorthand faces, in the E3 entry. - `InputSchema`, in this PR. - A compiler-API probe over the source reads `body` / `children` as `undefined` with their own `?: never` members on `TextSchema`, `ImageSchema`, `IconSchema`, `TabsSchema`, `AccordionSchema`, `CalendarSchema`, `ListSchema`, `TimelineSchema`, `DetailViewSchema`, `InputShorthandSchema` and `InputSchema`. The zod faces are pinned in the family-D, E3 and input pin files. - `ButtonSchema` and `AppComponentSchema` still resolve `children` to `SchemaNode | SchemaNode[]` (`ButtonSchema`'s zod mirror declares its own `children` union; `AppComponentSchema`'s extends `BaseSchema`, which declares one). That is the note's "still accept `children`" sentence. 4. **`.changeset/8499-node-slot-registered-arms.md`.** "`inputType` is deliberately NOT declared on this arm" is false on `main`. - `form.zod.ts` declares `inputType: retirementTombstone(SHORTHAND_INPUT_TYPE_REFUSAL)` on `InputShorthandSchema`, and `form.ts` declares `inputType?: never`. Both files are identical on `main` and at the base. - A runtime probe: `inputType` is a member of the mirror shape. `{ type: 'password', inputType: 'text' }` is refused with `invalid_type` at path `inputType`, the message opening with the objectui#8762 guidance. Bare `{ type: 'password' }` (control) parses. - objectui#8762's entry landed a day after 8499's (2026-09-10 against 2026-09-09), and both are pending on `main`. - A dated note in the file's own form follows the `InputShorthandSchema` bullet, the same place its objectui#10756 note sits under the `HtmlElementSchema` bullet. | gate at `6eb3397b4` | result | |---|---| | `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s) added, 3 modified, 0 deleted". Each of the three modified entries prints the same declaration at base and now (`@object-ui/types: patch`, `minor`, `minor`), so no declaration is lost: its case 2, a correction on purpose. | | `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:control-bytes` · `check:new-line-citations` | exit 0 each | | the 24 `scripts/__tests__` suites that name `.changeset` (the changeset gates' own suites, the polarity census, control bytes, pending literals, docs and workflow suites) | 24 files / 913 tests, exit 0 | ## Round 3: two more prose edits, head `876f6d7ea` These come from delta review `5865466392` ③ flags 2 and 3. They are prose only, and both frontmatter blocks are byte-identical. 1. **`.changeset/9256-content-channel-family-d.md`: a second dated note, directly after the AMENDED paragraph** and before round 2's note. The headline is "the two twins no longer take `body` — objectui#6771". - Old, still in the AMENDED paragraph: "The two twins are unchanged and go on inheriting `body` as the content slot". - New note: objectui#6771 retired `body` on `BaseSchema` itself, so both twins refuse `body` on both faces. The TypeScript face does it through `BaseSchema`'s `body?: never`; each mirror does it by name, pointing at `requestBody`. - Measured, TypeScript: a compiler-API probe over the source gives `BaseSchema` its own `body?: never`. `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` resolve `body` to `undefined` and declare only `children?: never` themselves. - Measured, zod, with a one-shot runtime probe (deleted): each twin's mirror refuses `body` with `invalid_type` at path `body`, and the message names `requestBody`. `requestBody` parses (control). - Provenance, from `git log -S`: `BaseSchema`'s `body?: never` and both twins' mirror tombstones were introduced by `2acd8e109` (objectui#6771, 2026-09-19). The AMENDED paragraph was written on 2026-09-17 (`c42554e94`), and `6771-retire-body-child-list-dialect.md` is pending on `main`. So "later in this same release" holds. - The reviewer's draft also cited objectui#9659. That card did not introduce the twin refusals, so the note cites objectui#6771 alone. - Round 2's note opens "most of the hold-outs above have since been narrowed". It is still true beside the new one: it names what this card narrowed, and it ends with `button` and `AppComponentSchema`, which still accept `children`. 2. **`.changeset/9256-e3-residual-content-channels.md`, in place.** - Old: "an authored child list on them rendered nothing: no error, no warning, no element." - New: "an authored child list on them rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it." - Measured for all twelve keys, not just the two in round 2. A one-shot runtime probe (deleted) loaded every plugin registration (`plugin-grid`, `-form`, `-kanban`, `-map`, `-tree`, `-view`, `-gantt`, `-calendar`, `-charts`, `-detail`, and `components`). It built the manifest from the live registry and ran `validateTree`. - Each of the twelve is known to the registry, and none declares a `children` input. - Carrying `children`, each draws `not-a-container` (severity `warning`). Without it, none does. `div` carrying `children` (control) draws nothing. - `object-chart`, `email` and `password` also draw one `unknown-prop` in both legs, from the probe's own `objectName` fixture key. It is not part of the difference. | gate at `876f6d7ea` | result | |---|---| | `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s) added, 4 modified, 0 deleted". Each modified entry prints the same declaration at base and now, so no declaration is lost. | | `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:control-bytes` · `check:new-line-citations` | exit 0 each | | the 24 `scripts/__tests__` suites that name `.changeset` | 24 files / 913 tests, exit 0 | ## Serial constraints - **Re-mapped at branch time (`244d516df`), before each round's push, and again before round 3's edits.** At round 3 the open PRs are objectui#10921, objectstack-ai#10915, objectstack-ai#10914, objectstack-ai#10912, objectstack-ai#10911, objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10777 and objectstack-ai#10278. objectstack-ai#10904, objectstack-ai#10902 and objectstack-ai#10891 have merged since the first map. - None touches any of the five changesets this PR adds or edits. - One touches a source path here: PR objectui#10914 edits `packages/types/src/zod/form.zod.ts`, in two comment / `.describe()` hunks on the `wrapperClass` key (in `InputSchema` and in the `InputShorthandSchema` restatement note). Neither overlaps or touches this PR's hunks, and `git merge-tree` of this head (`876f6d7ea`) against objectstack-ai#10914's head is clean. - PR objectui#10908 edits `zod-mirror-parity.test.ts` and `zod/index.zod.ts`, neither of which this PR touches. - **The release PR objectui#5400** (1732 files, enumerated to the end, last updated 2026-09-18) touches none of the source paths. As release consumption it deletes pending changesets, three of the four edited here among them (not the E3 entry); that is not a hunk overlap. - **`origin/main` moved four commits since the base** (`9f0c84a44`, `7ea8118f7`, `8522396c0`, `733fd5ac6`). None touches `packages/types` or the changesets edited here, and `git merge-tree` of this head (`876f6d7ea`) against `733fd5ac6` is clean. `main` has not moved since. ## Acceptance notes: out of scope, not fixed here - **The `PartialSchema` docblock and its pin file still sequence the alias's repair on objectui#5155.** That card is closed as a duplicate of objectui#5250, whose ruling keeps the tolerant face on renderer props. objectui#8347 (open) is the card on removing the index signature from the authoring face. "Once objectui#5155 removes the root index signature" therefore names a card that will not do it. Noted, not edited: rewriting the sequencing is a judgement about objectui#5250 / objectstack-ai#8347, not a comment correction. Carrier: none. - **The same "no error, no warning, no element" sentence lives in published refusal messages and docblocks,** not only in changesets. Examples: the family-D `retirementTombstone` strings on the zod mirrors (the chatbot twins' included) and the matching TypeScript docblocks. Wherever the node's registration declares no `children` input, the parser tier's `not-a-container` warning did fire. Correcting those strings is a published-message change, not prose, and it is not this PR's. Noted, not edited. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…he invalidation bus instead of remounting the page (objectui#10519) (objectstack-ai#10777) Fixes objectstack-ai#10519 Clause-②: no Dispatched by the `domain:ui` seat 2 PM, session `session_014mXUNuFomfj24w7s1pZzhN` (`mode:subagent`, so that id is also this run's: `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`), from claim comment 5855640194 on objectui#10519, executing the unblock 5854561985 and seat decision B 5830213285. Branch `claude/issue-10519-pageview-refresh-in-place`, cut from `origin/main` `b5b6f37531` (BASE); round-1 head `4620ba39d2`. Round 2 (after the contract review 5856227410, FAIL; the ruling 5856239953, readers first; the `Blocked-by` amendment 5857953021) merged `origin/main` `195052fff6` as `25c9aa6d3a` and re-ran the census there. It stopped at the census: see **H1, round 2**. Round 3 (after the seat ruling 5860118207, readers first, and objectui#10853 landing through PR objectui#10873 as `de1b879a6`) merged `origin/main` `1c5ee334a2` as `c5b63fb514`, re-ran the census there with every row green (**H1, round 3**), and carried review items 2 to 4 and the ratchet titles; its head was `42226f9ed1`. Round 4 (after the contract review 5862674975, FAIL; the ruling 5862691924, readers first; objectui#10887 landing through PR objectui#10898 as `9f0c84a44` and PR objectui#10973 as `deca847a8`; seat 2's release 5876662981; and the `domain:ui` seat 1 claim 5877617401, session `session_01DuWo5bdP9SdVebamn99GGk`, `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`) merged `origin/main` `f6fb83f0c5` as `e021bfaf24`, measured the rows that ruling names on the merged head through a real page action, every one green (**H1, round 4**), pinned four of them in this PR's pin, and carried the changeset and body items. Head `fecce55c33`. ## What changed - `packages/app-shell/src/views/PageView.tsx`: the `refreshKey` counter is gone, together with the `key` on both render branches (`InterfaceListPage`, ADR-0047 interface mode, and `SchemaRenderer`, the page). The console action runtime's `onRefresh` now points at a module-level `declarePageDataChanged`, which is `notifyDataChanged({ objectName: '*' })` from `@object-ui/react`: a page binds no object and the runtime's refresh carries none, so the bus's documented unknown-scope value is the page's scope. The node `context` handed to `SchemaRenderer` is `{ params }` alone. `useState` is no longer imported. No prop, export or schema key moves. - `packages/app-shell/src/views/__tests__/PageView.context-9673.test.tsx`: the expected node context is `{ params }` (it was `params` plus `refreshKey: 0`); the header sentence says why. - `packages/app-shell/src/no-refresh-key-remount.ratchet.test.ts`: the header no longer calls `PageView.onRefresh` a user "Refresh this page" affordance. By a source reading of `useConsoleActionRuntime`, `onRefresh` is fed only by the runtime's post-action refresh (`api`, flow and server-action success, undo, screen-flow completion), and `PageView` has no refresh control. `packages/app-shell/src/views/PageView` joins `IN_SCOPE`. Every other assertion is kept. Its two `it` titles now name the `PageView` page host beside the record-detail surfaces (round 3, titles only). - New pin `packages/app-shell/src/views/__tests__/PageView.refreshInPlace-10519.test.tsx` (below). Round 4 adds a describe block for the objectui#10887 readers (**H1, round 4**) and a second object (`account`) to its fixture; the round-1 cases are unchanged. - `.changeset/10519-pageview-refresh-in-place.md`: `'@object-ui/app-shell': patch`. Round 3: the refresh follows a page-level action "that asks for a refresh"; `PageView`, not the runtime, declares the change; the block list is exactly the set the round-3 census measured green. Round 4: the list adds a `dashboard`'s `optionsFrom` filter options and an `object-view` drawn as a kanban, calendar, gallery or timeline (the rows **H1, round 4** measured green), the dashboard filter's kept value is said beside the form's, and a sentence says what a page action now does to a `kind: 'react'` page: its own read re-runs in place when its effect names the injected `useDataInvalidation` nonce, and a read keyed on `useAdapter` alone is not re-run. - `.changeset/9673-pageview-drop-context-spread.md` (pending, not in the app-shell `CHANGELOG`): "The node's `context` is now exactly `{ params, refreshKey }`, built from the route and the refresh counter, and the `as any` cast at that spot is gone." becomes "The node's `context` is now built from the route alone, and the `as any` cast at that spot is gone." A plain replacement, not a dated note: the edit can only reach a release together with this PR's code (a release that consumed the entry first would ship the old sentence, true for that release, and turn this edit into a modify/delete conflict), so the replacement is the entry's one release-scoped reading. Frontmatter byte-identical (md5 of lines 1 to 3 `4e8ce55021fe3b14329c8b4cb3f9f354` before and after); `check-changeset-overwrite` reads `@object-ui/app-shell: patch` at base and now. Round 4 rewraps the replaced sentence to the file's width: the words are unchanged (the whitespace-normalised text is identical) and the frontmatter md5 is the same. - `.changeset/10887-react-page-data-invalidation.md` (pending, from PR objectui#10973, not in the components `CHANGELOG`): "(as `PageView` does after a page action)" becomes "(as `PageView` did after a page action)". This branch stops that remount, and the two entries ship in the same release. A plain replacement, on the `9673` entry's argument. Frontmatter byte-identical (md5 of lines 1 to 3 `3d9e2a1e59bc11924bb99ffd9eea81e3` before and after); `check-changeset-overwrite` reads `@object-ui/components: minor` at base and now. This file is outside the claim's listed surface; see Acceptance notes. ## H1 — premise: the census rows re-read in place on `origin/main` Runtime census on BASE `b5b6f37531`, through a scratch probe (deleted, never committed): the real `SchemaRenderer`, each plugin's real registration, one counting fake data source per row; reads on mount, then `notifyDataChanged({ objectName: '*' })`, then reads again. A bare `useDataInvalidation` reader beside each block is the positive control, and its nonce moved 0 → 1 in every row. The probe lived under `app-shell` because the root vitest alias map resolves every `@object-ui/plugin-*` specifier to source there; committed, it would trip `check:phantom-deps` (the objectui#10572 PR made the same call). | block | reads on mount → after the bus event | re-reads in place | |:--|:--|:--| | `object-grid` | 1 → 2 | yes | | `object-chart` (aggregate) | 1 → 2 | yes | | `detail-view` (findOne) | 1 → 2 | yes | | `list-view` | 1 → 2 | yes | | `object-kanban` | 1 → 2 | yes | | `object-metric` (aggregate) | 1 → 2 | yes | | `object-form`, default arm (findOne) | 1 → 2 | yes | | `object-form` `formType: drawer` | 1 → 2 | yes | | `object-form` `formType: modal` | 1 → 2 | yes | | `object-form` `formType: split` | 1 → 2 | yes | | `object-form` `formType: tabbed` | 1 → 2 | yes | | `object-form` `formType: wizard` | 1 → 2 | yes | | `object-calendar` | 1 → 2 | yes | | `object-gallery` | 1 → 2 | yes | | `object-timeline` | 1 → 2 | yes | | `object-gantt` | 1 → 2 | yes | | `element:number` (aggregate) | 1 → 2 | yes | | `element:repeater` | 1 → 2 | yes | | `object-map` | not probed here: its committed pin `ObjectMap.invalidationRefetch-10623` mocks `react-map-gl/maplibre`, which does not resolve from app-shell; that pin ran green on BASE in the same lock run (3 cases) | yes | | `list` (bare key) | n/a | not a data block: `@object-ui/components` owns the bare key as the display list, and plugin-list registers `ListViewRenderer` as `view:list` with `skipFallback: true` so that it never takes that key; `view:list` is the `list-view` row's renderer. | The studio palette's data category (`BLOCK_TYPE_META`) is `grid`, `object-grid`, `object-form`, `object-metric`, `object-kanban`; every object-bound entry is in the table. Every block in this table, the six `object-form` arms included, re-reads exactly once after the bus event on BASE. What this PR claims covers the census rows only, not every block a stored page can hold: this table; the rows of the round-2 table (`object-tree`, `object-pivot`, `object-data-table`, `report` / `spec-report` over a dataset, `record:line_items` with an authored parent, a `dashboard` dataset table widget), whose readers landed with objectui#10778 and objectui#10814; and the rows round 3 adds, `object-master-detail-form`'s edit-mode detail lines and `element:record_picker`'s options, whose readers landed with objectui#10853. On the merged head `c5b63fb514` every one of them re-reads once in place with no remount (**H1, round 3**). Round 4 adds the rows the ruling 5862691924 names, whose readers landed with objectui#10887: an `object-view` drawn as a kanban, calendar, gallery or timeline, a `dashboard`'s `optionsFrom` filter options, and a `kind: 'react'` page whose effect names the injected `useDataInvalidation` nonce. Each re-reads once in place after a real page action on the merged head (**H1, round 4**). ## H1, round 2 — census on the merged head `25c9aa6d3a` The round-1 probe, re-run unchanged on its 18 rows, plus the rows the ruling names and one for the `dashboard` block, whose dataset widgets read on a page since objectui#10815. Scratch probes again, never committed, archived in the seat scratchpad (`issue-10519/census-10519-r2.probe.test.tsx.archived`, md5 `ba70dc05bf7a266d6a7535c9c7990b2e`; `issue-10519/census-10519-r2-md.probe.test.tsx.archived`, md5 `2d830df32bf6975b2be2edc5cb2050ef`). New this round, a remount witness: the row's own registry entry (`ComponentRegistry.get`, which `SchemaRenderer` resolves the node through) is wrapped in a counter whose dependency-free mount effect runs once per instance and whose cleanup runs once per unmount. Witness control: `object-metric` under a host re-keyed on the bus nonce, the shape of the removed `key={refreshKey}`, reads `1 → 2` like an in-place re-read, and the witness reads mounts `1 → 2`, unmounts `+1`. So the read count alone cannot tell the two apart, and the witness can. Run under the verify lock, `Tests 29 passed (29)`, `VERDICT command-exit 0` (26 probe cases, the three `ObjectMap.invalidationRefetch-10623` cases). | block | reads on mount → after the bus event | mounts at the event → after · unmounts | re-reads in place, no remount | |:--|:--|:--|:--| | the 18 round-1 rows (`object-grid`, `object-chart`, `detail-view`, `list-view`, `object-kanban`, `object-metric`, `object-form` default / drawer / modal / split / tabbed / wizard, `object-calendar`, `object-gallery`, `object-timeline`, `object-gantt`, `element:number`, `element:repeater`) | 1 → 2 each | 1 → 1 · +0 each | yes | | `object-tree` | 1 → 2 | 1 → 1 · +0 | yes | | `object-pivot` | 1 → 2 | 1 → 1 · +0 | yes | | `object-data-table` | 1 → 2 | 1 → 1 · +0 | yes | | `report` over a dataset (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `spec-report` over a dataset (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `record:line_items` with an authored `parentId` | 1 → 2 | 1 → 1 · +0 | yes | | `dashboard`, a dataset table widget (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `object-map` | its committed pin `ObjectMap.invalidationRefetch-10623`, same run: one re-read after `'*'`, the map node unchanged (3 cases) | n/a | yes | | witness control: `object-metric` under a host re-keyed on the nonce | 1 → 2 | 1 → 2 · +1 | by remount only | **Two blocks a stored page can hold do not re-read. This is the stop condition 5854561985, and round 2 stopped here.** | block, as a stored page holds it | reads on mount → after the bus event | mounts · unmounts | re-reads in place | |:--|:--|:--|:--| | `object-master-detail-form`, `mode: 'edit'`, authored `recordId`: the header (`findOne` of the parent) | 1 → 2 | 1 → 1 · +0 | yes | | the same block: its detail lines (`find` of the child object) | 2 → 2 | 1 → 1 · +0 | **no** | | the same block under a host re-keyed on the nonce: its detail lines | 2 → 4 | 1 → 2 | by remount only | | `element:record_picker`: its options (`find` of its object) | 1 → 1 | 1 → 1 · +0 | **no** | - **A stored page can hold both.** Both are `ComponentPropsMap` keys in `@objectstack/spec` 17.4.0, the installed version. `PageSchema.safeParse` accepts a page holding `object-master-detail-form` with `mode: 'edit'`, `recordId` and `details`, and `ComponentPropsMap['object-master-detail-form']` parses those properties. objectui registers both: `object-master-detail-form` in `plugin-form`, which the console's `register-plugins.ts` imports eagerly, and `record_picker` in `components`. Both read through the page's adapter (`SchemaRendererContext`, `useAdapter()`). - **Source reading (context, not the measurement).** `MasterDetailForm`'s edit-mode children effect keys on `isEdit`, `dataSource`, `schema.recordId` and `resolvedEntries`. The picker's option effect keys on `adapter`, `object`, `filterKey`, `sortKey` and `limit`. Neither names a bus nonce, so after a page action nothing re-runs them, and before this PR the remount did. - **Recorded, not gating.** `object-form` with `subforms` routes to the same `MasterDetailForm` and measured the same (lines 2 → 2). But `ComponentPropsMap['object-form']` refuses `subforms` by name (`unrecognized_keys`), so a spec-valid page does not hold that route. - **Resolved in round 3.** objectui#10853 (PR objectui#10873, `de1b879a6`) gave both blocks a bus reader, and round 3 measures them green below. Review items 2 to 4 and the ratchet `it` titles are on the branch. ## H1, round 3 — census on the merged head `c5b63fb514` The round-2 probe, re-run unchanged on its rows, plus `element:record_picker`'s options in the same harness and a new section for `object-master-detail-form` in edit mode with an authored `recordId` (the `po` / `po_line` shape objectui#10853's own pin uses, with the real line grid). Real `SchemaRenderer`, real registrations, one counting fake data source per row, one `notifyDataChanged({ objectName: '*' })`, the same remount witness and its control. Scratch probe, never committed, archived in the seat scratchpad as `issue-10519/census-10519-r3.probe.test.tsx.archived` (md5 `b1d116c257b12b3e35928dc35ddb113b`), rows in `issue-10519/census-rows-r3-head.txt`. Run under the verify lock with `ObjectMap.invalidationRefetch-10623`: `Test Files 2 passed (2)`, `Tests 33 passed (33)`, `VERDICT command-exit 0` (30 probe cases, 3 map cases). The control nonce read 1 in every row. | block | reads on mount → after the bus event | mounts at the event → after · unmounts | re-reads in place, no remount | |:--|:--|:--|:--| | `object-grid` | 1 → 2 | 1 → 1 · +0 | yes | | `object-chart` (aggregate) | 1 → 2 | 1 → 1 · +0 | yes | | `detail-view` (findOne) | 1 → 2 | 1 → 1 · +0 | yes | | `list-view` | 1 → 2 | 1 → 1 · +0 | yes | | `object-kanban` | 1 → 2 | 1 → 1 · +0 | yes | | `object-metric` (aggregate) | 1 → 2 | 1 → 1 · +0 | yes | | `object-form`, default arm (findOne) | 1 → 2 | 1 → 1 · +0 | yes | | `object-form` `formType: drawer` | 1 → 2 | 1 → 1 · +0 | yes | | `object-form` `formType: modal` | 1 → 2 | 1 → 1 · +0 | yes | | `object-form` `formType: split` | 1 → 2 | 1 → 1 · +0 | yes | | `object-form` `formType: tabbed` | 1 → 2 | 1 → 1 · +0 | yes | | `object-form` `formType: wizard` | 1 → 2 | 1 → 1 · +0 | yes | | `object-calendar` | 1 → 2 | 1 → 1 · +0 | yes | | `object-gallery` | 1 → 2 | 1 → 1 · +0 | yes | | `object-timeline` | 1 → 2 | 1 → 1 · +0 | yes | | `object-gantt` | 1 → 2 | 1 → 1 · +0 | yes | | `element:number` (aggregate) | 1 → 2 | 1 → 1 · +0 | yes | | `element:repeater` | 1 → 2 | 1 → 1 · +0 | yes | | `object-tree` | 1 → 2 | 1 → 1 · +0 | yes | | `object-pivot` | 1 → 2 | 1 → 1 · +0 | yes | | `object-data-table` | 1 → 2 | 1 → 1 · +0 | yes | | `report` over a dataset (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `spec-report` over a dataset (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `record:line_items` with an authored `parentId` | 1 → 2 | 1 → 1 · +0 | yes | | `dashboard`, a dataset table widget (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | yes | | `element:record_picker`: its options (`find` of its object) | 1 → 2 | 1 → 1 · +0 | yes | | `object-master-detail-form`, `mode: 'edit'`, authored `recordId`: its detail lines (`find` of the child object) | 2 → 3 | 1 → 1 · +0; the first line input is the same DOM node | yes | | the same block: its header (`findOne` of the parent) | 1 → 2 | the block node of the row above | yes | | `object-map` | its committed pin `ObjectMap.invalidationRefetch-10623`, same run: one re-read after `'*'`, the map node unchanged (3 cases) | n/a | yes | | witness control: `object-metric` under a host re-keyed on the nonce | 1 → 2 | 1 → 2 · +1 | by remount only | The detail lines read twice on mount, as objectui#10853 measured at its BASE (the load effect runs again once the resolved details are set); the bus event adds exactly one read. **Unsaved lines are held, then re-read once after a revert or a save** (objectui#10853's rule, measured through the same block, one bus event each): | case | line reads at the event → while dirty → after | mounts · unmounts | lines on screen while held | |:--|:--|:--|:--| | type into the first line, `'*'`, then type the stored value back (revert) | 2 → 2 → 3 | 1 → 1 · +0 | `["first edited"]` | | type into the first line, `'*'`, then the form's save (`batchTransaction` once) | 2 → 2 → 3 | 1 → 1 · +0 | n/a | No block a stored page can hold, among the census rows, still refreshes only through the remount. The stop condition 5854561985 does not hold this round. ## H1, round 4 — census on the merged head, through a real page action The ruling 5862691924 names three rows: `object-view` (kanban, plus at least one of calendar, gallery, timeline, map or tree), a `dashboard` with an `optionsFrom` filter, and a `kind: 'react'` page, since objectui#10887 member 3 injected `useDataInvalidation`. This round measures them through the real `PageView` and one real page action each, not through a bare bus event. Each row is a stored page in the spec shape (`regions`, blocks as `{ type, properties }`) holding the page's `api` action (`target: /api/v1/environments`, run through the console runtime's `apiHandler`, so no `onMutation` fires), a stand-in with an instance id, and the block. The probe counts the block's reads of the object it queries on a counting fake data source. It wraps the block's own registry entry (`ComponentRegistry.get`, which `SchemaRenderer` resolves the node through) in a mount and unmount witness, and it reads state a remount resets. Scratch probe, never committed: `census-10519-r4.probe.test.tsx`, archived in the round-4 scratchpad (md5 `af5ede3a47187d8a201e3df4548b6499`). At head `fecce55c33`, under the verify lock: `Tests 8 passed (8)`, `VERDICT command-exit 0`. The probe writes each row to a file; it asserts nothing about the counts. | row | reads of its object, at the action → after | witness: mounts · unmounts | page stand-in | state a remount resets | pinned in this PR | |:--|:--|:--|:--|:--|:--| | `object-view`, `defaultViewType: 'kanban'` | 1 → 2 | 1 → 1 · +0 | 1 → 1 | the `Deal 1` card is the same node | yes | | `object-view`, calendar (a named view with `calendar.startDateField`) | 1 → 2 | 1 → 1 · +0 | 1 → 1 | the navigated month (October 2026) is kept | yes | | `object-view`, `defaultViewType: 'gallery'` | 1 → 2 | 1 → 1 · +0 | 1 → 1 | the `Deal 1` card is the same node | yes | | `object-view`, timeline (a named view with `timeline.startDateField`) | 1 → 2 | 1 → 1 · +0 | 1 → 1 | the `Deal 1` item is the same node | no, probe only | | `dashboard`, a `globalFilters` select with `optionsFrom` (the `find` fallback read) | 1 → 2 | 1 → 1 · +0 | 1 → 1 | the selected `retail` is kept; the trigger is the same node | yes | | the same filter, the dataset read (`queryDataset`) | 1 → 2 | 1 → 1 · +0 | 1 → 1 | as above | no, probe only | | `kind: 'react'` page whose effect names `useDataInvalidation('project')`; the action is a `Block` of type `action:button` inside the page | 1 → 2 | the page's `app` entry 1 → 1 · +0; the author's own mount effect 1 → 1 | n/a (the page is the block) | the counter clicked before the action still reads 1; the button is the same node | no, probe only | | control: the same react page, its read keyed on `[adapter]` alone | 1 → 1 | 1 → 1 · +0; author mounts 1 → 1 | n/a | the counter is kept | no | - **No row re-reads only through the remount.** No new remount-only reader was found, so "readers first" does not stop this round. - **The remount key.** `PageView` carries no `key` on either branch (the `no-refresh-key-remount` ratchet holds the file), and the witness saw no remount at or below each block's registry entry. - **The calendar row.** Its region node is not the same node after the action. While the view re-reads, `ObjectView` hands the calendar `loading`, and `ObjectCalendar` shows its loading placeholder in place of the grid, as PR objectui#10973's note (c) records. `ObjectCalendar` itself is not remounted: the witness reads 1 → 1, and the month it holds in its own state is kept. In the legs where nothing re-reads (A2, R3 below), the region node is the same, so the swap follows the re-read. - **What is pinned.** The round-4 describe block of `PageView.refreshInPlace-10519` holds four rows: the kanban, gallery and calendar `object-view`s, and the dashboard filter over the `find` read. Each asserts both halves: (a) the stand-in's instance id, plus the card node, the navigated month, or the filter's trigger node and selected value; (b) exactly one re-read of the block's object. Timeline, the dataset read and the react page stay probe-only. `@object-ui/app-shell` declares neither `@object-ui/plugin-timeline` nor `@object-ui/react-runtime`, and neither does the workspace root, which is what `check:phantom-deps` asks of a test import. A react page also loads its runtime with a dynamic `import()`, which a committed test would race inside a bounded wait (AGENTS.md, flaky-test discipline). Their readers' own pins fail on the round-3 code path: `ObjectView.busReread-10887` covers the timeline, `DashboardFilterBar.busReread-10887` covers both reads, and `react-page-invalidation` covers the react page scope (its base leg is a `ReferenceError`, per PR objectui#10973). Legs, each on committed state (`beac974876`; the commit after it changes changesets only), with the probe's first seven cases (the adapter-only control came after) and the pin. `PageView.tsx` and the three readers resolve to `src` (the relative import, and the root vitest alias for `@object-ui/plugin-view`, `@object-ui/plugin-dashboard` and `@object-ui/components`), so no `dist/` preflight applies. Every restore is by blob hash against `HEAD`, with `git diff HEAD` empty. | leg | mutation (on-disk proof) | probe | pin | |:--|:--|:--|:--| | A1, the pre-fix host | `PageView.tsx` = `main`'s blob `1ace6962da` (HEAD blob `979bb11eb7`; two `key={refreshKey}` sites) | every row: mounts 1 → 2, unmounts +1, stand-in 1 → 2, the node replaced; the month reset to September 2026, the filter back to `Industry: All`, the react counter back to 0 and the author's mount effect 1 → 2 | `Tests 6 failed \| 8 passed (14)` over probe and pin: all six in-place cases red on (a); the lit control green | | A2, the host declares nothing | `ablation-replace`: anchor `notifyDataChanged({ objectName: '*' });` 1 → 0, blob `979bb11eb7` → `7bb3888fd2` | every row: reads 1 → 1, no remount | `Tests 6 failed \| 8 passed (14)`: the same six red on (b) (metric, list, kanban, gallery, calendar, dashboard); the lit control green | | R3, `ObjectView` on the round-3 path | `ablation-replace`: the `invalidationNonce` entry deleted from the non-grid fetch's dependency list (1 → 0), blob `db64424727` → `90e198e990` | the four `object-view` rows: reads 1 → 1, no remount; the others unchanged | `Tests 3 failed \| 11 passed (14)`: kanban, gallery and calendar red on (b) | | R3, `SelectFilter` on the round-3 path | `ablation-replace`: `, dataSource, invalidationNonce]);` → `, dataSource]);` (1 → 0), blob `9d93a32b56` → `b636341b96` | both dashboard rows: reads 1 → 1, the value kept | `Tests 1 failed \| 13 passed (14)`: the dashboard case red on (b) | | R3, the react page scope on the round-3 path | `react-page.tsx` = its `42226f9ed1` blob `f70b8f4251` (HEAD blob `ab712dface`) | the react row fails at render: `ReferenceError: useDataInvalidation is not defined` in the page's error panel | not in the pin | | R3', the hook injected but never moving | `ablation-replace`: `useDataInvalidation,` in the scope becomes a function that always returns 0 (1 → 0), blob `ab712dface` → `ade9a50713` | the react row: reads 1 → 1, no remount | not in the pin | **The round 1 to 3 rows on this head.** They were not re-run through a census probe this round. Each block that sentence 5 names from those rounds has its own committed bus pin, and all of them ran green on `fecce55c33` in one lock hold: `Test Files 27 passed (27)`, `Tests 171 passed (171)`. The files are `ObjectGrid.invalidationRefetch-10035`, `ObjectChart.invalidationRefetch-10035`, `DetailView.invalidation`, `ListView.invalidationRefetch-10572`, `ObjectKanban.invalidationRefetch-10572`, `ObjectMetricWidget.invalidationRefetch-10572`, `ObjectCalendar.invalidationRefetch-10572`, `ObjectGallery.invalidationRefetch-10623`, `ObjectTimeline.invalidationRefetch-10623`, `ObjectGantt.invalidationRefetch-10035`, `ObjectMap.invalidationRefetch-10623`, `ObjectMap.busReread-10623`, `ObjectTree.invalidationRefetch-10778`, `ObjectPivotTable.invalidationRefetch-10778`, `ObjectDataTable.invalidationRefetch-10778`, `ObjectForm.invalidationRefetch-10572`, `formArmsInvalidation-10715`, `MasterDetailForm.busReread-10853`, `DatasetReportRenderer.invalidationRefetch-10814`, `DatasetWidget.invalidationRefetch-10815`, `LineItemsPanel.invalidationRefetch-10814`, `elementReaders.invalidationRefetch-10623`, `record-picker.busReread-10853`, `ObjectView.busReread-10887`, `DashboardFilterBar.busReread-10887`, `react-page-invalidation` and `drillDownReport.invalidationRefetch-10814`. ## H2 — the remount, reproduced on BASE The new pin run against BASE `PageView.tsx` (byte-identical to BASE by blob hash `9de300582c`) with the head pins kept, before any source edit: `Tests 2 failed | 1 passed (3)`. - SchemaRenderer branch, red on (a): `The page action REMOUNTED the page: the SchemaRenderer consumer mounted a second time` — the `object-gantt` stand-in's instance counter read 2 after one `api` action. - interface branch, red on (a): `The page action REMOUNTED the interface page` — the `interface-list-page` node after the action was not the node before it. - lit control green: a change declared on the bus directly reached the metric and the stand-in once each, with no action run. ## Pins and ablations `PageView.refreshInPlace-10519.test.tsx` renders `PageView` for real under `MemoryRouter`, `AdapterCtx` and `SchemaRendererProvider`, with the real console action runtime, the real `page`, `page:accordion` and `action:button` renderers, the real `object-metric` (plugin-dashboard) and `object-form` (plugin-form) blocks, and, on the interface branch, the real `InterfaceListPage` over plugin-list's `ListView`. The stand-in registered as `object-gantt` follows the `ObjectView.refreshInPlace-10035` precedent: it reads the real bus hook, counts its own queries, and carries an instance id and a scroll box. The page action is a real `api` action (`target: /api/v1/environments`) through the runtime's `apiHandler`, an authenticated raw-HTTP call that fires no dataSource `onMutation`. After one page action: - (a) the same tree: the stand-in mounted exactly once (instance id unchanged), its scroll box is the same node with `scrollTop` still 120, the metric node is the same, the form's `name` input is the same node and still reads the half-typed `User typed`, and the collapsed accordion panel still has `aria-expanded="false"`; on the interface branch the `interface-list-page` node and `ListView`'s own `refresh-button` node are unchanged. - (b) each bus reader re-read exactly once: the metric's `aggregate` delta is 1, the stand-in's query delta is 1, and `ListView`'s list query delta (its `$top: 0` count probe excluded) is 1. - lit control: `notifyDataChanged({ objectName: '*' })` with no action reaches the metric and the stand-in once each. Red on base, green on head, and two ablation legs, all on committed state with every restore spelled `git checkout HEAD -- ABSOLUTE_PATH` and proven by blob hash, `git diff HEAD` empty and `git status` clean: | leg | mutation (on-disk proof) | result over the pin, the ratchet and context-9673 | |:--|:--|:--| | A1, reverse run | `PageView.tsx` checked out at BASE (blob `9de300582c` == BASE blob; `key={refreshKey}` sites 2, `notifyDataChanged` sites 0), head pins kept | `Tests 5 failed | 2 passed (7)`: ratchet red (`expected [ …(2) ] to deeply equal []`, the two key sites), context-9673 red on both cases (`+ "refreshKey": 0`), the pin red on (a) on both branches; the ratchet's scan-path guard and the pin's lit control green. Restore: blob `8e96f4adbf` == HEAD blob, diff empty, status clean | | A2, the host declares nothing | through `ablation-replace.mjs`: anchor `notifyDataChanged({ objectName: '*' });` hit 1 → 0, replacement 0 → 1, blob `8e96f4adbf` → `83a296ea0a` | `Tests 2 failed | 5 passed (7)`: the pin red on (b) on both branches (`The metric did not re-read after the action`, `The list did not re-query after the action`); (a), the lit control, the ratchet and context-9673 green. Restore by the tool: blob == HEAD, `git diff HEAD` empty | Each leg fails on the half the defect predicts and nowhere else, so each pin fails for the reason it names. No `dist/` preflight applies: every test here imports `PageView` from `src`, and the root vitest alias map resolves every `@object-ui/*` specifier to source. ## Gates Round 1, at `4620ba39d2`: - Pins at head: the five `PageView` test files (`PageView.refreshInPlace-10519`, `no-refresh-key-remount.ratchet`, `PageView.context-9673`, `PageView.test`, `page-kind-writing-end-9718`): `Test Files 5 passed (5)`, `Tests 12 passed (12)`, `VERDICT command-exit 0`. - `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`, script name echoed), after `turbo run build` of the 28-task dependency closure (cache-restored, `dist/index.d.ts` present): `VERDICT command-exit 0`. - Lint, narrowed and measured: `eslint --format json` over the four touched files reads 4 files, 0 errors, 27 warnings, all `@typescript-eslint/no-explicit-any`, the warning-level rule the package already carries on every `as any` (the base `PageView.tsx` had them). Population: `eslint --format json .` in `packages/app-shell` (what CI's `turbo run lint` runs there) reads 1298 files, 0 errors, 3207 warnings. Invariance: `eslint.config.js` enables no type-aware linting (`projectService` / `parserOptions.project` absent), so this diff moves no untouched file's verdict. - `check-changeset-presence`: 4 source files of 1 released package changed, 1 changeset declared, exit 0. `check:new-line-citations`: 0 new citations. `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:control-bytes`, `check:phantom-deps`, `changeset:check` (fixed, no-major), `check:changeset-claims`, `check:pending-changeset-literals`, `check:shell-escape-residue`, `check:unreferenced-sources`: all exit 0 with their OK line. - Declared to CI: the full app-shell test suite and the repo-wide `pnpm test`; the scope named above is the union of every test file that imports `PageView` or that this diff edits. - Round 2, at `25c9aa6d3a`: the same five `PageView` pin files, `Test Files 5 passed (5)`, `Tests 12 passed (12)`, `VERDICT command-exit 0`. `check-changeset-presence`, `check-changeset-no-major`, `check-changeset-overwrite`, `check:changeset-claims`, `check:control-bytes` and `check:new-line-citations` each exit 0 there with their OK line. The type-check was not re-run this round, which stopped at the census; CI runs on `25c9aa6d3a`. - Round 3, at `42226f9ed1` (the final commit): the same five `PageView` pin files, `Test Files 5 passed (5)`, `Tests 12 passed (12)`, `VERDICT command-exit 0`. `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`, script name echoed), after `turbo run build --concurrency=2 --filter=@object-ui/app-shell^...` (`Tasks: 28 successful, 28 total`), in one lock hold: `VERDICT command-exit 0`. `check-changeset-presence` (4 source files of 1 released package, 1 changeset declared), `check-changeset-no-major`, `check-changeset-overwrite` (report-only: 1 added, 1 modified, the `9673` entry, declared bump unchanged), `check:changeset-claims`, `check:control-bytes`, `check:new-line-citations` (0 new citations), `check:pending-changeset-literals` and `changeset:check`'s fixed-group half each exit 0 with their OK line. `eslint --format json` over the edited ratchet file: 1 file, 0 errors, 0 warnings. Source-text pins: no test or script quotes a sentence this round changes (the two `9673` sentences, this changeset's sentences 2 and 5, the ratchet titles). Round 4: - At the merged head `e021bfaf24`, before anything was added: the six `app-shell` test files that import or read `PageView` (the five above, plus `PageView.editInStudioNeedsAuthoring-10899`, which the merge brought): `Test Files 6 passed (6)`, `Tests 16 passed (16)`, `VERDICT command-exit 0`. - At the final head `fecce55c33`, in one lock hold: the census probe, `Tests 8 passed (8)`; the same six `PageView` files, `Test Files 6 passed (6)`, `Tests 20 passed (20)`; the 27 reader pins (**H1, round 4**), `Test Files 27 passed (27)`, `Tests 171 passed (171)`; `VERDICT command-exit 0`. The other test that reads `PageView.tsx`, `residue-namespaces-3546` in `@object-ui/i18n`: `Tests 43 passed (43)`, `VERDICT command-exit 0`; the two in `scripts/__tests__` ran in the `scripts/` project below. - `pnpm --filter @object-ui/app-shell type-check` (echoed `tsc --noEmit && tsc -p tsconfig.test.json`), after `turbo run build --concurrency=2 --filter=@object-ui/app-shell^...` (`Tasks: 28 successful, 28 total`): `VERDICT command-exit 0`. `tsc -p tsconfig.test.json --listFiles` names the pin once, so the test program compiles it. - `pnpm exec vitest run scripts/ --maxWorkers=2`, the whole `scripts/` project, so every `scripts/__tests__` gate whose corpus reads a touched file: `Test Files 177 passed | 2 skipped (179)`, `Tests 5350 passed | 2 skipped (5352)`, `VERDICT command-exit 0`. - Each exits 0 with its OK line: `check-changeset-presence` (4 source files of 1 released package, 1 changeset declared), `check-changeset-no-major`, `check-changeset-fixed`, `changeset:check`, `check-changeset-overwrite` (report-only: 1 added, 2 modified, the `9673` and `10887-react-page` entries, declared bumps unchanged), `check:changeset-claims`, `check:pending-changeset-literals`, `check:control-bytes`, `check:new-line-citations` (0 new citations), `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:phantom-deps`, `check:unused-deps`, `check:shell-escape-residue` and `check:unreferenced-sources`. `check-governed-queue-guard --test` over the 7 changed paths: NOT GOVERNED. - Lint, narrowed and measured: `eslint --no-inline-config --format json` over the four touched source and test files reads 4 files, 0 errors and 29 warnings, all `@typescript-eslint/no-explicit-any`. The pin carries 19, one more than its round-3 blob; `PageView.tsx` carries 5, as `main`'s blob does; context-9673 carries 5 and the ratchet 0. `eslint.config.js` names no `projectService` and no `parserOptions`, so type-aware linting is off and the diff moves no untouched file's verdict. CI `Lint` is success on the head. - Eager closure, head against `main`. The only source file this PR changes against `main` is `PageView.tsx`. A console `vite build` at the head, and again with `main`'s `PageView.tsx` blob (restored by blob hash afterwards), both read `Console eager closure is 3105.0 KB gzipped across 330 of 2446 chunks (budget: 3149.4 KB, headroom: 44.4 KB)`, with equal gzip byte totals. `PageView` is its own lazy chunk, and the two eager sets differ only in the entry chunk's hashed name. CI `Bundle Analysis` is success on the head and on `main` `f6fb83f0c5`. - The full `app-shell` suite (869 test files): NOT MEASURED locally. The run was stopped after 20 minutes under the lock (its own PIDs, nobody else's) to free the shared lock. CI's `Test (shard 1/8)` to `Test (shard 8/8)` are success on `fecce55c33`. - CI on `fecce55c33`: 43 check runs, 40 success and 3 skipped (the two coverage runs and dependabot), `Spec Main Shape Gate` and `Bundle Analysis` among the successes. ## Serial Before the final push, `origin/main` was fetched into the private ref `refs/issue-10519/main` (`805abe240c`): one commit landed since BASE (objectui#10763), and it touched no file this branch touches (only two other cards' changesets). `git merge-tree --write-tree HEAD refs/issue-10519/main` exit 0 (clean). No merge commit was needed. Round 2: `origin/main` was fetched into the same private ref (`195052fff6`, 47 commits past `4620ba39d2`'s base, objectui#10809 and objectui#10836 among them) and merged as the merge commit `25c9aa6d3a` (parents `4620ba39d2`, `195052fff6`). There were no conflicts. It was pushed fast-forward, with no rebase and no force. Round 3: `origin/main` was fetched into the same private ref (`1c5ee334a2`, 17 commits past `195052fff6`, objectui#10873 among them) and merged as the merge commit `c5b63fb514` (parents `25c9aa6d3a`, `1c5ee334a2`), with no conflicts, pushed fast-forward. Before the final push, `origin/main` had moved two more commits (`24845c4f14`: objectui#8284 and objectui#7189); neither touches a file this branch touches, and `git merge-tree --write-tree HEAD refs/issue-10519/main` at `42226f9ed1` exits 0 (tree `fe8c05b333`). No further merge was made. Round 4: `origin/main` was fetched into the private ref `refs/issue-10519d/main` (`f6fb83f0c5`, 82 commits past `1c5ee334a2`, objectui#10898 and objectui#10973 among them). `git merge-tree --write-tree` against it exited 0 (tree `2808afb79b`), and the merge commit `e021bfaf24` (parents `42226f9ed1`, `f6fb83f0c5`) carries exactly that tree. The one file both sides changed is `PageView.tsx`: `main`'s hunk (PR objectui#10924, the `useCanAuthorMetadata` gate on the edit-in-studio button) and this branch's hunks do not touch the same lines, and both are kept. Pushed fast-forward, with no rebase and no force. Before the final push `origin/main` had not moved, and the open-PR file lists (objectui#11006, objectstack-ai#10930, objectstack-ai#10278, objectstack-ai#5400) name neither `PageView.tsx` nor anything under `packages/app-shell/src/views/`. ## Acceptance notes - The bare `list` key is the display list by design (plugin-list `src/index.tsx`); no finding. - Not filed, nit: `useConsoleActionRuntime`'s `onRefresh` doc comment still describes the refresh as "bump a refresh key to re-fetch embedded data"; `ObjectView` still bumps one (plus the bus, objectui#10035), so the sentence is stale only for this host. - A `Refresh` button inside `ListView`'s toolbar (`refresh-button`) is a user refresh affordance and is unrelated to `PageView.onRefresh`; the pin uses that node only as an identity witness. - Round 4, the calendar's loading placeholder (**H1, round 4**): `ObjectView`'s non-grid fetch sets `loading` on every run, a bus re-read included, and `ObjectCalendar` swaps its grid for the placeholder while it is set. That is PR objectui#10973's note (c), recorded there as not filed. Not filed here either: carrier none. The month and the view mode live in `ObjectCalendar`'s own state and are kept. - Round 4, the `10887-react-page` changeset edit: the claim's file surface does not list that file. It is edited under the in-place rule for a same-class planned edit: a pending entry this branch makes false in the same release, fixed the way the `9673` entry was (a plain replacement, frontmatter unchanged, same changeset gate family). The seat may take it out and file it instead. - The release PR objectui#5400 deletes `.changeset/9673-pageview-drop-context-spread.md` and `.changeset/10887-react-page-data-invalidation.md` when it consumes them. If it lands first, both entries ship with their current sentences, which are true for that release, and this branch's edits to them become modify/delete conflicts, resolved by dropping the edits. That is the round-2 argument for the `9673` edit, and it covers both. --- _Generated by [Claude Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…lumn objects (objectui#11105) (objectstack-ai#11119) Fixes objectstack-ai#11105 ## What was wrong A grid list view that declares `grouping` and writes its `columns` as objects (`{ field, width, ... }`) showed `Unknown field '[object Object]'` (400 INVALID_FIELD) in every group on `@objectstack` 17.5.0, instead of the group's rows. Mechanism, re-read on `origin/main` before the change: - `ListView`'s grid branch hands its effective column entries to the grid under both keys: `baseProps` carry `fields: effectiveFields`, and the grid case adds `columns: effectiveFields`. - In `ObjectGrid`'s load effect, `getSelectFields()` read the `schemaColumns` branch through `columnIdentity`, but the `schemaFields` branch (checked first) returned its entries unmapped: `return withHarvestedFields(ensureId((schemaFields as any[]).filter(passesProjectionGate)));`. The column objects went into `$select`. - `@object-ui/data-objectstack` serializes `$select` with `selectFields.join(',')`, which prints `[object Object]`. - Server-side grid grouping (objectui#7189) builds this query once (`setGroupRowQuery(groupQuery)`) and `useServerGroupRows` sends it on every group's row page. The 17.5 server refuses unknown select keys, so the latent projection bug became an error in every group. Before server grouping, a grouped grid under `ListView` received inline rows, so this projection never reached the wire. ## The change Inside `getSelectFields()` only: both branches now run one projection, `projectFieldNames`: gate each entry (`passesProjectionGate`), read its field name through `columnIdentity`, drop entries with no identity. `$select` carries names only (plus `id`, the grouping fields and the predicate operands, as before). A `fields` entry that is already a name comes back unchanged, so string projections ask for exactly what they asked for before. The diff stays inside `getSelectFields()`. Draft PR objectstack-ai#10278 also edits `ObjectGrid.tsx`'s fetch effect (the `params` window and the effect's dependency list); the two change different lines. ## Tests New pin: `packages/plugin-grid/src/__tests__/serverGroupedSelectIdentity-11105.test.tsx`, describe `a server-grouped grid with object columns asks for field names (objectui#11105)`. It mounts `ObjectGrid` with the node `ListView` builds (the same object entries under `fields` and `columns`) over a data source that answers the group header query and, like the 17.5 server, refuses a `$select` entry that is not a declared field name. - `every group page selects names only — id, the columns, and the grouping key`: both group row pages send `['id', 'name', 'list_price', 'category']`, every group shows its rows, and no `group-rows-error-*` element renders. - `an entry with no field identity is left out of the projection, not sent`: a `{ width: 80 }` entry is dropped. - `the ungrouped fetch the grid makes for itself selects names too`: the flat fetch uses the same projection. - `CONTROL — the same grid with string columns selects the same names`: green before and after the fix. Reverse verification, with the fix committed first (`a014b30b`): `scripts/ablation-replace.mjs` (from the objectstack checkout) swapped the new `schemaFields` return line back to the BASE line (anchor hit x1 to x0, blob changed on disk) and ran the file: ``` × every group page selects names only — id, the columns, and the grouping key × an entry with no field identity is left out of the projection, not sent × the ungrouped fetch the grid makes for itself selects names too AssertionError: expected [ 'id', …(3) ] to deeply equal [ 'id', 'name', 'list_price', …(1) ] Tests 3 failed | 1 passed (4) ``` The CONTROL stayed green. The restore was proven (blob after restore equals the HEAD blob, and `git diff HEAD` is empty). The subject is imported from `../ObjectGrid` source, so no `dist` build sits between the mutation and the run. ## Gates (at head `3541d68b`) - `pnpm exec vitest run packages/plugin-grid/`: 168 files, 1557 tests passed. - `pnpm --filter @object-ui/plugin-grid type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0, after building the dependency closure (`turbo run build --filter=@object-ui/plugin-grid^...`, 13 tasks). `tsc -p tsconfig.test.json --listFiles` includes the new test file. - ESLint on the two touched source files: 0 errors. The new test has 0 warnings. `ObjectGrid.tsx` has 207 warnings, down from 209 on BASE, because two `any` casts left. Type-aware linting is not configured, so the diff cannot change a verdict in an untouched file. - `node scripts/check-changeset-presence.mjs`, `pnpm changeset:check`, `pnpm check:new-line-citations` (0 new), `pnpm check:control-bytes`, `pnpm check:phantom-deps`, `pnpm check:test-path-roots`, `pnpm check:unreferenced-sources`, `pnpm check:pending-changeset-literals`: all exit 0. - `node scripts/check-governed-queue-guard.mjs --test` on the three changed paths: NOT GOVERNED. - `pnpm check:changeset-claims` lists five pending changesets that name `ObjectGrid.tsx`. I read each paragraph. None of them describes the `$select` projection, so none is made false by this change. Changeset: `.changeset/11105-grouped-grid-selects-field-names.md`, `@object-ui/plugin-grid: patch`. ## Acceptance notes - **Adapter hardening was not done here.** The issue also suggests that `@object-ui/data-objectstack` refuse a non-string `$select` entry instead of joining it. I left it out of this PR because neither option is small and clearly safe. Skipping the entry silently would be a lenient fallback of the kind AGENTS.md #0.1 rules out: it hides the producer's bug. Refusing it adds a new client-side error surface at two serialization sites (`rawFindWithPopulate` and `convertQueryParams`). It would also change behaviour for any other caller that sends a non-name entry to a server that ignores unknown select keys. That needs its own decision on the error's code and wording. - **Where the object entries come from.** `ObjectGridSchema.fields` is declared `string[]` in both the TypeScript interface and the zod mirror (`fields: z.array(z.string()).optional()`). `ListView` passes the column objects under that key anyway, because `baseProps` share `fields` across every child view. This fix reads names on the consumer side, which is the route the dispatch chose. The other option is to stop `ListView`'s grid node from carrying the objects under `fields`, which is on the producer side. That touches every child view's handoff and is outside this change. After this fix, no grid reader of `schema.fields` gives a wrong answer for that node. The render and expand paths prefer `columns`. No carrier. Session: `https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL` --- _Generated by [Claude Code](https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…that wins over schema.onNavigate, and the four onNavigate faces take one closed token, RecordNavigateAction (objectui#9547) (objectstack-ai#11218) Fixes objectstack-ai#9547 Clause-②: yes ## What this does Ruling C on this card (decision batch 166, item 1, comment `5731678470`, maintainer 「同意」), step 3, in one pass now that objectui resolves `@objectstack/spec` 17.5.0 (triage probe `5904192911`): - `ObjectGridComponentProps` declares `onNavigate?: (recordId: string | number, action: RecordNavigateAction) => void`, and `ObjectGrid` reads `onNavigate: onNavigate ?? schema.onNavigate` — the ruling's `props.onNavigate ?? schema.onNavigate`, spelled with the destructured prop the way its nine siblings are. When both are supplied, the prop wins. - `RecordNavigateAction = 'view' | 'new_window'` is a new type export of `@object-ui/types`, and all four faces take it: the new prop, `ObjectGridSchema.onNavigate`, `ListViewRuntimeProps.onNavigate` (so `ListViewSchema['onNavigate']`), and `UseNavigationOverlayOptions.onNavigate` in `@object-ui/react`. Each face opens with the same sentence. ⛔ No interim `string`, ⛔ no `'edit'`. - The two docblocks that counted "nine siblings" now count ten: the programmatic-only note on `ObjectGridSchema.onNavigate`, and the read-site exemption comment in `ObjectGrid.tsx`. Every pinned phrase of both is unchanged. - `gridNonAuthorKeys.test.tsx`: the read-line anchor is re-aimed at `onNavigate: onNavigate ?? schema.onNavigate,`, by content. - New `onNavigateProp-9547.test.tsx`: the ruling's two pins (the prop is wired in `page` mode; the prop beats the schema key), plus a modifier click handing the prop `'new_window'`, type-level pins that each face's second parameter IS `RecordNavigateAction` and is required, and a read of the spec catalogue row against the union. - `.changeset/9547-ongrid-onnavigate-prop.md`: `minor` for `@object-ui/plugin-grid`, `@object-ui/types`, `@object-ui/react`, with the Clause-② paragraph and what a host has to change. Two commits: `1432197b88` (the type and the three sibling faces), `0c73dfb843` (the prop, the read, the pins, the changeset), then `fc90ce532d` merges `main` at `0389650f3c` (it brought PR objectstack-ai#10278, objectui#9853, into `ObjectGrid.tsx` / `objectql.ts` / `objectql.zod.ts`; no conflict and no hunk near `onNavigate`). ## The PM's mechanism assumptions, measured 1. **Held, with one correction.** On `846cec0efe`: `ObjectGridComponentProps` declared no `onNavigate`; the grid read `onNavigate: schema.onNavigate`. In `objectql.ts`, `ObjectGridSchema.onNavigate` and `ListViewRuntimeProps.onNavigate` were `(recordId, action?: string)`; the `'view' | 'edit'` declaration is `ObjectViewSchema.onNavigate` — a different channel (`plugin-view`'s `ObjectView` invokes it with `'new'` / `'edit'` / `'view'` itself), not one of the ruling's three sibling faces, and it is untouched. `UseNavigationOverlayOptions` lives in `packages/react/src/hooks/useNavigationOverlay.ts`, typed `(recordId, action?: string)`. 2. **Falsified.** The installed `@objectstack/spec` 17.5.0 exports no TypeScript type for the union: `onNavigate` occurs in none of its `.d.ts` files, and `'view' | 'new_window'` occurs only as the `type` STRING of the `onNavigate` row in the `ListView` entry of `REACT_BLOCKS` (`dist/ui/index.mjs`). objectstack `main` (`05a7547c9f`) is the same. ⇒ the literal is spelled once, as `RecordNavigateAction`, and the spec-row case in the new test fails if the two vocabularies part. 3. **Held.** `handleClick` in `useNavigationOverlay` calls `onNavigate` at four sites: two pass `'new_window'`, two pass `'view'`, none passes `'edit'` (the retired `navigation.view` read is already gone). The option is now typed against the union, so a third token there is a compile error. The pins assert that `('1', 'view')` in `page` mode and `('1', 'new_window')` on a meta-click reach the prop. 4. **Held.** No test counts `ObjectGridComponentProps` members; the "nine" census is the two docblocks' prose. `NINE_SIBLINGS` in `gridNonAuthorKeys.test.tsx` asserts the nine are NOT declared on `ObjectGridSchema`, which stays true, so it is unchanged. ## Files outside the claim's list, and why - `packages/types/src/index.ts` — one barrel line exporting `RecordNavigateAction`; "one place, reused" across three packages needs the export. - `packages/types/src/zod/objectql.zod.ts`, `packages/types/src/__tests__/zod-mirror-parity.test.ts` — comments only. Two contrasted `ObjectViewSchema.onNavigate` with "the grid's `action?: string`" and two quoted the grid's read as `onNavigate: schema.onNavigate`. This change made all four false, so they now name the new type and quote the new read. ## What a host sees - A handler written `(id, action?: string) => …`, or with one parameter, still compiles and receives what it received before. - A handler annotated with a union that leaves out `'new_window'` (the old docblock's `'view' | 'edit'`) is refused with TS2322. Measured with a throwaway probe compiled by `plugin-grid`'s `tsconfig.test.json` and deleted afterwards: TS2322 on a `'view' | 'edit'` handler, TS2345 on a call passing `'edit'`, TS2554 on a call omitting the token, no error on the two accepted shapes. - In-repo consumers that pass a handler (`app-shell`'s `ObjectView`, `InterfaceListPage`, `ObjectDataPage`; `plugin-gantt`'s `ObjectGantt`; `plugin-list`'s `ListView`) all type-check unchanged. ## Tests — all on `fc90ce532d` (the head this PR opens at) unless marked Every heavy run went through the shared lock; the verdict lines quoted are the lock's own. - Build: `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-gantt^...' build` → `VERDICT command-exit 0` (covers the `plugin-grid` closure). - Type-check, touched packages and the downstream consumers of the narrowed faces: `pnpm --workspace-concurrency=2 --filter @object-ui/types --filter @object-ui/react --filter @object-ui/plugin-grid --filter @object-ui/plugin-list --filter @object-ui/plugin-gantt --filter @object-ui/plugin-view --filter @object-ui/app-shell run type-check` → all seven `type-check: Done`, `VERDICT command-exit 0`. `tsc -p tsconfig.test.json --listFiles` in `plugin-grid` lists `onNavigateProp-9547.test.tsx`, so the type-level pins are compiled. - `pnpm exec vitest run --maxWorkers=2 --shard=1/2 packages/plugin-grid/` → `Test Files 87 passed (87)`, `Tests 769 passed (769)`; `--shard=2/2` → `Test Files 86 passed (86)`, `Tests 824 passed (824)`. - `pnpm exec vitest run --maxWorkers=2 packages/types/ packages/react/` → `Test Files 393 passed (393)`, `Tests 8257 passed (8257)`. - `node scripts/check-changeset-presence.mjs` → exit 0, "8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)". - `pnpm check:control-bytes` → exit 0 · `check:spec-symbols` → exit 0 · `check:new-line-citations` → `VERDICT new-cross-file-line-citations: 0 new citation(s)` · `check:handler-key-reads` → exit 0, "every judged read is a declared member of its arm" · `check:doc-example-readers` → exit 0 · `check:changeset-claims` → exit 0 (report-only) · `check:pending-changeset-literals` → exit 0. - ESLint on the 8 changed TS/TSX files, the packages' own `eslint .` form: 8 file results in `--format json`, 0 errors, 252 warnings (1 of them in the new test: `no-explicit-any` on the fake adapter cast, the same idiom `gridNonAuthorKeys.test.tsx` uses). `eslint.config.js` sets no `parserOptions` / `projectService`, so linting is not type-aware and this diff cannot move a verdict on an untouched file. The repo-wide lint is CI's. ## Reverse verification (committed first, restored by blob hash) - **Behaviour**, on `0c73dfb843`, through `ablation-replace.mjs` with the read reverted to `onNavigate: schema.onNavigate,` (on-disk counts during the run: mutated line 1, real line 0): `Tests 4 failed | 41 passed (45)` — the three new prop cases and the re-aimed anchor went red; the schema-only control ("the renderer still reads it") stayed green. Restored: blob equals HEAD, `git diff HEAD` empty. - **Types**, on `0c73dfb843`, through the rebuilt `dist/` (plugin-grid resolves `@object-ui/types` through its exports): `ListViewRuntimeProps.onNavigate` widened back to `action?: string`, types rebuilt, the mutated declaration counted 1 in `dist/objectql.d.ts`, then `tsc -p tsconfig.test.json` in plugin-grid → exit 2 with exactly two TS2344 errors, on the `ListViewFace` and `ListViewArity` pins. Restore leg: source restored by blob hash, types rebuilt, mutated declaration 0 in `dist`, real declaration 2, the same `tsc` exit 0. ## Acceptance notes - `packages/plugin-grid/README.md` and `content/docs/plugins/plugin-grid.mdx` ("Row callbacks are component props") stay true — they already tell callers to prefer passing `onNavigate` as a prop — but neither lists it with the nine or names `RecordNavigateAction`. Not edited: outside the claimed file surface. Carrier: the dispatching seat. - `check:readme-exports` → exit 1 because 8 packages outside this change were unbuilt here ("could not be judged", 0 fabricated). NOT MEASURED; this diff edits no README. - The spec-row case was not ablated: it reads the installed catalogue, which this PR does not change. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9853
Clause-②: yes — the undeclared display page size changes on three surfaces (inline grid 10 → the spec default, grouped groups-per-page 10 → the spec default, paged list 100 → the spec default), as ruled. The PR waits as a draft for the director seat's contract review (
needs:contract-reviewstays on both carriers).Rulings executed:
5749197629(C-prime, items 1 and 2) as amended by5824040487(batch #223 item 4: the display default is 50, declared once in the protocol; structure B). The grid, list and kanban halves all land in this PR. The first line isPart of, notFixes, for one reason only: one pre-existing pin reads red, because structure B's paged/unpaged split re-issues the list's fetch on a view switch when nothing is declared. That needs a ruling, and it is described under Open question below. Once it is ruled, nothing else of the card stays open here, and the seat can switch the first line to the closing keyword.This head merges
origin/mainat0ffc423b1(a merge commit: no rebase, no force-push, no pushed commit rewritten). This session:https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm.What changed
Structure B: "page size" means one page, and only where there is a pager.
@object-ui/plugin-grid.ObjectGridfalls back toDEFAULT_DISPLAY_PAGE_SIZEon every page it shows: the server-paged table, the table over inlinedata, the grouped view's page of groups, and a server-grouped group's own page of rows.DEFAULT_DISPLAY_PAGE_SIZEisreadSpecDisplayPageSize(), which readsPaginationConfigSchema.parse({}).pageSizefrom@objectstack/spec/ui. There is no local number behind the read: if the spec stops declaring a positive default, the module throws at load.DEFAULT_FETCH_BATCH_SIZE(50, value kept) is the grid's fetch batch. It is read only when the grid buckets a fetched window in the browser (bucketsFetchedWindow), and never as a page size.@object-ui/plugin-list.DEFAULT_LIST_PAGE_SIZE = 100splits in two.DEFAULT_LIST_DISPLAY_PAGE_SIZEis the same spec read, and applies whenpaginateis true (the flat grid view, whichListViewpages on the server).DEFAULT_LIST_FETCH_BATCH_SIZE = 100(value unchanged) is the$topof the one unpaged fetch for every other view: kanban, calendar, gallery, timeline and the rest, plus a grouped grid's window.effectivePageSizenow resolves afterpaginateand passespaginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZEas the fallback. A declaredpagination.pageSizestill sizes every view, as before.describeRefusedPageSizenow names the fallback the view actually used.@object-ui/plugin-kanban.DEFAULT_KANBAN_LIMITis renamedDEFAULT_KANBAN_FETCH_BATCH_SIZE, value 100 unchanged. The refused-limitdiagnostic now reads "ObjectKanban fetch batch: … fell back to its default fetch batch (100)" instead of "row cap".packages/plugin-grid/README.mdandcontent/docs/plugins/plugin-grid.mdx(the undeclared page size, including a group's page and the bucketed window),packages/plugin-list/README.md(new section "Page size and fetch batch"), andcontent/docs/plugins/plugin-kanban.mdx(the section is retitled "Fetch batch (object-driven boards)" and says the default does not follow the display page size). No fenced code block was added or changed: the diff over*.md/*.mdxoutside.changeset/has 0 changed fence lines and 0 changed import lines, and every insertion sits after an even number of fences..changeset/9853-grid-display-page-size-from-spec.md(this PR's own) is rewritten:minorfor plugin-grid, plugin-list and plugin-kanban, with the per-surface table below, and hosts told to declarepagination.pageSize(or a board'slimit) to keep an old count.Measured before and after (renderer,
@objectstack/spec17.5.0)The installed spec is 17.5.0, and
PaginationConfigSchema.parse({}).pageSizeanswers 50. This table comes from a one-time reverse run. A probe (not committed) mounted the realObjectGrid,ListViewandObjectKanbanagainst a paging data source that also answers the group header query. It read rows off the screen and$topoff the wire. It ran at heada2c3e5ba7, and again withObjectGrid.tsx,ListView.tsxandObjectKanban.tsxswapped tomain's blobs (blob hashes equal to0ffc423b1's; on-disk markersDEFAULT_SERVER_WINDOW_SIZE2,DEFAULT_DISPLAY_PAGE_SIZE0,DEFAULT_LIST_PAGE_SIZE3,export const DEFAULT_KANBAN_LIMIT1). The restore was proven by hash equality withHEADand an emptygit diff HEAD.main0ffc423b1)object-gridfetching its own rows: rows per page /$topobject-gridover inlinedata: rows per pageobject-grid: groups per pageobject-grid:$topof each group's row pagelist-view, grid view (paged): rows per page /$toplist-viewkanban, gallery, calendar, timeline, grouped grid:$topobject-kanbanwith nolimit:$topPremises from the dispatch, re-measured:
DEFAULT_FETCH_BATCH_SIZEis 50 today" is partly overtaken bymain. Since objectui#7189, a grouped grid that fetches its own rows groups on the server:serverGroupedFetchstrips$top, and each group pages its rows by the display page size. Since objectui#10881, a data source with noqueryGroupHeadersis refused, and no rows are fetched. The batch therefore has one reader left: a grid that buckets a window in the browser. The case that reaches it today is a grouping key the principal may not read, over a source that answers the header query. It is its own constant (50) and is not read from the spec. The pin "a window the grid groups in the browser FETCHES the batch" runs that exact path.paginateiscurrentView === 'grid' && !(groupingConfig?.fields?.length). Every other kind issues one unpaged$top. The previous executor's cost reproduces: with nothing declared, a paged-to-unpaged switch re-issues the fetch ([50, 100]on grid → kanban). See Open question.Conflict resolution (merge of
origin/main0ffc423b1)git merge-treereported content conflicts in the three files the dispatch named. Every hunk:ObjectGrid.tsx, the loader effect's dependency list. Resolved asmain's list and comments (it addsschemaDefaultFilters,invalidationNonce,serverGroupedFetch,groupingNeedsHeaderQuery), with this branch'sfetchWindowin place ofserverPageSize, because the loader's$top/$skipreadfetchWindow.ObjectGrid.tsx, the "Is the grouping on screen page-scoped?" marker block.mainretired it (objectui#10881). This branch had only renamed what it compared against (groupingRowsLoaded >= fetchWindow), somain's deletion stands. Nothing else readsgroupingIsPartial(grep: 0).packages/plugin-grid/README.mdand 4.content/docs/plugins/plugin-grid.mdx: this branch's undeclared-page-size paragraph, followed bymain's retitled "Grouping is server-side" heading.There was also one semantic conflict that git merged cleanly. This branch's
groupedFetch = usableGroupingFields(schema.grouping?.fields).length > 0assumed that every grouped self-fetch buckets a window, andmain's server grouping made that false. Resolved in3445f3213: the predicate becamebucketsFetchedWindow, which is!serverGroupedFetch && !groupingNeedsHeaderQueryplus a grouping entry left after known masked keys are refused. It is counted the way the two server predicates count, so the first fetch does not flip from page size to batch when the object's types land. The docs paragraph was rewritten to match. Carried on the same file: the server pagination comment'sframework #2212is qualified asobjectstack-ai/objectstack#2212(carrier note on this PR).Census of the three constants
Instrument:
git grep -nover the whole tree for each old and new name, re-run at head.DEFAULT_SERVER_WINDOW_SIZE,DEFAULT_FLAT_PAGE_SIZE,DEFAULT_GROUPS_PER_PAGE,DEFAULT_LIST_PAGE_SIZE: 0 hits.DEFAULT_KANBAN_LIMIT: every reader updated toDEFAULT_KANBAN_FETCH_BATCH_SIZE. That coversObjectKanban.tsx,plugin-kanban/src/index.tsx, two plugin-kanban pins, the types docblock (objectql.ts) and the zodlimitdescribe text (objectql.zod.ts),app-shellblock-config.tsand three app-shell pins, and the types read-text pinobject-kanban-group-by-limit-7322.test.ts. That pin readsexport const DEFAULT_KANBAN_FETCH_BATCH_SIZE = 100;and$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_FETCH_BATCH_SIZE)off disk. Theblock-config.test.tsregex now reads the new name.ObjectKanban.tsx"Before that ruling it was named…"; the objectui#9925 note);plugin-kanban/src/index.tsx;Pins
One pin across every paged surface:
packages/app-shell/src/__tests__/displayPageSizeFromSpec-9853.test.tsx(new). It lives in app-shell because that package already depends on grid, list and board, so no new dependency edge is needed. It stands in a spec default the spec never shipped, and a probe records that the stand-in's factory ran and what the real default is. It then mounts the real components.$top, grid inline rows, grouped groups per page, group-row$top, paged list rows and$top. Every value equals the default read from the spec at test time.$top100, and neither equals the page size.No literal 50 appears in any assertion.
Per package:
ObjectGrid.displayPageSizeDefault-9853.test.tsx(reworked for the merged tree, stubbed spec default). Rows: stand-in live; every page the grid shows; the bucketed window fetches the batch; the selector shows the size in force (the stand-in is not a fixed step); the declared control on every surface including the bucketed fetch; the refused control. Grouped rows now use a data source that answers the group header query.ListView.pageSizeAndFetchBatch-9853.test.tsx(new, real spec). Rows: paged grid$topand hand-off = the spec default; five unpaged views$top= 100; the declared control on a paged and an unpaged view; the switch cost[default, 100]; the refused value names the fallback number.$top100 under the new name.Flipped, none deleted:
groupedPagination.test.tsx: the fixture is two pages plus ten of the spec default. The resize step is derived (a fixed step that is not the default and splits the fixture in two), and the one-page control declarespageSizeequal to the group count.ObjectGrid.pageSizeNonPositive-9853.test.tsx: the inline fixture is the spec default plus ten rows, so12.5still discriminates at 50.These were the reds on head
454232890(CI shards 4/8 and 5/8, and locally before rework: 9 failed / 17 passed across the three grid files). The grouped rows failed becausemain's objectui#10881 refuses a grouped grid over a data source with noqueryGroupHeaders.Ablations (predicted before running; each through
ablation-replace.mjs, restore proven by blob == HEAD and an emptygit diff HEAD)paginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE,becameDEFAULT_LIST_DISPLAY_PAGE_SIZE,(anchor 1 → 0, blob moved).ObjectGrid.tsx(const DEFAULT_DISPLAY_PAGE_SIZE = 50;) andListView.tsx. Both anchors went 1 → 0.groupedPaginationstay green (they cannot tell a hardcoded 50 from the spec's 50).10512file at head: 1 failed / 5 passed. With the fallback collapsed back to one number for every kind (DEFAULT_LIST_FETCH_BATCH_SIZE): 6 passed. The extra query is exactly the kind-dependent window.Open question (blocks landing; this seat does not decide it)
ListView.datasetChartAppliedFilter-10512.test.tsx, row "CONTROL (objectui#7394): with nothing held, the switch to the dataset chart re-issues no query", readsexpected 2 to be 1at head (CI shard 3/8 ona2c3e5ba7, reproduced locally).pageSize, the grid view's window is the display default (50) and the chart's is the fetch batch (100). A switch therefore changes$top, and the list issues a real, different query.The pin is untouched. The options, with their costs, are in the
os-dev-reporton the card.Gates (local, head
a2c3e5ba7; exit codes captured before any pipe)The three package suites,
pnpm exec vitest run --maxWorkers=2 packages/plugin-grid/ packages/plugin-list/ packages/plugin-kanban/: exit 1.Test Files 1 failed | 343 passed (344),Tests 1 failed | 3202 passed (3203). The one red is the open question's row inListView.datasetChartAppliedFilter-10512.test.tsx.Every test outside those packages that names
ObjectGrid,ListVieworObjectKanban, their registry types, or their source paths. The census isgit grep -lEover test files, plus the touched app-shell and types pins: 320 paths, 318 of them test files (the other two are a__mocks__module and a fixture). Result: exit 0.Test Files 318 passed (318),Tests 6339 passed | 9 skipped (6348).type-check: exit 0 for
@object-ui/plugin-grid,@object-ui/plugin-list,@object-ui/plugin-kanbanand@object-ui/types, after building the grid/list/kanban dependency closure (build exit 0).tsconfig.test.json --listFilesincludes every touched test file (1 each) and theListView.tsxcontrol.lint:
eslint .exit 0 for plugin-grid, plugin-list and plugin-kanban (0 errors). The touched app-shell and types files: 0 errors. Findings on the three touched sources,mainversus head, read through--stdin-filename:ObjectGrid.tsx0e/207w vs 0e/207w;ListView.tsx0e/188w vs 0e/188w;ObjectKanban.tsx0e/30w vs 0e/30w.exit 0:
check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check:changeset-claims(report-only; pending bodies read, none of the touched ones is falsified by this change),check:pending-changeset-literals;check:control-bytes,check:new-line-citations(0 new),check:spec-symbols,check:test-path-roots,check:phantom-deps,check:unreferenced-sources,check:element-data-source-declaration;check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape;check:doc-fences,check:doc-types,check:doc-example-ids;check-governed-queue-guard --teston the diff paths: NOT GOVERNED.check:spec-floors, after building plugin-grid, plugin-list and plugin-kanban (build exit 0): exit 1. The 11 findings are all[no-artifact]for packages this PR neither touches nor built, and plugin-grid, plugin-list and plugin-kanban draw none. The gate judges declaration files, where the new runtime import does not appear, so the floor question was also read directly:@objectstack/spec17.0.0 and 17.4.0, fetched from the registry, both exportPaginationConfigSchemafrom./ui, inindex.d.mtsand inindex.mjs.NOT MEASURED locally, left to CI:
check:doc-snippets,check:doc-examples,check:readme-exports. They need the near-whole-workspace build their--build-filternames. The docs diff changes no fenced block and no import line, so their verdict cannot move.type-check, which needs app-shell's whole dependency closure built.check:eager-closure, which needs a console build.Acceptance notes (not carded)
@object-ui/plugin-griddeclares@objectstack/spec^17.4.0and@object-ui/plugin-list^17.0.0. A host whose lockfile resolves an older spec gets that spec's default, on every surface alike. The floors are not raised here..changeset/9853-grid-non-positive-page-size.md, ends by saying the default question "is handed back on the issue". That was true of its PR, and it publishes in the same release as this one, which answers it. Left as history.pagination.pageSizestill caps unpaged list views and a bound board, as before. Ruling5824040487splits only the undeclared fallback, and this PR reads it that way.useStateseeds are read only at mount. The grouped view's$skipis(serverPage - 1) * fetchWindow, which now matters only on the bucketed-window path.43d21e6b1(a pushed commit of the previous executor) carries a model-named co-author trailer. It is left as is: pushed history is not rewritten. Every commit of this round carries the model-free pair.Round 2: ruling record
5909000462(head5a2d1e66a)The maintainer answered the Open question above with A: accept the one extra fetch. Ruling record
5909000462on the card records the ruling verbatim. The first line of this body is now the closing keyword. The paragraph under it that explainsPart of, and the Open question and Gates sections above, describe heada2c3e5ba7and are superseded by this section. They are left as written.origin/mainat574d82fdf, merged as commit118cdbe5a(no rebase, no force-push, no pushed commit rewritten). Git reported no conflict hunk. Both sides touched five files (ObjectGrid.tsx, the plugin-gridREADME.mdandplugin-grid.mdx, and typesobjectql.tsandobjectql.zod.ts), and all five auto-merged. The oneObjectGrid.tsxchange frommain(objectui#11021: the search term on the group header query) reads neither the page size nor the fetch batch. The old-name census, re-run on the merged tree, is unchanged from round 1.39994581a), inListView.datasetChartAppliedFilter-10512.test.tsx. It was "CONTROL (objectui#7394): with nothing held, the switch to the dataset chart re-issues no query". It is now "with nothing held and no pageSize declared, the switch to the dataset chart re-issues exactly one query, whose $top moves from the spec display default to the fetch batch".$topis read againstPaginationConfigSchema.parse({}).pageSize.$topis read againstDEFAULT_LIST_FETCH_BATCH_SIZE.ListView.tsxnow exports that constant as a module export; the package index does not re-export it. No literal 100 appears in the row.$topchanged, so nothing the chart withholds reached it.5909000462by id.$topis 7 and no further query follows. objectui#7394's own §1 (ListView.viewSwitchRefetch-7394.test.tsx) pins the same for grid to kanban.5a2d1e66a). The plugin-list bullet now says the switch issues one request, whose$topis the fetch batch. It also says that this qualifies the pending objectui#10512 note, which publishes in the same release and reads "a switch into a dataset chart with no user filter set issues no new request": that sentence now holds only when a page size is declared. The objectui#10512 changeset itself is not edited (check-changeset-overwritegreen).check:changeset-claimscannot flag that note, because it names no file path.$topassertion, and the other six rows stay green.ablation-replace.mjsreplacedpaginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE,withDEFAULT_LIST_DISPLAY_PAGE_SIZE,. The anchor count went 1 → 0 and the blob moved5789e6b50742→85a9025b2d66.Tests 1 failed | 6 passed (7), withAssertionError: expected [] to deeply equal [ 100 ]at the$topwait. As predicted.HEAD's andgit diff HEADis empty.../ListViewfrom source.Gates at head
5a2d1e66a, run after the last commit. Exit codes were captured before any pipe.packages/plugin-grid/ packages/plugin-list/ packages/plugin-kanban/, 4 shards (vitest listcounts the same 345 files)packages/typespinsmainadded that name these viewstype-checkplugin-grid / plugin-list / plugin-kanban, after the dependency-closure build (exit 0)tsconfig.test.json --listFilesincludes the 10512 filelint(eslint .) plugin-grid / plugin-list / plugin-kanbanListView.tsx0e/188w (as in round 1), the 10512 file 0e/0wcheck-changeset-presence,-no-major,-fixed,-overwritecheck:changeset-claimscheck:pending-changeset-literalscheck:control-bytescheck:new-line-citationscheck:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:phantom-deps,check:spec-symbols,check:unreferenced-sources,check:doc-fences,check:doc-types,check:doc-example-idsNOT MEASURED this round:
check:spec-floors,check:doc-snippets,check:doc-examplesandcheck:readme-exports. Reason: they need the near-whole-workspace build, and this round's diff adds no import to published source (the only new@objectstack/specimport is in a test) and touches no doc outside.changeset/.Generated by Claude Code