Filed unassigned by the os-dev seat working #16098 (session https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y). Not graded, no domain:* — routing and priority are the triage seat's. Established while establishing what summary STORES, so that a min/max measure over a summary field could be given a measured verdict rather than a guessed one.
What was measured
Three shipped statements, and the third contradicts the first two.
1. The spec's runtime value contract says a summary value is a finite number (packages/spec/src/data/field-value.zod.ts):
/** Value is a finite numeric scalar. `currency` IS a bare number (see header). */
export const NUMERIC_VALUE_TYPES: ReadonlySet<string> = new Set([
'number', 'currency', 'percent', 'rating', 'slider', 'progress', 'summary',
] as const satisfies readonly FieldType[]);
valueSchemaFor therefore answers z.number().finite() for a summary field.
2. driver-sql's DDL gives it a float column — its own case arm, not the catch-all:
case 'summary':
col = table.float(name);
break;
3. The roll-up vocabulary admits min/max over ANY child field, and the computed value is returned verbatim. FieldSchema.summaryOperations declares:
object: z.string().describe('Source child object name for roll-up'),
field: z.string().describe('Field on child object to aggregate (ignored for count)'),
function: z.enum(['count', 'sum', 'min', 'max', 'avg']).describe('Aggregation function to apply'),
Nothing correlates function with the child field's type, and aggregateSummaryValue (packages/objectql/src/summary-aggregate.ts) passes the driver's answer straight through, with only an empty-set fallback:
const value = rows?.[0]?.value;
return value == null ? summaryEmptySetValue(desc.fn) : value;
So { type: 'summary', summaryOperations: { object: 'invoice_line', field: 'shipped_at', function: 'max' } } — a perfectly ordinary "latest shipment" roll-up — computes an instant and stores it into a float column that the value contract says holds a finite number.
Why this is a defect independent of what the right answer is
Whether summary should carry the aggregated child field's type (making it a second measureResultType-shaped rule, one layer down), or whether the roll-up declaration should be REFUSED when min/max is paired with a non-numeric child field, is a contract question. This card holds either way: today the declaration and the stored value can disagree, and no layer says so.
The failure is not uniform, which is the usual shape of this family: on SQLite a float column has NUMERIC affinity and takes the ISO text, on a strict dialect the insert is a runtime error, and the ADR-0104 os migrate value-shapes scan — which parses stored values against valueSchemaFor — would report the column as violating a contract nobody declared it would break.
Out of scope here
Why #16098 did not fix it
That card answers "what should AnalyticsResult.fields[].type say for a min/max over a field of declared type X". For summary the two shipped statements agree — numeric — so the producer's number is the correct wire word and no correction applies; the case where that DECLARATION is itself wrong is this card, and it lives in packages/spec / packages/objectql, not in the analytics response.
Related: #16098 (where summary is recorded as measured-numeric, with this filed as the tension) · #16099 (needs-user-decision: no layer refuses an incoherent aggregate / field-type pair) · #11455 (the driver-level envelope for arithmetic aggregates over a boolean column, closed).
Filed unassigned by the
os-devseat working #16098 (sessionhttps://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y). Not graded, nodomain:*— routing and priority are the triage seat's. Established while establishing whatsummarySTORES, so that amin/maxmeasure over a summary field could be given a measured verdict rather than a guessed one.What was measured
Three shipped statements, and the third contradicts the first two.
1. The spec's runtime value contract says a
summaryvalue is a finite number (packages/spec/src/data/field-value.zod.ts):valueSchemaFortherefore answersz.number().finite()for asummaryfield.2.
driver-sql's DDL gives it a float column — its owncasearm, not the catch-all:3. The roll-up vocabulary admits
min/maxover ANY child field, and the computed value is returned verbatim.FieldSchema.summaryOperationsdeclares:Nothing correlates
functionwith the child field's type, andaggregateSummaryValue(packages/objectql/src/summary-aggregate.ts) passes the driver's answer straight through, with only an empty-set fallback:So
{ type: 'summary', summaryOperations: { object: 'invoice_line', field: 'shipped_at', function: 'max' } }— a perfectly ordinary "latest shipment" roll-up — computes an instant and stores it into afloatcolumn that the value contract says holds a finite number.Why this is a defect independent of what the right answer is
Whether
summaryshould carry the aggregated child field's type (making it a secondmeasureResultType-shaped rule, one layer down), or whether the roll-up declaration should be REFUSED whenmin/maxis paired with a non-numeric child field, is a contract question. This card holds either way: today the declaration and the stored value can disagree, and no layer says so.The failure is not uniform, which is the usual shape of this family: on SQLite a float column has NUMERIC affinity and takes the ISO text, on a strict dialect the insert is a runtime error, and the ADR-0104
os migrate value-shapesscan — which parses stored values againstvalueSchemaFor— would report the column as violating a contract nobody declared it would break.Out of scope here
avgover a datetime works on SQLite and errors on Postgres #16099, one layer down.Why #16098 did not fix it
That card answers "what should
AnalyticsResult.fields[].typesay for amin/maxover a field of declared type X". Forsummarythe two shipped statements agree — numeric — so the producer'snumberis the correct wire word and no correction applies; the case where that DECLARATION is itself wrong is this card, and it lives inpackages/spec/packages/objectql, not in the analytics response.Related: #16098 (where
summaryis recorded as measured-numeric, with this filed as the tension) · #16099 (needs-user-decision: no layer refuses an incoherent aggregate / field-type pair) · #11455 (the driver-level envelope for arithmetic aggregates over a boolean column, closed).