Repository navigation
Four client SDK routes answer a shape no published contract declares — automation.create / automation.update / search / data.clone #11924
Description
Activity
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: #8140already 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
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsMaintainer 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.searchanddata.clone: their shapes are stable and server-produced — declare them inpackages/specas 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).⚠️ TheSearchResultnear-miss trap stands: that exported contract is the WRONG type for this route (per-objectISearchService.search, different hit shape) — the compile-time guard client SDK drops the precise spec types at its boundary: 32 methods returnPromise< any >on a package that already depends on@objectstack/spec#8140 left inreturn-type-precision.test.tsmust 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'sBlocked-by: #8140stands until that sweep's landing state is verified at claim time.State:
needs-user-decision→pm:queuein the same write;domain:specstands.
Generated by Claude Code
- added a commit that references this issue
on Aug 25, 2026 Claim: PM loop round R6 (wave 2) — scope per the recorded ruling:
search+data.cloneONLY; 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: thesearch/data.cloneroute response contracts inpackages/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
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
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 #11924first 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 realsearchAll/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 #8140SearchResultnear-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. Clearingneeds:contract-reviewboth carriers in this write batch.Disposition of the remainder (owed at acceptance per the ruling): the
automation.create/automation.updatepair 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 gateswill 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
PR #12203 went
mergeable_state: dirtyafter 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
Closing as completed — the ruled half is landed; the reserved half has its own card.
- PR feat(spec,client): declare the search and data.clone route response contracts (#11924) #12203 (merged as
daae7aa) declares thesearch+data.cloneresponse contracts per the maintainer ruling (decision-inbox batch 8, option A):SearchAllResponseSchema/SearchAllHitSchemaandCloneDataResponseSchemain@objectstack/spec/api, producer + relay conformance suites, ledger rows filled, typed client bindings, and the two now-obsoleteexported-any-returns.jsonentries deleted in the same PR (the gate that guards exactly that staleness caught the initial omission pre-merge). - The
automation.create/automation.updatepair was explicitly NOT ruled in batch 8 — that remainder lives in automation.create / automation.update response contracts — consumer-survey first, then back to the decision inbox (the un-ruled half of the SDK route-contract card) #12206 (consumer-survey card, awaiting triage), not here. Nothing else on this card is open.
Session
session_01NDGG54XF5gbTLdQzCtnaVV, round R6.
Generated by Claude Code
- PR feat(spec,client): declare the search and data.clone route response contracts (#11924) #12203 (merged as
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