fix(data-objectstack): aggregate()'s spec-shape branch refuses the analytics branch's filter / field / function - #8612
Merged
os-justin merged 2 commits intoSep 8, 2026
Conversation
…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
Contributor
✅ Console Performance Budget
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
Size Limits
|
os-justin
marked this pull request as ready for review
September 8, 2026 16:28
os-justin
enabled auto-merge
September 8, 2026 16:28
os-justin
deleted the
claude/issue-6864-aggregate-spec-shape-remaining-keys
branch
September 8, 2026 16:55
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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,fieldandfunctionare the analytics branch's own parameters and were neither read, nor refused, nor warned about here — they were simply absent from the body posted toPOST /data/:object/query.Premise check (all of the card's premises re-verified on today's
origin/main)filter/field/functionvanishpackages/data-objectstack/src/index.ts, thelooksLikeSpecShapeblock (the card's:4925is now:5386; re-located by content)AggregateParamsdeclaresgroupBy: string,filter?: any, nowhere, noaggregationspackages/types/src/data.ts:1269. ⛔ NOT touched here (manual floor, raised separately)DashboardRenderer.tsx:618/:722,DashboardGridLayout.tsx:238/:301,ObjectMetricWidget.tsx:250isObjectProvidernarrows toaggregate?: any(plugin-dashboard/src/utils.ts:10) andcomputeOne(ds: any, …).ObjectMetricWidget's own prop type saysgroupBy?: string, but it is fed anany, so nothing refuses an arrayaggregate.groupBy⭐ 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/.ymlfiles (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.groupByin metadata: still zero. Every one of the 24 hits is a report/kanban/gridgroupBy(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-arrayaggregate.groupBy)".ObjectChart.runAggregategates its spec-shape call ongb && typeof gb === 'object' && !Array.isArray(gb)— the STRUCTURED node shape — so an ARRAYgroupBydoes not take that gate: it falls through to the LEGACY call,{ field, function, groupBy, filter }(plugin-charts/src/ObjectChart.tsx:608), andArray.isArray(params.groupBy)lands that call on the spec-shape branch anyway.ObjectMetricWidget.computeOneforwardsaggregate.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
groupBytogether with legacyfield/functiondo exist now (plugin-charts/src/ObjectChart.absentCategoryAxisRefusal-8168.test.tsx:125,plugin-dashboard/src/__tests__/DashboardChart.categoryAxisKey-8269.test.tsx:154and:158,core/src/utils/chart-category-key.test.ts:45and:56), and one passes an ARRAYgroupBywith a legacyfunction(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 takesObjectChart's structured path, which builds a clean spec-shape call. ⛔priority:p2is left exactly as it is; the grading is triage's.What changed
AnalyticsKeysOnSpecShapeError(exported). Carries theINVALID_FILTER/ 400 pair both siblings carry, soisMalformedFilterError()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. Pluskeys(the offending names) andreceived(what each carried).looksLikeSpecShapedisjunct put the call on this branch, and — the half that is worse than thewherehalf — says outright when the resulting query would have carried agroupByand noaggregationsat all, a grouping with no measure.assertNoAnalyticsKeysOnSpecShape, called AFTER the existingwheregate. 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 sendswhereunlowered, so one chart's filter is lowered or not depending on which aggregation shape it uses #6825's behaviour. Pinned.INVALID_FILTERcode line updated.scripts/check-doc-example-types.mjs: its declared-failure ledger keys rows byFILE:LINE symbol, and this diff shiftedcreateObjectStackAdapter's@examplefrom:6156to:6323. Only the line number in the key changes.Scoped deliberately — the two implementations that are worse than the bug
orderBy, a future spec key, or any host extra, and it would pass a naive "spec-shape refusesfilter" pin while breaking traffic nobody complained about. The gate names three keys because they are the OTHER branch's parameters.in, but!= null. A key spread in asundefinedcarries nothing to drop, and that is exactly how both in-tree producers build their params (filter: filterForRun,field: schema.aggregate.field).whereunlowered, 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 postsdimensions: [params.groupBy], so an array would go out nested.AggregateParamsis 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 inaggregate-spec-shape-where.test.ts— the row that used to observe the flipped call posting only itswhere, 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-existingwheregate. Every refusal row asserts the class, the exactkeysset, 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 comparinggit hash-objectto the HEAD blob andgit diff HEADbeing 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.looksLikeSpecShape= constanttruelooksLikeSpecShape= constantfalseNon-regression axis, derived from the plausible wrong fix. A legacy-shape call (string
groupBy) carryingfilter/field/functionmust still succeed and still lower all three: pinned on the analytics wire —whereequal toparseFilterAST(['stage','=','won']),measures: ['amount_sum'],dimensions: ['stage'], and rows keyed back underamount. Also pinned:groupBy: '_all', and the STRUCTURED spec-shape callObjectChartreally 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-treeaggregate()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-existingno-explicit-anywarnings.pnpm exec eslint scripts/check-doc-example-types.mjs --format json— 1 file judged, 0 errors, 0 warnings.check:control-bytes,check:doc-fences,check:self-import,check:vi-mock-specifiers,check:vi-mock-inherit,check:unreferenced-sources,check:changeset-no-majorall print their own OK line.check:doc-snippets,check:doc-examplesandcheck:readme-exportswere 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 --listjudges the new exportreal.Not run locally, left to CI: the remaining
check:*farm and the full test matrix over all 34turbo ls --affectedpackages. The narrowing above is the set that can observe this change —ObjectStackAdapteris constructed only inpackages/data-objectstack's own tests, and every other package drivesaggregate()through a double.Changeset
.changeset/6864-aggregate-spec-shape-analytics-keys.md,minoron@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