You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding: StrategyContext.executeAggregate declares aggregations[].method as string while the engine contract declares the six-value AggregationFunction #12776
Found while carrying out the analytics half of #11833 (deriving service-analytics/src/plugin.ts's consumer-local structural engine type from the declared contracts). Filed unassigned and ungraded as an observation; measured on origin/main at 87d3f9a0a.
The observation
Two spec-declared surfaces describe the same value and disagree about its type:
packages/spec/src/contracts/data-engine.ts:230 — IDataEngine.aggregate(objectName, query: EngineAggregateOptions, …), whose aggregations[].function resolves to AggregationFunction, the closed six-value enum count | sum | avg | min | max | count_distinct (packages/spec/src/data/query.zod.ts:149, via AggregationNodeSchema at :262).
packages/spec/src/contracts/analytics-service.ts:300 — StrategyContext.executeAggregate's aggregations?: Array of objects with field, method: string, alias, filter.
method and function are the same slot: the analytics bridge renames one to the other and forwards it to engine.aggregate. One side is a closed enum and the other is string, so nothing on the analytics side of that seam is compile-checked against the engine's vocabulary.
Why it is visible now
Until the analytics half of #11833 landed, service-analytics/src/plugin.ts named the engine through a consumer-local structural type that also declared function: string, so the two strings agreed and the seam looked closed. Deriving that local type from IDataEngine turns the forward into a compile error — measured, TS2322, Type 'string' is not assignable to type '"min" | "max" | "count" | "sum" | "avg" | "count_distinct"'.
That PR closes the gap at runtime, inside the bridge, by parsing the incoming method with the spec's own AggregationFunction enum and refusing anything else. That is a runtime narrowing standing in for a type that could have been exact: the producer side is still string, so the compiler cannot prove the bridge is handed a legal method, and every other consumer of StrategyContext.executeAggregate gets no help at all.
What was verified, and what was not
VERIFIED: both declarations, at the lines above, on origin/main at 87d3f9a0a.
NOT VERIFIED: whether narrowing StrategyContext.executeAggregate's method to AggregationFunction is safe for every implementor of the analytics strategy contract, in this repo and outside it. That is the whole question, and it is a public-surface narrowing on a published contract, so it is a spec-seat call rather than a consumer-side one.
Why it may deserve a card, and why it is filed rather than fixed
#11833's standing rule is that a consumer never widens a contract to fit, and the mirror of that is that a consumer does not narrow one either. The repair, if it is wanted, is one line in packages/spec/src/contracts/analytics-service.ts plus whatever the accept-set narrowing breaks in implementors — an accept/reject change on a published contract, which is a different tier from the consumer refactor that surfaced it.
Refs: #11833 (the sweep that surfaced it) · #12209 and PR #12318 (the custom-SQL refusal that keeps the reachable producers legal today) · #4157 (this class biting before, in its silent-wrong-answer variant) · #4251 B3 and #11493 (the declared-contract sweep pattern).
Found while carrying out the analytics half of #11833 (deriving
service-analytics/src/plugin.ts's consumer-local structural engine type from the declared contracts). Filed unassigned and ungraded as an observation; measured onorigin/mainat87d3f9a0a.The observation
Two spec-declared surfaces describe the same value and disagree about its type:
packages/spec/src/contracts/data-engine.ts:230—IDataEngine.aggregate(objectName, query: EngineAggregateOptions, …), whoseaggregations[].functionresolves toAggregationFunction, the closed six-value enumcount | sum | avg | min | max | count_distinct(packages/spec/src/data/query.zod.ts:149, viaAggregationNodeSchemaat:262).packages/spec/src/contracts/analytics-service.ts:300—StrategyContext.executeAggregate'saggregations?: Array of objects with field, method: string, alias, filter.methodandfunctionare the same slot: the analytics bridge renames one to the other and forwards it toengine.aggregate. One side is a closed enum and the other isstring, so nothing on the analytics side of that seam is compile-checked against the engine's vocabulary.Why it is visible now
Until the analytics half of #11833 landed,
service-analytics/src/plugin.tsnamed the engine through a consumer-local structural type that also declaredfunction: string, so the twostrings agreed and the seam looked closed. Deriving that local type fromIDataEngineturns the forward into a compile error — measured, TS2322,Type 'string' is not assignable to type '"min" | "max" | "count" | "sum" | "avg" | "count_distinct"'.That PR closes the gap at runtime, inside the bridge, by parsing the incoming
methodwith the spec's ownAggregationFunctionenum and refusing anything else. That is a runtime narrowing standing in for a type that could have been exact: the producer side is stillstring, so the compiler cannot prove the bridge is handed a legal method, and every other consumer ofStrategyContext.executeAggregategets no help at all.What was verified, and what was not
origin/mainat87d3f9a0a.ObjectQLStrategy.resolveMeasureAggregation(packages/services/service-analytics/src/strategies/objectql-strategy.ts:1262) returns either a metric type that survived the A custom-SQL measure reachesengine.aggregateun-refused on the ObjectQL path and answers a silentnull— the repair #12053's probe scoped #12209 custom-SQL refusal or one of a hard-coded six-element alias list, so no known producer emits a method outside the enum today.StrategyContext.executeAggregate'smethodtoAggregationFunctionis safe for every implementor of the analytics strategy contract, in this repo and outside it. That is the whole question, and it is a public-surface narrowing on a published contract, so it is a spec-seat call rather than a consumer-side one.Why it may deserve a card, and why it is filed rather than fixed
#11833's standing rule is that a consumer never widens a contract to fit, and the mirror of that is that a consumer does not narrow one either. The repair, if it is wanted, is one line inpackages/spec/src/contracts/analytics-service.tsplus whatever the accept-set narrowing breaks in implementors — an accept/reject change on a published contract, which is a different tier from the consumer refactor that surfaced it.Refs: #11833 (the sweep that surfaced it) · #12209 and PR #12318 (the custom-SQL refusal that keeps the reachable producers legal today) · #4157 (this class biting before, in its silent-wrong-answer variant) · #4251 B3 and #11493 (the declared-contract sweep pattern).