diff --git a/.changeset/analytics-where-names-the-route-hop.md b/.changeset/analytics-where-names-the-route-hop.md new file mode 100644 index 0000000000..e20f8ebe8e --- /dev/null +++ b/.changeset/analytics-where-names-the-route-hop.md @@ -0,0 +1,12 @@ +--- +"@objectstack/spec": patch +--- + +Documentation: the analytics `where` contract and the `element:number` D3 entry now name the hop an array filter is lowered at. + +Text only — no schema, accept-set, runtime or test behaviour changes. `AnalyticsQuerySchema.where` is still `FilterConditionSchema` and still refuses an array, which is the protocol working as `FilterArray`'s docblock (#5158 ruling C) declares it: a `FilterArray` is input-only authoring sugar, lowered to a `FilterCondition` at the single sink `parseFilterAST` (`@objectstack/spec/data`) the moment it arrives, and only the lowered `FilterCondition` travels any further. + +- `AnalyticsQuerySchema.where`'s `.describe()` gains one sentence pointing array authors at that lowering: an authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire, and this field admits only the lowered `FilterCondition`. It lands in the generated `content/docs/references/{api,data}/analytics.mdx` prop tables, which is where an author reads it. +- The `element-number-filter-rule-array` semantic migration entry recorded its runtime prerequisite one hop too late: "authored array → adapter lowering → filter AST → accepted by `lowerAnalyticsWhere`". `lowerAnalyticsWhere` (`service-analytics`) is the in-process door (#5334) for callers reaching `analyticsService.query` directly. The wire's door is the runtime route `POST /analytics/query`, which parses `where` with `AnalyticsQueryRequestSchema` before any service code runs, so an un-lowered array is refused there. The entry's reason clause now names that route hop and the `parseFilterAST` lowering the adapter owes before the wire (#15828; the adapter-side fix is objectui#7752). + +The sibling entry `element-record-picker-filter-rule-array` was read for the same claim and does not make it — its measured path is `find()` / `convertQueryParams`, not the analytics wire — so it is unchanged. diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 95fdd34731..294a736f7e 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -82,7 +82,7 @@ const result = AnalyticsEndpoint.parse(data); | **cube** | `string` | ✅ | Target cube name | | **measures** | `string[]` | ✅ | List of metrics to calculate | | **dimensions** | `string[]` | optional | List of dimensions to group by | -| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition) | +| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). | | **timeDimensions** | `{ dimension: string; granularity?: Enum<'second' \| 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: string \| string[] }[]` | optional | | | **order** | `Record>` | optional | | | **limit** | `number` | optional | | diff --git a/content/docs/references/data/analytics.mdx b/content/docs/references/data/analytics.mdx index b08e1aebdf..89abfe0f5b 100644 --- a/content/docs/references/data/analytics.mdx +++ b/content/docs/references/data/analytics.mdx @@ -55,7 +55,7 @@ const result = AggregationMetricType.parse(data); | **cube** | `string` | optional | Target cube name (optional when provided externally, e.g. in API request wrapper) | | **measures** | `string[]` | ✅ | List of metrics to calculate | | **dimensions** | `string[]` | optional | List of dimensions to group by | -| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition) | +| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). | | **timeDimensions** | `{ dimension: string; granularity?: Enum<'second' \| 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: string \| string[] }[]` | optional | | | **order** | `Record>` | optional | | | **limit** | `number` | optional | | diff --git a/packages/spec/src/data/analytics.zod.ts b/packages/spec/src/data/analytics.zod.ts index 150d203d41..0dcbbf57b6 100644 --- a/packages/spec/src/data/analytics.zod.ts +++ b/packages/spec/src/data/analytics.zod.ts @@ -296,7 +296,11 @@ export const AnalyticsQuerySchema = lazySchema(() => strictObject( * { where: { is_active: true, stage: { $nin: ['lost'] } } } * ``` */ - where: FilterConditionSchema.optional().describe('Filtering criteria (canonical Query DSL FilterCondition)'), + where: FilterConditionSchema.optional().describe( + 'Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is ' + + 'lowered by `parseFilterAST` on the client before the wire; this field admits only the ' + + 'lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`).' + ), /** * Time-bucketed dimensions. Strict as of #4001 batch D — and this item is diff --git a/packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts b/packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts index d2049059f8..ba6ef476e8 100644 --- a/packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts +++ b/packages/spec/src/migrations/entries/semantic/18.element-number-filter-rule-array.ts @@ -24,11 +24,21 @@ export const entry: SemanticMigration = { + 'the filter a list view stores and renders was refused by the KPI element beside it, ' + 'and the objectui parity gate had to carry a reasoned exemption to look away. The ' + 'convergence was sequenced consumer-first (ruling recorded 2026-08-25, Option A): ' - + 'objectui#6828 made `ObjectStackAdapter.aggregate()` lower a rule array through the ' - + 'same `translateFilterArray` its `find()` path runs before the analytics wire, and the ' - + 'objectui pin carrying it was re-measured before this entry moved — authored array → ' - + 'adapter lowering → filter AST → accepted by `lowerAnalyticsWhere`, which still refuses ' - + 'a RAW rule-object array by design. The ruled migration check ran with the change: the ' + + 'objectui#6828 made `ObjectStackAdapter.aggregate()` run the same `translateFilterArray` ' + + 'its `find()` path runs, and the objectui pin carrying it was re-measured before this ' + + 'entry moved — but that measurement named the wrong hop, and #15828 corrects it here. ' + + '`translateFilterArray` yields AST tuples, which are still a `FilterArray` — input-only ' + + 'sugar — so the real path is: authored array → `translateFilterArray` → lowered by ' + + '`parseFilterAST` (`@objectstack/spec/data`, the single sink the `FilterArray` docblock ' + + 'names, #5158 ruling C) in the adapter, BEFORE the wire → a `FilterCondition` on the ' + + 'body. The hop that decides it is the runtime route `POST /analytics/query`, which ' + + 'parses `where` with `AnalyticsQueryRequestSchema` — a `FilterCondition` and nothing ' + + 'else — so an un-lowered array is refused there before any service code runs. ' + + '`lowerAnalyticsWhere` (`service-analytics`), where that earlier measurement stopped, ' + + 'is the IN-PROCESS door (#5334) for callers reaching `analyticsService.query` ' + + "directly, not the wire's; it too still refuses a RAW rule-object array by design. " + + 'objectui#7752 lands the adapter-side lowering. ' + + 'The ruled migration check ran with the change: the ' + 'sweep of first-party corpora (examples/, skills/, create-objectstack, content/docs/, ' + 'packages/apps/, spec fixtures) found ONE `element:number` author writing a record-form ' + '`filter` — a spec test fixture, rewritten to the array form in the same change — and ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index d70701822d..b437dd1efd 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6441,11 +6441,21 @@ const step18: MigrationStep = { + 'the filter a list view stores and renders was refused by the KPI element beside it, ' + 'and the objectui parity gate had to carry a reasoned exemption to look away. The ' + 'convergence was sequenced consumer-first (ruling recorded 2026-08-25, Option A): ' - + 'objectui#6828 made `ObjectStackAdapter.aggregate()` lower a rule array through the ' - + 'same `translateFilterArray` its `find()` path runs before the analytics wire, and the ' - + 'objectui pin carrying it was re-measured before this entry moved — authored array → ' - + 'adapter lowering → filter AST → accepted by `lowerAnalyticsWhere`, which still refuses ' - + 'a RAW rule-object array by design. The ruled migration check ran with the change: the ' + + 'objectui#6828 made `ObjectStackAdapter.aggregate()` run the same `translateFilterArray` ' + + 'its `find()` path runs, and the objectui pin carrying it was re-measured before this ' + + 'entry moved — but that measurement named the wrong hop, and #15828 corrects it here. ' + + '`translateFilterArray` yields AST tuples, which are still a `FilterArray` — input-only ' + + 'sugar — so the real path is: authored array → `translateFilterArray` → lowered by ' + + '`parseFilterAST` (`@objectstack/spec/data`, the single sink the `FilterArray` docblock ' + + 'names, #5158 ruling C) in the adapter, BEFORE the wire → a `FilterCondition` on the ' + + 'body. The hop that decides it is the runtime route `POST /analytics/query`, which ' + + 'parses `where` with `AnalyticsQueryRequestSchema` — a `FilterCondition` and nothing ' + + 'else — so an un-lowered array is refused there before any service code runs. ' + + '`lowerAnalyticsWhere` (`service-analytics`), where that earlier measurement stopped, ' + + 'is the IN-PROCESS door (#5334) for callers reaching `analyticsService.query` ' + + "directly, not the wire's; it too still refuses a RAW rule-object array by design. " + + 'objectui#7752 lands the adapter-side lowering. ' + + 'The ruled migration check ran with the change: the ' + 'sweep of first-party corpora (examples/, skills/, create-objectstack, content/docs/, ' + 'packages/apps/, spec fixtures) found ONE `element:number` author writing a record-form ' + '`filter` — a spec test fixture, rewritten to the array form in the same change — and '