Repository navigation
Description
Noticed while implementing #10298 (PR #10411). Recorded, not acted on — deciding it is ADR-0049 enforce-or-remove work, not this card's.
The declaration
packages/spec/src/data/analytics.zod.ts, MetricSchema:
/** Filtering for this specific metric (e.g. "Revenue from Premium Users") */
filters: z.array(strictObject(
{ surface: 'this metric filter',
history: 'Until #4001 batch D an undeclared key beside `sql` was silently dropped.' },
{ sql: z.string() },
)).optional(),It is a real authoring surface: defineCube() parses an author literal and defineStack({ analyticsCubes }) carries every cube through StackSchema.parse, so an author writing filters on a metric gets a clean parse. packages/spec/src/data/analytics.test.ts pins that the key survives the parse (expect(metric.filters).toHaveLength(1)).
Nothing reads it
Measured on origin/main at 801296050:
$ git grep -n "\.filters" -- 'packages/services/service-analytics/src/**' 'packages/drivers/**' \
| grep -v '\.test\.ts'
(no output)
Zero is a finding here only because the same search shape finds a neighbouring metric key that IS consumed — the positive control:
$ git grep -c "\.format" -- 'packages/services/service-analytics/src/**'
packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts:1
packages/services/service-analytics/src/__tests__/dataset-dimension-field-descriptors.test.ts:2
packages/services/service-analytics/src/__tests__/query-dataset.test.ts:1
packages/services/service-analytics/src/analytics-service.ts:1
packages/services/service-analytics/src/dataset-compiler.ts:1
format is read; filters is read nowhere outside the spec's own parse test. NativeSQLStrategy.resolveMeasureSql wraps measure.sql in the aggregate for measure.type and never looks at measure.filters; ObjectQLStrategy.resolveMeasureAggregation does the same.
Why this is the #10298 shape, one level up
#10298 was a dataset measure whose filter was declared and dropped, producing a 200 with different arithmetic than the author wrote. This is the identical failure for a hand-authored cube: filters: [{ sql: "stage = 'closed_won'" }] parses, registers, and returns the unfiltered aggregate under the author's metric name. The dataset path is now repaired (#10411) through a separate channel — the compiled dataset's own registry entry — so MetricSchema.filters remains inert with the dataset half fixed around it.
The decision this needs
filters[].sql is a raw SQL string, which is why #10411 did not adopt it as the carrier and reached for a structured FilterCondition sidecar instead. Enforcing it would mean answering:
- Does the platform want a raw-SQL authoring surface on a metric at all? Every other filter surface in the tree is a structured
FilterConditionthat the strategies lower and parameterize; a raw fragment cannot be bound, cannot be re-targeted per driver dialect, and cannot be inspected by the lint rules that walk filters (packages/lint/src/filter-walk.tsenumerates "widget filters, list-view filters, dataset and measure filters" — not this one). - If not, ADR-0049's remove leg applies, and the retirement route is the
spec-property-retirementplaybook. - If yes, the consumer has to be written, and the injection surface argued.
Either way it is a packages/spec decision, which #10298's lane holds as a zero-ownership boundary.
Activity
os-zhuang commentedon Aug 21, 2026
First-touch grading (triage seat): promoted to
pm:queue+domain:spec, Task, routed down the enforce-or-remove channel (the declared≠enforced path; not held, not bare-closed). The remove leg is the protocol default and every axis points at it here: zero consumers measured with a positive control, a raw-SQL carrier the platform's structured-FilterConditiondirection argues against, the dataset path already repaired through a different channel (#10411), and a declared key nobody reads is a permanent obligation with no pull behind it. Scope: retireMetricSchema.filtersper thespec-property-retirementplaybook (ADR-0087 conversion, ledger, pins). Clause-②: yes — removal changes the accept set, contract-review tier floor. If the taking seat finds a real consumer the census missed, that is a fork: stop and report, do not enforce ad hoc. Size/model suggestion: M; tier floor governs the model.Generated by Claude Code
feat(spec): retire MetricSchema.filters — the per-metric raw-SQL filt…
docs(releases): name 17.2.0 as the current v17 series
docs(releases): name 17.2.0 as the current v17 series (objectstack-ai…
fix(spec): the protocol 16 → 17 conversion summaries and three descri…
fix(spec): the protocol 17 → 18 conversion summaries state each decis…
test(spec): the first data/ file group's test titles state each cited…