@@ -67,18 +67,31 @@ type TemporalDriverSurface = Pick<
6767> ;
6868
6969/**
70- * Narrow a strategy-supplied aggregation `method` to the engine contract's
71- * `AggregationFunction`, refusing anything else.
70+ * Re-parse a bridge-supplied aggregation `method` as the engine contract's
71+ * `AggregationFunction` before it is forwarded as `function`, refusing
72+ * anything else.
7273 *
73- * The two sides genuinely differ: `IDataEngine.aggregate`'s
74- * `aggregations[].function` is the six-value enum, while the analytics
75- * strategy contract that feeds this bridge declares `aggregations[].method` as
76- * `string`. Parsing with the spec's OWN enum keeps a single vocabulary — no
77- * local literal list to drift, and `AggregationFunction`'s error map already
78- * knows the retired `array_agg` / `string_agg` spellings.
74+ * Both sides now declare the same six-value enum: `IDataEngine.aggregate`'s
75+ * `aggregations[].function`, and — since #12776 — the analytics strategy
76+ * contract (`StrategyContext.executeAggregate`) plus the two consumer-local
77+ * config mirrors this package keeps in lockstep with it (#12940). So this
78+ * parse is DEFENCE IN DEPTH behind a compile-time check, not the only check
79+ * (#11833).
80+ *
81+ * That is a reason to keep it, not to delete it. Types are erased: a
82+ * JavaScript app supplying its own `executeAggregate`, or host drift arriving
83+ * through a cube object that never met `CubeSchema`'s parse (the path
84+ * `aggregate-bridge-function-vocabulary.test.ts` drives end to end), still
85+ * reaches this seam carrying a method the engine does not declare. What the
86+ * refusal buys is in `plugin.ts`'s forward below and in #12209: the engine is
87+ * never handed a `function` no driver declares.
88+ *
89+ * Parsing with the spec's OWN enum keeps a single vocabulary — no local
90+ * literal list to drift, and `AggregationFunction`'s error map already knows
91+ * the retired `array_agg` / `string_agg` spellings.
7992 */
8093function parseEngineAggregateFunction (
81- method : string ,
94+ method : AggregationFunction ,
8295 alias : string ,
8396) : NonNullable < Parameters < IDataEngine [ 'aggregate' ] > [ 1 ] [ 'aggregations' ] > [ number ] [ 'function' ] {
8497 const parsed = AggregationFunction . safeParse ( method ) ;
@@ -116,13 +129,23 @@ export interface AnalyticsServicePluginOptions {
116129 executeAggregate ?: ( objectName : string , options : {
117130 groupBy ?: string [ ] ;
118131 /**
119- * Per-aggregation `filter` (#10576, the #10413 contract field) — a
120- * CUSTOM bridge (an app author's own `executeAggregate`, as opposed to
121- * the auto-bridge below) MUST forward it to the real engine the same way
122- * the auto-bridge does, or a measure-scoped filter this plugin lowers
123- * onto the aggregation silently never reaches storage.
132+ * The CUSTOM bridge's view of the aggregation entries — an app author's
133+ * own `executeAggregate`, as opposed to the auto-bridge below. Mirrors
134+ * `StrategyContext.executeAggregate`
135+ * (`packages/spec/src/contracts/analytics-service.ts`) and must stay in
136+ * lockstep with it; the two members that lockstep is load-bearing for:
137+ *
138+ * - `filter` (#10576, the #10413 contract field) — a custom bridge MUST
139+ * forward it to the real engine the same way the auto-bridge does, or a
140+ * measure-scoped filter this plugin lowers onto the aggregation
141+ * silently never reaches storage.
142+ * - `method` is the spec's OWN six-value `AggregationFunction`, not
143+ * `string`: #12776 narrowed the contract, #12940 brought this mirror
144+ * back into line. This is the declaration a custom-bridge author types
145+ * their handler against, so it is where the compile-time vocabulary
146+ * #12776 bought for strategy authors reaches them too.
124147 */
125- aggregations ?: Array < { field : string ; method : string ; alias : string ; filter ?: Record < string , unknown > } > ;
148+ aggregations ?: Array < { field : string ; method : AggregationFunction ; alias : string ; filter ?: Record < string , unknown > } > ;
126149 filter ?: Record < string , unknown > ;
127150 /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
128151 timezone ?: string ;
@@ -288,15 +311,24 @@ export class AnalyticsServicePlugin implements Plugin {
288311 // vacuous-filter convention.
289312 aggregations : aggregations ?. map ( ( a ) => ( {
290313 // [#11833] `function` is the engine contract's SIX-value
291- // `AggregationFunction`, while this bridge's own input declares
292- // `method: string` (`StrategyContext.executeAggregate`, spec
293- // `contracts/analytics-service.ts:300`). Narrowing the engine side
294- // to the contract turned that forward into a compile error — the
314+ // `AggregationFunction`. This bridge's own input declared
315+ // `method: string` when that history was written
316+ // (`StrategyContext.executeAggregate`, spec
317+ // `contracts/analytics-service.ts`), so the two ends of this
318+ // rename spoke different vocabularies: narrowing the engine side
319+ // to the contract turned the forward into a compile error — the
295320 // correct signal, and the one the deleted structural type hid by
296321 // declaring `function: string` on both sides.
297322 //
298- // Closed by PARSING with the spec enum itself rather than by
299- // widening back to `string` (what hid it) or casting past it
323+ // Since #12776 (contract) and #12940 (this plugin's own config
324+ // mirror above), BOTH ends declare the enum, so the rename is
325+ // enum-to-enum and the parse below is defence in depth behind a
326+ // compile-time check rather than the only check — see
327+ // `parseEngineAggregateFunction` for why erased types still leave
328+ // it load-bearing.
329+ //
330+ // It was closed by PARSING with the spec enum itself rather than
331+ // by widening back to `string` (what hid it) or casting past it
300332 // (which keeps the hole and adds a lie). `AggregationFunction` is
301333 // the same schema `AggregationNodeSchema.function` is built from,
302334 // so there is one vocabulary, and its own error map already
0 commit comments