Skip to content

fix(data-objectstack): aggregate()'s spec-shape branch refuses the analytics branch's filter / field / function - #8612

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-6864-aggregate-spec-shape-remaining-keys
Sep 8, 2026
Merged

fix(data-objectstack): aggregate()'s spec-shape branch refuses the analytics branch's filter / field / function#8612
os-justin merged 2 commits into
mainfrom
claude/issue-6864-aggregate-spec-shape-remaining-keys

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #6864

Applies objectui#6825's maintainer ruling (2026-08-30, option A — refuse at the producer) to the rest of the same branch. aggregate()'s spec-shape branch builds its request from exactly four keys (groupBy, aggregations, where, limit); filter, field and function are the analytics branch's own parameters and were neither read, nor refused, nor warned about here — they were simply absent from the body posted to POST /data/:object/query.

Premise check (all of the card's premises re-verified on today's origin/main)

card premise verdict
the branch reads only four keys; filter/field/function vanish holdspackages/data-objectstack/src/index.ts, the looksLikeSpecShape block (the card's :4925 is now :5386; re-located by content)
AggregateParams declares groupBy: string, filter?: any, no where, no aggregations holdspackages/types/src/data.ts:1269. ⛔ NOT touched here (manual floor, raised separately)
the three forwarding points still exist holds, movedDashboardRenderer.tsx:618 / :722, DashboardGridLayout.tsx:238 / :301, ObjectMetricWidget.tsx:250
the seam is untyped, so the type system cannot help holdsisObjectProvider narrows to aggregate?: any (plugin-dashboard/src/utils.ts:10) and computeOne(ds: any, …). ObjectMetricWidget's own prop type says groupBy?: string, but it is fed an any, so nothing refuses an array
reachability: zero authored array/object aggregate.groupBy partly falsified — see below

⭐ Reachability re-measured on 2026-09-08, and one #6911 premise is falsified

The census was re-run on today's tree, not copied. Population: 6557 tracked .json/.ts/.tsx/.mdx/.md/.yaml/.yml files (the 2026-08-30 census read 5503). Lit control on the same command shape — git grep -nIE '"?groupBy"?[[:space:]]*:' — fires 208 lines across 103 files, so the zero below is a reading and not a broken command. Target — the same shape with [[:space:]]*[\[{] appended — returns 24 lines across 14 files, classified one by one.

Authored ARRAY aggregate.groupBy in metadata: still zero. Every one of the 24 hits is a report/kanban/grid groupBy (a different key), documentation prose, or a test fixture.

But the in-tree producer chain is complete, and PR #6911's docblock says otherwise. That docblock states "this repo has no producer that can reach it (ObjectChart's gate requires a non-array aggregate.groupBy)". ObjectChart.runAggregate gates its spec-shape call on gb && typeof gb === 'object' && !Array.isArray(gb) — the STRUCTURED node shape — so an ARRAY groupBy does not take that gate: it falls through to the LEGACY call, { field, function, groupBy, filter } (plugin-charts/src/ObjectChart.tsx:608), and Array.isArray(params.groupBy) lands that call on the spec-shape branch anyway. ObjectMetricWidget.computeOne forwards aggregate.groupBy || '_all' into the same legacy shape (:247). So the chain from authored metadata to the drop is entirely in-tree; the only missing link is an authored array value. That is a strictly stronger reachability statement than 2026-08-30's, and it is why the pins below transcribe the exact params those two call sites build.

On the stated p1 escalation condition, reported precisely and not graded. Author-shaped sites passing an OBJECT groupBy together with legacy field/function do exist now (plugin-charts/src/ObjectChart.absentCategoryAxisRefusal-8168.test.tsx:125, plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx:154 and :158, core/src/utils/chart-category-key.test.ts:45 and :56), and one passes an ARRAY groupBy with a legacy function (chart-category-key.test.ts:79). All of them are test fixtures rather than shipped app metadata, and the OBJECT form does not reach this defect — it takes ObjectChart's structured path, which builds a clean spec-shape call. ⛔ priority:p2 is left exactly as it is; the grading is triage's.

What changed

  • AnalyticsKeysOnSpecShapeError (exported). Carries the INVALID_FILTER / 400 pair both siblings carry, so isMalformedFilterError() recognises it and a failed widget renders "this filter is malformed" rather than "check your connection" (fix(list,i18n): a 400 from the server no longer reads as "check your connection" #3066) — one branch, one envelope. Plus keys (the offending names) and received (what each carried).
  • The message names each offending key on its own, says what that key's spec-shape equivalent is, states which looksLikeSpecShape disjunct put the call on this branch, and — the half that is worse than the where half — says outright when the resulting query would have carried a groupBy and no aggregations at all, a grouping with no measure.
  • assertNoAnalyticsKeysOnSpecShape, called AFTER the existing where gate. Deliberate: no input that already refused changes which error it gets, so this is strictly additive over finding(data-objectstack): aggregate()'s spec-shape branch sends where unlowered, so one chart's filter is lowered or not depending on which aggregation shape it uses #6825's behaviour. Pinned.
  • README: a new "the two shapes are alternatives, not a mixture" subsection, the error listed in the Error Types block, and the INVALID_FILTER code line updated.
  • scripts/check-doc-example-types.mjs: its declared-failure ledger keys rows by FILE:LINE symbol, and this diff shifted createObjectStackAdapter's @example from :6156 to :6323. Only the line number in the key changes.

Scoped deliberately — the two implementations that are worse than the bug

  1. Not "every key the branch does not read." That would refuse orderBy, a future spec key, or any host extra, and it would pass a naive "spec-shape refuses filter" pin while breaking traffic nobody complained about. The gate names three keys because they are the OTHER branch's parameters.
  2. Not in, but != null. A key spread in as undefined carries nothing to drop, and that is exactly how both in-tree producers build their params (filter: filterForRun, field: schema.aggregate.field).
  3. Not a re-route to the analytics branch. That is the tolerant-consumer direction finding(data-objectstack): aggregate()'s spec-shape branch sends where unlowered, so one chart's filter is lowered or not depending on which aggregation shape it uses #6825 refused, and it could not work anyway: that branch posts dimensions: [params.groupBy], so an array would go out nested.
  4. AggregateParams is untouched. The contract widening is a manual floor and is raised separately, per triage.

Tests — every pin observed RED on purpose

packages/data-objectstack/src/aggregate-spec-shape-analytics-keys.test.ts (19 tests, built on the sibling file's three-door harness) and one updated row in aggregate-spec-shape-where.test.ts — the row that used to observe the flipped call posting only its where, which is to say it pinned the very drop this card reports. It now pins the refusal, and still proves the flip: only the spec-shape branch has this gate.

The pins assert the REASON, not the envelope. This branch already refuses one thing with the same INVALID_FILTER / 400 pair, so an envelope-only pin would pass on a throw from the pre-existing where gate. Every refusal row asserts the class, the exact keys set, and that the message names ITS key and not the others.

Four ablation legs, each: mutate, prove the mutation on disk by anchor count, run, restore by git checkout HEAD -- path, prove the restore by comparing git hash-object to the HEAD blob and git diff HEAD being empty. Classification is from vitest's JSON reporter; no harness-death marker in any leg, and all four legs report the same 41-test population, so nothing silently failed to load.

leg RED reads as
remove the guard call (revert the fix) 12 every refusal pin; the non-regression rows stay green, as they must
looksLikeSpecShape = constant true 3 the caricature is caught by the legacy non-regression rows
looksLikeSpecShape = constant false 38 the caricature is caught by everything
refuse EVERY unrecognised key (the strictly-worse fix) 5 caught by "keys OUTSIDE the analytics set are not refused" and by the nullish row

Non-regression axis, derived from the plausible wrong fix. A legacy-shape call (string groupBy) carrying filter / field / function must still succeed and still lower all three: pinned on the analytics wire — where equal to parseFilterAST(['stage','=','won']), measures: ['amount_sum'], dimensions: ['stage'], and rows keyed back under amount. Also pinned: groupBy: '_all', and the STRUCTURED spec-shape call ObjectChart really builds, transcribed from that call site.

Commands, all at c84d46240.

  • pnpm exec vitest run packages/data-objectstack/src — 804 passed, 0 failed, 239 suites.
  • pnpm exec vitest run packages/plugin-charts packages/plugin-dashboard — 1378 passed, 0 failed.
  • pnpm exec vitest run packages/components/src/renderers/basic packages/core/src/utils apps/console/src/pages/system — 1335 passed, 0 failed (every other in-tree aggregate() caller).
  • pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts — 46 passed (the edited gate script's own suite).
  • pnpm --filter @object-ui/data-objectstack run type-check — clean, on a built dependency closure.
  • pnpm --filter @object-ui/data-objectstack run lint (eslint ., whole package, plain form) — 0 errors, 448 pre-existing no-explicit-any warnings. pnpm exec eslint scripts/check-doc-example-types.mjs --format json — 1 file judged, 0 errors, 0 warnings.
  • Gates: check:control-bytes, check:doc-fences, check:self-import, check:vi-mock-specifiers, check:vi-mock-inherit, check:unreferenced-sources, check:changeset-no-major all print their own OK line. check:doc-snippets, check:doc-examples and check:readme-exports were re-run on a BUILT tree — an unbuilt tree makes them exit non-zero as a precondition, not a verdict — and all three print their own pass line. check:readme-exports --list judges the new export real.

Not run locally, left to CI: the remaining check:* farm and the full test matrix over all 34 turbo ls --affected packages. The narrowing above is the set that can observe this change — ObjectStackAdapter is constructed only in packages/data-objectstack's own tests, and every other package drives aggregate() through a double.

Changeset

.changeset/6864-aggregate-spec-shape-analytics-keys.md, minor on @object-ui/data-objectstack, spelling out what now throws, what deliberately does not, and the migration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

os-justin and others added 2 commits September 8, 2026 16:03
…alytics branch's keys

The spec-shape branch builds its request from exactly four keys — `groupBy`,
`aggregations`, `where`, `limit` — and reads nothing else. `filter`, `field`
and `function` are the analytics branch's own parameters and were neither
read, nor refused, nor warned about on this branch: they were simply absent
from the body posted to `POST /data/:object/query`.

That is worse than the `where` half objectui#6825 ruled on. `field` +
`function` are the analytics branch's whole measure and this branch takes a
measure only out of `aggregations`, so the legacy shape
`{ field, function, groupBy, filter }` whose `groupBy` happened to be an ARRAY
produced a query with a `groupBy` and no `aggregations` at all — a grouping
with no measure — and with the author's filter gone too. The chart rendered,
the numbers were wrong, and there was nothing to look at.

Apply #6825's ruling (option A: refuse at the producer, never degrade
quietly) to the rest of the same branch. `AnalyticsKeysOnSpecShapeError`
carries the `INVALID_FILTER` / 400 pair its siblings carry, names each
offending key and what its spec-shape equivalent is, states which
`looksLikeSpecShape` disjunct selected the branch, and says outright when the
query would have had no measure.

Scoped to those three keys and to non-nullish values on purpose: refusing
every unrecognised key would break legitimate traffic, and a key spread in as
`undefined` carries nothing to drop — which is how both in-tree producers
build their params.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
`check:doc-example-types` keys its declared-failure ledger by `FILE:LINE
symbol`. Inserting the new refusal moved `createObjectStackAdapter`'s
`@example` block from `index.ts:6156` to `:6323`, so the row went stale and
the same pre-existing `process`-is-undeclared failure came back as an
UNDECLARED FAILURE. Only the line number in the key changes; the codes, the
reason and the row count are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.1 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BKJswcUk.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 196.02KB 54.44KB
fields (index.js) 243.74KB 61.55KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 250.72KB 64.81KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Labels

data-adapter documentation Improvements or additions to documentation tests

Projects

None yet

1 participant