Skip to content

Four client SDK routes answer a shape no published contract declares — automation.create / automation.update / search / data.clone #11924

Description

@os-zhuang

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
(FlowParsedcanonicalizeStoredFlow 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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions