Found while binding the five in-repo return res.json() methods of #12104. Out of that
card's declared scope and deliberately not fixed there — that card binds client return types;
this is a producer-side declaration defect in packages/spec, and it changed what #12104 could
bind.
ⓘ Angle brackets are spaced throughout (Promise< any >) on purpose — GitHub's body sanitizer
strips tag-shaped spans, backticks and fenced code included.
The defect
Both schemas below look like the authoritative response contract for their route. Both declare a
data that is a strict subset of what the route's producer declares and actually serves. A
consumer that binds either — which is exactly what a sweep reaches for — ships a declaration
that refuses reads the wire really carries.
1. AnalyticsResultResponseSchema (packages/spec/src/api/analytics.zod.ts)
POST /analytics/query ends deps.success(await analyticsService.query(body, ctx)), so data
is IAnalyticsService.query's declared return, AnalyticsResult
(packages/spec/src/contracts/analytics-service.ts).
| declared on the schema |
declared on AnalyticsResult |
rows |
rows |
fields: { name, type }[] |
fields: { name, type, label?, format?, currency?, percentScale? }[] |
sql? |
sql? |
| — |
totals?: { dimensions, rows }[] |
fields[].label is not hypothetical: it is served by a real AnalyticsService, measured in
packages/client/src/analytics-automation-json-erasure.test.ts. currency and percentScale
are the ADR-0053 / objectui#3136 renderer chains, and totals is the ADR-0021 marginal-aggregate
channel — every one of them a key a dashboard reads.
2. TriggerFlowResponseSchema (packages/spec/src/api/automation-api.zod.ts)
POST /automation/:name/trigger (and the legacy POST /automation/trigger/:name) end
deps.success(result) where result is IAutomationService.execute's declared return,
AutomationResult.
| declared on the schema |
declared on AutomationResult |
success, output?, error?, durationMs? |
those four, plus code?, status?, runId?, screen?, successMessage?, errorMessage?, summary? |
The missing members are not decoration. status: 'paused' + runId + screen is the whole
third state of the #9378 / #9510 trigger contract — the payload a caller resumes a screen flow
with — and the SDK's own docblock on automation.trigger tells callers to read exactly those.
Why this is the same shape #6442 already fixed once, in this same file
AnalyticsMetadataResponseSchema used to declare { cubes: Cube[] } for an endpoint that has
always answered a bare CubeMeta[]. The maintainer ruled (2026-08-08) "narrow the declaration",
and the schema's own docblock records the reasoning: the TS contract already agreed with the
runtime and the schema was the lone outlier. These two are the same class, caught the same
way — by reading the producer's declared return next to the schema.
Remedy shape (not prescribed — this is the question the card carries)
Either bring each schema's data up to its producer's contract, or state in each docblock that
the schema is a deliberate PROJECTION and name what a consumer must not bind it to. What must
not stay is the current state: a response schema that reads as the route's contract, is
narrower than it, and has no note saying so. AnalyticsResultResponse additionally has no
exported type at all (protocol.zod.ts keeps a module-local z.infer alias), so a consumer
cannot name it even if it were right.
What #12104 did about it, so this is not re-derived
client.analytics.query and client.automation.trigger were bound to the producer contracts
(BaseResponse & { data: AnalyticsResult } / BaseResponse & { data: AutomationResult })
rather than to these two schemas, and the near-miss is pinned in
packages/client/src/return-type-precision.test.ts so a later sweep cannot quietly retarget
them. Their two siblings, AnalyticsMetadataResponseSchema and AnalyticsSqlResponseSchema,
DO agree with their producer and were used as-is.
Generated by Claude Code
Found while binding the five in-repo
return res.json()methods of #12104. Out of thatcard's declared scope and deliberately not fixed there — that card binds client return types;
this is a producer-side declaration defect in
packages/spec, and it changed what #12104 couldbind.
The defect
Both schemas below look like the authoritative response contract for their route. Both declare a
datathat is a strict subset of what the route's producer declares and actually serves. Aconsumer that binds either — which is exactly what a sweep reaches for — ships a declaration
that refuses reads the wire really carries.
1.
AnalyticsResultResponseSchema(packages/spec/src/api/analytics.zod.ts)POST /analytics/queryendsdeps.success(await analyticsService.query(body, ctx)), sodatais
IAnalyticsService.query's declared return,AnalyticsResult(
packages/spec/src/contracts/analytics-service.ts).AnalyticsResultrowsrowsfields: { name, type }[]fields: { name, type, label?, format?, currency?, percentScale? }[]sql?sql?totals?: { dimensions, rows }[]fields[].labelis not hypothetical: it is served by a realAnalyticsService, measured inpackages/client/src/analytics-automation-json-erasure.test.ts.currencyandpercentScaleare the ADR-0053 / objectui#3136 renderer chains, and
totalsis the ADR-0021 marginal-aggregatechannel — every one of them a key a dashboard reads.
2.
TriggerFlowResponseSchema(packages/spec/src/api/automation-api.zod.ts)POST /automation/:name/trigger(and the legacyPOST /automation/trigger/:name) enddeps.success(result)whereresultisIAutomationService.execute's declared return,AutomationResult.AutomationResultsuccess,output?,error?,durationMs?code?,status?,runId?,screen?,successMessage?,errorMessage?,summary?The missing members are not decoration.
status: 'paused'+runId+screenis the wholethird state of the #9378 / #9510 trigger contract — the payload a caller resumes a screen flow
with — and the SDK's own docblock on
automation.triggertells callers to read exactly those.Why this is the same shape #6442 already fixed once, in this same file
AnalyticsMetadataResponseSchemaused to declare{ cubes: Cube[] }for an endpoint that hasalways answered a bare
CubeMeta[]. The maintainer ruled (2026-08-08) "narrow the declaration",and the schema's own docblock records the reasoning: the TS contract already agreed with the
runtime and the schema was the lone outlier. These two are the same class, caught the same
way — by reading the producer's declared return next to the schema.
Remedy shape (not prescribed — this is the question the card carries)
Either bring each schema's
dataup to its producer's contract, or state in each docblock thatthe schema is a deliberate PROJECTION and name what a consumer must not bind it to. What must
not stay is the current state: a response schema that reads as the route's contract, is
narrower than it, and has no note saying so.
AnalyticsResultResponseadditionally has noexported type at all (
protocol.zod.tskeeps a module-localz.inferalias), so a consumercannot name it even if it were right.
What #12104 did about it, so this is not re-derived
client.analytics.queryandclient.automation.triggerwere bound to the producer contracts(
BaseResponse & { data: AnalyticsResult }/BaseResponse & { data: AutomationResult })rather than to these two schemas, and the near-miss is pinned in
packages/client/src/return-type-precision.test.tsso a later sweep cannot quietly retargetthem. Their two siblings,
AnalyticsMetadataResponseSchemaandAnalyticsSqlResponseSchema,DO agree with their producer and were used as-is.
Generated by Claude Code