Skip to content

fix(plugin-grid,i18n): a paged grid's footer reads the whole matched set, or says it reads the page (objectui#12081 item 3) - #12123

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-12081-footer-sum-scope
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-12081-footer-sum-scope

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #12081 (item 3)
Clause-②: yes

Items 4 and 7 of objectui#12081 remain open on the card; this pull request carries item 3 only.

What was wrong (reproduced red on main)

Under server paging a grid holds one page of the matched set. useColumnSummary summed that page, and ObjectGrid drew the figure beside the count of the whole set. The card's reading on objectstack-ai/hotclm#87 (17.7.0): "Planned Amount: Sum: 3,685,900.00 · 300 records", where the 300 rows sum to 40,607,000.

The new suite footerSummaryScope-12081.test.tsx was committed alone first (95e2271, atop main 206505c): 11 failed, 2 passed (13). Every defect pin failed, for example expected 'Amount: Sum: 32,500' to be 'Amount: Sum: 4,515,000'. The two CONTROL pins passed. Every footer aggregate had the same fault: Name: Count: 25 beside 300 rows.

What changed

  • The whole set's figures, from the server. When the rows are one page of more, the grid asks for the figures in ONE aggregate query through DataSource.queryGroupHeaders, with no groupBy (new useWholeSetSummary.ts). The query is the one behind the rows on screen, the same single value the cross-page fan-out replays (bulkFanoutParams, hoisted above the hook): $filter lowered through the spec's parseFilterAST (the same lowering the grouped header query uses), $search and $searchFields. Its window, order and projection are not part of the query, so turning the page or sorting asks nothing. A refresh or a write to the object (the data-invalidation bus) asks again and keeps the answer on screen until the new one arrives. The aggregation nodes are built from the spec's COLUMN_SUMMARY_AGGREGATION and columnSummaryAlias, and the answer is read back through deriveColumnSummary, so the mapping is the spec's in both directions. compileListViewGroupQuery itself refuses an empty grouping.
  • Never a figure over other rows than the count's. The answer is used only when its count equals the count the grid shows (resolvedTotalMatching, hoisted). It is not used when the data source has no door, refuses, or answers with anything but one row. It is not used when the query carries a key outside the window keys and the three it repeats. In each of those cases the footer shows the page's figure under a label that says so. In en that reads Amount: Sum (this page): 32,500, from the new grid.summary.pagePattern in all ten packs. The answer is also not used while it is pending.
  • Unchanged: rows handed in whole, and a first page that holds every match, keep their footer and ask nothing (the CONTROL pins). useColumnSummary keeps its public signature and behaviour. The grid reads the new module-internal useScopedColumnSummary.

Where the fix lands, and why not aggregate()

The claim's file list names useColumnSummary.ts and the footer in plugin-grid. That is where the fix lands, plus the new plugin-grid/src/useWholeSetSummary.ts. data-objectstack needs no change. I measured the PM's door assumption: DataSource.aggregate() takes the analytics-shaped AggregateParams (one field, one function). Neither of its branches carries a search: the spec-shape branch copies groupBy / aggregations / where / limit only, and the analytics payload carries cube / measures / dimensions / where only. The analytics branch can also fall back to a client-side reduction over a find(). So it cannot answer "these rows' figures" under a search. queryGroupHeaders sends EngineAggregateOptions as-is to POST /data/:object/query and has no fallback.

Measured on the platform (17.7.0, in process)

@objectstack/objectql + driver-memory + metadata-protocol + rest 17.7.0, through the POST /api/v1/data/:object/query route handler, with 300 rows:

query flat total (limit 25) ungrouped aggregate count / sum truth
none 300 300 / 45150224.25 300 / 45150224.25
where: { stage: 'won' } 100 100 / 14950074.25 100 / 14950074.25
where matching nothing 0 one row: 0 / 0, avg/min/max null 0
search: '1', searchFields: ['name'] 26 138 / … 138 (the flat query without a limit returns 138 rows)

The aggregate door answers ONE row over the same where and the same search expansion as the flat find. Under a search, though, the paged flat total is offset + page + 1 (engine.count takes no search), so the count the console shows there is an estimate. The count check sends that case to the labelled page figure. Pinned by (b) the answer counts other rows than the count shown.

Tests and proofs

  • packages/plugin-grid/src/__tests__/footerSummaryScope-12081.test.tsx, 18 passed. It covers: (a) the grid's own fetch and a host-driven page both read the whole set, and a page turn asks nothing; (c) a filter plus a search narrow the count and the footer alike, asserted on the exact query sent; every footer aggregate; a currency column (the card's shape); string figures from a driver; a write asks again; (b) no door, a refusal, a non-one-row answer, a count that differs, a host with no query, an unrepeatable key; the zh pack's own placement; CONTROL ×2.
  • Ablation, committed state, scripts/ablation-replace.mjs (anchor hit 1→0, blob 0e8b5d84fea5 → 75859734926d and back, git diff HEAD empty after restore). Removing the count check turns exactly (b) the answer counts other rows than the count shown red: 1 failed, 17 passed.

Gate readings at head d61feee (run after the last commit; the box is shared, so durations are not idle-box figures)

  • pnpm exec vitest run packages/plugin-grid/: 199 files, 1874 tests passed. pnpm exec vitest run packages/i18n/: 82 files, 1316 passed, 13 skipped (the describe.runIf(DRIVEN) timezone legs).
  • pnpm --filter @object-ui/plugin-grid type-check, @object-ui/i18n and @object-ui/types: exit 0, each echoing type-check. tsc -p tsconfig.test.json --listFiles reads the new test and the new hook.
  • Exit 0 with their own verdict lines: check:phantom-deps, check:self-import, check:esm-specifiers, check:new-line-citations (0 new), check:control-bytes, check-changeset-presence (15 source files of 3 released packages, 1 changeset), check-changeset-no-major, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, the three check:vi-mock-*, the four i18n checks (i18n-keys · i18n-drift · i18n-dead-keys, which does not list the new key · i18n-designer-parity), check:readme-exports and check:unreferenced-sources.
  • Eager closure, base 206505c → head d61feee (two console builds, check:eager-closure green on both): +965 bytes gzipped (3,247,241 → 3,248,206). Of that, +949 is the plugin-grid chunk, +21 i18n-locale-en and +11 vendor-objectstack; the rest is hash churn of a few bytes. The chunk count is unchanged at 290. Headroom went from 33.4 KB to 32.5 KB. check:eager-locale-catalogues: en alone is eager.
  • Narrowed lint, the three-part proof: (1) the population comes from eslint's own config: eslint --print-config resolves a config for each of the 15 touched .ts/.tsx files, and none is ignored; (2) the count comes from --format json: 15 entries, 0 errors; (3) invariance: eslint.config.js declares no parserOptions.project / projectService (no type-aware linting), and no custom object-ui rule reads the filesystem, so this diff cannot move the verdict of a file it does not touch. Base vs head per file: ObjectGrid.tsx 205 → 207 warnings, the +2 being react-hooks/refs on the hoisted bulkFanoutParams line. That is the same ref read as before, now with two more readers. useColumnSummary.ts 1 → 1, types/src/data.ts 27 → 27, and the two new files and the ten packs 0.

Clause-② inventory (what this diff adds to a published surface)

  • Locale key: grid.summary.pagePattern, in all ten packs (@object-ui/i18n minor).
  • Contract doc: the DataSource.queryGroupHeaders JSDoc in @object-ui/types now says that the same query with no groupBy answers the whole filtered set as one header row, and that a source may refuse it. The member's type is unchanged.
  • No new export, prop, type member or hook-signature member reaches a package barrel. useScopedColumnSummary, ColumnSummaryScope, useWholeSetSummary, WholeSetSummary, WholeSetSummaryInput and compileWholeSetSummaryQuery are module exports. plugin-grid's exports is . alone (plus ./style.css), and src/index.tsx exports none of them. useColumnSummary's signature is byte-identical.

Overlap

objectui#12119 (in review) also edits ObjectGrid.tsx: its imports block and the bulk-dispatch region. This diff touches the summary-hook region, one import line, and two constant blocks moved up unchanged except for comments (bulkFanoutParams, resolvedTotalMatching). The hunks do not overlap. Whichever pull request lands second merges main.

Acceptance notes

  • File surface. Beyond the claim's list, this diff edits the plugin-grid README, content/docs/plugins/plugin-grid.mdx (AGENTS.md rule Add automated testing infrastructure and CI/CD workflows #2: docs follow the code), and the queryGroupHeaders JSDoc in packages/types/src/data.ts. That JSDoc edit is a comment, and the contract doc should name the query this diff now sends. Each is a separate hunk and easy to drop if the seat prefers.
  • Server "empty" vs the footer's. A whole-set count_empty or percent_* uses the server's meaning (a stored null). The client footer also counts '' and [] as empty. The spec's ColumnSummarySchema text names this "the renderer's to converge". This pull request does not converge it: that would change the one-page footer, which the CONTROL pins keep as it was.
  • Width of a whole-set number figure with no declared scale. It rounds to the widest decimal count among the page's values (plus the figure itself for min / max). The other pages' values are not in hand.
  • Field-level security. A summary over a field the server refuses to aggregate makes the whole query fail. Every footer figure then falls back to the labelled page figure: honest, but coarse.
  • The DataTable pager's icon buttons have no accessible names. The test reaches next-page through its chevron. Listed for the seat in the report.
  • No browser pass. The console path is covered by composition: ListView.findParamsHandoff.test.tsx pins that the host hands down exactly the params it fetched with, plus manualPagination and rowCount, and this suite's host-driven pins render that handoff.

Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz (domain:ui seat 1 dispatch, os-dev).


Generated by Claude Code

…n main (objectui#12081 item 3)

The footer sums the rows the grid holds, which under server paging are one
page, and draws the figure beside the count of the whole set. Eleven pins
fail on main ("Amount: Sum: 32,500" where the 300-row sum is 4,515,000);
the two CONTROL pins (rows handed in whole, a one-page set) pass.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…set, or says it reads the page (objectui#12081 item 3)

The footer summed the rows the grid holds, which under server paging are one
page, and drew the figure beside the count of the whole set ("Sum:
3,685,900.00 · 300 records" where the 300 rows sum to 40,607,000).

When the rows are one page of more, the grid now asks the whole set's
figures as ONE aggregate query through DataSource.queryGroupHeaders with no
groupBy: the query behind the rows (filter lowered through parseFilterAST,
search and searchFields) less its window, with the node set compiled from
the spec's COLUMN_SUMMARY_AGGREGATION and read back through
deriveColumnSummary. A page turn or a sort asks nothing; a refresh or a write
re-asks. The answer is used only when its count equals the count the grid
shows. Otherwise (no door, a refusal, an unaskable query, a count that
differs) the figure is the page's and the label says so: new key
grid.summary.pagePattern in all ten packs.

useColumnSummary's public signature is unchanged; the grid reads the new
module-internal useScopedColumnSummary.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…queryGroupHeaders answers with no groupBy (objectui#12081 item 3)

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…s effect, and a typed test double (objectui#12081 item 3)

The pending state needs no write: the read compares the held question with
the render's. The new test file and hook carry no explicit any.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3172.8 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-D2XG2Dcm.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 586.97KB 141.32KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 274.56KB 69.81KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.33KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 254.97KB 70.85KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Brings in objectui#12119 (ObjectGrid.tsx bulk dispatch), objectui#12120
(locale packs) and objectui#12122 before the rework round. Merged without
conflicts.

Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…regate (objectui#12081 item 3, rework round 1)

summaryColumns is every column object, and the whole-set query always seeded
the count node, so every paged grid with object-form columns and no declared
summary (every console list view ListView hosts) sent one queryGroupHeaders
request per question for a footer it never draws.

wholeSetSummaryNodes now answers null when no column declares a summary that
maps to an aggregate (none at all, only none, only unknown members);
compileWholeSetSummaryQuery answers null for it, and useWholeSetSummary is
off: no request, no invalidation subscription. Pinned through the real
ObjectGrid with a control leg; changeset and docs name the case.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3172.9 KB 3204.6 KB
Main entry chunk (gzip) 74.1 KB 350 KB
Entry file index-BWo6XZCb.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 586.97KB 141.32KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 274.56KB 69.81KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.33KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.59KB 46.00KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 255.10KB 70.92KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.77KB 31.19KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1da27b41b2dd7f7d646111f09a010afaf01fec60
Local-runs: none

Reviewed, and nothing else: card objectui#12081 (body and all 17 comments: triage 6093502833, claim 6105957678, reports 6106402263 and 6106613032, REWORK 6106424021), PR objectui#12123 (body, its 18-file list, the net diff against main at this head), and the 43 check-runs on the head: 40 success, 3 skipped (the two coverage legs and dependabot), none failing or pending. Those conclusions are the gate verdicts here. Cross-read read-only at objectstack main: packages/spec/src/ui/view-grouping-query.ts, packages/spec/src/data/data-engine.zod.ts, packages/spec/src/data/filter.zod.ts, packages/metadata-protocol/src/protocol.ts (findData); at this head: plugin-grid/src/index.tsx, plugin-grid/package.json, types/src/data.ts, react/src/data-invalidation.ts, data-objectstack/src/index.ts (queryGroupHeaders) and its pin queryGroupHeaders-7189.test.ts, plugin-list/src/ListView.tsx and ListView.findParamsHandoff.test.tsx. This PR carries item 3 only; items 4 and 7 stay open on the card.

① Derived judgments

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

  1. Route A: the flat footer's whole-set read rides DataSource.queryGroupHeaders with no groupBy (new plugin-grid/src/useWholeSetSummary.ts). RIGHT. Checked against the two sources the REWORK named. The spec's EngineAggregateOptionsSchema declares where, groupBy, aggregations, search and searchFields all optional, so an aggregations-only body is a well-formed EngineAggregateOptions. The platform's findData routes on hasGroupBy || hasAggregations and hands engine.aggregate the query's where, search, searchFields, having and context, so the answer is one row over exactly the rows the flat find matches. The console adapter's queryGroupHeaders posts the query verbatim to POST /data/:object/query, with no client-side groupBy check and no fallback (an answer without records throws), so the console path does reach that branch. No new DataSource member and no widened type.

  2. The question is the query behind the rows, less its window. RIGHT. compileWholeSetSummaryQuery keeps $filter (lowered through the spec's parseFilterAST, the lowering the grouped header query uses), $search and $searchFields; drops $top, $skip, $orderby, $select, $expand, $count; and refuses (returns null) any other key. Both producers stay inside that set: ListView's findParams literal ($filter, $orderby, $top, $skip, $select, $expand, $search, $searchFields) and the grid's own loader ($select, $top, $skip, $filter, $orderby, $search, $searchFields, $expand). A filter that lowers to undefined (an empty array) is refused rather than dropped: conservative in the right direction (a page label, never an unfiltered sum), and both producers already omit an empty $filter.

  3. The aggregation nodes and the read-back are the spec's. RIGHT. wholeSetSummaryNodes reads COLUMN_SUMMARY_AGGREGATION by the four kinds the spec declares (aggregate with function and fieldless; derived as a count node over the field; none and unmapped ask nothing), aliases through columnSummaryAlias, seeds count under LIST_VIEW_GROUP_COUNT_ALIAS and deduplicates: the node set compileListViewGroupQuery builds for a grouped view. useScopedColumnSummary reads each figure back through deriveColumnSummary; the two percents come back as a 0..1 ratio and are multiplied by 100 to the client footer's points unit (computeAggregation answers points). An unknown member never reaches deriveColumnSummary (which would throw summary_unknown): the SUPPORTED_SUMMARY_TYPES guard runs first, and the node builder checks the table with hasOwnProperty.

  4. Never a figure over other rows than the count's. RIGHT. The answer is used only when it is ONE row and Number(row.count) equals resolvedTotalMatching. The platform's search arm answers total = pageOffset + records.length + (hasMore ? 1 : 0) (verified in protocol.ts), so that case lands on the labelled page figure, as the PR says and pin (b) holds. externalManualPagination requires typeof hostRowCount === 'number', so the (hostRowCount as number) cast in footerRowsAreTheSet and the count check are both sound under a host.

  5. The REWORK's blocking defect is closed at the root. RIGHT. wholeSetSummaryNodes answers null when no column declares a mapped summary; useWholeSetSummary computes active = enabled && wholeSetSummaryNodes(columns) !== null and returns off before it compiles, requests, or subscribes to the invalidation bus; compileWholeSetSummaryQuery returns null for the same input. The new pin renders the real ObjectGrid in the console's shape (manualPagination + rowCount + findParams) for no-summary, only-none and only-unknown columns, asserts zero queryGroupHeaders calls and no footer, with a control leg that sees exactly one call once a column declares sum. The changeset's Unchanged bullet, the README and the mdx name the case.

  6. A labelled page figure (grid.summary.pagePattern) under every no-answer state. RIGHT. off, pending and unavailable all map to scope page; a whole-set answer that lacks one column's alias falls back per column to the page figure under the page label. The label is a whole pattern per pack, not a suffix glued to the label (zh places its own brackets; fr keeps its spaced colon), and SUMMARY_DEFAULT_TRANSLATIONS carries the en fallback for a render outside an I18nProvider.

  7. Public surface of @object-ui/plugin-grid: unchanged. RIGHT. src/index.tsx still exports useColumnSummary and the three named types only; package.json exports is . and ./style.css. useScopedColumnSummary, ColumnSummaryScope, useWholeSetSummary, WholeSetSummary, WholeSetSummaryInput, compileWholeSetSummaryQuery and wholeSetSummaryNodes are module exports that reach no barrel. useColumnSummary's signature is structurally identical (fieldMetadata? now names the local alias SummaryFieldMetadata for the same object type; data: any[] unchanged) and it delegates with scope rows, so every caller outside the package keeps the behaviour it had.

  8. @object-ui/types: the DataSource.queryGroupHeaders JSDoc widening. RIGHT. The member's type is unchanged; the doc now states what the spec already admits (the same query with no groupBy answers the whole filtered set as ONE header row) and that a source may refuse it. No new obligation on an implementer: a throw is the documented refusal, and the footer degrades to the page label.

  9. @object-ui/i18n: one key, grid.summary.pagePattern, in all ten packs. RIGHT, a published-surface addition; check:i18n-designer-parity and check:i18n-keys are green on the head.

  10. ObjectGrid.tsx: bulkFanoutParams and resolvedTotalMatching hoisted above the footer hook. RIGHT. Both expressions are byte-identical to the removed ones, both are plain derived consts, and lastFindParamsRef.current is stable within a render, so the move changes no reading. Every input the new block reads (hasInlineData, hostFindParams, lastFindParamsRef, externalManualPagination, hostRowCount, totalMatching, serverPage, fetchWindow, isGrouped, serverGroupedFetch, refreshKey, dataSource, objectName) is declared earlier in the component. The two added react-hooks/refs warnings are the pre-existing ref read, accepted by the seat.

  11. Behavioural accept set: one extra request per question. RIGHT and bounded. Only a flat, ungrouped grid with at least one mapped summary and rows that are one page of more asks; the question is keyed on content (objectName plus the compiled query), so a page turn or a header sort asks nothing; a refresh or an object write re-asks and keeps the prior answer on screen until the new one lands; a refusal is terminal for that question (no retry loop); a stale response is discarded through the effect's cancel flag.

Noted, not wrong: (i) whole-set count_empty and percent_* use the server's emptiness (a stored null) while the one-page footer still counts '' and []; the spec's ColumnSummarySchema text leaves that convergence to the renderer, and the changeset states the server meaning, so a paged and an unpaged grid can differ on a text field holding ''. (ii) A field the server refuses to aggregate fails the whole query and drops every footer figure to the page label: honest, coarse. Both are written down in the PR's Acceptance notes and the changeset.

② Semver level

.changeset/12081-footer-sum-scope.md declares @object-ui/plugin-grid: patch, @object-ui/i18n: minor, @object-ui/types: patch. That matches what the diff publishes: a bug fix in plugin-grid with no barrel change (patch); a new locale key in every pack (minor); a doc-comment-only change that ships in the .d.ts (patch). No major, nothing removed or renamed, so no migration text and no ADR-0087 marker is owed. The PR carries no skip-changeset label, and check-changeset-presence counted 15 source files across these 3 released packages against this one changeset. The frontmatter names are the packages' real names.

Clause-②: the PR body declares Clause-②: yes with neither arm. Correct: yes takes at least minor, and the i18n minor carries it; the body's inventory (one locale key, one JSDoc widening, no export, prop or type member) is what the diff adds, and it is still true after the rework (the round-1 module export wholeSetSummaryNodes reaches no barrel). Head check-runs Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are all success.

③ Boundary flags

Round-0 report 6106402263:

  • open_questions[0] (route A, B or C): ANSWERED by the seat in REWORK 6106424021 (A stands). This review confirms A against the spec's EngineAggregateOptions (groupBy optional; search and searchFields declared on the aggregate verb) and the platform's findData branch (hasGroupBy || hasAggregations, search carried). Closed.
  • deviation 1 (file surface beyond the claim: README, mdx, the types/src/data.ts JSDoc, the new useWholeSetSummary.ts): ACCEPTED. Docs follow the code; the contract doc names the query now sent; the new module is the fix's home; data-objectstack is untouched, as the claim allowed.
  • deviation 2 (two hoisted constant blocks): ACCEPTED, pure moves (①.10).
  • deviation 3 (the test reaches next-page through the chevron): ACCEPTED for the test; the cause is the class-a finding below.
  • deviation 4 (no browser pass): ACCEPTED. The console shape is covered by composition: ListView.findParamsHandoff.test.tsx pins the exact findParams + manualPagination + rowCount handoff, the new suite drives that shape through the real ObjectGrid, and the adapter's verbatim post is pinned by queryGroupHeaders-7189.test.ts.
  • deviation 5 (PR body not PATCHed): the seat's to write; this record judges the diff and the changeset, not the body.

REWORK 6106424021, owed items 1 to 3: ALL MET (①.5). The ablation reading (gate removed, exactly the new pin red) is the dev's; it is consistent with the test's content, since the count seed alone would make asked read 1.

Round-1 report 6106613032: open_questions is empty. Deviations: the PR body is stale in five places (the seat's; the Clause-② line and the inventory remain true); the branch was synced by merging main 5a65f7d with no force-push (fine); the worktrees were removed (fine).

out_of_scope_findings (the same four in both rounds):

  • class b, the findData search arm answers a page-local total because engine.count takes no search: CONFIRMED in protocol.ts. It is objectstack's, with the objectui count bar as consumer. ESCALATED to the seat: file, or dedupe against, a card on objectstack-ai/objectstack. This PR's count check already keeps the footer honest under it.
  • class a, the DataTable pager's four icon-only buttons carry no accessible name: ESCALATED to the seat: file an objectui card (packages/components data-table pager).
  • the two "noted, not filed" acceptance notes (server versus client emptiness; width rounding and the field-level-security refusal): ACCEPTED as documented behaviour within the spec's stated latitude. Whether to open a follow-up card on emptiness convergence is the seat's call, not a blocker.

Implemented-by: claude/issue-12081-footer-sum-scope
Reviewed-by: session_01AswpQDLCKiZos2jCXknwKz

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 07:37
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 07:37
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 69bca0a Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12081-footer-sum-scope branch October 11, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants