Skip to content

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

Closed

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
    domain:specDomain lane — seat card indexed by label:pm:seat
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 25, 2026
  2. added theissue type on Aug 25, 2026
  3. 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

  4. 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

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 25, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 25, 2026
  7. self-assigned this
    on Aug 25, 2026
  8. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    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

  9. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    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

  10. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 25, 2026
  11. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    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

  12. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 25, 2026
  13. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    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

  14. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    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

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions