Found while binding the five in-repo return res.json() methods of #12104. Out of that card's
declared scope and deliberately not changed there: #12104 is a type-erasure card and moved only
declarations, never a runtime value. This is the wire-shape question its measurement made
visible, and answering it changes what callers receive.
ⓘ Angle brackets are spaced throughout (Promise< any >) on purpose — GitHub's body sanitizer
strips tag-shaped spans, backticks and fenced code included.
The inconsistency
ObjectStackClient has two response readers:
Four methods take the second path against a dispatcher-served route, so their callers must
write .data where every neighbouring method needs no such step:
| method | route | what the caller receives |
|---|
analytics.query | POST /analytics/query | { success, data: AnalyticsResult } |
analytics.meta | GET /analytics/meta | { success, data: CubeMeta[] } |
analytics.explain | POST /analytics/sql | { success, data: { sql, params } } |
automation.trigger | POST /automation/trigger/:name | { success, data: AutomationResult } |
The sharpest illustration is inside one namespace: automation.trigger and
automation.execute call the same door with the same body. execute unwraps and resolves
to AutomationResult; trigger does not and resolves to the envelope around it. Two methods,
one route, two calling conventions.
A fifth method, analytics.queryDataset, also uses res.json() but is correct as it stands:
its route is mounted by @objectstack/rest and ends res.json(result) with no envelope, so
there is nothing to strip. It is listed here only so a sweep does not "fix" it into the others'
shape.
Why it was invisible until now
All five were erased to Promise< any > — no return annotation, and lib.dom declares
Response.json(): Promise< any >. With any, (await client.analytics.query(q)).rows
compiled and was undefined at runtime; so did .data.rows. Nothing in the type system
distinguished the two conventions, so neither a caller nor a reviewer met the difference.
#12104 annotated them truthfully, which is what turned an invisible inconsistency into a
visible one.
The decision this card carries
Should these four be converted to unwrapResponse so the SDK has ONE convention?
- For. One reading rule across the SDK;
trigger and execute stop disagreeing about one
route; the annotations become payload types like every other bound method. - Against / cost. It is a runtime breaking change to four published methods, not a
declaration move — every existing caller that reads .data (or that reads the envelope's
success flag) breaks. The erasure means both spellings compiled, so the population of
affected callers is not knowable from types alone and needs measuring across this repo,
objectui and cloud before anyone commits to it.
Recorded rather than acted on: with the declarations now truthful, the current state is honest
and non-breaking, and the convergence is a deliberate API decision with a migration, not a
cleanup. Whoever takes it should measure the caller population first.
Generated by Claude Code
Found while binding the five in-repo
return res.json()methods of #12104. Out of that card'sdeclared scope and deliberately not changed there: #12104 is a type-erasure card and moved only
declarations, never a runtime value. This is the wire-shape question its measurement made
visible, and answering it changes what callers receive.
The inconsistency
ObjectStackClienthas two response readers:this.unwrapResponse(res)— strips the dispatcher's{ success, data }envelope and handsback
data. This is what the overwhelming majority of SDK methods use, and every return typeclient SDK drops the precise spec types at its boundary: 32 methods return
Promise< any >on a package that already depends on@objectstack/spec#8140 / Four client SDK routes answer a shape no published contract declares —automation.create/automation.update/search/data.clone#11924 / A fifth client-SDK erasure spelling no grep in #8140's census counted: 38 methods with NO return annotation, typed fromunwrapResponse< …any… >#11925 /client.packagesdeclares envelopes two mounted surfaces do not emit:getdiverges between them, andinstall/enable/disabledeclare{ package }where the only serving surface sends the bare row #12034 / 17 client-SDK methods have no published response contract to bind to: the wholemeta.*history/diagnostics family (9) plus 8packages.*routes whose handlers call(protocol as any).<method>#12038 bound is a POST-unwrap payload.res.json()— strips nothing, so the caller receives the envelope itself.Four methods take the second path against a dispatcher-served route, so their callers must
write
.datawhere every neighbouring method needs no such step:analytics.queryPOST /analytics/query{ success, data: AnalyticsResult }analytics.metaGET /analytics/meta{ success, data: CubeMeta[] }analytics.explainPOST /analytics/sql{ success, data: { sql, params } }automation.triggerPOST /automation/trigger/:name{ success, data: AutomationResult }The sharpest illustration is inside one namespace:
automation.triggerandautomation.executecall the same door with the same body.executeunwraps and resolvesto
AutomationResult;triggerdoes not and resolves to the envelope around it. Two methods,one route, two calling conventions.
A fifth method,
analytics.queryDataset, also usesres.json()but is correct as it stands:its route is mounted by
@objectstack/restand endsres.json(result)with no envelope, sothere is nothing to strip. It is listed here only so a sweep does not "fix" it into the others'
shape.
Why it was invisible until now
All five were erased to
Promise< any >— no return annotation, andlib.domdeclaresResponse.json(): Promise< any >. Withany,(await client.analytics.query(q)).rowscompiled and was
undefinedat runtime; so did.data.rows. Nothing in the type systemdistinguished the two conventions, so neither a caller nor a reviewer met the difference.
#12104 annotated them truthfully, which is what turned an invisible inconsistency into a
visible one.
The decision this card carries
Should these four be converted to
unwrapResponseso the SDK has ONE convention?triggerandexecutestop disagreeing about oneroute; the annotations become payload types like every other bound method.
declaration move — every existing caller that reads
.data(or that reads the envelope'ssuccessflag) breaks. The erasure means both spellings compiled, so the population ofaffected callers is not knowable from types alone and needs measuring across this repo,
objectui and cloud before anyone commits to it.
Recorded rather than acted on: with the declarations now truthful, the current state is honest
and non-breaking, and the convergence is a deliberate API decision with a migration, not a
cleanup. Whoever takes it should measure the caller population first.
Generated by Claude Code