Blocked-by: objectstack-ai/objectstack#8140
Filed out of #8140's implementation half, which bound 51 of its 55 erased return-type sites to
@objectstack/spec contracts. These four could not be bound, and the reason is the same in
each case: there is no type anywhere to bind to. They are a missing contract, not a missing
annotation, so they need a decision about what each endpoint promises rather than a sweep.
⛔ packages/spec was read-only on #8140 (domain:spec's single-owner surface), so nothing was
authored there. All four keep Promise< any > on main today, each with a docblock naming this
card's reasoning so the next sweep does not "finish" them by reaching for a near neighbour.
Measured at 1f6d04703.
The four
1–2. automation.create / automation.update — the route promises nothing
Both echo the request body, unvalidated as to shape:
POST /automation ends deps.success(body) (packages/runtime/src/domains/automation.ts:982).
PUT /automation/:name ends deps.success(definition) (same file, :1586) where
definition = body.definition ?? body.
IAutomationService.registerFlow(name, definition: unknown): void
(packages/spec/src/contracts/automation-service.ts:389) returns nothing, so the service
contract has no return shape to relay. The body is checked for "is a non-array object" and handed
to the engine, whose refusal becomes a 400 (#8123) — but it is never parsed through FlowSchema,
so what comes back is the caller's own bytes. Declaring Flow here would be a claim about the
request that no validation backs.
The decision this needs: should these routes answer the registered, parsed flow
(FlowParsed — canonicalizeStoredFlow already produces exactly that), or keep echoing? The
first is a behaviour change with a real benefit (the caller learns what the engine actually
stored, defaults materialised); the second stays untypeable.
3. search — declared inline on the implementation, unreachable from the client
GET /api/v1/search relays protocol.searchAll(...) verbatim
(packages/rest/src/rest-server.ts:8259-8266). Its shape is declared as an inline return
annotation at packages/metadata-protocol/src/protocol.ts:9845-9863:
{ query: string;
hits: Array< { object: string; id: string; title: string; snippet?: string; record: any } >;
totalObjects: number; totalHits: number; truncated: boolean }
Not in @objectstack/spec, and @objectstack/metadata-protocol is not a dependency of
packages/client (its deps are @objectstack/core and @objectstack/spec only), so it is
unreachable even by import. Note hits[].record is itself any in the implementation's own
declaration — the erasure is not only at the SDK boundary here.
⚠️ Near-miss trap, verified at head. SearchResult
(packages/spec/src/contracts/search-service.ts:53) exists, is exported from
@objectstack/spec/contracts, and is the wrong type: it contracts the per-object
ISearchService.search, whose hits carry score / document, not this route's
object / title / snippet / record. Binding it would typecheck and ship a false
declaration. #8140 left a compile-time guard on that mismatch in
packages/client/src/return-type-precision.test.ts.
4. data.clone — a stable server-produced shape declared nowhere
POST /data/:object/:id/clone answers { object, id, sourceId, record }, produced at
packages/metadata-protocol/src/protocol.ts:9488-9493. Stable and server-produced, but declared
in no spec module. It is the structural sibling of the client's own CreateDataResult< T > plus
sourceId — which is what makes it tempting and why it is being written down instead: minting
that equivalence inside a consumer would create an undeclared second contract.
Why this is its own card rather than a rider
#3877's standing ruling (quoted in packages/rest/src/rest-route-ledger.ts:80-102) is that
authoring the missing route response schemas wholesale is not scheduled, because a response
schema is a product decision about what an endpoint promises and mass-producing them is how
declarations nobody validated come to exist — "⛔ DO NOT FILL A ROW THAT HAS NO CONFORMANCE
COVERAGE". #8140's 51 bound sites are outside that ruling (they relay contract types that already
exist and are already the declared return of the service method the route calls); these four are
squarely inside it. Each wants its own answer plus conformance coverage, which is a different
piece of work from a typing sweep.
Suggested shape
Four decisions, not one sweep. search and data.clone are the cheap pair — both have a stable
shape a schema can simply describe. automation.create / automation.update are the interesting
pair and should probably be answered together, since they are the same route class.
Generated by Claude Code
Blocked-by: objectstack-ai/objectstack#8140Filed out of #8140's implementation half, which bound 51 of its 55 erased return-type sites to
@objectstack/speccontracts. These four could not be bound, and the reason is the same ineach case: there is no type anywhere to bind to. They are a missing contract, not a missing
annotation, so they need a decision about what each endpoint promises rather than a sweep.
⛔
packages/specwas read-only on #8140 (domain:spec's single-owner surface), so nothing wasauthored there. All four keep
Promise< any >onmaintoday, each with a docblock naming thiscard's reasoning so the next sweep does not "finish" them by reaching for a near neighbour.
Measured at
1f6d04703.The four
1–2.
automation.create/automation.update— the route promises nothingBoth echo the request body, unvalidated as to shape:
POST /automationendsdeps.success(body)(packages/runtime/src/domains/automation.ts:982).PUT /automation/:nameendsdeps.success(definition)(same file,:1586) wheredefinition = body.definition ?? body.IAutomationService.registerFlow(name, definition: unknown): void(
packages/spec/src/contracts/automation-service.ts:389) returns nothing, so the servicecontract has no return shape to relay. The body is checked for "is a non-array object" and handed
to the engine, whose refusal becomes a 400 (#8123) — but it is never parsed through
FlowSchema,so what comes back is the caller's own bytes. Declaring
Flowhere would be a claim about therequest that no validation backs.
The decision this needs: should these routes answer the registered, parsed flow
(
FlowParsed—canonicalizeStoredFlowalready produces exactly that), or keep echoing? Thefirst is a behaviour change with a real benefit (the caller learns what the engine actually
stored, defaults materialised); the second stays untypeable.
3.
search— declared inline on the implementation, unreachable from the clientGET /api/v1/searchrelaysprotocol.searchAll(...)verbatim(
packages/rest/src/rest-server.ts:8259-8266). Its shape is declared as an inline returnannotation at
packages/metadata-protocol/src/protocol.ts:9845-9863:Not in
@objectstack/spec, and@objectstack/metadata-protocolis not a dependency ofpackages/client(its deps are@objectstack/coreand@objectstack/speconly), so it isunreachable even by import. Note
hits[].recordis itselfanyin the implementation's owndeclaration — the erasure is not only at the SDK boundary here.
SearchResult(
packages/spec/src/contracts/search-service.ts:53) exists, is exported from@objectstack/spec/contracts, and is the wrong type: it contracts the per-objectISearchService.search, whosehitscarryscore/document, not this route'sobject/title/snippet/record. Binding it would typecheck and ship a falsedeclaration. #8140 left a compile-time guard on that mismatch in
packages/client/src/return-type-precision.test.ts.4.
data.clone— a stable server-produced shape declared nowherePOST /data/:object/:id/cloneanswers{ object, id, sourceId, record }, produced atpackages/metadata-protocol/src/protocol.ts:9488-9493. Stable and server-produced, but declaredin no spec module. It is the structural sibling of the client's own
CreateDataResult< T >plussourceId— which is what makes it tempting and why it is being written down instead: mintingthat equivalence inside a consumer would create an undeclared second contract.
Why this is its own card rather than a rider
#3877's standing ruling (quoted in
packages/rest/src/rest-route-ledger.ts:80-102) is thatauthoring the missing route response schemas wholesale is not scheduled, because a response
schema is a product decision about what an endpoint promises and mass-producing them is how
declarations nobody validated come to exist — "⛔ DO NOT FILL A ROW THAT HAS NO CONFORMANCE
COVERAGE". #8140's 51 bound sites are outside that ruling (they relay contract types that already
exist and are already the declared return of the service method the route calls); these four are
squarely inside it. Each wants its own answer plus conformance coverage, which is a different
piece of work from a typing sweep.
Suggested shape
Four decisions, not one sweep.
searchanddata.cloneare the cheap pair — both have a stableshape a schema can simply describe.
automation.create/automation.updateare the interestingpair and should probably be answered together, since they are the same route class.
Generated by Claude Code