Skip to content

fix(plugin-list,plugin-grid): a grouped list view under a toolbar search groups on the server, with the searched counts (objectui#11021) - #11243

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11021-listview-grouped-search
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11021-listview-grouped-search

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11021

Clause-②: no

The ListView half of objectui#11021. The grid half (PR objectui#11178, merged as 7e4fa1bb2) made a server-grouped grid send its search term on the group header query and on every group's row query. This PR makes the console's grouped list view use it. With this, all four of the card's acceptance lines hold through the list view, so the PR closes the card.

What changes

  • packages/plugin-list/src/ListView.tsx: gridOwnsGroupedFetch loses its !searchTerm carve-out and its comment. That comment gave "$search has no counterpart on the header query" as the reason, which is false against the installed @objectstack/spec plus PR objectui#11178. Under a toolbar search, a grouped grid over a data source that answers queryGroupHeaders is still handed no rows. It gets the list's term as its search host prop (an empty one too: the list owns the term), and the view's searchableFields on its node, beside the effective filter it already carried. ListView's own fetch sends that same pair as $search / $searchFields.
  • packages/plugin-grid/src/ObjectGrid.tsx, only in the three regions the claim names:
    • Host search. hostOwnsSearch / querySearchTerm: a host that passes search owns the term, under host-driven paging (unchanged) and also when the grid fetches for itself. The load effect's $search reads that term, so groupSearchOf(groupRowQuery) carries it to both group queries. With no search prop, the term is the grid's own box, as before.
    • Re-entry reset. groupRowQuery goes back to null when the grid leaves server grouping. Before, re-entering grouping asked the first header query with the query held from the last grouped phase, and painted that answer before the fresh one (see the measurements).
    • Comments. The false manualSearchOn comment is rewritten to say what the code does, and so is the manualSearch comment. ObjectGridExternalPaginationProps gains one paragraph: search is read outside host-driven paging too.
    • These regions are disjoint from objectui#9547's hunks (merged into this branch from main, and the merge was clean).
  • packages/plugin-grid/package.json: the @objectstack/spec floor goes ^17.4.0 to ^17.5.0 on both lines. pnpm install rewrote one lockfile specifier line, and the resolved version did not move (it was already 17.5.0).
  • Prose this change made false (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2): three docs and one comment said a ListView keeps grouping its own window under a search. Those are packages/plugin-list/README.md, packages/plugin-grid/README.md, content/docs/plugins/plugin-grid.mdx, and a useGroupedData.ts comment. The grid half's still-pending changeset .changeset/11021-grouped-grid-search-both-queries.md ended on the same sentence, and that paragraph is removed; this is a prose correction to a not-yet-released declaration, and its package names are untouched. Three plugin-list test comments carried the same claim and are corrected too.
  • Changeset: .changeset/11021-listview-grouped-search.md, with @object-ui/plugin-list and @object-ui/plugin-grid at patch.

Measurements of the dispatch's mechanism assumptions (all at origin/main 3c13675e5f)

  1. Carve-out: confirmed, and reproduced red first through the real ListView + real ObjectGrid, with no source edits. The new pin packages/app-shell/src/__tests__/listViewGroupedSearch-11021.test.tsx uses 186 rows in five units (86/61/31/7/1) and the term #1. The term matches 34 rows, and the view's page is 20. On base, the searched grouped list drew { Northgate Operations: 11, Northgate Plant: 9 }: the page of 20 grouped in the browser. The query's answer is { 11, 11, 11, 1 }. The result was Tests 3 failed | 2 passed (5).
  2. Channel: confirmed, one channel. ListView fed the grid no search at all before, since its flat grid is handed a window that is already searched. The grid's existing host prop search now carries the term. The measured widening beyond "server-grouped mode" is deliberate. Under gridOwnsGroupedFetch, the grid can still fetch a flat or bucketed window of its own, when the grouping key is unreadable or known-masked. Reading search only when server-grouped would drop the term there, which is a regression, so a passed search is read wherever the grid fetches for itself. Searchable fields ride the node's declared searchableFields, the key the grid's own $searchFields already reads. No new prop was added. The box stays hidden: a host that owns the term but passes no onSearchChange gets none.
  3. Re-entry staleness: confirmed and fixed. On base, the tightened grid pin (search, flat, clear, regroup) saw a header query after re-entry that carried search: '#1' and searchFields: ['subject']. A second ablation leg (below) switched the query-level check off and left only the paint check. Without the reset, both pins then went red on a painted searched header set, { 11, 11, 11, 1 }, before the unsearched one. With the reset, no re-entry header query carries the term and no searched set paints, while at least one set was sampled.
  4. manualSearchOn comment: rewritten. manualSearchOn itself does not change. A grouped grid renders no flat table, and each group's table is searchable: false, so this flag governs only the flat table.
  5. Floor: raised. The specifier moved; the resolved version stayed 17.5.0. pnpm install --frozen-lockfile answered "Lockfile is up to date".

Tests

  • New packages/app-shell/src/__tests__/listViewGroupedSearch-11021.test.tsx mounts the real ListView and the real registered object-grid. It has five cases:
    • the fixture precondition: the matches outnumber the page;
    • SEARCHED: only the groups holding matches, each count equal to its matches, each group drawing exactly those rows, and both queries carrying search / searchFields;
    • UNSEARCHED: the five whole units, with no search key on either query;
    • clearing through the toolbar box, which restores the five units, and whose last header query and each unit's latest row page carry no term;
    • re-entry: grouping off, the term cleared, grouping on. No header query after re-entry carries the term, and no searched group set is painted.
  • plugin-grid serverGroupedSearch-11021.test.tsx: the clearing case now asserts no stale ask and no stale paint on re-entry. It used to tolerate both, in a comment. A new case covers a host's search prop driving both queries and clearing them.
  • plugin-list: ListView.groupedGridOwnsFetch-7189.test.tsx gains the searched handoff (no rows; search and searchableFields equal to the list's own $search / $searchFields; the filter unchanged) and an ungrouped control (a searched window, no term handed). In ListView.groupingNeedsHeaderQuery-10881.test.tsx, the case that pinned "a grouped grid handed a searched window" pinned the very branch this PR deletes. It is replaced by the same question asked of the new behaviour: no window and no host paging.

Ablation, one-shot, from committed 620c07cd76, through objectstack's scripts/ablation-replace.mjs. Every mutation was proven on disk (anchor x1 to x0, replacement x0 to x1, blob moved). Every restore was proven by the blob equal to HEAD and an empty git diff HEAD, and no permanent test file was left. The pins resolve @object-ui/plugin-grid and @object-ui/plugin-list through the vitest alias to src, so no dist/ is in the path.

  • Leg A, the reset line removed: Tests 2 failed | 7 passed (9). The grid clearing case and the list re-entry case went red on a re-entry header query carrying search: "#1".
  • Leg A-paint, the reset removed and both query loops skipped: Tests 2 failed | 7 skipped (9), both on expect(counts).not.toEqual(SEARCHED).
  • Leg B, && !searchTerm restored: Tests 5 failed | 10 passed (15). SEARCHED, clearing and re-entry through the list failed, and so did the 7189 searched handoff and the 10881 replacement.
  • Leg C, host search read only under host paging: Tests 4 failed | 5 passed (9). The grid host-search case failed, and so did the three searched list cases.

Gates

Branch head cee5c02158: 620c07cd76 merged with main at dded788ada, which brought in objectui#9547. Every exit was captured before any pipe. Heavy runs went through the shared verify lock.

At cee5c02158:

  • pnpm install --frozen-lockfile: exit 0, "Lockfile is up to date".
  • pnpm turbo run build --filter='@object-ui/app-shell^...' --concurrency=2: exit 0, 28/28 tasks. This closure contains both @object-ui/plugin-list^... and @object-ui/plugin-grid^....
  • Type-check, pnpm --filter @object-ui/plugin-grid type-check && … plugin-list … && … app-shell …: exit 0. Each run echoed tsc --noEmit && tsc -p tsconfig.test.json.
  • pnpm exec vitest run packages/plugin-grid/: exit 0, Test Files 174 passed (174), Tests 1596 passed (1596).
  • The pins plus the importer set, 14 files: exit 0, Tests 148 passed (148). That is the new app-shell pin, the six changed plugin-list files, displayPageSizeFromSpec-9853, four plugin-view ObjectView suites, two app-shell ObjectView suites and data-objectstack queryGroupHeaders-7189.
  • The 18 suites that scripts/markdown-test-inputs.mjs --list names as readers of the five changed markdown files (doc-version-claims.test.ts among them): exit 0, Tests 841 passed (841).
  • Exit 0:
    • check-changeset-presence ("10 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)");
    • check-changeset-no-major, check-changeset-fixed;
    • check-changeset-claims and check-changeset-overwrite (both report-only);
    • check:pending-changeset-literals, check:installed-pin-claims, check:control-bytes;
    • check:new-line-citations ("0 new citation(s)");
    • check:doc-fences, check-doc-links ("Links are valid across 17 scan roots"), check-doc-expression-carriage, check:doc-types, check-doc-example-ids;
    • check-lockfile-integrity ("VERDICT clean"), check-lockfile-dedupe ("VERDICT deduped"), check:phantom-deps;
    • check:test-path-roots, check-vi-mock-specifiers, check:unreferenced-sources, check-type-check-coverage, check-lint-coverage, check:spec-symbols;
    • markdown-test-inputs --audit;
    • check-governed-queue-guard --test over the changed paths ("An ordinary pull request").
  • NOT MEASURED:
    • check:spec-floors exited 1 on 5 no-artifact findings, for app-shell, cli, plugin-gantt, plugin-timeline and plugin-tree: packages outside this diff that this worktree did not build. That is a prerequisite refusal, not a verdict. plugin-grid, whose floor moved, was built and judged, with no finding.
    • check:readme-exports exited 1 on 88 "./dist/index.d.ts is not on disk" refusals, none of them for the two READMEs changed here. This diff changes 0 fence lines in any doc.

At 620c07cd76 (the same diff before the merge):

  • pnpm exec vitest run packages/plugin-list/, in two shards: exit 0 both times, 55 + 54 files, 387 + 814 tests. The merge changed no plugin-list file.
  • pnpm exec vitest run scripts/__tests__/, in two shards: exit 0 both times, 177 files passed and 2 skipped of 179, 5371 tests passed and 2 skipped.
  • tsc -p tsconfig.test.json --listFiles counts: the new app-shell pin appears once, the grid pin once, and the five changed plugin-list suites once each.
  • eslint on the 10 changed TypeScript files: 0 errors. Compared per rule with base, the only delta is +1 react-hooks/set-state-in-effect in ObjectGrid.tsx, from the reset effect. The config is not type-aware, so no untouched file's verdict can move.

CI is not waited on; its verdict is the merge queue's.

Deviations from the declared file surface (each forced by what the change made false, or by the pin the dispatch asks for)

  • The real-ListView-plus-real-ObjectGrid pin lives in packages/app-shell/src/__tests__/, not packages/plugin-list/src/__tests__/. plugin-list declares no plugin-grid edge, so a pin there would need either a vitest.config.mts heavyDomTests entry (a root config that dozens of scripts/__tests__ suites read) or an undeclared test import (check:phantom-deps). app-shell already declares both packages, the route displayPageSizeFromSpec-9853.test.tsx takes.
  • The docs, the useGroupedData.ts comment, the pending grid-half changeset and the three test comments listed above.

Acceptance notes

  • ObjectGrid.tsx gains one react-hooks/set-state-in-effect warning, from the reset effect. The file carries that pattern already (its setSearchTerm('') reset on an object change), and eslint reports 0 errors. Every other changed file's per-rule warning set is identical to base.
  • Commits 7470afd89b and 620c07cd76 end with a Co-Authored-By trailer that carries a model name, copied from the harness's attribution reminder. The model-free pair is what AGENTS.md asks for, and the merge commit carries it. History is not rewritten: objectui forbids force-push.

Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ


Generated by Claude Code

…rch groups on the server, with the searched counts

ListView no longer takes a grouped grid's fetch back while a toolbar search
is active: the header query takes the term since @objectstack/spec 17.5.0,
so the grid that groups on the server is handed the term as its `search`
prop and the view's searchable fields on its node, and puts them on both of
its queries. ObjectGrid reads a host's `search` whenever one is passed, and
drops its held group row query when it leaves server grouping, so turning
grouping back on never asks, or paints, the query it held before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
…et, and correct the prose this change made false

plugin-grid declares @objectstack/spec ^17.5.0, the release whose group
header query takes a search (the lockfile specifier moves; the resolved
version does not). The plugin-list and plugin-grid READMEs, the plugin-grid
docs page, a useGroupedData comment, three plugin-list test comments and the
grid half's pending changeset said ListView keeps grouping its own window
under a toolbar search; they now say what the code does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Brings in objectui#9547 (ObjectGrid onNavigate prop), which touches
ObjectGrid.tsx in regions disjoint from this branch's hunks.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 16 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/5793-spec-range-floors.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on this edge after the bump. The change is to the declared floor only, which is why it is scored patch rather than minor — the same reasoning objectui#5753 used for the other direction on this dependency.

.changeset/6361-spec-floor-17-2-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    Nothing a consumer installs today changes: normal resolution already picks the newest 17.x, and pnpm-lock.yaml still resolves 17.2.0 on both edges after the bump — only the recorded specifier: moves. No source and no behaviour changes, which is why this is scored patch, on the reasoning 111741454 used for the same remediation on @object-ui/plugin-detail.

.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/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/7122-objectstack-family-17-3-0.md

  • names pnpm-lock.yaml → pnpm-lock.yaml — edited by this change

    @objectstack/client, core, formula and lint each pin @objectstack/spec EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO copies of it. Moving the family with it in pnpm-lock.yaml collapses the duplicate; every declared range already admitted 17.3.0, so no manifest moved.

.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/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/7773-kanban-adapter-groupfield-write.md

  • 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

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

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

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

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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3577.1 KB 3607.4 KB
Main entry chunk (gzip) 149.8 KB 350 KB
Entry file index-C4jWnC8D.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) 569.02KB 136.04KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.91KB 63.54KB
fields (index.js) 261.17KB 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.88KB 11.46KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 216.32KB 44.56KB
plugin-detail (index.js) 242.01KB 63.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.07KB 44.44KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.88KB 63.41KB
plugin-kanban (index.js) 48.46KB 15.12KB
plugin-list (index.js) 116.42KB 28.89KB
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.20KB 3.89KB
plugin-view (index.js) 90.32KB 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.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 21.42KB 7.05KB
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: cee5c021584685b93644160988e69f39f540adc6
Local-runs: none

Inputs: card objectui#11021 (body and all 11 comments), PR objectui#11243 (body, 17-file list, net diff against main at the merge-base dded788ada), the 44 check-runs on the head, and the installed spec source at objectstack 0f6dcac5e9 (the @objectstack/spec@17.5.0 tag). Governing text: triage direction 5878389215 (the term on BOTH queries, never one), objectui#7189 ruling A, seat 2's in-seat answers at 5906660062 (Q1 = A, Q2 = A), the claim 5912520655 (Clause-②: no). Out of scope and untouched: @object-ui/data-objectstack (not in the file list) and the spec's compileListViewGroupQuery, which at the tag takes where and depth only; the grid spreads the pair onto the compiled query itself, as the grid half already did.

① Derived judgments

(a) One channel, both queries — RIGHT. ListView.tsx hands the grid search: searchTerm only under gridOwnsGroupedFetch (head :5204-5209) and, under the same predicate, the view's searchableFields on the grid node (:3333-3339) beside the effective filter it already carried. Its host-paging block (paginate && serverTotal != null) passes no search, and {...props} is ListView's own rest props: no second channel. In the grid, hostOwnsSearch = externalManualPagination || hostSearch !== undefined and querySearchTerm feeds the load effect's one $search line (:2678). The serverGroupedFetch branch of that effect strips only $top/$skip and stores the rest as groupRowQuery; groupSearchOf(groupRowQuery) (:3054) hands the pair to useServerGroupHeaders, which spreads it onto every depth's compiled query, and useServerGroupRows takes baseParams: groupRowQuery. One resolved object feeds both queries, so the header counts and the rows cannot disagree — the direction's ⛔ holds. The installed spec declares search and searchFields on EngineAggregateOptionsSchema beside where (data-engine.zod.ts at the tag), so the header query the adapter posts verbatim is well-formed.

The widening is correct and bounded. querySearchTerm replaces searchTerm in exactly four reads: the load effect's $search, that effect's dependency list, the setServerPage(1) dependency list, and manualSearch. The load effect is guarded if (hasInlineData) return; (:2285), so a host that hands rows — ListView's flat grid under host paging — never has the grid send $search at all: the term cannot double-apply on the host-paged path, and that grid's manualSearch reads hostSearch ?? '' exactly as before. A grid that fetches for itself with no search prop keeps its own box term, as before. The read beyond server-grouped mode is needed, not cosmetic: under gridOwnsGroupedFetch with a grouping key this principal may not read, serverGroupedFetch is false and bucketsFetchedWindow is true (:2246-2251), the grid fetches its own batch window while ListView hands it no rows — a server-grouped-only read would drop the term on that path (the dev's leg C measured this). An empty host term is safe: trimmedSearch is falsy so no $search goes out, and groupSearchOf returns {} for ''. No box appears for a host-owned term: manualOnSearchChange = hostOnSearchChange (ListView passes none) and DataTable draws no box without onSearchChange (pinned in data-table-manual-search.test.tsx); ListView's grid node also carries showSearch: false (:3285), and each group's table is searchable: false (:5523).

(b) Re-entry reset — RIGHT, both stale ask and stale paint. useServerGroupHeaders already drops to IDLE when canAsk goes false, so the stale searched set painted at re-entry only because groupRowQuery survived the flat phase: enabled went true with the held query, one stale ask, one stale paint, then the fresh answer. The reset (if (!serverGroupedFetch) setGroupRowQuery(null)) leaves groupRowQuery null at re-entry, so enabled: ... && groupRowQuery !== null holds the header hook off (and groupRows, gated on groupHeaders.headers) until the load effect resolves the current query after its awaited object read. No extra fetch: the reset writes only while serverGroupedFetch is false, when both group hooks are disabled anyway, and it does not fire on a search change while grouped (serverGroupedFetch does not move). No flash on a normal entry: the entering render shows the serverGroupsFirstLoad skeleton, as a first load always did. The dev's ablation legs A and A-paint turned both pins red without the line, and the grid pin's recordPaintedHeaders sampler now asserts no painted set equals SEARCHED.

(c) Every rewritten or removed comment states what the code does — RIGHT. manualSearchOn (:4606-4616): a flat grid over inline rows filters client-side (manualPaginationOn is false when hasInlineData and no host paging); a grouped grid draws no box (searchable: false at :5523); a server-grouped grid sends its term on both queries via groupSearchOf. gridOwnsGroupedFetch (ListView :2153-2159): the header query takes the pair beside where (spec at the tag), the grid sends it on both queries, and the grid is handed the term as search plus searchableFields on its node — the pair ListView's own fetch sends as $search/$searchFields (:2683-2688). useGroupedData.ts :383-385: ListView hands a grouped grid no window over a header-answering source, search included — true at :5196-5209. The three plugin-list test comments (7215, 7179, 7216): the declared queryGroupHeaders keeps the window fetch the projection pins read, which feeds the filter chip counts and the client export — ListView's fetch effect has no early return under gridOwnsGroupedFetch, so that window is still fetched and still feeds those readers. The ObjectGridExternalPaginationProps paragraph and the grid pin's header comment match the code.

(d) Test triage — RIGHT. The replaced 10881 case pinned data as a non-empty searched window, the branch this diff deletes. Its replacement asks round 2's own question (does ListView ever declare a host window for a grouped grid?) of the new behaviour and adds assertions — the searched state reached on the list's $search, data undefined, manualPagination and rowCount undefined, no refusal. Stronger, not weakened. The app-shell placement is justified: packages/plugin-list/package.json declares no @object-ui/plugin-grid edge in any dependency block, packages/app-shell/package.json declares both packages in devDependencies, and displayPageSizeFromSpec-9853.test.tsx already takes that route importing both. The four acceptance lines are pinned through the real ListView plus the real registered object-grid: only the groups holding matches (the Depot absent), each count equal to its matches with each group drawing those rows, both queries carrying the pair, clearing restoring the five units with the term gone from both queries, and the re-entry case. The grid pin's clearing case no longer tolerates a stale re-entry ask (it used to read the settled answer and say so), and gains a host-search case. The 7189 suite gains the searched handoff and an ungrouped control (searched window handed, no term handed).

Check-runs on the head (the gate verdicts): 44 runs; 39 success, 3 skipped (the two coverage jobs and dependabot, as on every PR), 0 failure. Concluded green: Type Check, Lint, Test (dist pins), Test shards 2, 3, 5, 6, 7 and 8 of 8, Spec Main Shape Gate, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Declaration, Changeset Overwrite Report, Changeset Claim Re-read, README Export Check, Build & E2E, Build Docs, Doc Snippet Type Check, Lockfile Integrity and Dedupe, Governed Surface Queue Guard, Line Citation Gate. Not yet concluded at this read: Test (shard 1/8) and Test (shard 4/8). Neither is treated as pass or fail here; their conclusions govern the merge.

② Semver level

Changeset .changeset/11021-listview-grouped-search.md: @object-ui/plugin-list: patch, @object-ui/plugin-grid: patch. What the diff publishes: plugin-list behaviour (the carve-out gone, the term and searchableFields handed); plugin-grid behaviour (a passed search honoured wherever the grid fetches for itself; the re-entry reset); plugin-grid's runtime dependencies floor on @objectstack/spec ^17.4.0 to ^17.5.0 (and the same on devDependencies), with the lockfile moving one specifier line and the resolved version staying 17.5.0. No export is added or removed; ObjectGridExternalPaginationProps keeps its type (search is still the same Partial Pick member) and gains one doc paragraph. Patch is right and Clause-②: no is right: no schema, prop type or export changes what it accepts or rejects. Two things ARE consumer-visible and both are declared in the changeset prose: (i) a host that passed search without the three host-paging members was silently inert before and now owns the term (its box goes unless it passes onSearchChange) — a behaviour widening on an existing optional prop that corrects a declared-but-dead configuration, stated verbatim in the changeset and in the interface doc, which is what that paragraph's changed meaning tells a consumer; (ii) the floor move is dependency metadata within the same major, declared in the changeset's last sentence, and the Changeset Bump Policy run is green. The grid half's pending changeset .changeset/11021-grouped-grid-search-both-queries.md loses only its last paragraph (a sentence about ListView this diff makes false); its package line and bump are untouched, and the Changeset Overwrite Report run is green. plugin-list's own floor stays ^17.0.0, which is right: it consumes no new spec key itself, and the @object-ui/* fixed group releases the two packages together.

③ Boundary flags

Every dev deviation in report 5916093629, answered; open_questions is empty.

  1. Pin under packages/app-shell/src/__tests__/ — justified by the dependency graph (①d); the alternative was an undeclared test import or a root config edit.
  2. Prose surface widened (packages/plugin-list/README.md, packages/plugin-grid/README.md, content/docs/plugins/plugin-grid.mdx, the useGroupedData.ts comment, three plugin-list test comments, the grid half's pending changeset paragraph) — each line read against the diff at head and true (①c, ②). The README and mdx paragraphs are identical to each other and carry no version literal; the doc-reader runs (Doc Snippet Type Check, Build Docs, Internal Docs Link Check) are green.
  3. ObjectGrid.tsx hunks beyond the three named regions — the querySearchTerm swap in the load effect and its two dependency lists, the manualSearch/manualOnSearchChange read, and the interface doc paragraph. All are the host-search read itself (the claim's second region) or the doc that read makes necessary; none touches objectui#9547's ObjectGridComponentProps/onNavigate hunks, and the merge commit cee5c02158 is a clean merge of main. Accepted.
  4. Premise 2 refinement (host search read wherever the grid fetches for itself) — judged correct and bounded in ①a.
  5. 10881 fixture triage — judged a real re-ask in ①d.
  6. Commits 7470afd89b and 620c07cd76 end with a harness-written Co-Authored-By trailer that names a model. Recorded as a boundary flag. Under objectui AGENTS.md at main (the commit-message rule, which quotes the objectstack text verbatim), a harness-written Co-Authored-By trailer is one of the two named exemptions: reporting only, not a deviation, and landed history is not rewritten — and the same file forbids force-push absolutely, so the two commits stay. The merge commit carries the model-free pair. No model identifier appears in the PR title or body, the changeset, the docs or any code comment in the diff (each says "a model name" without naming one). Not blocking.
  7. Declared narrowing after the merge (the full plugin-list suite and scripts/__tests__/ measured at 620c07cd76, not at the head) — the head's own check-runs are the verdict: six of eight test shards have concluded green; shards 1 and 4 are the two still open, named in ①.
  8. main moved after the merge — the queue rebuilds on current main, per AGENTS.md; no re-merge is owed.
  9. +1 react-hooks/set-state-in-effect warning from the reset effect — the Lint run on the head is green; the file already carries the same pattern for its own term reset.
  10. check:spec-floors and check:readme-exports NOT MEASURED locally — README Export Check succeeded on the head; no check-run named spec-floors exists on this repo, so the floor judgment rests on the dev's local run of it against the built plugin-grid (no finding) and on the Changeset Bump Policy run. Not a gate this record can re-read; flagged, not blocking.
  11. Out of scope held — @object-ui/data-objectstack is not in the file list, and no spec change was asked of compileListViewGroupQuery.

Implemented-by: claude/issue-11021-listview-grouped-search
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 17:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit d0ae5d0 Sep 30, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11021-listview-grouped-search branch September 30, 2026 18:05
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
…d displayField

PR #11243 has landed, so the three column-building comments in ObjectGrid are
in scope; they named reference_to and display_field, neither of which the
copy set carries any more.

Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…ational-target spelling objectui writes or reads; reference_to retires (objectui#11070, round 4) (objectstack-ai#11264)

Refs objectstack-ai#11070
Clause-②: yes (narrowing). `LookupFieldMetadata`,
`MasterDetailFieldMetadata`, and `DetailViewField` retire `reference_to`
and declare `reference`, and the ingestion pass no longer stamps
`reference_to` onto served defs, under the startup no-gradualism rule.
It is priced in the changeset.

Round 4 of objectui#11070, the `reference_to` round: the seat's answer A
to Q1 (`5915992755`), under the maintainer ruling on objectui#6837
(`5475017957`, 2026-08-31, verbatim:
「objectui不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀」, with the earlier question 「按
spec 拒绝面直接删不是更合理吗」). Claim `5916640555`.

`reference` is now the only spelling ObjectUI writes or reads for a
relational field's target object. The one fold that remains is the
ruling's choke point: `normalizeFieldReferenceKeys` folds a foreign
`reference_to` / `referenceTo` onto `reference` when `reference` is
absent, keeps its dev warning, and never drops a key.

## What changed

- **Types (`@object-ui/types`).** `LookupFieldMetadata` and
`MasterDetailFieldMetadata` declare `reference` typed by reference to
`@objectstack/spec`'s `FieldSchema.reference`. `DetailViewField` and its
zod mirror `DetailViewFieldSchema` move from `reference_to` to
`reference` in the same commit, so the two faces keep equal key sets.
The `form.ts` comments and the form-field mirror's comment now say that
the lookup and user widgets read `reference` only.
`strict-face-read-keys-11070.test.ts` moves `reference_to` from the
pending list to the retired list, and gains a type-level pin: both field
metadata types carry the spec member, and none of the three contracts
has a `reference_to` key.
- **Ingestion (`@object-ui/core`, `reference-keys.ts`).** The pass folds
onto `reference` and no longer stamps `reference_to`. The header prose
that described a stamp of "BOTH snake_case keys" now describes the fold,
and it records why the stamp protected nothing once both ends moved. The
leave-arm rule stands.
- **Readers read `reference` only:** `LookupField`, `UserField`,
`LookupCellRenderer` and `UserCellRenderer` (only those two reads in
`fields/src/index.tsx`), `InlineFieldInput`'s reference fallback,
`schemaDefaults`' `current_user` seed, `sectionFields`' view override,
and `MetadataProvider`'s inline-subform parent.
- **Emitters write `reference`:** `paramToField`, `RecordDetailView`,
`FlowReferenceField` (its one target line), `RecordDetailPanel`,
`RecordMetaFooter`, `RelatedList`, `fieldEnrichment`,
`buildDefaultPageSchema`, `sectionFields`, `bulkParamToField` and
`ObjectGallery`.
- **Copy sets and ledgers.** `plugin-grid`'s `RELATIONAL_META_READ_SET`
loses its `reference_to` row and the `adapter-stamped` verdict that row
alone held. `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` and
`FieldMeta` drop `reference_to`. `resolveActionParams`' unread
`RuntimeField.reference_to` member goes. `FieldDefaultsSchemaLike` pins
three members.
- **Docs and fixtures.** `lookup.mdx` (typed fences, plaintext fences,
prose, plus one paragraph on the retirement), `record-edit-modes.md`,
`plugin-form.mdx`'s field-slot row and `plugin-form`'s README. The tests
that fed `reference_to` to a widget, a cell or a served-def stub now
feed `reference`. The pins whose job is the fold, or a refusal, keep
`reference_to` as their input and assert the fold to `reference` with no
stamp back, or the refusal.
- **One changeset**, `.changeset/11070-reference-to-round4.md`, minor
with the break stated (AGENTS.md §9: never `major`). Fifteen pending
changesets that this round made false get an appended dated note, in the
append-only shape: `6837-reference-to-arm-deletion`,
`6837-gantt-tree-referenceto-arms`,
`6837-recorddrawer-invented-target-arms`,
`6694-dashboard-lookup-reference-meta`,
`6597-retire-fieldmeta-referenceto`,
`7166-retire-inert-fieldmeta-copies`,
`paramtofield-reference-rule-derives-from-core-5312`,
`6711-retire-reference-to-field`, `6874-retire-titleformat`,
`7155-converge-lookup-dialect-camelcase`,
`6528-resolve-reference-to-census`,
`7324-plugin-form-nameable-parameter-types`,
`11070-field-metadata-spec-spellings`, `10535-person-name-read-gate` and
`6875-grid-relational-meta-derive`.

### Pins that flipped, and why

These pins asserted the stamp. Each now asserts that no stamp happens,
and that the fold still does:
- `reference-keys.test.ts`: a `reference` def gains no `reference_to`;
`referenceTo` folds to `reference` only; idempotence; the map and array
containers.
- `reference-keys.legacyWarning-6837.test.ts`, case 5: "the STAMP is
unchanged" becomes "the FOLD still runs". The `referenceTo` case asserts
no `reference_to`; the `reference_to` case keeps the input key, because
the pass never drops one.
- `MetadataProvider.itemReferenceKeys-7650.test.tsx`: the first pin
asserted the stamp and now asserts its absence. The rest feed a legacy
spelling, because a `reference`-only def comes back unchanged and cannot
show that the pass ran.
- `getObjectSchema.test.ts` (data-objectstack).
- The emit-side pins that read `reference_to` off an emitted bag:
`referenceArms-6837.divergent.test.ts` (plugin-detail),
`RecordDetailDrawer.referenceArms-6837.test.tsx`,
`HeaderHighlight.editable.test.tsx`, `relationalMetaCopySet-6711` /
`-6874`, `lookupRelationalMeta-6694`, `sectionFields*`,
`paramToField.test.ts`, `resolveActionParams.test.ts`,
`bulkParamToField.test.ts`.
- New refusal pins: the lookup cell, the user cell and `LookupField`
each read no target from a `reference_to`-only def.

Pins that assert something else were not "fixed green". Three copy-set
floors, which required more than two keys in `RELATIONAL_META_KEYS`, now
require more than one, because the set is now `reference` and
`displayField`, and each floor now also names `reference`.

### Beyond the claim's file surface, stated

- `ObjectDataTable.tsx` gains
`ObjectDataTableRetiredReferenceToSnakeTombstone`, and both
ObjectDataTable test files gain a directive and a counter-control for
it. Retiring `FieldMeta.reference_to` removed the key from the derived
refusal bands' pool, and `recordFields.tsx`'s own `FieldMeta` docblock
requires that a retired member be re-refused by hand at that seam. This
is the third tombstone, beside `decimals` and `referenceTo`.
- Comment-only corrections to statements this round made false:
`auditHistoryDisplay.ts`, `deriveRelatedLists.ts`,
`useDatasetFields.ts`, `object-fields-io.ts`,
`components/custom/field.tsx`, core `ActionRunner.ts` /
`chart-series.ts` / `expand-fields.ts`, `data-objectstack/src/index.ts`,
`RecordPickerDialog.tsx`, `lookupColumnDisplay.tsx`,
`HeaderHighlight.tsx` and `ObjectGantt.tsx`, plus the stamp prose in the
gantt / tree / drawer 6837 pins.
- The named tail is folded. PR objectstack-ai#11243 landed, so the three
`ObjectGrid.tsx` relational-metadata comments now name `reference` and
`displayField` (seat message, 18:53Z). `ListView.tsx`'s two lines were
left as they are, because both are true after this round: one says
`FieldSchema` refuses `reference_to`, the other that a list column
declares neither spelling. PR objectstack-ai#11233 landed, so the `form.zod.ts`
comment, the `FormPage.tsx` comment, the `plugin-form.mdx` row and the
`FormPage.sharedFieldResolver-10179` pin were folded as well (seat
message, 19:54Z; the comment only in `form.zod.ts`, with no new
declaration). `FormPage.sharedWidgets-10179`'s lookup fixture moved
first, because that test went red on the change.

## Census, before and after

Base: merge base `615346d` for the round's own diff. Code lines are
non-comment lines in `packages/*/src`, `apps/console/src` and
`examples/*/src`, excluding tests. The typed half comes from a
TypeScript LanguageService `findReferences` over 5568 source files, with
workspace packages resolved to source.

| kind | before | after |
| --- | --: | --: |
| emitter sites writing `reference_to` onto a def or widget `field` | 11
| 0 |
| ingestion stamp of `reference_to` | 1 | 0 |
| copy sets carrying `reference_to` (grid ledger row, dashboard cell
keys) | 2 | 0 |
| reader lines reading `reference_to` (widgets, cells, inline editor,
seeding, section override, subform parent) | 10 | 0 |
| the ingestion fold's read of the legacy key (the ruling's choke point)
| 1 | 1 |
| declared `reference_to` members (field-types ×2, views, views.zod,
RuntimeField, FieldMeta, schemaDefaults ×2) | 8 | 0 |
| `reference_to?: never` refusal (tombstone) | 0 | 1 |
| typed references to a `reference_to` member: production writes / test
writes | 2 / 7 | no member left to reference |

The eleven emitters are the PM census's eleven. The type-checker census
found two of them typed (`FlowReferenceField`, `RelatedList`); the other
nine write through `any` or `Record` bags. It found no typed reader:
every reader was an untyped read.

## Runtime probe — the user-visible proof

A scratch vitest file, never committed, ran against the source at merge
base `615346d` and at head (`1972d01`, re-run at `338b19d` with
identical readings). `ObjectForm` in create mode opens its lookup
picker; `ObjectGrid` renders a lookup column with a primitive id; the
referenced record is `{ id: 'a1', name: 'Acme Corp' }`.

| case | base | head |
| --- | --- | --- |
| (a) served def with `reference` only, through `ObjectStackAdapter` |
picker queries `probe_account`; cell shows `Acme Corp`; served keys
`label, reference, reference_to, type` | picker queries `probe_account`;
cell shows `Acme Corp`; served keys `label, reference, type` |
| (a) the same def from a BYO `DataSource` | works | works |
| (b) action param `referenceTo` through `paramToField` into
`LookupField` | field carries `reference_to`; picker queries
`probe_account` | field carries `reference`; picker queries
`probe_account` |
| (c1) `reference_to`-only def served through `ObjectStackAdapter` |
folded (`reference` added); dev warning fires; picker and cell work |
the same: folded, warned, picker and cell work |
| (c2) `reference_to`-only def from a BYO `DataSource` | picker queries
`probe_account`; cell shows `Acme Corp` | **no `find` call at all; the
cell shows the raw id `a1`** |

(c2) is the break, and the changeset's BREAKING paragraph names that
host.

## Hypotheses

- **H1 holds.** Read at objectstack `origin/main` `165c1d49e`. The
example apps write `reference_to` zero times, against a control of 30
`Field.lookup` / `Field.masterDetail` calls in 19 files; both helpers
write `reference`. `FieldSchema` refuses `reference_to` by name
(installed 17.5.0: `unrecognized_keys`, "Did you mean `reference_to` →
`reference`?"), and so does the spec's form-field schema. The
`field-reference-to-alias` conversion
(`packages/spec/src/conversions/registry.ts`) rewrites stored rows on
rehydration and in `os migrate meta`. The only other hits are refusals
(the SQL and Mongo doors, verify, lint), a reader of three spellings in
`plugin-security`, and a comment in `plugin-approvals` that describes
this repo's widget key (noted below).
- **H2 holds.** Every production write is one of the eleven emitters,
the ingestion stamp, or one of the two copy sets the brief listed as
readers. No twelfth writer turned up, typed or untyped.
- **H3 holds.** On the ObjectStack path the cells receive folded defs,
as probe cases (a) and (c1) show. `RELATIONAL_META_KEYS` is derived to
`reference, displayField`, and the grid's derivation gate re-extracts
the cell's read set with `reference` in it.
- **H4:** **false as posed.** The `reference_to` pair was not in M3's
class (ii) at this base. Round 1 (PR objectstack-ai#11115) moved both `fields-lookup`
fixtures to `reference`, and round 3 reported class (ii) without it.
Measured with `scripts/measure-strict-authoring-face.mjs --json` at
merge base `af9e957` and at `338b19d`: every refusal figure is identical
(2224 nodes, 105 strict-refused, 66 strict-only, 39 red today, 599
documents, 98 refused whole-tree), and `reference_to` occurs in zero
refusals at both. The only differences are the renderer-source
word-mention counts, which the comment edits moved, and the error text
of one authored module that fails to load under either build state. So
this round moves M3 by zero in every class. The absolute class (ii)
figure at this base is NOT MEASURED: round 3's shipped-face port of PR
objectstack-ai#11069's `findUndeclaredKeys` was a scratch script and was never
committed.

## Checks

Heavy runs went through `os-verify-lock` (slot `issue-11070`). Exit
codes were captured before any pipe.

- **Build closure** (`check-doc-snippet-types --build-filter`, turbo
`--concurrency=2`): `35 successful, 35 total`, before and after the
first merge.
- **type-check** of 21 packages, the ones this diff touches plus the
console and the schema catalog (the type-checker census found no other
typed reader): `Tasks: 56 successful, 56 total`, before and after the
first merge. After the second merge, `@object-ui/types` was rebuilt and
type-checked again, exit 0.
- **Tests, full affected set** (the 21 packages' paths, 3536 files),
before the first merge: `4 failed | 3527 passed | 5 skipped (3536)`. The
four were pins that asserted the old shape: three copy-set floors of
more than two keys, and the console shared-widget lookup fixture. They
were fixed in `c684de4` and re-run: `Test Files 4 passed (4)`, `Tests 30
passed (30)`.
- **Tests after the first merge** (`338b19d`), narrowed to the files
both sides touched plus this round's relational surface (types, fields
tests and widgets, the Flow inspectors, console components, plugin-grid,
plugin-detail and plugin-dashboard tests, plugin-form, core
reference-keys, the adapter, and app-shell providers and utils): `Test
Files 1137 passed | 3 skipped (1140)`, `Tests 16884 passed | 31 skipped
(16915)`. After the second merge (`16b43a7`, no file overlap):
`packages/types/` `295 passed (295)`, `7375 passed (7375)`, including
`zod-mirror-parity.test.ts`, whose totals did not move.
- **Doc gates** at `338b19d`: `check:doc-snippets` `696 of 696 block(s)
judged, 0 failed`; `check:doc-examples`, `check:doc-types`,
`check:spec-symbols`, `check:doc-example-readers` and
`check:doc-example-ids` all exit 0.
- **At `16b43a7`**, all exit 0: `check:doc-fences`, `docs:check-links`,
`check:control-bytes`, `check:pending-changeset-literals`,
`check-changeset-presence` (`174 source file(s) of 20 released
package(s) changed, and this change declares 1 changeset(s)`),
`changeset:check` (no major), `check:new-line-citations` (`0 new
citation(s)`), `check:test-path-roots`, `check:changeset-claims`
(report-only; every named changeset was read), and
`check-governed-queue-guard --test` over the 194 paths (`NOT GOVERNED`).
- **Lint, a measured narrowing.** The population is `eslint.config.js`'s
`**/*.{ts,tsx}` files entry; there are no `parserOptions.project` or
`projectService`, so type-aware linting is off and this diff cannot move
an untouched file's verdict. The 174 changed `.ts` / `.tsx` files were
linted at `16b43a7` and at merge base `54a7830`: 0 errors at both, 1909
warnings at both, and no file is worse than base.
- **Ablations**, each run on a committed, clean tree through
objectstack's `ablation-replace.mjs`. In every case the anchor hit once,
the blob moved, the blob was restored equal to HEAD, and `git diff HEAD`
was empty afterwards.
- A1: `lookup.mdx`'s fence writes `reference_to`. `check:doc-snippets`
exited 1 with `696 of 696 block(s) judged, 1 failed`, TS2561
"'reference_to' does not exist in type 'LookupFieldMetadata'. Did you
mean to write 'reference'?", which proves the gate read the rebuilt
`.d.ts`.
- A2: the fold stamps `reference_to` again. 6 red / 83 green, against 5
predicted: the three core stamp pins, the adapter pin and the provider
pin, plus the empty-target pin, because an empty `reference` also gained
a stamp.
- A3: `LookupCellRenderer` reads `reference || reference_to` again. 2
red / 14 green, as predicted: the new cell refusal pin, and the grid
derivation gate's unclassified-spelling check.
- **Type-checker census** at `338b19d`: the seven contracts carry no
`reference_to` member. The control run of the same instrument on
`reference` finds 7 declarations, the same two typed production writes
that moved, 1 production read, 3 test reads and 10 test writes.
- **Diff size:** 194 files, +1022 / −647 against merge base `54a7830`.
That is under the 5000-line human-merge threshold: 62 non-test files (16
of them changesets) and 132 test files.

NOT MEASURED:
- The repository-wide lint and the test shards outside the sets above.
CI runs those.
- Tests after the second merge outside `packages/types/`. Its three
incoming commits share no file with this branch, and none contains
`reference_to`.
- A browser dogfood against a live objectstack app. The probe used
`ObjectForm` / `ObjectGrid` with a stub or adapter-wrapped `DataSource`.
- The absolute M3 class (ii) figure (see H4).
- CI convergence. It was not waited on, per the dispatch contract.

## Acceptance notes

- `docs/adr/0059-action-params-shared-field-widgets.md`'s decision
section still records `referenceTo` → `reference_to` as `paramToField`'s
mapping. It was left as written: it is the accepted decision's record
(2026-07-19), and the ruling kept the historical record. Editing
`docs/adr/**` would also make this PR governed. The seat may want a
dated amendment line in a docs-only PR.
- `ROADMAP.md`'s two hits are completed items and changelog history, not
current behaviour, so they were not changed.
- `.changeset/audit-field-def-reference-narrow.md` says the choke point
"stamps both snake_case spellings". It has empty front matter, so
nothing publishes it, and no note was appended.
- objectstack `plugin-approvals`' `sys-approval-request.object.ts` has a
comment saying the console resolves a lookup config as `reference_to:
sys_user`. That spelling is now `reference`. It is a comment in the
sister repo, not a producer; noted, not filed.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grouped grid under a toolbar search: send the search on the group header and group row queries once objectstack honours search on grouped queries

2 participants