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

Activity

  1. added theissue type on Aug 25, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Triage: → needs-user-decision + domain:spec — the four missing shapes are missing contracts (packages/spec), and the ruling the body cites (⛔ do not fill a row that has no conformance coverage) puts them squarely on the manual floor: each wants a product answer plus conformance coverage, not a typing sweep. Blocked-by: #8140 already in the body (typing sweep in flight). Type: Feature.

    ① 项目长远合理性:给这四个端点补契约声明是在缩小「路由回声、无承诺」的特例面;但 automation 两端点若声明 FlowParsed 会把「回声调用方字节」改成「回声已解析流程」,是一次真实的行为收敛 —— 方向与 contract-first 一致,代价是要动运行时回包。
    ② 实际业务拉动:今天写集成的客户从这四个端点拿到的要么是自己发出去的字节、要么是无声明的形状,SDK 侧全是 any —— 撞上的是「返回值没法安全消费」;search / data.clone 已有稳定形状,只是没人声明。
    ③ 防 AI 犯错:回声式响应是 AI 生成集成代码「看起来能跑」的温床 —— 声明并校验返回形状让错误在编译期响亮暴露;继续回声则错形静默流进客户数据。
    ④ 创业阶段不扩散:四个决定拆两对 —— search / data.clone 是便宜的一对(照实声明 + conformance 即可);automation 一对牵动运行时回包行为,零拉动时可先 defer,不必四个一起做。
    推荐:A = 先裁 search / data.clone(照实声明 + conformance 覆盖),automation 一对单独再裁;B = 四个一起做;C = 全部 defer。回一个字母即可(「A,但 X」= 附加约束照录)。
    置信缺口:本分析未实测 automation 回包在真实部署里是否已有消费方依赖「回声」形状 —— 若有,改回包即破坏性变更,需先做消费面盘点。


    Generated by Claude Code

  3. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling — A: declare the cheap pair now; the automation pair returns with a consumer-survey reading

    Source: maintainer, 2026-08-25, live PM chat (decision-inbox batch 8 review, session session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「同意」 — accepting the batch's presented recommendation for this card: A.

    • search and data.clone: their shapes are stable and server-produced — declare them in packages/spec as they actually are, with conformance coverage, honouring the Response bodies are never checked against the schemas that declare them — staged plan, not a repo-wide sweep #3877 rule (⛔ no row filled without conformance coverage — these two get it as part of the same work). ⚠️ The SearchResult near-miss trap stands: that exported contract is the WRONG type for this route (per-object ISearchService.search, different hit shape) — the compile-time guard client SDK drops the precise spec types at its boundary: 32 methods return Promise< any > on a package that already depends on @objectstack/spec #8140 left in return-type-precision.test.ts must stay green, and the new declaration must not be built by reaching for that neighbour.
    • automation.create / automation.update: NOT ruled here. They return to the decision inbox as their own card carrying a consumer-survey reading first — does any real consumer depend on the echo shape (the caller's own bytes back)? The candidate direction (answer the registered, parsed flow — canonicalizeStoredFlow's output — so the caller learns what the engine actually stored) is a behaviour change and is decided on that reading, not before.
    • Option B (all four at once) and C (defer all) are not taken.

    Discipline: Clause-② yes for the declared pair (public contract surface widens); CONTRACT_REVIEW_TIER; spec seat. The body's Blocked-by: #8140 stands until that sweep's landing state is verified at claim time.

    State: needs-user-decision → pm:queue in the same write; domain:spec stands.


    Generated by Claude Code

  4. self-assigned this
    on Aug 25, 2026
  5. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    Claim: PM loop round R6 (wave 2) — scope per the recorded ruling: search + data.clone ONLY; the automation pair is explicitly out of scope (returns to the decision inbox as its own card with a consumer-survey reading — this seat files it at ACCEPT).
    Session: session_01NDGG54XF5gbTLdQzCtnaVV
    Branch: claude/issue-11924-declare-search-clone-contracts
    Worktree: objectstack-issue-11924
    Domain: domain:spec
    File surface: the search / data.clone route response contracts in packages/spec/src (located from the card + PR #11929's fresh bindings) + conformance coverage for both (the #3877 rule: no row without conformance) + tests + changeset; return-type-precision.test.ts's #8140 guard must stay green — the ruled SearchResult near-miss trap (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5 — dispatch-gates 2026-08-25 @8619f95: "no path-derived mandate … floor sonnet · default opus · ceiling fable"; clause-② content leg (public contract surface widens, per the recorded ruling) ⇒ fable mandatory
    Clause-②: yes
    Serial constraints cleared: Blocked-by #8140 verified EXHAUSTED at claim per the ruling's own condition — #8140 CLOSED (completed) 2026-08-25T02:46Z via PR #11929 MERGED ("bind erased SDK return types to their spec contracts"). Sole writer of its contract files this batch; no migrations touch (widening — gate consulted, not assumed). No pin assertions on sibling behavior.


    Generated by Claude Code

  6. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 11924,
      "status": "done",
      "branch": "claude/issue-11924-declare-search-clone-contracts",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12203",
      "premise_still_valid": true,
      "summary": "Declared the ruled pair as produced: SearchAllResponseSchema/SearchAllHitSchema and CloneDataResponseSchema (+ z.input aliases, isomorphism pins Iso859-861) in packages/spec/src/api/protocol.zod.ts; filled both REST route-ledger rows WITH their conformance coverage per the #3877 rule — producer half drives the real searchAll/cloneData in metadata-protocol, relay half drives the real mounts in rest with ledger identity pins. Client bound type-only: search returns Promise of SearchAllResponse, data.clone returns Promise of CloneDataResult of T (new exported mirror interface); the #8140 SearchResult near-miss guard's assertions are untouched and green, its stale prose updated, and two new direction-2 pins refuse the trap at the binding. Automation pair untouched per the ruling. Premise verified at head: #8140 CLOSED via merged PR #11929, whose four docblocked deliberate Promise-of-any sites matched the card exactly; both route handlers answer bare (search res.json, clone 201), so the schemas name whole bodies.",
      "tests": "All heavy runs via scripts/pm/os-verify-lock.sh, exit codes captured pipe-free, verdicts quoted from gate output. Final head 0968af1 (merge of origin/main 22c42c9 included; clean merge, both sides' client edits verified coherent). spec: full test at merge commit 3c1cda7 = 'Test Files 1 failed | 425 passed (426) · Tests 1 failed | 11347 passed (11348)' — sole red was the pin file's own count self-check ('expected [ Array(838) ] to have a length of 835'), fixed per that file's receipt convention in 0968af1; spec typecheck + that file re-run green at 0968af1 (3/3). client at 0968af1: typecheck green (compiles the type pins) + 'Test Files 25 passed (25) · Tests 332 passed (332)'. metadata-protocol conformance at 0968af1: 6/6 (suite proved falsifiable during development: one red on a wrong snippet-casing expectation, corrected to the measured producer behavior). rest at 0968af1: new conformance + rest-route-ledger.conformance = 13/13. spec check:generated on merged tree: 'All 14 generated artifacts are up to date' (pre-merge regen: api-surface +6/-0, export-origins, docs, strictness-ledger; authorable-surface + json-schema.manifest shard rewrites are gen:schema products). Derived gate farm (dispatch-gates.mjs, no args, answer-tree line verified = this repo @577fabf) at 0968af1: 36/37 pass incl. nul-bytes, spec-parsed-alias, strictness-ledger, merge-driver, cross-package-test-inputs, adr-0087-registration (additive changeset, no marker owed), slot-lookup, published-files, empty-state, liveness, variant-docs, doc-formula-expressions. DEVIATIONS declared: check-dev-prereqs reds on a worktree precondition (36 unrelated workspace packages have no dist here — only targeted closures were built; CI's lint job builds the workspace first); docs-drift-check.yml's two scripts are CI-owned PR-context runs; spec full test ran at the merge commit with only the pin-test count edit after it, delta re-verified at 0968af1; package tests scoped to the four touched packages by --filter; downstream client-consumer sweep by call-site grep: zero client.search(/.data.clone( call sites outside packages/client (downstream direction ...@objectstack/client). No ablation legs were run (additive declarations; no guard was deleted or mutated). PR body's angle-bracket generics were eaten by the sanitizer on first store; repaired with word spellings and read back intact.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  7. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    ACCEPT (ruled half) — PR #12203 (R6 W2, reviewer of record: spec seat, session session_01NDGG54XF5gbTLdQzCtnaVV).

    Checklist conclusion (verified against GitHub + local branch diff): draft ✓ · base main ✓ · Part of #11924 first line (correct — the automation half stays on the books) ✓ · 15 files +706/−37: protocol.zod.ts +115 (SearchAll/CloneData declarations as-produced, bare-body per the measured relay), BOTH conformance halves in the same change (producer suite 189 lines driving the real searchAll/cloneData; relay suite 181 lines with ledger IDENTITY pins — the #3877 rule honored to the letter), route-ledger rows filled with coverage notes, client bindings type-only, generated surfaces regenerated (+6 exports), iso-pin receipt 835→838 ✓ · the #8140 SearchResult near-miss guard: assertions untouched and green, stale prose updated, TWO new direction-2 pins make the trap a compile error at the binding — exactly what the ruling demanded ✓ · automation routes untouched ✓ · no governed path ✓. The one mid-flight red (the pin file's own count self-check) was the receipt convention working, fixed per its own file's rules.

    Contract review (clause-② content leg: public contract surface widens by the ruled pair): fuse reading get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (this seat, self-dispatched-card path). Verdict: PASS — additive declarations written from the producer (not the trap neighbour), double-sided conformance proves declared=produced in both directions, client narrowing is type-only with zero downstream call sites measured. Clearing needs:contract-review both carriers in this write batch.

    Disposition of the remainder (owed at acceptance per the ruling): the automation.create/automation.update pair moves to its own card — filed next as the consumer-survey-first task the ruling prescribed. Once PR #12203 merges and that card exists, THIS card closes as completed-with-remainder-moved.

    Landing note: this PR's Type Check · consumer gates will red on the fleet-wide stale-ledger incident (#12180 / fix PR #12186) — inherited main damage, not this diff's. Ready + queue rides on #12186 landing + a branch update + full green.


    Generated by Claude Code

  8. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    PR #12203 went mergeable_state: dirty after PR #12120 and PR #12169 reached main (conflict surface: regenerated spec artifacts + the type-alias pin file's count receipts — #12120 retired pin Iso839, 835 → 834, while #12203's self-check had corrected its count to 835). Conflict-resolution continuation dispatched on the same branch (claude/issue-11924-declare-search-clone-contracts), same session (session_01NDGG54XF5gbTLdQzCtnaVV), contract-review tier per the card's clause-②: merge origin/main, regenerate artifacts with repo tooling (no hand-merged generated files), re-derive pin receipts on the merged tree, full scoped verification, then push. Ready + auto-merge follows once CI is green on the resolved head; the PM re-reviews the merge diff before queueing.


    Generated by Claude Code

  9. os-litant commented on Aug 25, 2026

    @os-litant
    Collaborator

    Closing as completed — the ruled half is landed; the reserved half has its own card.

    Session session_01NDGG54XF5gbTLdQzCtnaVV, round R6.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions