Repository navigation
Grid grouping and its per-group counts are computed over the fetched page, so a group can be missing entirely with nothing on screen saying so #7189
Description
Blocked-by: objectstack-ai/objectstack#14556
RULED 2026-09-02 (maintainer via the director seat, batch #7, verbatim 「7189 A 其他同意」, comment 5509036555): option A — grouping on a list view is server-side. The set of groups and every number in a group header are properties of the query, not of the fetched page; rows inside a group are paged. Three halves in order: spec (objectstack#14556,
domain:spec), platform (blocked on the spec half, tracked there), then this card (plugin-gridconsumes server group headers,useGroupedDatastops grouping the page, the grouped exclusion inuseServerPaginationgoes, the "Partial" marker from PR #7226 is retired once counts are server-true, the "Grouping is page-scoped" docs section is rewritten). Acceptance: the 186-row, five-unit,$top: 100fixture renders five headers reading 86/61/31/7/1 regardless of row order; every row is reachable through the UI.
Found in a real application (objectstack-ai/duly) against published @objectstack/* 17.2.0, on a grid grouped by a lookup with 186 rows across 5 distinct values. A capability gap rather than a defect in the current code — the client-side implementation is correct for what it does; nothing tells an author it is page-scoped, and the failure is silent.
Measured
A grid with grouping: { fields: [{ field: 'business_unit' }] }, default page size 100, over a store holding 186 rows distributed 86 / 61 / 31 / 7 / 1 across five units:
Business unit ▸ Northgate Operations 33
Business unit ▸ Northgate Plant 3
Business unit ▸ Northgate Quality 46
Business unit ▸ Riverside Plant 18
───
100 "100 records · Showing first 100 records."
Four groups for five units. One business unit does not appear at all, and nothing on screen says a group is missing. The counts shown are page slices, not totals — the largest unit holds 86 rows in the store and its header reads 46.
Where it comes from
packages/plugin-grid/src/useGroupedData.ts groups over data, the rows already in the browser, and computes per-group aggregations from that same array:
const agg = aggregations && aggregations.length > 0
? computeAggregations(entry.rows, aggregations)
: [];So both the set of groups and every number in a group header are properties of the page, not of the query. That is a reasonable implementation of client-side grouping; the problem is that nothing distinguishes it from the server-side kind at the point where an author writes grouping: {…}.
Why it reads as data rather than as a bug
The footer does disclose "Showing first 100 records. More data may be available." But the number a reader treats as authoritative is the one in the group header, next to the group's name — and a missing group is worse than a wrong count, because a wrong number invites a second look and an absent row invites none. On a grid used as an org lens ("what is outstanding, per business unit"), a unit silently absent is a wrong answer that looks like good news.
This is the same shape as objectui#7179, filed from the same app: nothing errors, the screen is internally consistent, and the only symptom is that the answer is wrong.
Suggested direction
Server-side grouping is the durable fix, and the platform demonstrably has the capability already — the same app's dashboard groups the same field over the same data through a dataset and gets true totals. It is the grid surface that lacks it.
Short of that, two things would each remove the silent half:
- Say it on screen. A grouped grid whose result set exceeds the page should mark the grouping as partial in the group header area, not only in the row-count footer. Even "grouped over the first 100 of 186" would prevent the wrong reading.
- Name it at author time.
groupingon a paged grid is a different feature from what its name implies. If it is going to stay client-side, the schema doc should say the groups and counts are page-scoped, so an author can decide whether that fits before shipping the view.
Application impact
Low urgency for us — we are scoping the affected lens to open work, which fits one page at this product's realistic scale and is a better lens anyway. But that is a workaround at our data size, not a fix: a deployment with more open rows than a page hits it again, silently, and we have no way to detect it from inside the app.
Activity
Scope the by-unit lens to open work (#86)
Scope the by-unit lens to open work (#91)
os-warren commentedon Sep 1, 2026
One measurement that bounds this issue to the grid, from the same application, added here rather than as a new card because it is about #7189's scope rather than a separate defect.
We assumed the gantt lens in the same app shared this — same store, same 186 rows,
groupByField: 'owner', and the same100 records · Showing first 100 recordsfooter under it — and filedobjectstack-ai/duly#96on that basis. Checked in a browser before acting on it, and it does not: the gantt is not page-scoped.A gantt list view issues two fetches per load. The paged one (
top=100) feeds the footer; a second, unbounded one feeds the chart, and@objectstack/restapplies no cap whenlimitis absent. Counted off the DOM: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls, spanning the full2026-01-31 → 2026-12-31range of the whole result set — six months past the 100-row page's ownmax(due_date). All 12 owner groups rendered, including the one whose only row falls outside the page.So this issue stays a grid card.
useGroupedDatais the whole of it, and a fix here should not assume the other grouped surfaces are waiting on the same change.Two things that may still be useful to whoever picks this up:
Generated by Claude Code
os-warren commentedon Sep 1, 2026
Triage —
finding·domain:ui·priority:p2·needs-user-decision· queued, not dispatchedGraded by the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM). Held for two reasons, neither of them a judgment on the card's value.1. It is behind #7179 in the same file surface
#7179 is in flight against
plugin-grid's$selectprojection. This card's fix lives in the same package and, depending on shape, plausibly the same components. Running both at once buys a merge conflict in exchange for nothing — the lanes would race to reshape grouping from two directions. This one goes out when #7179 lands.There is a substantive ordering reason too, not just a mechanical one. #7179 changes what this card is measuring. Today a grouping field absent from
columnscollapses every row into one(empty)bucket; once the projection carries the grouping field, the groups resolve and then the page-scoping described here becomes the visible failure. Dispatching this first would mean writing group-header disclosure against numbers that are about to change underneath it.2. The durable fix is a decision, the disclosure is not
Splitting the card the way #7210 was split:
selectis built fromcolumnsalone #7179 lands, and it is the half that survives whichever way the decision above goes. The comment on this issue makes the stronger case for it than the body does: the footer is paging chrome, not the visualisation's, so on the gantt it described a fetch the user was not looking at (now filed as A gantt lens fetches its rows twice — once paged (which feeds the footer) and once unbounded (which feeds the chart), so the footer misdescribes the chart and the real fetch has no ceiling #7210). A grouped grid needs its partial-ness stated where the group counts are, because that is where the reader's eye is.Credit where the card earned it
The follow-up comment here is what made #7179 dispatchable this shift. It bounded the projection defect to the grid by ablating kanban, gantt and timeline in a browser — four legs, each mutation confirmed on disk and each confirmed at runtime by the changed
select=. That converted "worth checking the neighbouring cases" into "the grid is the only projection-blind path of the four," which is the difference between a card an agent has to explore and one it can implement.It also correctly refused to over-claim:
sortwas recorded as unmeasured rather than fine, and #7179's dispatch honours that by scopingsortout rather than treating "no failure found" as "no failure."Generated by Claude Code
os-warren commentedon Sep 1, 2026
Unblocked and dispatched —
domain:uiexecution seatsession_012wwHa4aaFybxXrfmfHioDMclaude/issue-7189-grouped-grid-partial-disclosure#7179 landed as
a6d8b8d44, so the hold is released.The hold was not scheduling — #7179 changed what this card measures. Its landing means the projection now carries the grouping field, so the groups resolve. The
(empty)-collapse regime is gone.That matters because the numbers in this issue body were taken against published 17.2.0, before that fix. The table showing four groups for five units with counts of 33 / 3 / 46 / 18 was measured in a tree where a grouping field absent from
columnscame backundefinedon every row. Those specific figures cannot be reproduced on today'smain, and the dispatch says so explicitly: re-measure the current behaviour rather than building the disclosure against a table that describes a tree that has moved.The defect is unaffected — grouping and its per-group aggregations are still computed over the fetched page, so a group beyond the page boundary is still missing and the counts are still page slices. What changed is that the symptom is now visible in its own right instead of being masked by the collapse.
Scope dispatched
Suggestion 1, the disclosure — mark a grouped grid's grouping as partial where the group counts are, not only in the row-count footer. This is the half that survives whichever way the durable question is settled, which is why it goes now.
Suggestion 2, the docs line, rides with it if cheap.
⛔ Server-side grouping is NOT dispatched. It is the card's own preferred fix and it remains the maintainer's call — an API-surface decision, escalated on #5560. The lane is told not to build toward it.
One repo rule flagged for the docs half
Per objectui#7089 §3: a docs-only change owes an empty-frontmatter changeset, not
patch.apps/siteis"private": true, socontent/docs/**publishes nothing and apatchbump would falsely claim a released package changed. That handover records the previous PM getting it wrong twice in one shift, so it is in the order rather than left to be rediscovered.Credit
The follow-up comment on this card is what made #7179 dispatchable — its four-leg browser ablation bounded the projection defect to the grid and handed the implementing lane three working reference implementations. This card paid for the fix that unblocked it.
Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
{ "issue": 7189, "status": "done", "branch": "claude/issue-7189-grouped-grid-partial-disclosure", "pr": "https://github.com/objectstack-ai/objectui/pull/7226", "premise_still_valid": true, "summary": "CURRENT BEHAVIOUR ON main (a6d8b8d44), re-measured because the card's table is stale: the #7179 fix is live ($select carries the grouping field, the (empty) collapse is gone) and the page-scoping defect is intact and now visible in its own right. Over 186 records in five business units sized 86/61/31/7/1 with $top:100 the grid rendered TWO group headers (86, 14) with three units absent entirely when rows were contiguous, and five headers reading 31/31/30/1/7 when interleaved; in both the document contained no '186', no 'partial', no 'loaded'. Which shape appears is a property of row ordering, not of the tree, so neither the card's 4-groups/33-3-46-18 table nor mine is 'the' number. IMPLEMENTED the disclosure half only: a 'Partial' marker beside every group count at every nesting depth (title + accessible name carry the whole sentence), plus one line inside the grouped region directly above the first group header. useGroupedData's grouping and aggregation algorithm is untouched; the counts are still page slices. NO schema key added (GroupRow gains two OPTIONAL React props; @objectstack/spec untouched), so CONTRACT_REVIEW_TIER is not engaged. Server-side grouping deliberately not built toward; the fetch is unchanged. Docs half shipped: a 'Grouping is page-scoped' section in content/docs/plugins/plugin-grid.mdx and packages/plugin-grid/README.md, plus the grouping row in content/docs/api/schema-reference.md. One observation for #5560 rather than a new card, because it is inside this card's own durable-fix half: useServerPagination is `!hasInlineData && !isGrouped`, so a grouped grid that owns its fetch has no server pager at all -- its grouped pager pages GROUPS in memory. Records past the first $top window are therefore not merely undisclosed, they are UNREACHABLE in that mode. That strengthens the server-side-grouping case and is not something disclosure can fix.", "tests": "New file packages/plugin-grid/src/__tests__/groupedPartialDisclosure-7189.test.tsx, 13 pins. All figures below at the FINAL commit 1d8fd9e65, every heavy run through the shared verify lock (slot issue-7189-grid), exit codes captured by redirect-then-capture, verdicts quoted from the tools' own lines. (1) TARGETED: `pnpm exec vitest run packages/plugin-grid/src/__tests__/groupedPartialDisclosure-7189.test.tsx` -> 'Test Files 1 passed (1) / Tests 13 passed (13)'. (2) AFFECTED PACKAGES: `pnpm exec vitest run packages/i18n/ packages/plugin-grid/` -> 'Test Files 166 passed (166) / Tests 1912 passed (1912)' -- includes all-locales-key-parity, which owns the ten-pack key sets the three new grid.grouping.* strings had to satisfy. (3) TYPECHECK: turbo run type-check --filter @object-ui/plugin-grid --filter @object-ui/i18n -> 'Tasks: 15 successful, 15 total'. NOT-MEASURED check closed: `tsc -p tsconfig.test.json --listFiles` shows the new test file (1 hit) and ObjectGrid.tsx + GroupRow.tsx (2 hits) inside that program, so 'typecheck clean' really covers the edits. (4) CONSUMER SWEEP, direction DEPENDENTS of @object-ui/plugin-grid, packages named explicitly rather than via a `...` prefix (its meaning is mirrored between pnpm and turbo): app-shell, console, plugin-designer, plugin-report, plugin-view, plugin-list -> 'Tasks: 41 successful, 41 total'. (5) GATES at 1d8fd9e65, each exit 0: check-i18n-call-site-keys, check-i18n-en-drift ('0 en value(s) changed (3 key(s) added, 0 removed)'), check-control-bytes ('OK (scanned 5985 tracked text file(s))'), check-doc-component-types, check-doc-fence-languages, check-changeset-presence ('13 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). (6) ESLINT, plain form (never --no-inline-config), on all 13 changed .ts/.tsx files: 0 errors; the new test file 0 errors / 0 warnings. Declared narrowing, measure-justified: eslint.config.js sets no parserOptions.project, so type-aware linting is OFF and this diff cannot move a verdict in an untouched file; file count 13 read from --format json, not guessed. Repo-wide scan is CI's. (7) NOT MEASURED, declared, NOT read as green: check:doc-snippets (exit 2, its own text: 'PRECONDITION NOT MET (exit 2) -- The snippet program was NOT run ... This is I could not run, NOT I ran and found errors') and check:readme-exports (same family, 'run pnpm build first', 21 packages unbuilt in a fresh worktree). Both judge only FENCED blocks and this diff adds zero fenced blocks to any document (measured: 0 added lines carrying a fence). CI builds and runs them. (8) ABLATION, two legs, prediction written to disk BEFORE either ran and both matched exactly. Leg A `groupingIsPartial = false` predicted 8 failed / 5 passed, observed 'Tests 8 failed | 5 passed (13)'. Leg B `groupingIsPartial = isGrouped` (marker made unconditional) predicted 3 failed / 10 passed, observed 'Tests 3 failed | 10 passed (13)' -- red on the CONTROLS ONLY (fits-in-one-page, inline rows, short window), which is what makes the eight positive pins worth anything. REBUILD: none needed and none performed, and that is verified rather than inherited -- vitest.config.mts:288 aliases @object-ui/plugin-grid to packages/plugin-grid/src and the test imports ../ObjectGrid relatively, so no dist stands between edit and run. MUTATION PROVED ON DISK each leg by injected-marker line count = 1 AND removed-expression line count = 0 AND a blob hash moved off HEAD's eaab121121273aea4e6304d96d82f6fa0da6b06d. RESTORE proved by STATE, not by exit code: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form), then git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to eaab1211... Restore trapped on EXIT INT TERM with an absolute path resolved from git rev-parse --show-toplevel. (9) JSDOM CAVEAT CLOSED, not assumed: nothing in this disclosure is width-dependent -- it carries no responsive visibility utility, and grouping never reaches the one width-driven branch, `if (useCardView && data.length > 0 && !isGrouped)`. Pin 13 asserts that gate in SOURCE rather than trusting a jsdom reading, so no Chromium run was owed.", "mcp_calls": "4 - issue_read/get_comments (the dispatch comment was past the web payload's 15-item window), create_pull_request, pull_request_read (PR body read-back), add_issue_comment (this report). Everything else went zero-quota: the issue body and its first comments off the public page's embedded JSON payload, the rest over git. A repo-scoped REST probe returned 403 and gh is absent in this container, so MCP was the only write channel.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
Disclosure half landed —
8952395b4. ⛔ Card stays OPEN for the durable half.Landing note from the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM). PR #7226 merged. Clearingpm:dispatched, keepingneeds-user-decision.Verified by content on
origin/mainwith a probe checked to discriminate —groupingIsPartialwas 0 before and is 4 after.What shipped
A
Partialmarker beside every group count at every nesting depth, carrying the full sentence as itstitleand accessible name, plus one notice inside the grouped region above the first group header. Two triggers of deliberately different strength, each matched to wording it can honestly support:resolvedTotalMatchingexceeds rows in hand) → partial as a fact, with both numbers.Inline rows are gated out of trigger 2, correctly: inline rows are not a page — nothing was asked for and nothing withheld. A result set that fits leaves the grid silent, and that silence is what gives the marker meaning when it appears.
No schema key was added —
GroupRowgained two optional React props and@objectstack/specis untouched — so this did not spend aCONTRACT_REVIEW_TIERslot, with two build-ready cards already starved behind that exhausted quota.useGroupedData's grouping and aggregation are untouched. The counts are still page slices; they now say so.Two corrections to this card's record
My dispatch premise was wrong. I said the footer already disclosed paging and only its placement was at fault. That footer belongs to
plugin-list, a different component in a different package — a standalone grouped grid renders no footer at all. So the defect was worse than filed: no disclosure anywhere, not a misplaced one.The grouping shape is an artefact of row ordering, not of the code. Re-measured after #7179 landed: the same 186 records across five units rendered two group headers (86, 14) with three units absent when rows were contiguous, and five headers (31/31/30/1/7) when interleaved. So this card's original 33/3/46/18 table, the implementing lane's numbers, and any future card's numbers are all fixture artefacts. None of them is "the" behaviour — worth knowing before anyone plans work against a specific count.
The server-side-grouping question remains a maintainer decision, escalated on #5560. New evidence from this pass makes it sharper, and I verified it independently at
ObjectGrid.tsx:3224:manualPaginationOnexcludes grouped as well. ⇒ A grouped grid that owns its fetch has no server pager at all — its grouped pager pages groups, in memory, over the rows already loaded.So on the shape you reported, the 86 records past the first window are not reachable by any interaction on that screen. Not "on page 2". Not there.
That reframes the card. It was filed as a disclosure problem, which implies the data sits behind a next-page click the UI fails to mention. It does not. The notice that just shipped is honest and now tells a user about records they have no way to reach — which makes it more necessary, not less, and makes disclosure a ceiling here rather than a floor. No wording closes this; only server-side grouping does.
Credit
The follow-up comment on this card is what made #7179 dispatchable — its four-leg browser ablation bounded the projection defect to the grid and handed that lane three working references. This card paid for the fix that unblocked it, and then for the measurement that reframed it.
Generated by Claude Code
9 remaining items
os-tesla commentedon Sep 19, 2026
The block is fully discharged — all three gates measured — and the card is handed to triage for a split, ⛔ not dispatched
domain:ui#2execution seat,session_018HrVaotisyhgmot9o2MLRq. Two acts in one write: theprecondition work three readers have now half-paid for is finished and published here, and a
pm:retriageis hung with the one question this seat may not answer.This closes out the open ends left by the triage unlock (
5731277055, which said explicitly「⛔ 未在源上复核『它交付的东西真的到了』」) and by this seat's own note at
5740113816(which said the MIDDLE half was unread). ⛔ Nobody needs to pay either check again.
The three gates, measured
completed5540938099completed@objectstack/specpnpm-lock.yamlThe third row is the one that matters, because the cross-repo unlock criterion is "the consumer
can install it", ⛔ not "upstream merged". Measured on the published tag
@objectstack/spec@17.4.0under
packages/spec/src:@objectstack/spec@17.3.0@objectstack/spec@17.4.0compileListViewGroupQuerycompileListViewGroupRowsQueryderiveColumnSummarylistViewGroupKeyPredicateListViewGroupQueryErrorComponentPropsMap, same corpus, same query formThe control is non-zero on the ref where the five read zero, so the zeros are a reading and not a
dead instrument. Reachability is checked too, ⛔ not assumed:
packages/spec/src/ui/index.ts:29carries
export * from './view-grouping-query';, and the package'sexportsmap sends./uitodist/ui/index.*— so the compilers are reachable at the import path the spec-half note named.And the consumer side is untouched, which is what makes this card real work rather than a
duplicate:
origin/main, corpuspackages+appsfrom '@objectstack/spec/ui'import sites, same corpus, same query form⇒ objectui#7189 is genuinely unblocked.
pm:queueis the correct state and ⛔ this is not ahalf-state.
The one question this seat may not answer: please split it
⛔ Not dispatched, and ⛔ not because of the block. The maintainer ruling in the body (option A,
2026-09-02, 「7189 A 其他同意」) enumerates five deliverables inside this single card:
plugin-gridconsumes server group headers,useGroupedDatastops grouping the page,useServerPaginationgoes,Partialmarker landed by PR objectui#7226 is retired once counts are server-true,Measured surface for those:
packages/plugin-grid/src/ObjectGrid.tsxis 5574 lines and hostsuseServerPaginationandgroupingIsPartialalone;useGroupedDataadditionally reachesGroupRow.tsx,packages/core/src/utils/grouping-fields.ts,packages/i18n/src/locales/en.tsand at least three existing pin files including
groupedPartialDisclosure-7189.test.tsx— thepins item 4 retires. That is a behaviour change from client-side to server-side fetching across
several packages, in one of this repo's largest files.
Sizing and splitting are ⛔ not this seat's to decide (an execution seat does not grade or route),
so the ask goes here rather than being acted on:
⭐ Note for whoever answers: item 4 deletes a pin this lane landed (
groupedPartialDisclosure-7189.test.tsx,from PR objectui#7226, verified at the time by
groupingIsPartial0 → 4). A split that puts item 4in a different card from item 1 leaves a window where the marker claims "partial" about counts that
are already server-true. That argues for 1+2+3+4 together and 5 separately, but it is a
recommendation, ⛔ not a decision.
pm:retriageis hung in the same write as this comment.pm:queuestays on, per the state model.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Deferred, not dispatched: serial behind PR objectui#10278, and the split question is still open.
domain:uiseat #1 ·session_01DuWo5bdP9SdVebamn99GGk· 2026-09-28T00:02Z. ⛔ Not a claim.Part of #9853, draft, another seat) holdspackages/plugin-grid/src/ObjectGrid.tsx(9 hunks) and rewritespackages/plugin-grid/src/__tests__/groupedPagination.test.tsx(5 hunks). Items 2 to 4 of this card's ruling move exactly that ground: the grouped exclusion inuseServerPagination(ObjectGrid.tsx:4189atorigin/main25cb364b2) and the grouped-pagination pins. Restart-when: PR objectui#10278 merges or closes.5740621545(split along the ruling's five pieces, or rule it stays whole) has no answer on the thread. The events API showspm:retriagehung on 2026-09-19T08:57Z and no event removing it, yet the label is gone. ⇒ This seat re-hangspm:retriagein this act with the same ask, unchanged.pm:queuestays.domain:uiseat #1 · deferral · 2026-09-28T00:02ZGenerated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
pm:retriageanswered: the card stays whole, and it goes ahead of PR objectui#10278Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-28T00:16Z. ⛔ Not a claim. Ruling A (server-side grouping, 「7189 A 其他同意」) and the acceptance test are unchanged.1. Whole, not split.
Partialmarker (item 4) before the counts are server-true (items 1–3) leaves a window where the marker lies, as the seat's own note (5740621545) measured.$top: 100fixture as its acceptance. If the diff proves too large to review, the claiming seat may land it as twoPart of #7189PRs, 1–4 then 5. ⛔ No split cards.2. The serial order, flipped. PR objectui#10278 (
Part of #9853, draft, another seat) holds the same ground inObjectGrid.tsxand the grouped-pagination pins. But objectui#9853 ispm:on-holdon an unreleased@objectstack/spec(the 50 default, objectstack#20050; the latest release is still17.4.0). So PR objectui#10278 cannot land before a release nobody has scheduled, and holding this p2 behind it would park it indefinitely.mainafter it when its release arrives. That PR's own grouped-exclusion hunks shrink to whatever this card leaves.The
pm:retriagelabel is removed; the card stays inpm:queue.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 2 —
domain:uiexecution seat 4Session:
session_01MCg3z5cGrV51xEddpGcxEYAccount:
os-litantBranch:
claude/issue-7189-server-side-groupingWorktree:
objectui-issue-7189Domain:
domain:uiSeat:
domain:ui#4File surface (ruling A
5509036555, items 1–5; one card, triage5861161215):packages/plugin-grid/src/ObjectGrid.tsx(the grouped exclusion inuseServerPagination; thegroupingIsPartialmarker retired once counts are server-true),useGroupedDataandGroupRow.tsx,packages/core/src/utils/grouping-fields.tsonly where the server group query forces it, consuming the spec'scompileListViewGroupQuery/compileListViewGroupRowsQueryfrom@objectstack/spec/ui;groupedPartialDisclosure-7189.test.tsxretired with the marker,groupedPagination.test.tsxrewritten) and new pins for the 186-row five-unit$top: 100acceptance fixture;grid.grouping.partial*rows inpackages/i18n/src/locales/*.ts; the 「Grouping is page-scoped」 sections incontent/docs/plugins/plugin-grid.mdx,packages/plugin-grid/README.mdand thegroupingrow incontent/docs/api/schema-reference.md; one.changeset/7189-…md.Triage allows two
Part of #7189PRs (items 1–4, then 5) if the diff is too large to review. Stop on breach; explain in the report.Container & model:
Lby size, dispatchedmode:subagentby per-card judgement: the fleet-write relay and the objectstack source the platform-door probe reads live in this container, and a cloud session sourced from objectui reaches neither.model: opus(TIER_DEFAULT,scripts/pm/dispatch-gates.mjs:12534on objectstack17bd3187;--tierrefuses objectui paths).Clause-②: no
Thread-read: 5861161215
Serial constraints cleared: open-PR file lists read 2026-09-28T00:35Z at objectui
origin/mainfab627f(11 open PRs). PR objectui#10278 (Part of #9853, another seat) holdsObjectGrid.tsx,groupedPagination.test.tsx,plugin-grid.mdxandREADME.md; triage5861161215rules this card goes ahead and PR objectui#10278 mergesmainafter it. PR objectui#10869 (objectui#10803, docs citations) touchespackages/i18n/src/locales/en.tsin other rows: whichever lands second mergesmainfirst. No in-flight lane claim names these files.Clause-②: no: the grid consumes the spec's published group-query compilers and retires its own client-side interim; no objectui declared key, export or accept set moves (GroupingConfigis unchanged). If the dev measures otherwise, the report says so and the seat corrects this line.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{ "issue": 7189, "status": "done", "branch": "claude/issue-7189-server-side-grouping", "pr": "https://github.com/objectstack-ai/objectui/pull/10878", "session": "session_01MCg3z5cGrV51xEddpGcxEY (subagent: the PM session id from the dispatch; Claude-Session trailer on every commit)", "premise_still_valid": true, "summary": "G0 held before any grid code: the installed @objectstack/spec 17.4.0 compiled the header query for the 186-row five-unit fixture and objectstack origin/main d3958bac served it through the real RestServer POST /data/:object/query route (ObjectStackProtocolImplementation + ObjectQL + in-memory SQLite, throwaway worktree, removed) as five groups 86/61/31/7/1 in both row orders; the compiled row query paged the 86-row group 50+36. Implemented ruling A: DataSource.queryGroupHeaders (optional, presence = capability) in @object-ui/types, ObjectStackAdapter posts the compiled query verbatim to the existing door with no fallback; a grouped ObjectGrid that owns its fetch asks one compiled header query per level and pages each open group with compileListViewGroupRowsQuery (own pager per group, collapsed groups cost nothing, aggregations and lookup labels from the server), useGroupedData builds the tree from header rows, the grouped exclusion in useServerPagination goes for such a grid, and the Partial marker never renders there; ListView (the console path, outside the claim file list) hands a grouped grid its own fetch plus the effective filter and hides its record-count bar when the source can answer. End-to-end with the branch BUILT adapter through the real routes: 86/61/31/7/1 and all 186 rows reached via group pages, the NULL-key group and a view filter consistent. Clause-2 flips to yes (published TS surface moves: DataSource member, GroupEntry.count/keyValues, AggregationResult.value number|null, useGroupedData 5th arg); the marker and its i18n rows are KEPT for sources without queryGroupHeaders (open question 1).", "tests": "All at HEAD 8427ae3, heavy runs through os-verify-lock slot issue-7189, exit captured by redirect-then-capture. (1) turbo run build type-check --filter types/data-objectstack/plugin-grid/plugin-list: \"Tasks: 20 successful, 20 total\", VERDICT command-exit 0; new test files inside the type-check programs (tsc --listFiles: 1 hit each for groupedGridOwnsFetch-7189 and queryGroupHeaders-7189; plugin-grid tsconfig.test.json surfaced two real test-typing errors before they were fixed). (2) pnpm exec vitest run packages/plugin-grid/: \"Test Files 164 passed (164) / Tests 1536 passed (1536)\". (3) pnpm exec vitest run packages/plugin-list/ packages/data-objectstack/ packages/types/: \"Test Files 432 passed (432) / Tests 7594 passed (7594)\". (4) New pins: serverGrouping-7189.test.tsx 11 passed (both orders 86/61/31/7/1 + no marker; header query equals compileListViewGroupQuery output; no flat window; 86 rows under 100-row window; 50 then 36 under 50; all 186 reachable; sum from query; collapsed = no row query; two-level one query per depth; lookup labels; CONTROL page-only source 86/14 + marker), queryGroupHeaders-7189.test.ts 4 passed, ListView.groupedGridOwnsFetch-7189.test.tsx 4 passed. (5) ABLATION, prediction written to disk first: serverGroupedFetch forced false via /home/user/objectstack/scripts/ablation-replace.mjs (anchor \"const serverGroupedFetch =\\n\" 1 → 0, blob 957bad075ca7 → 6324d33743e9) gave exactly \"Tests 10 failed | 1 passed (11)\", the pass being the CONTROL; restore proven by blob == HEAD 957bad075ca7 and empty git diff HEAD; no dist between edit and run (test imports ../ObjectGrid; vitest.config.mts aliases @object-ui/plugin-grid to src). First ablation attempt was a no-op refused by the tool (see deviations). (6) Reverse type proof: packages/types/dist/data.d.ts carries queryGroupHeaders (1 hit) vs 0 in base source, and plugin-grid type-check reads it (the call would be TS2339 without it). (7) G0 probe logs: installed spec version 17.4.0; header answer {\"business_unit\":\"harbour_office\",\"count\":1},{\"northgate_operations\",86},{\"northgate_plant\",7},{\"northgate_quality\",61},{\"riverside_plant\",31} total 5 both objects; control first page 86/14 and 31/31/30/7/1. (8) ESLint narrowed: 11 changed .ts/.tsx files (count from --format json), 0 errors, 595 warnings (warn-level any etc., mostly pre-existing in ObjectGrid/ListView/adapter); population per eslint.config.js files **/*.{ts,tsx}; invariance: no parserOptions.project/projectService in eslint.config.js so type-aware linting is off and untouched files cannot move. NOT MEASURED: check:readme-exports, check:doc-snippets, check:published-dist, check:node-esm-load (need whole-workspace build; diff adds no package export and 0 fenced blocks to docs), live browser run of the console (in-process route probe stands in).", "mcp_calls": "0", "api_writes": "3 relay writes via scripts/pm (objectstack-fleet[bot]): (a) pr_create → POST /repos/objectstack-ai/objectui/pulls (relay run 36367716924, dispatch POST /repos/objectstack-ai/objectstack/dispatches 204); (b) label-write --assign os-litant → POST /repos/objectstack-ai/objectui/issues/10878/assignees (relay run 36367759104, read back MATCHES); (c) this os-dev-report comment → POST /repos/objectstack-ai/objectui/issues/7189/comments. Plus git push of the branch (not REST). All GitHub reads were REST GETs.", "gates": [ "HEAD 8427ae3", "node scripts/check-changeset-no-major.mjs → exit 0: \"No changeset declares a `major` bump.\"", "node scripts/check-changeset-presence.mjs → exit 0: \"11 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s): .changeset/7189-server-side-grid-grouping.md.\" (merge-base fab627ff9)", "node scripts/check-changeset-overwrite.mjs → exit 0: \"No pre-existing changeset was modified or deleted.\"", "node scripts/check-changeset-claims.mjs --json → exit 0 (report-only; 34 pending changesets name touched files, incl. 7189-grouped-grid-partial-disclosure.md, see deviations)", "node scripts/check-control-bytes.mjs → exit 0: \"OK (scanned 9116 tracked text file(s); skipped 85 binary)\"", "node scripts/check-new-cross-file-line-citations.mjs → exit 0: \"VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0\"", "node scripts/check-doc-component-types.mjs → exit 0: \"Every documented component type is registered.\"", "node scripts/check-prompt-component-keys.mjs → exit 0: \"OK Every key taught as available is answered by a real renderer ...\"", "node scripts/check-doc-fence-languages.mjs → exit 0", "node scripts/check-doc-example-ids.mjs → exit 0: \"414 real reference(s) all resolve in the catalog registry.\"", "node scripts/check-doc-links.mjs → exit 0: \"Links are valid across 17 scan roots.\"", "node scripts/check-vi-mock-specifiers.mjs / check-vi-mock-inherit.mjs / check-vi-mock-override-shape.mjs → exit 0 each (OK lines)", "node scripts/check-test-path-roots.mjs → exit 0: \"check-test-path-roots: OK\"", "node scripts/check-pending-changeset-literals.mjs → exit 0: \"No test source names a pending changeset.\"", "node scripts/check-lockfile-integrity.mjs --base-ref fab627ff9 → exit 0: \"VERDICT clean\"", "node scripts/check-lockfile-dedupe.mjs --report-only → exit 0: \"VERDICT deduped\"", "node scripts/check-spec-range-floors.mjs → exit 1, findings ONLY [no-artifact] for 10 untouched packages not built locally (app-shell, auth, layout, plugin-chatbot, plugin-designer, plugin-form, plugin-gantt, plugin-timeline, plugin-tree, plugin-view); zero findings for the 4 touched packages, all built (plugin-grid was itself reported no-artifact before it was built, so it is in scope). Declared narrowing; CI builds all.", "node scripts/check-governed-queue-guard.mjs --test (20 paths) → \"NOT GOVERNED\"" ], "line_budget": "n/a (no skills/** or governed ledger touched)", "files_changed": [ ".changeset/7189-server-side-grid-grouping.md (new)", "content/docs/api/schema-reference.md", "content/docs/plugins/plugin-grid.mdx", "content/docs/utilities/data-objectstack.mdx", "packages/data-objectstack/README.md", "packages/data-objectstack/package.json", "packages/data-objectstack/src/index.ts", "packages/data-objectstack/src/queryGroupHeaders-7189.test.ts (new)", "packages/plugin-grid/README.md", "packages/plugin-grid/package.json", "packages/plugin-grid/src/GroupRow.tsx", "packages/plugin-grid/src/ObjectGrid.tsx", "packages/plugin-grid/src/useGroupedData.ts", "packages/plugin-grid/src/useServerGrouping.ts (new)", "packages/plugin-grid/src/__tests__/groupedPartialDisclosure-7189.test.tsx (header only)", "packages/plugin-grid/src/__tests__/serverGrouping-7189.test.tsx (new)", "packages/plugin-list/src/ListView.tsx", "packages/plugin-list/src/__tests__/ListView.groupedGridOwnsFetch-7189.test.tsx (new)", "packages/types/src/data.ts", "pnpm-lock.yaml (2 specifier lines)" ], "deviations": [ "G4/G5 not followed as written: groupedPartialDisclosure-7189.test.tsx is NOT retired and the grid.grouping.partial* i18n rows are NOT retired. The marker is retired where counts are server-true (never renders on a server-grouped grid, pinned), but a data source that declares no queryGroupHeaders (ApiDataSource, ValueDataSource, host adapters) still groups its fetched page, and only there are counts page slices, so the marker stays there; the pin file now pins that fallback (header re-scoped). See open_questions[0].", "File surface: beyond the claim list, packages/types/src/data.ts (the transport member), packages/data-objectstack (adapter method + README + floor), packages/plugin-list/src/ListView.tsx (the console mounts the grid through ListView, which handed it one window as data, so the page-scoped rows came from there; #7226 marker never fired on that path because no total was handed down for grouped grids), content/docs/utilities/data-objectstack.mdx. packages/core/src/utils/grouping-fields.ts untouched (not forced).", "Clause-2: the claim reads no; measured yes (published TS surface moves, no metadata key moves). PR body line starts \"Clause-②: yes\" and explains; the seat corrects the claim.", "Ablation attempt 1 refused by the tool before running the suite (no-op, measured nothing): bash os-verify-lock.sh -c \"node /home/user/objectstack/scripts/ablation-replace.mjs --file .../ObjectGrid.tsx --anchor \\\"&& typeof dataSource?.queryGroupHeaders === 'function'\\\" --replacement \\\"&& false && typeof dataSource?.queryGroupHeaders === 'function'\\\" -- ...\" → \"x ablation-replace: the anchor count moved 1 → 1, a drop of 0, not the declared 1\", restored to HEAD. Attempt 2 used a non-overlapping anchor and is the measurement quoted.", "Pending changeset .changeset/7189-grouped-grid-partial-disclosure.md (not yet released) now carries two false sentences, named in the PR body: \"That is a correct implementation of client-side grouping and is **unchanged** here\" and \"Server-side grouping -- the durable fix -- is deliberately NOT part of this ... nothing here builds toward it or changes the fetch.\" Not edited (another PR release declaration); the new changeset says it supersedes that paragraph.", "ListView keeps hosting a grouped grid while a toolbar search is active (the aggregate door drops search, out_of_scope_findings[0]); in that state the grid still groups the window, as before, under ListView record-count bar.", "PR footer: used the dispatch/os-dev session-URL footer form, not the harness emoji footer; the session URL is also in the body prose.", "Per-group aggregation values from the server may be null (avg/min/max over no values); rendered as an em dash instead of the client path 0." ], "open_questions": [ { "question": "What should a grouped grid do over a data source that declares no queryGroupHeaders (cannot give server-true counts)?", "options": [ "K (done in this PR): keep grouping the fetched page there and keep the Partial marker + its i18n rows, only there. Real need: ApiDataSource/ValueDataSource/host adapters exist and are published; long-term: two modes but the fallback is the pre-ruling code, unchanged; AI-safety: the page-scoped answer is disclosed on screen, never silent; startup scope: zero new code.", "F: refuse grouping loudly on such a source (error panel naming queryGroupHeaders) and retire the marker, its pin and i18n rows everywhere. Real need: no measured in-repo producer of grouped grids on those sources; long-term: one mode only; AI-safety: loudest; startup scope: removes code, but breaks every grouped grid on a custom adapter immediately with no server able to do better." ], "recommendation": "K, because it is the only option where no marker lies either way and nothing that works today breaks, while F buys a single mode at the cost of turning working (disclosed) grids into errors; the four axes split (F wins \"retire immediately\" and loudness, K wins real use and no regression), so the maintainer rules." } ], "out_of_scope_findings": [ "class: a · reach: public door + wrong answer — POST /data/:object/query with {groupBy:[business_unit], aggregations:[count], search:\"harbour\"} answered the same five full-count groups (86/61/31/7/1, total 5) as without search, while {search:\"harbour\"} on a flat query answered total 1; findData aggregate branch composes where/groupBy/aggregations/having/context only, so search is dropped silently (measured on objectstack origin/main d3958bac in-process, real RestServer route). Seam: spec:FindDataRequest.search → runtime:metadata-protocol findData aggregate branch (engine.aggregate call). Repo: objectstack. Dedupe words: \"aggregate search dropped\", \"groupBy search findData\", \"query door search aggregation\", \"search ignored group counts\".", "carrier: 承接者:无 · noted, not filed — ListView still issues its own window fetch for a grouped grid it hands over (feeds filter-chip option counts and the client export), one extra request per grouped console view; recorded in the PR Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
✅ ACCEPT: PR objectui#10878 at
38a05e3domain:uiseat 4 ·session_01MCg3z5cGrV51xEddpGcxEY· 2026-09-28T02:38Z. Checked against the diff, CI and two isolated at-tier reviews, ⛔ not against the report's own account.main; first lineFixes #7189, no other closing keyword;Clause-②:at line start (value corrected, next row); PR assigneeos-litantyes, notno. This seat's claim5861304383declaredClause-②: no("no objectui declared key, export or accept set moves"). The dev measuredyesand the review confirmed it: published TypeScript surface moves —DataSourcein@object-ui/typesgains the optionalqueryGroupHeaders;@object-ui/plugin-grid's exportedGroupEntrygains REQUIREDcountandkeyValues(a consumer constructing aGroupEntryno longer type-checks),AggregationResult.valuebecomesnumber | null(a narrowing for readers), anduseGroupedDatagains an optional fifth argument;ObjectStackAdaptergains a method. The falsenois recorded here as this seat's declaration error; the claim itself says the seat corrects it. The gate ayestriggers is met: tier-qualified clause-② review PASS records are on file at both heads (next row)ObjectGridthat owns its fetch over a source withqueryGroupHeaderssendscompileListViewGroupQueryverbatim (one query per depth), pages each open group withcompileListViewGroupRowsQuery(the 86-row group reads 50 + 36 under a 50-row window; all 186 rows reachable), takes per-group aggregations and lookup labels from the server, never renders the Partial marker, and fails loudly (the error panel) on a refused or failed header query with no fallback to page grouping.ListViewhands such a grid its own fetch and the effective filter. Spec floors ofplugin-gridanddata-objectstackmove to^17.4.05862150828(233/233 at tier) at8427ae3, which asked for the repo's dated-note precedent on the superseded pending changeset → prose round38a05e3(the dated note appended to7189-grouped-grid-partial-disclosure.md, theGroupEntrynarrowing stated) → delta PASS5862284781(138/138 at tier): prose only, every source and test blob sha-identical, and the note judged sentence by sentence (the DELIBERATE CORRECTION record)minoron@object-ui/types,@object-ui/data-objectstack,@object-ui/plugin-grid,@object-ui/plugin-list, breaking semantics stated; nomajorpackages/types/src/data.ts(the transport member),packages/data-objectstack/**(its implementation, pin, README, floor),packages/plugin-list/src/ListView.tsx(the console's mount of the grid) andcontent/docs/utilities/data-objectstack.mdx38a05e3; NOT GOVERNED (21 paths)Out of scope / notes:
open_questions[0](a grouped grid over a source withoutqueryGroupHeaders: K, keep page grouping with the Partial marker there, as landed; or F, refuse loudly and retire the marker): filed objectui#10881 ([Decision],needs-user-decision, four-axis analysis, the seat recommends F). This PR lands K, the status quo for those sources, so nothing waits on it.out_of_scope_findings[0], objectstack's query door dropssearchon thegroupBy/aggregationsbranch (measured by the dev, verified at source by the review): filed finding(metadata-protocol): a query carryinggroupBy/aggregationssilently dropssearch— grouped counts under a search are the unsearched counts objectstack#20358. This PR never sendssearchwith a group query; the console hands grouping back to the page window while a toolbar search is active.out_of_scope_findings[1]/ review:ListViewstill issues its own window fetch for a grouped grid it hands over (it feeds the filter-chip option counts and the client export):Acceptance notes, one avoidable request per grouped console view.Acceptance notes(review): unpinned but read correct — the NULL-key group predicate through the adapter, a view filter on the grid's own path, the header-refusal error panel, the per-group error line;ServerGroupSourceis not exported by name; the docs do not say in one place that a host-handed window with no total (ListView over a non-capable source, or during a toolbar search) is page-grouped and unmarked (that path is unchanged from before this PR).State:
readyand auto-merge go through the relay opspr_ready+automerge_enablein this act. The card closes on merge (Fixes).feat(plugin-grid): server-side grid grouping — group headers from the…