Skip to content

feat(plugin-grid): one display page size read from the spec, and a distinct fetch batch (objectui#9853) - #10278

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-9853-one-display-page-size
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-9853-one-display-page-size

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

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-review stays on both carriers).

Rulings executed: 5749197629 (C-prime, items 1 and 2) as amended by 5824040487 (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 is Part of, not Fixes, 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/main at 0ffc423b1 (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. ObjectGrid falls back to DEFAULT_DISPLAY_PAGE_SIZE on every page it shows: the server-paged table, the table over inline data, the grouped view's page of groups, and a server-grouped group's own page of rows. DEFAULT_DISPLAY_PAGE_SIZE is readSpecDisplayPageSize(), which reads PaginationConfigSchema.parse({}).pageSize from @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 = 100 splits in two. DEFAULT_LIST_DISPLAY_PAGE_SIZE is the same spec read, and applies when paginate is true (the flat grid view, which ListView pages on the server). DEFAULT_LIST_FETCH_BATCH_SIZE = 100 (value unchanged) is the $top of the one unpaged fetch for every other view: kanban, calendar, gallery, timeline and the rest, plus a grouped grid's window. effectivePageSize now resolves after paginate and passes paginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE as the fallback. A declared pagination.pageSize still sizes every view, as before. describeRefusedPageSize now names the fallback the view actually used.
  • @object-ui/plugin-kanban. DEFAULT_KANBAN_LIMIT is renamed DEFAULT_KANBAN_FETCH_BATCH_SIZE, value 100 unchanged. The refused-limit diagnostic now reads "ObjectKanban fetch batch: … fell back to its default fetch batch (100)" instead of "row cap".
  • Docs: packages/plugin-grid/README.md and content/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"), and content/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 / *.mdx outside .changeset/ has 0 changed fence lines and 0 changed import lines, and every insertion sits after an even number of fences.
  • Changeset .changeset/9853-grid-display-page-size-from-spec.md (this PR's own) is rewritten: minor for plugin-grid, plugin-list and plugin-kanban, with the per-surface table below, and hosts told to declare pagination.pageSize (or a board's limit) to keep an old count.

Measured before and after (renderer, @objectstack/spec 17.5.0)

The installed spec is 17.5.0, and PaginationConfigSchema.parse({}).pageSize answers 50. This table comes from a one-time reverse run. A probe (not committed) mounted the real ObjectGrid, ListView and ObjectKanban against a paging data source that also answers the group header query. It read rows off the screen and $top off the wire. It ran at head a2c3e5ba7, and again with ObjectGrid.tsx, ListView.tsx and ObjectKanban.tsx swapped to main's blobs (blob hashes equal to 0ffc423b1's; on-disk markers DEFAULT_SERVER_WINDOW_SIZE 2, DEFAULT_DISPLAY_PAGE_SIZE 0, DEFAULT_LIST_PAGE_SIZE 3, export const DEFAULT_KANBAN_LIMIT 1). The restore was proven by hash equality with HEAD and an empty git diff HEAD.

surface, nothing declared before (main 0ffc423b1) after
object-grid fetching its own rows: rows per page / $top 50 / 50 50 / 50
object-grid over inline data: rows per page 10 50
grouped object-grid: groups per page 10 50
server-grouped object-grid: $top of each group's row page 50 50
list-view, grid view (paged): rows per page / $top 100 / 100 50 / 50
list-view kanban, gallery, calendar, timeline, grouped grid: $top 100 100
object-kanban with no limit: $top 100 100

Premises from the dispatch, re-measured:

  • The self-fetching grid is 50 → 50, the inline grid 10 → 50, and groups 10 → 50. Holds.
  • The spec default is read, not hardcoded. Holds: the stubbed pins below move with the spec, and ablation 2 reddens them.
  • "The grouped fetch batch DEFAULT_FETCH_BATCH_SIZE is 50 today" is partly overtaken by main. Since objectui#7189, a grouped grid that fetches its own rows groups on the server: serverGroupedFetch strips $top, and each group pages its rows by the display page size. Since objectui#10881, a data source with no queryGroupHeaders is 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.
  • The list half. Only the flat grid view pages on the server: paginate is currentView === '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/main 0ffc423b1)

git merge-tree reported content conflicts in the three files the dispatch named. Every hunk:

  1. ObjectGrid.tsx, the loader effect's dependency list. Resolved as main's list and comments (it adds schemaDefaultFilters, invalidationNonce, serverGroupedFetch, groupingNeedsHeaderQuery), with this branch's fetchWindow in place of serverPageSize, because the loader's $top / $skip read fetchWindow.
  2. ObjectGrid.tsx, the "Is the grouping on screen page-scoped?" marker block. main retired it (objectui#10881). This branch had only renamed what it compared against (groupingRowsLoaded >= fetchWindow), so main's deletion stands. Nothing else reads groupingIsPartial (grep: 0).
  3. packages/plugin-grid/README.md and 4. content/docs/plugins/plugin-grid.mdx: this branch's undeclared-page-size paragraph, followed by main'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 > 0 assumed that every grouped self-fetch buckets a window, and main's server grouping made that false. Resolved in 3445f3213: the predicate became bucketsFetchedWindow, which is !serverGroupedFetch && !groupingNeedsHeaderQuery plus 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's framework #2212 is qualified as objectstack-ai/objectstack#2212 (carrier note on this PR).

Census of the three constants

Instrument: git grep -n over 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 to DEFAULT_KANBAN_FETCH_BATCH_SIZE. That covers ObjectKanban.tsx, plugin-kanban/src/index.tsx, two plugin-kanban pins, the types docblock (objectql.ts) and the zod limit describe text (objectql.zod.ts), app-shell block-config.ts and three app-shell pins, and the types read-text pin object-kanban-group-by-limit-7322.test.ts. That pin reads export const DEFAULT_KANBAN_FETCH_BATCH_SIZE = 100; and $top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_FETCH_BATCH_SIZE) off disk. The block-config.test.ts regex now reads the new name.
  • Left on purpose, all historical prose:
    • two docblock sentences that describe the old spelling as history (ObjectKanban.tsx "Before that ruling it was named…"; the objectui#9925 note);
    • one "re-measured then" sentence in plugin-kanban/src/index.tsx;
    • one in the 7322 types pin's header;
    • three pending changesets of other cards (7322, 7772, 8307), which describe the tree they were written about.

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.

  • ONE assertion over grid self-fetch rows and $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.
  • A second row: the unpaged list view and the standalone board both send $top 100, 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 $top and 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.
  • Kanban pins keep $top 100 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 declares pageSize equal to the group count.
  • ObjectGrid.pageSizeNonPositive-9853.test.tsx: the inline fixture is the spec default plus ten rows, so 12.5 still 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 because main's objectui#10881 refuses a grouped grid over a data source with no queryGroupHeaders.

Ablations (predicted before running; each through ablation-replace.mjs, restore proven by blob == HEAD and an empty git diff HEAD)

  1. Tie the list's unpaged fallback to the page size. Anchor paginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE, became DEFAULT_LIST_DISPLAY_PAGE_SIZE, (anchor 1 → 0, blob moved).
    • Predicted: list pin 7 red / 3 green, app-shell pin 1 red / 2 green.
    • Observed: 8 failed / 5 passed = the five unpaged rows, the switch-cost row, the refused-name row, and the app-shell fetch-batch row. As predicted.
  2. Hardcode the display number in place of the spec read in both ObjectGrid.tsx (const DEFAULT_DISPLAY_PAGE_SIZE = 50;) and ListView.tsx. Both anchors went 1 → 0.
    • Predicted: stubbed grid pin 4 red / 2 green, stubbed app-shell pin 1 red / 2 green, while the unstubbed list pin and groupedPagination stay green (they cannot tell a hardcoded 50 from the spec's 50).
    • Observed: 5 failed / 21 passed: the grid rows every-page, bucketed, selector and refused, plus the app-shell every-paged-surface row. All unstubbed rows green. As predicted. This is why the cross-surface pin stands in the default.
  3. Root cause of the one remaining red (below). The 10512 file 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", reads expected 2 to be 1 at head (CI shard 3/8 on a2c3e5ba7, reproduced locally).

  • The cause, proven by ablation 3: with no declared 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.
  • It is not objectui#7394's defect, which was a byte-identical duplicate request.
  • It is inherent to structure B: the two views need different windows.

The pin is untouched. The options, with their costs, are in the os-dev-report on 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 in ListView.datasetChartAppliedFilter-10512.test.tsx.

  • Every test outside those packages that names ObjectGrid, ListView or ObjectKanban, their registry types, or their source paths. The census is git grep -lE over 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-kanban and @object-ui/types, after building the grid/list/kanban dependency closure (build exit 0). tsconfig.test.json --listFiles includes every touched test file (1 each) and the ListView.tsx control.

  • 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, main versus head, read through --stdin-filename: ObjectGrid.tsx 0e/207w vs 0e/207w; ListView.tsx 0e/188w vs 0e/188w; ObjectKanban.tsx 0e/30w vs 0e/30w.

  • exit 0:

    • changeset gates: 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;
    • hygiene gates: 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;
    • mock gates: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape;
    • doc gates: check:doc-fences, check:doc-types, check:doc-example-ids;
    • check-governed-queue-guard --test on 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/spec 17.0.0 and 17.4.0, fetched from the registry, both export PaginationConfigSchema from ./ui, in index.d.mts and in index.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-filter names. The docs diff changes no fenced block and no import line, so their verdict cannot move.
    • The app-shell type-check, which needs app-shell's whole dependency closure built.
    • check:eager-closure, which needs a console build.
    • The full lint and test farm.

Acceptance notes (not carded)

  • The display default follows the installed spec. @object-ui/plugin-grid declares @objectstack/spec ^17.4.0 and @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.
  • The earlier pending changeset of this card, .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.
  • A declared pagination.pageSize still caps unpaged list views and a bound board, as before. Ruling 5824040487 splits only the undeclared fallback, and this PR reads it that way.
  • Carried from the previous body, still true: the page-size useState seeds are read only at mount. The grouped view's $skip is (serverPage - 1) * fetchWindow, which now matters only on the bucketed-window path.
  • Commit 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 (head 5a2d1e66a)

The maintainer answered the Open question above with A: accept the one extra fetch. Ruling record 5909000462 on the card records the ruling verbatim. The first line of this body is now the closing keyword. The paragraph under it that explains Part of, and the Open question and Gates sections above, describe head a2c3e5ba7 and are superseded by this section. They are left as written.

  • Merge. origin/main at 574d82fdf, merged as commit 118cdbe5a (no rebase, no force-push, no pushed commit rewritten). Git reported no conflict hunk. Both sides touched five files (ObjectGrid.tsx, the plugin-grid README.md and plugin-grid.mdx, and types objectql.ts and objectql.zod.ts), and all five auto-merged. The one ObjectGrid.tsx change from main (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.
  • Flipped row (commit 39994581a), in ListView.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".
    • The first query's $top is read against PaginationConfigSchema.parse({}).pageSize.
    • The one further query's $top is read against DEFAULT_LIST_FETCH_BATCH_SIZE. ListView.tsx now exports that constant as a module export; the package index does not re-export it. No literal 100 appears in the row.
    • The further query equals the one on screen with only $top changed, so nothing the chart withholds reached it.
    • A comment cites ruling record 5909000462 by id.
  • New control, same file: "CONTROL (objectui#7394 §1): with nothing held and a pageSize declared, the switch to the dataset chart re-issues no query". With a declared 7, the first $top is 7 and no further query follows. objectui#7394's own §1 (ListView.viewSwitchRefetch-7394.test.tsx) pins the same for grid to kanban.
  • No other assertion in the file changed. The diff removes three lines: the old row's title, one header bullet and one import line.
  • Changeset (commit 5a2d1e66a). The plugin-list bullet now says the switch issues one request, whose $top is 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-overwrite green). check:changeset-claims cannot flag that note, because it names no file path.
  • Ablation. Predicted before running: the flipped row goes red on its $top assertion, and the other six rows stay green.
    • Mutation: ablation-replace.mjs replaced paginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE, with DEFAULT_LIST_DISPLAY_PAGE_SIZE,. The anchor count went 1 → 0 and the blob moved 5789e6b50742 → 85a9025b2d66.
    • Observed: Tests 1 failed | 6 passed (7), with AssertionError: expected [] to deeply equal [ 100 ] at the $top wait. As predicted.
    • Restore: the blob equals HEAD's and git diff HEAD is empty.
    • No build was involved: the test imports ../ListView from source.

Gates at head 5a2d1e66a, run after the last commit. Exit codes were captured before any pipe.

gate exit reading
vitest packages/plugin-grid/ packages/plugin-list/ packages/plugin-kanban/, 4 shards (vitest list counts the same 345 files) 0 ×4 345 files, 3207 tests passed
cross-package union: round 1's 320 paths, plus the three packages/types pins main added that name these views 0 ×5 parts 321 test files, 6638 passed, 9 skipped
type-check plugin-grid / plugin-list / plugin-kanban, after the dependency-closure build (exit 0) 0 / 0 / 0 tsconfig.test.json --listFiles includes the 10512 file
lint (eslint .) plugin-grid / plugin-list / plugin-kanban 0 / 0 / 0 0 errors each; ListView.tsx 0e/188w (as in round 1), the 10512 file 0e/0w
check-changeset-presence, -no-major, -fixed, -overwrite 0
check:changeset-claims 0 report-only
check:pending-changeset-literals 0
check:control-bytes 0
check:new-line-citations 0 0 new
check: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-ids 0

NOT MEASURED this round: check:spec-floors, check:doc-snippets, check:doc-examples and check: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/spec import is in a test) and touches no doc outside .changeset/.


Generated by Claude Code

…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
@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests labels Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 48 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/4730-retire-dead-locale-key-batch.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • Superseded twin vocabularies. cellRender.* and rowAction.* duplicated a grid.* vocabulary that won. RowActionMenu.tsx is fully i18n-wired and reads grid.openMenu / grid.edit / grid.delete; ObjectGrid.tsx reads grid.empty / grid.yes / grid.no / grid.systemFields. The twins had no reader on either side. - Labels that outlived their control. calendar.agenda labelled a view mode b55a34647 retired from CalendarViewMode (now 'month' | 'week' | 'day'). home.quickActions.createApp*, layout.systemNav.createApp, actionDialog.defaultActionTitle / .ok and grid.bulk.selectPlaceholder sit in namespaces whose consumers are live and wired but demonstrably read other siblings. - Surfaces that left the product. map.* is the strongest form: @object-ui/plugin-map declares no @object-ui/i18n dependency and contains no t() call at all, so it cannot consume a locale string. home.stats.* and recordDetail.viewersTooltip name surfaces nothing renders.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

  • names src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

.changeset/6597-retire-fieldmeta-referenceto.md

  • names plugin-grid/src/ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    No authoring story survived the search either. ObjectGrid's own relational-meta pass-through (applyRelationalMeta, plugin-grid/src/ObjectGrid.tsx) copies reference_to / reference / display_field / etc. from the SCHEMA field def only, at all three of its call sites — never from an authored column override. No doc, example, or fixture in this repo shows a table column pinning a lookup's target away from what its schema field already says. Under the maintainer's standing startup-stage rule (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no measured demand selects withdraw.

.changeset/6650-listcolumn-wrap-implemented.md

  • names packages/plugin-grid/README.md → packages/plugin-grid/README.md — edited by this change

    @objectstack/spec declares ListColumn.wrap and describes it to authors as "Allow text wrapping", and packages/plugin-grid/README.md shows it in its authored-column example. No renderer anywhere implemented it. Long cell text stayed clipped to one line, with no error, no warning and no feedback of any kind — a promise made at authoring time and silently broken at render time.

.changeset/6940-rowactions-boolean-mirror.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The list view's same-named rowActions in zod/objectql.zod.ts — z.array(z.string()), the legacy bare-name action list on ObjectGridSchema — is a different key that is correct as it stands, is in parity with its own TS twin (rowActions?: string[]), and is not touched.

.changeset/7070-no-invented-gantt-date-fields.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7113-chart-data-model.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7200-object-form-section-style-keys-undeclared.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The authored-metadata type now agrees with @objectstack/spec, whose FormSectionSchema is a strict object declaring neither key, and with the ruling's rationale (maintainer 2026-09-01, verbatim): "retire the reads … Declaring the keys was weighed and not adopted: it would formally invite free Tailwind strings into authored metadata, the exact class the boundary exists to keep out." A ?: never tombstone was not used: ObjectFormSection has no zod mirror (ObjectFormSchema in zod/objectql.zod.ts does not declare sections), so there is no parse door to refuse at, and a tombstone is still a declaration in completion and in the published .d.ts.

.changeset/7265-types-user-filter-field-derives.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    zod/objectql.zod.ts declared two schemas under names @objectstack/spec/ui already exports. They were triaged separately, by reading their sites, and went different ways.

.changeset/7322-object-kanban-component-props.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    Inside ObjectKanban.tsx, three schema-key reads drop their as any: titleField (two sites, now honest because the object-kanban arm declares it) and cardFields / cardTitle (already declared; the casts were redundant). (schema as any).navigation stays — navigation is declared on neither face, so removing the cast would change nothing but the spelling of an index-signature read.

.changeset/7322-object-kanban-group-by-limit.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

  • names packages/plugin-kanban/src/ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What was measured, on this branch's base (53ded82b). packages/plugin-kanban/src/ObjectKanban.tsx reads schema.groupBy at thirteen sites (lane materialisation at :601 / :625 / :640, card moves at :747 / :865, and their effect deps) and schema.limit at two (:264, $top: schema.limit ?? DEFAULT_KANBAN_LIMIT, and the effect deps at :291). groupField has ZERO read sites anywhere under packages/plugin-kanban/ — against a control of those thirteen groupBy reads in the same query, so the zero is a reading, not a blind grep. Yet the declaration REQUIRED groupField and declared neither groupBy nor limit. Measured from source: the documented, tested, working shape — { type: 'object-kanban', objectName, groupBy, limit } — failed ObjectKanbanSchema.safeParse and safeValidateSchema on the missing groupField, and only ever reached the renderer through BaseSchema's [key: string]: any and .passthrough(), admitted unexamined. An author who followed the declaration wrote groupField and got a board that grouped nothing, with no diagnostic on either face.

.changeset/7352-drill-down-config-mirror.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    DrillDownConfigSchema is the zod mirror of DrillDownConfig, and both declarations that carry drillDown reference it — ChartSchema (zod/data-display.zod.ts) and ObjectDataTableSchema (zod/objectql.zod.ts) — so the published validator under @object-ui/types/zod reads the key for the first time (objectui#7352).

.changeset/7363-objectql-union-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7632-shared-record-source-config.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    That ladder is published contract on both faces — packages/types/src/objectql.ts and its zod mirror both ship .describe() strings naming getDataConfig's order (77cb489b4, maintainer ruling 2026-09-02), pinned by objectql-record-source-refinement-6939.test.ts — and it was hand-copied into five plugin components with no gate holding them together. A change to the ruled order had five edit sites and nothing that noticed a missed one; that is the AGENTS.md #0.1 drift class.

.changeset/7712-kanban-calendar-filter-input.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    ObjectKanban.tsx sends the authored key to the query as $filter: schema.filter and ObjectCalendar.tsx does the same, and @objectstack/spec's ComponentPropsMap declares filter on both blocks (measured: safeParse accepts it, and refuses an undeclared key by name on the same call). But none of the four registrations that publish those two renderers listed filter in inputs, and sdui-parser's validateTree reports unknown-prop for every key no inputs entry claims. So an author writing the one spelling that WORKS was told it was unknown — objectui#6678's shape, where a correct write draws the same diagnostic as a write that does nothing. That is worse than an inert key: it actively punishes the correct behaviour, and the honest response to it is to delete working metadata.

.changeset/7762-object-grid-export-options-bare-array-refusal.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    What was measured, on this branch's base. The mirror declared NO exportOptions member at all, and BaseSchema is .passthrough(), so ObjectGridSchema.safeParse({ type: 'object-grid', objectName: 'accounts', exportOptions: ['csv', 'xlsx'] }) returned success: true with the array back VERBATIM — as did { formats: ['csv', 'pdf'], compression: 'gzip' }. Nothing on the render path parses, and ObjectGrid.tsx reads schema.exportOptions?.formats and only that, so the authored array then lost SILENTLY to the ['csv', 'json'] default: the useEffect that warns about dropped formats reads .formats too and returns early when it is absent, while !!schema.exportOptions kept the export button on screen. An author declared ['csv', 'xlsx'] and got csv/json with no error, no warning and no console line. The two authoring faces disagreed in the direction opposite to objectui#6956's: the TypeScript interface already declared the object form only, so TS refused what zod admitted.

.changeset/7772-page-block-kanban-group-by-control.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What was measured, on this branch's base (0c8dbc49). BLOCK_CONFIG['object-kanban'] offered exactly four controls — objectName / groupField / titleField / cardFields. ObjectKanbanSchema.groupBy is REQUIRED on both published faces (objectql.ts:2765 groupBy: string, no ?; objectql.zod.ts:1001 groupBy: z.string(), no .optional()) and had no control at all, while groupField — the only control able to set grouping — has been a retirementTombstone() on the Zod face and ?: never on the TS face since objectui#7322. ObjectKanban.tsx reads schema.groupBy at thirteen sites and groupField at zero (control: those same thirteen hits in the one query, so the zero is a reading).

  • names previews/__tests__/block-config.test.ts → packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts — edited by this change

    Run against the node those four controls produced, ObjectKanbanSchema.safeParse reported two issues, not one: groupBy "expected string, received undefined", and groupField carrying the retirement guidance verbatim. So this panel could not author a valid object-kanban node however it was filled in, and the board it produced grouped nothing with no diagnostic on any face. Both halves are now pinned against the schema itself rather than against a spelling, in previews/__tests__/block-config.test.ts — scripts/check-designer-field-key-parity.mjs judges PAYLOAD_SHAPES and does not read BLOCK_CONFIG, so nothing mechanical was watching this table.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    The object-kanban renderer never read it: groupField has ZERO hits anywhere under packages/plugin-kanban/, against a control of thirteen schema.groupBy read sites in ObjectKanban.tsx from the same query — so the zero is a reading, not a blind grep. The write was inert; the board grouped by groupBy and groupField rode along unread.

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

  • names packages/plugin-list/README.md → packages/plugin-list/README.md — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7780-object-kanban-record-source.md

  • names packages/plugin-kanban/src/ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    packages/plugin-kanban/src/ObjectKanban.tsx resolves a board's rows in four steps: the pre-fetched data PROP a parent passes (hasExternalData), then bind through useDataScope(schema.bind), then the inline ROW ARRAY on schema.data, and only then a fetch keyed by schema.objectName — rawData = (hasExternalData ? externalData : undefined) || boundData || schema.data || fetchedData, with the fetch itself gated on schema.objectName && !boundData && !schema.data. Every objectName read is guarded. Both published faces nevertheless REQUIRED objectName, so a bind-only or data-only board — one that renders correctly today — was refused by the shipped validator and could not be annotated with its own type.

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    groupBy is untouched and stays REQUIRED (objectui#7322, PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774). A record source and a lane key are different questions: every lane-less document above is still refused at groupBy, and the two readings PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774 excluded from counting as a lane-less mode — the dataSource json fragment in content/docs/utilities/data-objectstack.mdx and ListView.tsx's runtime-generated node — are asserted still-refused in the new pin. The retired groupField tombstone is likewise still refused by name.

.changeset/7804-objectql-handler-key-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The four plain objectql.ts node faces declare the nine handler keys their registered renderers read (the objectql.ts slice): ObjectFormSchema.onCancel / .onError / .onOpenChange / .onStepChange / .onSuccess, ObjectGallerySchema.onCardClick / .onRowClick, ObjectGridSchema.onNavigate and ObjectViewSchema.onNavigate.

.changeset/7804-tree-view-handler-slot.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    'runtime-slot' and not 'retired', measured at this key's own channel. 'retired' publishes "no renderer reads this key, so nothing could ever run it" — true of the two siblings already tombstoned on this arm (onSelectChange, onExpandChange) and flatly false here, since the read is live and INVOKED. ⚠️ No in-repo host builds a tree-view node carrying the key: the channel is wired end to end and only the supplier is absent, which is the same shape as ObjectFormSchema.onStepChange in this card's objectql.ts slice and is not evidence of a dead read. The TypeScript declaration is unchanged and still callable, so a programmatic host supplies it exactly as before.

.changeset/7917-export-breadcrumb-object-tree-zod-schemas.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    AnyComponentSchema declares 107 node component types. 105 of them could be named on the ./zod barrel — ButtonSchema.safeParse(node), which is what a designer, a form builder or a targeted test needs. The arms declaring type: 'breadcrumb' (navigation.zod.ts) and type: 'object-tree' (objectql.zod.ts) could not: both were already export const in their own module, but index.zod.ts — the package's only zod entry point — did not re-export them, so the schemas existed, were maintained, and were applied by the union while no consumer could name them.

.changeset/7963-alert-dialog-footer-keys-retired.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Nothing else moves. These spellings are overloaded across the tree and every other owner is a live key on a different declaration — FormSchema.cancelLabel, objectql.ts's confirmLabel, plugin-designer's ConfirmDialog React props, plugin-grid's def.confirmLabel, and plugin-form's ModalForm / DrawerForm, which build a local cancelLabel from schema.cancelText. None is an AlertDialogSchema; none is touched, and a pin asserts it. No fixture, catalog schema, example app or doc fence authored any of the three on an alert-dialog node, so no shipped document is stranded.

.changeset/8174-kanban-calendar-filter-sort.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    filter had four declaration faces and only three of them named it: @objectstack/spec declares it (ComponentPropsMap['object-kanban'] and ['object-calendar']), both plugins' registration inputs publish it, and both renderers read it — ObjectKanban.tsx lowers schema.filter onto $filter, ObjectCalendar.tsx lowers schema.filter onto $filter and schema.sort onto $orderby through convertSortToQueryParams. This package's own published faces (the TypeScript interface and its zod mirror) named none of them, so an authored value reached the renderer only through BaseSchema's index signature and the mirror's .passthrough() — admitted, never examined. That is the same reasoning finding(types,plugin-kanban): ObjectKanbanSchema requires groupField (zero read sites) and declares neither groupBy nor limit — no working object-kanban node is assignable to any declared type #7322 used to move groupBy and limit into this same interface.

.changeset/8278-page-tabs-item-value.md

  • names block-config.test.ts → packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts — edited by this change

    • ComponentPropsMap['page:tabs'].safeParse returns success: false with unrecognized_keys naming key at items.0 — and 17.3.0's own message now spells the remedy out, "Did you mean key -> value?"; - PageTabsRenderer builds itemsWithValue from it.value and reads it.key nowhere, so the tab falls back to the index-derived tab-IDX; - PageTabsProps.items[].value is a real, declared schema member, which this repo's own block-config.test.ts already stated in prose.
  • names block-config.ts → packages/app-shell/src/views/metadata-admin/previews/block-config.ts — edited by this change

    The control is renamed to value on the producer side — AGENTS.md #0.1 and the rule stated in block-config.ts's own file header ("keep each field name aligned with the property name the corresponding renderer reads"). Teaching the renderer a second spelling would have been the lenient-fallback shape that rule forbids. The field name is the last segment of the i18n key, so both locale tables move with it in this one commit and the retired key leaves both, the objectui#3829 / objectui#5212 pattern.

.changeset/8279-definition-list-term-description.md

  • names block-config.ts → packages/app-shell/src/views/metadata-admin/previews/block-config.ts — edited by this change

    The controls are renamed to term / description — the producer side, per AGENTS.md #0.1 and the rule stated in block-config.ts's own file header ("keep each field name aligned with the property name the corresponding renderer reads"). Two of the three faces already agreed: the block's registry declaration names items as "Term/description pairs [{ term, description }]". The designer was the outlier, so moving the renderer instead would have put it at odds with its own declaration.

.changeset/8478-describe-line-addresses.md

.changeset/8735-objectql-mirror-docblocks-not-defaulted.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Correct four zod/objectql.zod.ts docblocks that described the behaviour objectui#8317 removed. Since that change the zod mirrors strip imported @objectstack/spec defaults at this package's import boundary, but the docblocks on HttpRequestSchema, ListColumnSchema, SelectionConfigSchema and PaginationConfigSchema still said, in the present tense, that method, prefix.type, type and pageSize are defaulted on parse — the opposite of what each export does. Each now says the key is declared and accepted but NOT defaulted on parse.

.changeset/8767-object-grid-refuses-string-sort.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Migration. Write the array: sort: [{ field: 'name', order: 'desc' }]. Both keys are required. SortConfig.order carries no ? in @object-ui/types (packages/types/src/objectql.ts) and no .optional() in its zod mirror, and the protocol's own reusable SortItemSchema requires order as well — measured: that schema refuses [{ field: 'name' }] with invalid_value at 0.order. Do not omit it: this block's array arm interpolates whatever is present, so an omitted order lowers to $orderby: 'name undefined' today. That is pre-existing behaviour on the arm this change does not touch, and it is filed as a successor card rather than widened into here.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.
  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8802-8257-8008-kanban-gantt-family-retirement.md

  • names content/docs/plugins/plugin-kanban.mdx → content/docs/plugins/plugin-kanban.mdx — edited by this change

    The diagnostic's remedy text moves from a tag to a component. It used to end "render ANGLE-BRACKETS(kanban-ui) from a React host that passes onQuickAdd". That sentence is falsified by this change: kanban-ui is no longer a node type key, so a page written to the old advice draws unknown-component. It now names KanbanRenderer from @object-ui/plugin-kanban — still exported, still forwarding both halves by identity — which is the surviving way to get the pair. content/docs/plugins/plugin-kanban.mdx says the same thing the same way.

.changeset/8827-kanban-empty-state-settled.md

  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    A genuinely empty board still paints DataEmptyState. The reverse regression — "gating on a truthy definition would leave those boards empty forever", named in ObjectKanban.tsx itself — is what the settle contract exists to prevent, and every exit settles: the query succeeding, the query throwing, no readable source, the non-fetch record sources (external, bound and inline data are settled from the first frame), and the schema-only kanban-ui entry, which has no ObjectKanban and therefore no provider and so takes the context's settled default. Lanes, headers, counts and drop targets keep rendering while the rows are in flight; only the claim is withheld.

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/8913-object-kanban-columns-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

  • names packages/types/src/zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.
  • names ObjectKanban.tsx → packages/plugin-kanban/src/ObjectKanban.tsx — edited by this change

    What a lane-less board does, measured rather than assumed. Every schema.groupBy read in ObjectKanban.tsx is a guarded early-return, so the board degrades instead of breaking: with no lane key and no columns it renders an empty board; with bare-string columns it draws those lanes, titled by the raw strings; card moves are inert (persistCardMove and the move callback both open if (!groupBy) return). ⚠️ Every lane-less board holds zero cards — bucketCardsIntoColumns returns before distributing records when there is no lane key — so omitting groupBy is not a way to configure a board, it is a board that groups by nothing.

.changeset/8992-user-actions-collapse-and-docblock.md

  • names objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    objectql.zod.ts's UserActionsSchema read stripImportedDefaults(Spec).extend({ group, hideFields, rowColor }), an extension that existed only because @objectstack/spec did not declare those three keys while normalizeListViewSchema folded objectui's legacy showGroup / showHideFields / showColor onto them. The protocol adopted all three in 17.3.0 (objectui#5435's ruling), so the extension is now a second local copy of a protocol declaration — the shape two faces start drifting from — and it collapses into the plain by-reference re-export its own note always said it would become.

.changeset/9092-inline-locale-declared-face.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    • AppComponentSchema.label (app.ts) - ObjectGridSchema.label and .description (objectql.ts) - PageNodeSchema.aria.ariaLabel (layout.ts)

.changeset/9309-object-gallery-filter-destination-typed.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

.changeset/9511-record-id-is-a-string.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

.changeset/9549-tree-filter-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

.changeset/9550-object-tree-root-barrel.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectQLComponentSchema declares the node types an ObjectQL block may be. Every one of its arms was a named export of this package's root barrel except ObjectTreeSchema, which was declared in objectql.ts, applied by the union, and re-exported by the ./zod barrel (objectui#7917) — while no TypeScript consumer could name it. There is no ./objectql subpath to reach around the barrel: the package's exports map is pinned by packages/types/src/__tests__/package-exports-manifest.test.ts, and the root barrel was the only route to this type.

.changeset/9606-object-kanban-card-title.md

  • names zod/objectql.zod.ts → packages/types/src/zod/objectql.zod.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

.changeset/9628-kanban-column-collapsed-honoured.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The key was declared on both published faces of the object-kanban arm — the lane element of ObjectKanbanSchema (objectql.ts and its Zod mirror) and the runtime lane KanbanColumn (complex.ts and its mirror) — and read by KanbanEnhanced alone, a module no production source imports. An authored { "id": "todo", "title": "To Do", "collapsed": true } therefore parsed green on both faces and reached a board that did nothing with it: KanbanImpl's only collapse is the SWIMLANE row's, held in viewer state under objectui:kanban-collapsed:ANGLE-BRACKETS(swimlaneField) and never keyed to a lane's declared value. That is the ADR-0049 declared-but-unhonoured shape.

.changeset/9722-bulk-executor-datasource-face.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    • ObjectGrid.tsx → RecordDetailPanel: removing it type-checks GREEN. It was paying for nothing — both sides already declare the same DataSource | undefined from @object-ui/types. Removed outright. - ObjectGrid.tsx → BulkActionDialog: removing it reddens, and it was paying for TWO things at once — the optional-vs-required arm (the grid declares dataSource?: DataSource, the dialog demands one) AND the bulk-door contravariance. Only the first still needs erasing, so it is now a non-null assertion: today's runtime behaviour is preserved exactly, and any future drift of the face reddens at that site instead of passing silently.

.changeset/9853-grid-non-positive-page-size.md

  • names ObjectGrid.tsx → packages/plugin-grid/src/ObjectGrid.tsx — edited by this change

    Refuse a non-positive pageSize at all three of ObjectGrid.tsx's read points, instead of giving two different answers for one authored value (objectui#9853).

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with a8198de22 (merge-base with origin/main): 23 file(s) changed outside .changeset/, read against 1797 pending declaration(s) that publish a body (2405 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3037.4 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-CCKoF_Je.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.88KB 130.96KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.40KB 63.99KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 215.98KB 44.34KB
plugin-detail (index.js) 257.69KB 67.12KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.65KB 37.10KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.91KB 59.02KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 106.60KB 35.16KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 15.71KB 5.30KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

This was referenced Sep 24, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review deferred at head eab4c8e52b: this carrier cannot land on this head, so a record now would be spent on the next push. Director seat (summon #30 续) · session_01AsCNgFBs8HCjwhyHQsFbx3 · 2026-09-27T13:32Z.

  • Conflict. git merge-tree --write-tree origin/main refs/pull/10278/head at objectui origin/main 17b323e5ca reports a content conflict in packages/plugin-grid/src/ObjectGrid.tsx (mergeable_state: dirty on the PR). The head must change before any landing.
  • Blocked upstream. The display page size this PR reads from the spec lands with objectstack#20050 (spec default 25 → 50, claimed by the objectstack spec seat), then a @objectstack/spec release and this repository's pin; the PR's numbers (50 → 25, the ⚠️ in its body) are re-measured against that release in the same patch round that resolves the conflict.
  • What happens next. The owning seat's patch round rebases onto main, re-pins, and pushes; the at-tier record is rendered on that head. needs:contract-review stays on until then, as the marker says: no same-shape PASS is in place for this head.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3570.1 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-BmJPcreQ.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.08KB 62.96KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.30KB 63.29KB
plugin-kanban (index.js) 48.44KB 15.11KB
plugin-list (index.js) 116.29KB 28.91KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3571.0 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-CE-ZDf9Y.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.72KB 63.37KB
plugin-kanban (index.js) 48.44KB 15.11KB
plugin-list (index.js) 116.29KB 28.91KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3571.7 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-CM2sy1-v.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
fields (index.js) 261.19KB 66.27KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.51KB 11.36KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.72KB 63.37KB
plugin-kanban (index.js) 48.44KB 15.11KB
plugin-list (index.js) 116.28KB 28.88KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a2d1e66af11484b6adf857de3b058f69f83baa1
Local-runs: none

Inputs read, and nothing else: card #9853 (its body and all 18 comments, the rulings 5749197629, 5824040487 and 5909000462 included), PR #10278 (its body, its 24-file list, and the net diff against main at merge-base 574d82fdf), and the 43 check-runs on the head: 40 success, 3 skipped (the two coverage matrix jobs and dependabot), 0 failures. Nothing was built, run or re-run locally.

① Derived judgments

Every accept-set or public-surface change the diff implies, each named right or wrong:

  1. @object-ui/plugin-grid: the undeclared display page size is read from the spec. DEFAULT_FLAT_PAGE_SIZE, DEFAULT_GROUPS_PER_PAGE and DEFAULT_SERVER_WINDOW_SIZE are gone. Every display seed (groupedPageSize, serverPageSize, the flat pageSize) resolves through resolvePageSize to DEFAULT_DISPLAY_PAGE_SIZE = readSpecDisplayPageSize(), which is PaginationConfigSchema.parse({}).pageSize from @objectstack/spec/ui, with no local literal behind it and a throw at module load if the spec stops declaring a positive integer. Published behaviour: inline grid 10 → the spec default, grouped groups-per-page 10 → the spec default; the self-fetching grid and a server-grouped leaf's row page stay 50 because the 17.5.0 default equals the old window. Right. This is ruling 5749197629 item 2 as amended by 5824040487 item 1 ("objectui keeps reading the spec default … never hardcodes 50"), and the changeset table names every row with the remedy (declare pagination.pageSize).
  2. @object-ui/plugin-grid: the fetch batch is split from the page size. DEFAULT_FETCH_BATCH_SIZE = 50 (value kept) is read only through fetchWindow when bucketsFetchedWindow holds (not server-grouped, not refused, at least one usable grouping key left); every other fetch's $top and $skip are serverPageSize, and no display path reads the batch. Right. Ruling 5749197629 item 1 ("Fetch batch size is not page size … stays a named constant of the grid"); the predicate's re-derivation against main's server grouping is sound and is pinned by the row "a window the grid groups in the browser FETCHES the batch, which no display path reads".
  3. @object-ui/plugin-grid: the grouped size selector merges the size in force. The fixed steps [5, 10, 20, 50, 100] gain groupedPageSize when it is not one of them. A visible UI change, named in the changeset. Right, and necessary once the default may be a non-step value.
  4. @object-ui/plugin-grid: the public export surface is unchanged. ObjectGrid.tsx exports no new symbol; the package index re-exports the same names as at the merge-base; the exports map is . and ./style.css. Right that no export change is declared.
  5. @object-ui/plugin-list: the undeclared fallback splits by whether the view pages. effectivePageSize now resolves after paginate, with paginate ? DEFAULT_LIST_DISPLAY_PAGE_SIZE : DEFAULT_LIST_FETCH_BATCH_SIZE; the display default is the same spec read as the grid's, the batch is 100 (value kept). Published behaviour: the paged grid view goes 100 → the spec default (rows on screen and $top); every unpaged kind and a grouped grid keep $top 100; a declared pagination.pageSize still sizes every kind; with nothing declared, a paged-to-unpaged switch re-issues one fetch whose $top is the batch. Right. Ruling 5824040487 item 2 verbatim, and the switch cost is what ruling record 5909000462 (A) accepted; all four facts are stated in the changeset's plugin-list bullet and table.
  6. @object-ui/plugin-list: the refused-pageSize diagnostic names the fallback actually used. describeRefusedPageSize gains a paged argument; module-private; a runtime string change. Right, named in the changeset.
  7. @object-ui/plugin-list: export const DEFAULT_LIST_FETCH_BATCH_SIZE. A module export of ListView.tsx. The package index does not re-export it and exports is . → dist/index only, so it is unreachable from the published entry and is not an accept-set change (the published face is the exports map, not shipped bytes). Right that the changeset does not claim it as public and that it carries no semver weight.
  8. @object-ui/plugin-kanban: DEFAULT_KANBAN_LIMIT becomes DEFAULT_KANBAN_FETCH_BATCH_SIZE, 100 unchanged. Same reasoning as 7: exported from ObjectKanban.tsx, never re-exported by the index, exports is . and ./style.css, so no published symbol moves; the changeset still states the FROM → TO, which is correct prose and costs nothing. The refused-limit diagnostic's prefix changes from "row cap" to "fetch batch" (a runtime string; the objectui#9925 pin's filter moved with it). Right. Ruling 5824040487 item 2 ("kanban's limit default … renamed to a fetch-batch size, value 100 unchanged").
  9. @object-ui/types: describe text and TSDoc only. ObjectKanbanSchema.limit stays z.number().int().positive().optional(); only the .describe() string and the @default docblock swap the constant's name. The accept set is unchanged; the published doc-string cites an internal constant of another package. Right that no separate changeset line is owed: it changes nothing an author can write, and the fixed group in .changeset/config.json versions @object-ui/types together with the three declared packages regardless.
  10. @object-ui/app-shell: one comment in block-config.ts and four pins (one new). No published behaviour; correctly absent from the changeset frontmatter. Right.
  11. A new runtime dependency edge. PaginationConfigSchema is imported at module scope from @objectstack/spec/ui in plugin-grid and plugin-list, and the read runs at load. The floors (^17.4.0 on plugin-grid, ^17.0.0 peer on plugin-list) are not raised. Judged under ③ item 6.
  12. Docs (content/docs/plugins/plugin-grid.mdx, packages/plugin-grid/README.md, packages/plugin-list/README.md, content/docs/plugins/plugin-kanban.mdx). Each sentence was read against the code paths above: the browser-bucketed window as "not a page" and a fetch batch of the grid's own; the grouped page of groups and a group's own row page following the display default; the unpaged list window as a fetch batch of 100 with the dataLimitReached bar; the board's default not following the display page size. Right. The #2212 → objectstack-ai/objectstack#2212 qualification held for this PR in carrier note 5883668762 is carried.
  13. Pins. Standing in the spec default (vi.mock('@objectstack/spec/ui') with importOriginal plus spread, the default replaced by 13) in the grid pin and in the cross-surface app-shell pin is the right instrument: at 17.5.0 the display default equals the grid's fetch batch (50), so an unstubbed pin cannot separate "reads the spec" from "hardcodes 50" (the PR body's ablation 2 shows exactly that), and the first row of each file proves the stub is live. The unstubbed list pin and groupedPagination.test.tsx derive every fixture from PaginationConfigSchema.parse({}).pageSize; no literal 50 sits in any assertion; the Inert vi.mock Specifier Check on the head is success. Right.

Nothing in the diff widens or narrows what an author can write.

② Semver level

  • Changeset .changeset/9853-grid-display-page-size-from-spec.md: @object-ui/plugin-grid, @object-ui/plugin-list, @object-ui/plugin-kanban at minor. Under objectui AGENTS.md's version-alignment rule the fixed group never declares major, and objectui's own breaking behaviour is marked minor with the breaking semantics written in the body. The body carries the per-surface before/after table measured on 17.5.0, the remedy, the load-time-throw behaviour, the one further request on a view switch, and the qualification of the pending objectui#10512 note. Matches what the diff publishes; skip-changeset would be wrong here and is not used.
  • The Clause-②: line in the PR body reads Clause-②: yes — the undeclared display page size changes on three surfaces …. Value yes, no (widening) or (narrowing) arm. Right: published rendering behaviour moves, which ruling 5749197629 itself declares as "Clause-②: yes (published rendering behaviour)", so yes is owed and minor is the floor it takes; no accept set moves (judgments 7 to 9), so no arm is owed. Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report on this head are all success.

③ Boundary flags

Every dev flag and every open_questions entry on the card and in the PR body, answered or escalated:

  1. Report 5906747490, open question (the objectui#10512 CONTROL row reds under structure B): answered by ruling record 5909000462 (A) and executed on this head. The row is flipped to "exactly one further query, whose $top moves from the spec display default to the fetch batch", both numbers read by name rather than as literals, the further query proven equal to the one on screen with only $top changed; a declared-pageSize CONTROL keeps the objectui#7394 guarantee; the body's ablation (collapse the split, the flipped row alone reds) is the right proof. Closed.
  2. Report 5909804708: no open questions. Its deviations: (a) DEFAULT_LIST_FETCH_BATCH_SIZE as a module export, answered in ① item 7, not public; (b) this PR's own changeset qualifies the pending objectui#10512 note, right and bounded: the objectui#10512 changeset is untouched (Changeset Overwrite Report green) and without the qualification the release would publish a sentence this head makes false; (c) the new declared-pageSize CONTROL, right; (d) sharded local runs and (e) extra gates, not contract matters; the head's check-runs are the verdict.
  3. Report 5906747490, deviations: the surface widening to the kanban constant's readers in types and app-shell is a mechanical rename with no behaviour (① items 9 and 10), accepted; the model-named co-author trailer on pushed commit 43d21e6b1 falls under objectui AGENTS.md's exemption for a harness-written trailer, and pushed history is not rewritten, accepted; the spec-default stand-in in two pins, answered in ① item 13; dispatch premise 3 overtaken by main's server grouping, answered in ① item 2.
  4. Report 5816889745, open question (kanban and list are not "a page of records"): answered by ruling 5824040487 (structure B) and executed here. Closed.
  5. PR body acceptance notes: a declared pagination.pageSize still caps unpaged list views and a bound board, which is ruling 5824040487 item 2 read correctly ("A DECLARED pagination.pageSize still sizes this fetch"); the earlier pending changeset 9853-grid-non-positive-page-size.md saying the default question "is handed back on the issue" is history, and the same release carries this entry that answers it; the page-size useState seeds read at mount only, and the grouped $skip multiplier, are pre-existing and outside this card, correctly left alone.
  6. Escalated, not blocking: "the floors are not raised" (@objectstack/spec ^17.4.0 on plugin-grid, ^17.0.0 peer on plugin-list). The module-load read throws when the resolved spec declares no positive default. From the inputs, a positive default is established at 17.4.0 and above (ruling 5749197629 quotes .default(25) while 17.4.0 was the latest release, and 17.5.0 answers 50 per 5904205172); the dev verified only that the symbol exists at 17.0.0, so whether a host that pins @objectstack/spec to 17.0.0 through 17.3.x with plugin-list would throw at load is not established. The exposure is narrow (this fixed group's own lockfile resolves 17.5.0, and the console already floors at ^17.4.0 through plugin-grid), so it is a follow-up for the owning seat, raise plugin-list's peer floor to ^17.4.0 with its own changeset or record the 17.0.0 reading, and not a reason to hold this head.
  7. Out-of-scope findings named across the reports (the changeset-claims gate cannot see a prose-only pending note; the objectui#7394 pending changeset's mechanism sentence; the ElementDataSourceGate and RelatedList readings): noted on the card by their carriers; none is falsified by this head and none is this record's to card.

Deferral 5856311218 and director note 5890944687 are discharged on this head: the conflict is resolved by merge commits with no rewritten history, main resolves 17.5.0, the body's numbers are re-measured against it, and the carrier note's #2212 qualification is carried.

Implemented-by: claude/issue-9853-one-display-page-size
Reviewed-by: session_012UwY3ahMixEFkfTUxMVkYm

VERDICT: PASS

Readings taken 2026-09-30T11:24Z.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 11:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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 main

From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T11:44Z.

  • Dequeued: 2026-09-30T11:44Z (CI_FAILURE). Merge-group commit 7a4e49af0 (main 846cec0ef + this PR).
  • The failing check: Spec Main Shape Gate, required in ruleset main. Run 36709295452, job 109866965192, compiled against objectstack main 30839063b561. It was the group's only red; Type Check and the Test shards were still running when the queue removed the PR.
  • The signature: the same 4 diagnostics in 2 files that dequeued PR objectui#11185 and PR objectui#11202, and this PR touches neither file:
    • packages/app-shell/src/providers/writeWarningToast.ts:119 TS2741 (computed missing from STRIPPED_LINE);
    • packages/data-objectstack/src/spec-symbol-batch6.test.ts:242 TS2344.
  • Why it is not this PR's: objectstack b2805465 (fix(objectql)!: a caller-supplied formula value is stripped on every write path and reported as computed, and engine.validate runs the write doors (#20805) objectstack#20834) added computed to DroppedFieldsEvent['reason']. This PR's own gate run on 5a2d1e66a compiled against an older objectstack main and was green.
  • The fix: objectui#11206, claimed by the domain:ui seat as the stop-the-bleed. No fix is published yet, so there is nothing to port.
  • Next: no re-run and no base merge now, because either would fail the same way. The contract-review record 5910185062 on 5a2d1e66a stands. Once objectui#11206's fix is on main, this seat re-arms auto-merge on this head. The merge group then carries main with the fix.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit de5d400 Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9853-one-display-page-size branch September 30, 2026 13:07
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants