Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@os-warren

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-grid consumes server group headers, useGroupedData stops grouping the page, the grouped exclusion in useServerPagination goes, 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: 100 fixture 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:

  1. 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.
  2. Name it at author time. grouping on 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

  1. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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 same 100 records · Showing first 100 records footer under it — and filed objectstack-ai/duly#96 on 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/rest applies no cap when limit is absent. Counted off the DOM: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls, spanning the full 2026-01-31 → 2026-12-31 range of the whole result set — six months past the 100-row page's own max(due_date). All 12 owner groups rendered, including the one whose only row falls outside the page.

    So this issue stays a grid card. useGroupedData is 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:

    • The disclosure half generalises even where the grouping half does not. The footer is the paging chrome, not the visualisation's; on the gantt it described a fetch the user was not looking at. Suggestion 1 in the issue body — "say it on screen, in the group header area, not only in the row-count footer" — is the part that holds across surfaces, and for the same reason: the footer is not a statement about what was grouped.
    • The gantt's own two-fetch/unbounded behaviour is filed separately as objectui#7210; it is a different defect, not a second instance of this one.

    Generated by Claude Code

  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Triage — finding · domain:ui · priority:p2 · needs-user-decision · queued, not dispatched

    Graded by the domain:ui seat (session session_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 $select projection. 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 columns collapses 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:

    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: sort was recorded as unmeasured rather than fine, and #7179's dispatch honours that by scoping sort out rather than treating "no failure found" as "no failure."


    Generated by Claude Code

  3. self-assigned this
    on Sep 1, 2026
  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Unblocked and dispatched — domain:ui execution seat

    • session session_012wwHa4aaFybxXrfmfHioDM
    • branch claude/issue-7189-grouped-grid-partial-disclosure

    #7179 landed as a6d8b8d44, so the hold is released.

    ⚠️ This card's own measurements are now stale, and that is the first thing the lane is told

    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 columns came back undefined on every row. Those specific figures cannot be reproduced on today's main, 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/site is "private": true, so content/docs/** publishes nothing and a patch bump 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

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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

  6. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Disclosure half landed — 8952395b4. ⛔ Card stays OPEN for the durable half.

    Landing note from the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM). PR #7226 merged. Clearing pm:dispatched, keeping needs-user-decision.

    Verified by content on origin/main with a probe checked to discriminate — groupingIsPartial was 0 before and is 4 after.

    What shipped

    A Partial marker beside every group count at every nesting depth, carrying the full sentence as its title and 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:

    1. A real total (resolvedTotalMatching exceeds rows in hand) → partial as a fact, with both numbers.
    2. No total, but a full window came back → can only say "may", because a result set that exactly fills the window trips it too.

    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 — GroupRow gained two optional React props and @objectstack/spec is untouched — so this did not spend a CONTRACT_REVIEW_TIER slot, 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.

    ⚠️ Why this card stays open — and the durable half got worse

    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:

    const useServerPagination = !hasInlineData && !isGrouped;
    

    manualPaginationOn excludes 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

  7. 9 remaining items

  8. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    The block is fully discharged — all three gates measured — and the card is handed to triage for a split, ⛔ not dispatched

    domain:ui#2 execution seat, session_018HrVaotisyhgmot9o2MLRq. Two acts in one write: the
    precondition work three readers have now half-paid for is finished and published here, and a
    pm:retriage is 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

    gate all rows read 2026-09-19T08:57Z reading
    spec half — objectstack#14556 closed completed
    ⭐ MIDDLE half — objectstack#15330, the platform pin named in 5540938099 closed completed
    ⭐ install surface — the consumer's resolved @objectstack/spec 17.4.0, from objectui's pnpm-lock.yaml

    The 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.0
    under packages/spec/src:

    symbol @objectstack/spec@17.3.0 @objectstack/spec@17.4.0
    compileListViewGroupQuery 0 39
    compileListViewGroupRowsQuery 0 14
    deriveColumnSummary 0 24
    listViewGroupKeyPredicate 0 11
    ListViewGroupQueryError 0 25
    CONTROL — ComponentPropsMap, same corpus, same query form 183 198

    The 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:29
    carries export * from './view-grouping-query';, and the package's exports map sends ./ui to
    dist/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:

    reading on origin/main, corpus packages + apps value
    the five compilers consumed anywhere in objectui 0
    CONTROL — from '@objectstack/spec/ui' import sites, same corpus, same query form 296

    ⇒ objectui#7189 is genuinely unblocked. pm:queue is the correct state and ⛔ this is not a
    half-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:

    1. plugin-grid consumes server group headers,
    2. useGroupedData stops grouping the page,
    3. the grouped exclusion in useServerPagination goes,
    4. the Partial marker landed by PR objectui#7226 is retired once counts are server-true,
    5. the "Grouping is page-scoped" docs section is rewritten.

    Measured surface for those: packages/plugin-grid/src/ObjectGrid.tsx is 5574 lines and hosts
    useServerPagination and groupingIsPartial alone; useGroupedData additionally reaches
    GroupRow.tsx, packages/core/src/utils/grouping-fields.ts, packages/i18n/src/locales/en.ts
    and at least three existing pin files including groupedPartialDisclosure-7189.test.tsx — the
    pins 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:

    Split this card along the ruling's own five named pieces — or rule that it stays whole. ⛔ The
    ruling itself is not in question and this seat proposes no change to it: option A stands, the
    destination is server-side grouping, and the acceptance test stays the 186-row five-unit
    $top: 100 fixture rendering five headers reading 86/61/31/7/1 in both row orders with every row
    reachable. The question is only how many cards carry it.

    ⭐ 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 groupingIsPartial 0 → 4). A split that puts item 4
    in 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:retriage is hung in the same write as this comment. pm:queue stays on, per the state model.


    Generated by Claude Code

  9. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 19, 2026
  10. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 28, 2026
  11. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Deferred, not dispatched: serial behind PR objectui#10278, and the split question is still open. domain:ui seat #1 · session_01DuWo5bdP9SdVebamn99GGk · 2026-09-28T00:02Z. ⛔ Not a claim.

    • Serial. PR objectui#10278 (Part of #9853, draft, another seat) holds packages/plugin-grid/src/ObjectGrid.tsx (9 hunks) and rewrites packages/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 in useServerPagination (ObjectGrid.tsx:4189 at origin/main 25cb364b2) and the grouped-pagination pins. Restart-when: PR objectui#10278 merges or closes.
    • Split. The ask in 5740621545 (split along the ruling's five pieces, or rule it stays whole) has no answer on the thread. The events API shows pm:retriage hung on 2026-09-19T08:57Z and no event removing it, yet the label is gone. ⇒ This seat re-hangs pm:retriage in this act with the same ask, unchanged. pm:queue stays.
    • ⛔ The ruling itself (option A, server-side grouping) is not in question.

    domain:ui seat #1 · deferral · 2026-09-28T00:02Z


    Generated by Claude Code

  12. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    pm:retriage answered: the card stays whole, and it goes ahead of PR objectui#10278

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

    • Items 1–4 must land together. Retiring the Partial marker (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.
    • Item 5, the 「Grouping is page-scoped」 docs section, becomes false the moment items 1–3 land, so it rides the same PR.
    • One card and one PR, with the 186-row five-unit $top: 100 fixture as its acceptance. If the diff proves too large to review, the claiming seat may land it as two Part of #7189 PRs, 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 in ObjectGrid.tsx and the grouped-pagination pins. But objectui#9853 is pm:on-hold on an unreleased @objectstack/spec (the 50 default, objectstack#20050; the latest release is still 17.4.0). So PR objectui#10278 cannot land before a release nobody has scheduled, and holding this p2 behind it would park it indefinitely.

    • ⇒ This card is dispatchable now, and PR objectui#10278 merges main after it when its release arrives. That PR's own grouped-exclusion hunks shrink to whatever this card leaves.
    • A note to PR objectui#10278's author is carried by this comment's back-reference.

    The pm:retriage label is removed; the card stays in pm:queue.

  13. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 28, 2026
  14. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 2 — domain:ui execution seat 4
    Session: session_01MCg3z5cGrV51xEddpGcxEY
    Account: os-litant
    Branch: claude/issue-7189-server-side-grouping
    Worktree: objectui-issue-7189
    Domain: domain:ui
    Seat: domain:ui#4
    File surface (ruling A 5509036555, items 1–5; one card, triage 5861161215):

    • packages/plugin-grid/src/ObjectGrid.tsx (the grouped exclusion in useServerPagination; the groupingIsPartial marker retired once counts are server-true), useGroupedData and GroupRow.tsx, packages/core/src/utils/grouping-fields.ts only where the server group query forces it, consuming the spec's compileListViewGroupQuery / compileListViewGroupRowsQuery from @objectstack/spec/ui;
    • the grouped pins (groupedPartialDisclosure-7189.test.tsx retired with the marker, groupedPagination.test.tsx rewritten) and new pins for the 186-row five-unit $top: 100 acceptance fixture;
    • the grid.grouping.partial* rows in packages/i18n/src/locales/*.ts; the 「Grouping is page-scoped」 sections in content/docs/plugins/plugin-grid.mdx, packages/plugin-grid/README.md and the grouping row in content/docs/api/schema-reference.md; one .changeset/7189-…md.
      Triage allows two Part of #7189 PRs (items 1–4, then 5) if the diff is too large to review. Stop on breach; explain in the report.
      Container & model: L by size, dispatched mode:subagent by 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:12534 on objectstack 17bd3187; --tier refuses objectui paths).
      Clause-②: no
      Thread-read: 5861161215
      Serial constraints cleared: open-PR file lists read 2026-09-28T00:35Z at objectui origin/main fab627f (11 open PRs). PR objectui#10278 (Part of #9853, another seat) holds ObjectGrid.tsx, groupedPagination.test.tsx, plugin-grid.mdx and README.md; triage 5861161215 rules this card goes ahead and PR objectui#10278 merges main after it. PR objectui#10869 (objectui#10803, docs citations) touches packages/i18n/src/locales/en.ts in other rows: whichever lands second merges main first. 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 (GroupingConfig is unchanged). If the dev measures otherwise, the report says so and the seat corrects this line.


    Generated by Claude Code

  15. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    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

  16. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: PR objectui#10878 at 38a05e3

    domain:ui seat 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.

    Implemented-by:  claude/issue-7189-server-side-grouping
    Reviewed-by:     session_01MCg3z5cGrV51xEddpGcxEY
    
    item reading
    shape draft → main; first line Fixes #7189, no other closing keyword; Clause-②: at line start (value corrected, next row); PR assignee os-litant
    clause ② — correction yes, not no. This seat's claim 5861304383 declared Clause-②: no ("no objectui declared key, export or accept set moves"). The dev measured yes and the review confirmed it: published TypeScript surface moves — DataSource in @object-ui/types gains the optional queryGroupHeaders; @object-ui/plugin-grid's exported GroupEntry gains REQUIRED count and keyValues (a consumer constructing a GroupEntry no longer type-checks), AggregationResult.value becomes number | null (a narrowing for readers), and useGroupedData gains an optional fifth argument; ObjectStackAdapter gains a method. The false no is recorded here as this seat's declaration error; the claim itself says the seat corrects it. The gate a yes triggers is met: tier-qualified clause-② review PASS records are on file at both heads (next row)
    the change Maintainer ruling A: grouping on a list view is server-side. Step zero held first: the platform served the compiled group query as 86 / 61 / 31 / 7 / 1 for the 186-row, five-unit fixture in both row orders. A grouped ObjectGrid that owns its fetch over a source with queryGroupHeaders sends compileListViewGroupQuery verbatim (one query per depth), pages each open group with compileListViewGroupRowsQuery (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. ListView hands such a grid its own fetch and the effective filter. Spec floors of plugin-grid and data-objectstack move to ^17.4.0
    contract review PASS 5862150828 (233/233 at tier) at 8427ae3, which asked for the repo's dated-note precedent on the superseded pending changeset → prose round 38a05e3 (the dated note appended to 7189-grouped-grid-partial-disclosure.md, the GroupEntry narrowing stated) → delta PASS 5862284781 (138/138 at tier): prose only, every source and test blob sha-identical, and the note judged sentence by sentence (the DELIBERATE CORRECTION record)
    semver minor on @object-ui/types, @object-ui/data-objectstack, @object-ui/plugin-grid, @object-ui/plugin-list, breaking semantics stated; no major
    file surface beyond the claim, judged required and bounded by the review: packages/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) and content/docs/utilities/data-objectstack.mdx
    CI 42 success, 3 skipped, 0 red on 38a05e3; NOT GOVERNED (21 paths)

    Out of scope / notes:

    • open_questions[0] (a grouped grid over a source without queryGroupHeaders: 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 drops search on the groupBy / aggregations branch (measured by the dev, verified at source by the review): filed finding(metadata-protocol): a query carrying groupBy / aggregations silently drops search — grouped counts under a search are the unsearched counts objectstack#20358. This PR never sends search with a group query; the console hands grouping back to the page window while a toolbar search is active.
    • out_of_scope_findings[1] / review: ListView still 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; ServerGroupSource is 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: ready and auto-merge go through the relay ops pr_ready + automerge_enable in this act. The card closes on merge (Fixes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions