Measured while running the census for #6825. Same predicate, different failure mode — filed separately so a narrowly-scoped ruling on #6825 cannot lose it. ⛔ Nothing was changed; this is a source read.
The measurement
ObjectStackAdapter.aggregate() (packages/data-objectstack/src/index.ts:4556-4573, on objectui aca70f6) selects the spec-shape branch with:
const looksLikeSpecShape =
params != null &&
(Array.isArray(params.groupBy) ||
Array.isArray(params.aggregations) ||
params.where !== undefined);
Inside that branch it reads only four keys:
if (Array.isArray(params.groupBy)) queryAst.groupBy = params.groupBy;
if (Array.isArray(params.aggregations)) queryAst.aggregations = params.aggregations;
if (params.where !== undefined) queryAst.where = params.where;
if (typeof params.limit === 'number') queryAst.limit = params.limit;
params.filter, params.field and params.function are never read. They are not rejected and not warned about — they are simply absent from queryAst.
Why that is reachable from the LEGACY caller shape
Array.isArray(params.groupBy) is one of the three entry conditions, and the legacy analytics shape is { field, function, groupBy, filter }. So a caller that sends the legacy shape but whose groupBy happens to be an array lands in the spec-shape branch, where:
- its
filter is dropped — the server aggregates the whole table while the caller believes a filter was applied;
- its
field / function are dropped too, so queryAst carries a groupBy with no aggregations at all — a grouping with no measure requested.
Three in-tree callers forward an authored groupBy into that predicate without constraining its type:
packages/plugin-dashboard/src/DashboardRenderer.tsx:623 and :704 — groupBy: providerAgg.groupBy, alongside filter: widgetData.filter || widget.filter
packages/plugin-dashboard/src/DashboardGridLayout.tsx:280 — same forward
packages/plugin-dashboard/src/ObjectMetricWidget.tsx:246 — groupBy: aggregate.groupBy || '_all', alongside filter: filterForRun
In every one of them groupBy comes straight from authored widget metadata, and none of them is typed: the datasource is ds: any / adapter at those seams.
Contract status
The declared contract at this seam, AggregateParams (packages/types/src/data.ts:1248-1258), says:
export interface AggregateParams {
field: string;
function: string;
groupBy: string; // a STRING
filter?: any;
}
It declares no where and no aggregations key at all, and groupBy as a plain string. So the whole spec-shape branch accepts a params shape this interface does not describe, and the drop above happens between two keys (filter vs where) of which only one is declared.
Reachability today: NOT established (same as #6825)
The census for #6825 swept the whole worktree (5503 files across .json/.ts/.tsx/.mdx/.md/.yaml/.yml, excluding node_modules/dist/build/.git/.next/coverage/.turbo) and found zero authored aggregate.groupBy written as an array or an object — every one of the 18 aggregate + groupBy sites passes a string. So this is latent, exactly like #6825, and I am NOT claiming a live defect.
Recording it anyway because the severity judgement is triage's, not mine, and because this variant is the one whose symptom matches the p1 condition triage wrote on #6825: the filter is dropped silently, the chart still renders, and the numbers are wrong with nothing to see.
Relationship to #6825
#6825 asks what branch 2 should do with a where it does not lower. This card asks what branch 2 should do with the keys it does not read. Both are answered by the same maintainer ruling about what that branch's accepted shape actually is, so this is probably a sub-question of that ruling rather than an independent repair — noted so it can be folded in rather than fixed separately.
Refs: #6825 (the census this came out of) - #6302 / PR #6828 (the analytics-branch lowering) - #6206 (the sibling contract question about element:number's filter being declared an object while every other filter input is an array)
Generated by Claude Code
Measured while running the census for #6825. Same predicate, different failure mode — filed separately so a narrowly-scoped ruling on #6825 cannot lose it. ⛔ Nothing was changed; this is a source read.
The measurement
ObjectStackAdapter.aggregate()(packages/data-objectstack/src/index.ts:4556-4573, on objectuiaca70f6) selects the spec-shape branch with:Inside that branch it reads only four keys:
params.filter,params.fieldandparams.functionare never read. They are not rejected and not warned about — they are simply absent fromqueryAst.Why that is reachable from the LEGACY caller shape
Array.isArray(params.groupBy)is one of the three entry conditions, and the legacy analytics shape is{ field, function, groupBy, filter }. So a caller that sends the legacy shape but whosegroupByhappens to be an array lands in the spec-shape branch, where:filteris dropped — the server aggregates the whole table while the caller believes a filter was applied;field/functionare dropped too, soqueryAstcarries agroupBywith noaggregationsat all — a grouping with no measure requested.Three in-tree callers forward an authored
groupByinto that predicate without constraining its type:packages/plugin-dashboard/src/DashboardRenderer.tsx:623and:704—groupBy: providerAgg.groupBy, alongsidefilter: widgetData.filter || widget.filterpackages/plugin-dashboard/src/DashboardGridLayout.tsx:280— same forwardpackages/plugin-dashboard/src/ObjectMetricWidget.tsx:246—groupBy: aggregate.groupBy || '_all', alongsidefilter: filterForRunIn every one of them
groupBycomes straight from authored widget metadata, and none of them is typed: the datasource isds: any/adapterat those seams.Contract status
The declared contract at this seam,
AggregateParams(packages/types/src/data.ts:1248-1258), says:It declares no
whereand noaggregationskey at all, andgroupByas a plain string. So the whole spec-shape branch accepts a params shape this interface does not describe, and the drop above happens between two keys (filtervswhere) of which only one is declared.Reachability today: NOT established (same as #6825)
The census for #6825 swept the whole worktree (5503 files across
.json/.ts/.tsx/.mdx/.md/.yaml/.yml, excludingnode_modules/dist/build/.git/.next/coverage/.turbo) and found zero authoredaggregate.groupBywritten as an array or an object — every one of the 18aggregate+groupBysites passes a string. So this is latent, exactly like #6825, and I am NOT claiming a live defect.Recording it anyway because the severity judgement is triage's, not mine, and because this variant is the one whose symptom matches the p1 condition triage wrote on #6825: the filter is dropped silently, the chart still renders, and the numbers are wrong with nothing to see.
Relationship to #6825
#6825 asks what branch 2 should do with a
whereit does not lower. This card asks what branch 2 should do with the keys it does not read. Both are answered by the same maintainer ruling about what that branch's accepted shape actually is, so this is probably a sub-question of that ruling rather than an independent repair — noted so it can be folded in rather than fixed separately.Refs: #6825 (the census this came out of) - #6302 / PR #6828 (the analytics-branch lowering) - #6206 (the sibling contract question about
element:number'sfilterbeing declared an object while every otherfilterinput is an array)Generated by Claude Code