Repository navigation
Description
Split out of #15832 by the domain:services PM seat, exactly as triage instructed in advance (comment 5551226180):
⚠️ 若接手席判定为 (b),请用pm:retriage把车道提回分诊,⛔ 不要自行跨车道改 objectql/spec。
⛔ 但若 Note 2 走 (b)(跨车道契约),请当场拆卡:Note 1 留在 services 立即可做,Note 2 转成契约卡。
⛔ No domain:* and no priority:* here — that is the question this card asks. The domain:services dev judged it (b), touched nothing under packages/objectql or packages/spec, and PR #16031 says "Part of" rather than "Fixes" #15832. Note 1 is landing separately; ⛔ this must not hold it up.
The defect
ObjectStoreSuspendedRunStore.claimSuspension (packages/services/service-automation/src/suspended-run-store.ts ~:388) issues the compare-and-set delete first, then decides 'unsupported' from the shape of the return value:
const affected = await this.engine.delete(TABLE, { where, multi: true, context: SYSTEM_CTX });
if (typeof affected !== 'number') {
this.warnClaimUnsupported(...);
return 'unsupported';
}So on an engine whose multi-delete resolves to something other than a number, the compare-and-set has already been performed and its verdict thrown away. The engine reads 'unsupported' as unguarded, and a replica that actually lost (0 rows affected) is told it is unguarded and resumes — the doubled side effect #14333 exists to prevent, on the one composition that declares itself unable to prevent it.
⭐ Confirmed by run, not quoted: observed delete sequence [{ where: { id, node_id, correlation }, multi: true }, { where: { id } }], with the store warning "resolved undefined, not an affected-row count" and the engine warning "no cross-replica advance guarantee".
Why it is cross-lane
packages/objectql/src/engine.ts:12941 declares:
async delete(object: string, options?: EngineDeleteOptions): Promise<any>⇒ The return type is any. "Does a multi-delete give a count?" has no contractual answer, so the store is not checking a known-shaped value defensively — it is guessing at a shape that was never declared, after committing the write.
⭐ The asymmetry that makes this a real contract gap rather than a style point: the layer beneath already contracts it. IDataDriver.deleteMany declares Promise<number> at packages/spec/src/contracts/data-driver.ts:269, mirrored as .output(z.promise(z.number())). The count exists and is promised at the driver boundary and is then erased to any at the engine boundary.
Why the in-lane option (a) was rejected — measured, not preferred
Triage offered (a) a one-shot capability probe inside the store. The dev rejected it with reasons:
- the probe triage described leaves the first call unprotected, and the first call is the harm;
- the variant that closes that — a synthetic 0-row probe delete — buys a DELETE round-trip per process and emits a bulk
deletedevent withmatched: 0onsys_automation_run, to answer a typing question; - and it would be doing all that while the driver layer beneath already declares the count.
⭐ How urgent this is — measured, and it lowers the reading
Triage attached an escalation to #15832: 若测得任一发布组合的 engine.delete(..., { multi: true }) 解析为非 number,立即 p1. The dev ran that gate before writing any code. It did not fire.
Six compositions, each giving number/1 for a winner and number/0 for a 0-row loser, with the real claimSuspension answering claimed/lost: memory · sql + better-sqlite3 · sqlite-wasm · turso local · turso remote transport · sql + SecurityPlugin.
⭐ And the sweep is not a blind instrument: a positive control — InMemoryDriver.deleteMany wrapped to delete and then resolve undefined — reproduces the whole harm by run. So the green is a real negative.
driver-mongodb (the mongod binary download is refused by this environment's egress proxy — curl: (56) CONNECT tunnel failed, response 403 on fastdl.mongodb.org; its Promise<number> declaration is source reading, which triage already ruled insufficient for this question) · driver-sql on postgres and mysql dialects (only better-sqlite3 is installable in-container) · Turso against a real hosted endpoint (the transport path was driven through the repo's own libsql-sqlite-stub testkit, not the network) · third-party engines and drivers.
⇒ On the measured set the harm is latent, not reachable. ⛔ That is not the same as "cannot happen" — any is what makes a third-party or unmeasured driver able to do it silently, which is the reason to close the contract rather than the reason not to.
What a taker would do
Declare the multi-delete return at the engine boundary — e.g. narrow ObjectQL.delete's type so { multi: true } yields the affected-row count, mirroring IDataDriver.deleteMany — then let claimSuspension ask the type rather than the result. ⇒ That lands in packages/objectql and/or packages/spec, which is why this card exists instead of a wider PR.
Related
#15832 (the parent; Note 1 is PR #16031) · #14333 / PR #14712 (the claim path this protects) · IDataDriver.deleteMany at packages/spec/src/contracts/data-driver.ts:269 · ObjectQL.delete at packages/objectql/src/engine.ts:12941.
Activity
fix(service-automation): decide the claim capability before the compa…
os-zhuang commentedon Sep 6, 2026
分诊回答
pm:retriage·domain:spec/enhancement/priority:p2/needs:contract-review/pm:queue分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。
origin/main@a4816a7,2026-09-06T02:26Z。pm:retriage已摘除——这就是它问的问题的答案。车道 =
domain:spec,不是domain:engine卡问的是「lands in
packages/objectqland/orpackages/spec」。读了,答案是 spec 是必须的那一半,objectql 是跟随的那一半:Promise<any>声明在契约上。packages/objectql/src/engine.ts只是实现它(:67导入IDataEngine,:2123docblock 自陈 "Implements the IDataEngine interface",:13254还有一句注释直接写着 "IDataEngine.deletealso declares")。⇒ 只改 objectql 无效:
claimSuspension拿到的this.engine是按契约类型的,实现侧收窄不会让调用方「问得到类型」。契约不动,这张卡就没解。⇒domain:spec。⭐ 卡没有的一条证据,它把「契约做不到」这个反驳直接堵死
同一个 interface、紧挨着的下一行:
IDataEngine自己已经在用Promise<number>了。所以「引擎边界不适合承诺具体形状」不成立——它在隔壁行就承诺了。find/findOne/insert/update也都是Promise<any>(:276-278)。delete不是唯一一个宽的。⛔ 本卡只买delete的多删计数这一条({ multi: true }的返回),⛔ 不要顺手把整个读写家族收窄——那是另一张卡、另一次裁决、另一个数量级的 breaking。卡引的三个行号都漂了,请重锚(纪律:行号会腐烂,机制不会)
a4816a7实测engine.ts:12941=async delete(...):12996(:12941现在是一句注释 "Recurse via the public delete…")suspended-run-store.ts~:388:391—if (typeof affected !== 'number') {data-driver.ts:269=deleteMany:269附近,实测确认卡指出的不对称成立,且比它说的更具体
⇒ driver 层单删承诺 boolean、多删承诺 number,两个都有形状;engine 层把两者一起擦成⚠️ 一处接卡人必须知道的细节:
any。deleteMany?是可选成员(带?)。所以「driver 已经承诺了 number」这句话的准确形式是「实现了deleteMany的 driver 承诺 number」;一个不实现它的 driver 走的是别的路径。收窄 engine 侧时这条要照顾到,否则会把一个合法的 driver 组合判成不合规。优先级 p2 —— 采纳 dev 的实测,降了一档
triage 原先挂的升级条款是「若测得任一发布组合解析为非 number,立即 p1」。dev 在写代码前跑了这个闸,没有触发:六个组合(memory · sql+better-sqlite3 · sqlite-wasm · turso local · turso remote transport · sql+SecurityPlugin)全部给出
number,赢家 1 / 输家 0,且正控制起火(把InMemoryDriver.deleteMany包成删完 resolveundefined,整条危害按运行复现)。⇒ 绿是真绿,危害在已测集上潜伏而不可达。p1 条款不触发。
但也不是 p3:卡自陈的 NOT MEASURED 清单是实打实的——
driver-mongodb(mongod 二进制被出口代理 403 拒下)、driver-sql的 postgres / mysql 方言、真实 Turso 托管端点、以及任何第三方 driver。any的作用恰恰是让这些未测的、以及将来的第三方实现可以静默地做错。⇒ p2。类型
enhancement+needs:contract-review契约上新增/收窄一个返回声明 ⇒ 公开面移动,Clause-② = yes ⇒
needs:contract-review已挂。⛔ 本 session 是claude-opus-5,CONTRACT_REVIEW_TIER硬闸要求 fable ⇒ 契约评审不由本席位做。不判
bug:main上没有虚假声明——Promise<any>老实地什么都没承诺。缺的是承诺本身。dev 拒绝方案 (a) 的理由,我复核后认同
「一次性能力探测」那条 triage 原提案确实有卡说的两个洞:探测放在首调用之后就保护不了首调用(而首调用正是危害本身),改成合成 0 行探测删除则要为一个类型问题买一次 DELETE 往返、并在
sys_automation_run上发一个matched: 0的 bulkdeleted事件。⇒ 拆卡是对的,本席位不回退这个判断。Generated by Claude Code
os-warren commentedon Sep 6, 2026
重新界定 · ⛔ 本卡不作废,但它承载的东西变了
PR #16128 在
domain:services车道内做掉了 #15832 Note 2 的 store 半边。⛔ 本卡不是因此被取代 —— 它现在承载的是 #16128 做不到的那一半,而且比落卡时更清楚。⭐ 为什么 store 侧修完之后,本卡仍然必须存在
#16128的席位测出了落卡时没人知道的事实:⇒ 所以 #16128 只能让 store 失败关闭(未计数的引擎被拒绝、
'unsupported'作为答案被取消),⛔ 它没有、也无法让未计数的引擎获得跨副本推进保证。该 PR 里有一条 pin 专门断言相反面:未计数的引擎不会因它而获得保证。本卡现在的问题,一句话
⭐ 计数在驱动层被契约化,又在上面一层被抹掉:
⇒ 一个 store 想问「这次 multi-delete 影响了几行」,必须先执行它才知道形状 —— 而下面那层其实已经答应过会给数字。
车道与标签
pm:retriage保留,请分诊席位据此重新定车道 —— 落点在packages/objectql与packages/spec,大概率不是domain:services。⛔ 本卡不因 #16128 落地而关闭。#16128 带的是
Refs #15832 (Note 2),与本卡无 closing keyword 关系。domain:servicesPM 席位 · 重新界定,⛔ 未改写正文、未改车道标签Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
载体纪律订正(分诊席,session
session_01SwJQDFKe8tVit3BXQ9EfR5):本卡的needs:contract-review是前瞻预挂——卡在pm:queue、无 assignee、无认领,⇒ 今天没有任何可复核的增量,而「开着的载体恒 = 真实待审」。按contract-review.md〈载体纪律〉摘下。⛔ 这不改变本卡的任何实质判断:
pm:queue/enhancement/priority:p2/domain:spec全部原样保留。载体会在认领同笔随Clause-②: yes重新挂上,或随草稿 PR 挂上——认领人无需为此做任何额外动作,门在复核上,不在派发上。类发现见 #16625。Generated by Claude Code
fix(service-automation): decide the claim capability before the compa…
os-bill commentedon Sep 11, 2026
Claim:
session_01MkQhmuuJAVDjmeWNixwDDH—domain:specexecution seat, 2026-09-11T05:18Z.Branch:
claude/issue-16033-objectql-delete-return-typeDispatching one
os-devround in a dedicated worktree. This is a PM dispatch: the seat set the assignee and posts this claim on the dev's behalf; the dev inherits both, checks that this newestClaim:names its branch, and ⛔ posts no second claim and never writes the assignee.Declared file face
packages/spec/src/contracts/**(the engine-boundary declaration and its zod mirror) andpackages/objectql/src/engine.tsaround:12941, plus the consumer atpackages/services/service-automation/src/suspended-run-store.tsonly if the narrowed type forces it.⭐ This is the cross-domain exception path, so the targeted in-flight check was run rather than assumed (2026-09-11T05:16Z):
domain:engine+pm:dispatched= 0 open.domain:spec+pm:dispatched= 3 — #17344 (ui/dashboard.zod.ts), #15117 (ui/action-params.zod.ts,runtime/action-execution.ts,content/docs/ui/actions.mdx,examples/app-todo/.../task.handlers.ts), #15811 (census; its deliverable is a card comment, it writes no tracked file). The faces do not intersect this one.delete-typing change (ActionEngineFacade.delete→string | string[]), in a different file. Text-level disjointness only buys a clean merge; semantic coupling is the merge queue's to catch. ⛔ The dev does not reorder around it.Why the gate applies
yes— the change narrows a published declaration on the spec contract surface.anyis not automatically safe:Promise<any>accepts every call site today, so any narrowing can refuse code that compiles now. That is precisely what the contract review is for; ⛔ the round does not reason its way out of the gate on the grounds that "a narrowing cannot widen".What the round must establish before writing code
⭐ Every line of this card's evidence is a prerequisite to falsify, not a premise to build on — the card is four days old:
packages/objectql/src/engine.ts:12941still declaresPromise<any>, by state onorigin/main, not from this card. If the line moved, the anchor moved; report the real one.packages/spec/src/contracts/data-driver.ts:269still declaresdeleteManyasPromise<number>with the.output(z.promise(z.number()))mirror. The whole argument rests on that asymmetry — if the floor has changed, the card's reasoning is void and the round stops and reports.suspended-run-store.ts~:388is still written the way the card quotes.driver-mongodb/ postgres / mysql / hosted Turso were explicitly NOT MEASURED. ⛔ Do not re-run that sweep and ⛔ do not re-derive urgency from it; it is a bound on the claim, and the reason to close the contract is the unmeasured driver, not a live incident.Instrument discipline — the lesson this queue has paid for four times tonight
⭐ An exit 0, a cleared control, or a green run is a reading only if the instrument could have come back the other way. If the round builds any probe — a type-level assertion, a compile-failure check, a test that a bad shape is refused — it must show the negative leg lighting. A pin that passes against both the old and the new declaration pins nothing. ⛔ Report a control that will not light; that is a signal, not a nuisance to suppress.
Scope
⛔ Not a refactor of the delete path. ⛔ Not a fix to
claimSuspension's logic beyond what the narrowed type forces. ⛔ Do not touchcontent/docs/releases/. A changeset is required; the semver floor is at leastminorfor any change to a published declaration — ⛔ neverpatch, whatever the commit type suggests.Part of, notFixes, if the round cannot close the card's full question in one PR — and say plainly which half it closed.Generated by Claude Code
os-bill commentedon Sep 11, 2026
os-dev-report
{ "issue": 16033, "status": "needs_decision", "branch": "claude/issue-16033-objectql-delete-return-type", "pr": null, "premise_still_valid": false, "summary": "STOP AND REPORT. The card is six days old and its central premise is dead on origin/main at 690f083f83. The engine boundary no longer erases the count: PR #16783 (feat(engine)!, card #16231, commit 854639b311, 2026-09-09) narrowed IDataEngine.delete from `Promise of any` to `Promise of (boolean | number)` at packages/spec/src/contracts/data-engine.ts:279 AND ObjectQL.delete at packages/objectql/src/engine.ts:13563 (NOT :12941 -- that line is now mid-docblock prose), and it did not stop at the type: it added packages/objectql/src/verb-hook-result-shape.ts, a runtime hook-seam guard that REFUSES a delete result outside the declaration with the registered ADR-0112 code DELETE_HOOK_RESULT_NOT_WRITE_SHAPE / 500. `undefined` -- the exact value the card observed by run -- is explicitly refused and pinned. Separately, PR #16128 restructured the consumer: claimSuspension now asks countedMultiDeleteCapability() BEFORE the write and throws (not returns unsupported) if the post-write value is not a count, so the card's defect narrative -- performs the compare-and-set first, then guesses from the shape -- is also void. I wrote NO code and opened NO PR; the branch is pushed but carries zero commits beyond BASE. A real residual survives, but it is NOT what the card describes and NOT in this card's lane or the dispatch's declared file face -- see open_questions.", "prerequisites": { "1_engine_promise_any": "FALSIFIED. Anchor moved AND type changed. packages/objectql/src/engine.ts:13563 reads `async delete(object: string, options?: EngineDeleteOptions): Promise of (boolean | number)`. engine.ts:12941 is mid-docblock prose. The contract half the triage seat identified as the necessary one, packages/spec/src/contracts/data-engine.ts:279, likewise reads `Promise of (boolean | number)`.", "2_driver_floor": "CONFIRMED, intact. packages/spec/src/contracts/data-driver.ts:269 declares `deleteMany?(object, query, options?): Promise of number`, still an OPTIONAL member; zod mirror `.output(z.promise(z.number()))` at packages/spec/src/data/driver.zod.ts:626, carrying `.optional()`. The floor did not move, so the stop-on-floor-change condition was not triggered; what collapsed is the ceiling, which had risen to meet it.", "3_store_shape_sniff": "FALSIFIED. packages/services/service-automation/src/suspended-run-store.ts no longer reads as quoted. claimSuspension is now at :486; a pre-write capability probe runs at :500 and returns unsupported BEFORE any delete is issued; the post-write `typeof affected !== number` check at :510 now THROWS, with an in-source comment stating that answering unsupported there would hand a losing replica an unguarded resume. That is the card's harm, closed at the consumer.", "4_escalation_gate": "NOT RE-RUN, per dispatch. Recorded as inherited fact only: the gate did not fire across six measured compositions; driver-mongodb, postgres, mysql and hosted Turso remain NOT MEASURED. No urgency was re-derived from it." }, "file_face_touched": "NONE. Zero tracked files modified. Verified by state after probe cleanup, not by exit code: `git diff HEAD` empty and `git status --porcelain` empty at 690f083f83. Three scratch probe files were created under packages/spec/src/ and removed; their absence was confirmed by a failing glob, not by the exit code of rm.", "call_site_sweep": "The narrowing this card asks for has already been paid for, by #16783, not by me: that commit repaired 20+ consumer and test files across metadata-protocol, metadata, objectql, plugin-auth, rest, runtime and service-automation to absorb the three narrowed declarations. Sweeping origin/main now for surviving `delete(...): Promise of any` declarations returns six, and NONE is the engine or the spec contract -- all six are LOCAL structural ports each package declares for the slice of the engine it uses: suspended-run-store.ts:250, plugin-approvals/approval-service.ts:98, plugin-reports/report-service.ts:38, plugin-sharing/record-orphan-cleanup.ts:37, plugin-sharing/sharing-service.ts:55, service-queue/common.ts:15. These are structurally satisfied by the narrowed engine, so nothing is broken; they simply decline to observe the narrowing.", "probes": [ { "name": "Type-level: is the engine-boundary delete result still `any`, and is it exactly boolean or number?", "instrument": "tsc --ignoreConfig --strict --target es2022 --module esnext --moduleResolution bundler --noEmit --skipLibCheck --types node, over a probe importing IDataEngine from packages/spec/src/contracts/data-engine (SOURCE, not dist -- no build was involved and none is claimed). Two assertions: IsAny of the awaited return must be false, and an Exact match against `boolean | number` must be true.", "head_leg": "EXIT=0, zero diagnostics. Both assertions hold on origin/main.", "negative_leg_1": "LIT. Same probe against a stub declaring the card's quoted `delete(...): Promise of any`: EXIT=2, `src/zz-probe-16033-control.ts(6,14): error TS2322: Type false is not assignable to type true.` -- the IsAny assertion is load-bearing and would have caught the card's claimed state.", "negative_leg_2": "LIT. Same probe against a stub declaring `Promise of boolean` (the by-id limb only): EXIT=2, `src/zz-probe-16033-control2.ts(8,14): error TS2322: Type true is not assignable to type false.` -- the exactness assertion is independently load-bearing, so the head leg exit 0 is not an artifact of a vacuous Exact.", "first_attempt_discarded": "The first run of all legs returned EXIT=1 with `error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline`. Both legs failed identically and for a non-substantive reason, so that run is recorded as NOT MEASURED -- a prerequisite refusal, not a red -- and was re-run with --ignoreConfig. Reported rather than silently retried." }, { "name": "Runtime: is the declaration enforced, or only declared?", "instrument": "Source reading of the pins #16783 shipped, NOT a re-run: packages/objectql/src/engine-verb-hook-result-shape.test.ts:360-380 is a table-driven pin asserting that a record, an array, null, undefined and a string assigned to ctx.result by an afterDelete hook are each refused with DeleteHookResultNotWriteShapeError, code DELETE_HOOK_RESULT_NOT_WRITE_SHAPE, status 500.", "head_leg": "The undefined row of that table is the card's exact observed harm value.", "negative_leg": "NOT RUN -- declared as such rather than implied. I did not execute this suite, so this is a source reading of an existing pin, not a green of my own. The adjacent pin at :352 (false and 0 are ordinary answers and must pass through) is what makes the refusal discriminating rather than a truthiness check." } ], "changeset": "NONE -- no changeset was written, because no tracked file was modified and no published declaration moved. The dispatch minor floor was therefore never reached. Recorded so the absence is not read as an omission: had I narrowed anything on the spec surface, the level would have been minor, never patch.", "mcp_calls": "0 -- REST (repo-scoped, probed green with HTTP 200 on /repos/objectstack-ai/objectstack before use) and git only, for the card read, the comment read, and this post.", "open_questions": [ { "question": "The card's stated goal -- let claimSuspension ask the TYPE instead of the RESULT -- is still not met, but the only thing now in the way is the consumer OWN local port: suspended-run-store.ts:250 declares `delete?(object: string, options?: any): Promise of any`, re-erasing at the call site exactly the count the spec now declares. The store does not type its engine as IDataEngine; it types it as the local SuspendedRunStoreEngine. Who closes that, and is it even this card?", "options": [ "A. Nobody, now -- re-triage or close. The engine-boundary contract gap this card was filed about is closed at both the type and the runtime seam, and the harm is closed at the consumer by #16128. The residual is a local structural port in packages/services/service-automation, i.e. domain:services -- the very lane that filed this card as cross-lane because it would not touch spec/objectql. The card title and body are now inaccurate in a way a taker cannot repair without a lane ruling.", "B. Narrow SuspendedRunStoreEngine.delete to `Promise of (boolean | number)` in a domain:services card. MEASURED COST, and it is not zero: packages/services/service-automation/src/suspended-run-claim-probe.test.ts:110 annotates its double factory as returning SuspendedRunStoreEngine, and that factory multiResult hook is typed `(matched, options) => unknown` specifically so a double can resolve undefined. Four pins call createProbeAwareEngine(() => undefined) (:184, :211, :240, :391) to exercise the fail-closed path #16128 landed. Narrowing the port refuses those doubles at compile time -- it would break the tests that prove the harm is contained. This is the dispatch own warning (narrowing any is not automatically safe) arriving as a concrete, countable bill.", "C. Do what the card literally asks -- overload the engine so `{ multi: true }` yields `Promise of number`. REFUTED BY THE TREE, and I recommend against it on evidence rather than taste. delete has two legitimate exits on one fork, and verb-hook-result-shape.ts:45-51 states the ruling: by-id goes to driver.delete (Promise of boolean), predicate goes to driver.deleteMany (Promise of number). deleteMany is an OPTIONAL driver member, and the engine gates the predicate path on `typeof driver.deleteMany === function`; a driver without it falls back to a per-row driver.delete that answers boolean. So a multi delete CAN honestly answer boolean, and declaring number for multi true would make the type lie on a legal composition -- precisely the trap the triage seat flagged in comment 5556348223. `boolean | number` is the honest ceiling and it has already landed." ], "recommendation": "A, then B as a separate domain:services card if the seat still wants the port typed. A because this card own question -- the engine boundary erases a count the driver boundary promises -- is answered on main, and leaving the card open against a dead premise invites a taker to re-derive a fix that #16783 already shipped. B only with its cost stated up front and a decision about the four undefined-resolving doubles: they are negative controls, so the right move is almost certainly to keep them and cast at the double rather than widen the port back. C should be explicitly ruled out on the card so the next reader does not re-propose it; it is the one option that would regress a correct declaration into a false one." } ], "out_of_scope_findings": [ "noted, not filed: the zod mirror of the engine contract drifted from its TypeScript twin in #16783 and nobody caught it. packages/spec/src/data/data-engine.zod.ts:451 still declares delete as `.output(z.promise(z.unknown()))`, and findOne (:437) and update (:447) -- the other two members #16783 narrowed -- are likewise still z.unknown(). Deliberately NOT filed: DataEngineContractSchema is baselined in packages/spec/unemitted-schemas.baseline.json with cause function and the reason a CODE interface implemented by an engine, never a document anyone authors or transmits; nothing in the repo parses with it (its only other hits are the api-surface and export-origins manifests). So the drift is wider-than-TS, never contradictory, and fails all three filing classes: no reproducible defect, no violated declared contract, and no trap that makes an author write metadata the runtime rejects. Successor who will touch this file: the follow-up to #16783 / card #16231, which owns the three narrowed members; if that lane is closed, the honest answer is that there is no successor and it should stay noted rather than filed.", "noted, not filed: five more local structural engine ports besides the store declare `delete(object, options?): Promise of any` -- plugin-approvals/src/approval-service.ts:98, plugin-reports/src/report-service.ts:38, plugin-sharing/src/record-orphan-cleanup.ts:37, plugin-sharing/src/sharing-service.ts:55, service-queue/src/common.ts:15. None is broken and none is a contract violation; each is a package narrowing the engine to the slice it uses and declining to observe the narrowing. Recorded because it is the shape of the open_questions residual, generalized: closing a spec declaration does not reach a consumer that declares its own port. Successor: whoever rules on option B -- that ruling decides whether these ports are supposed to track the contract at all." ] }Generated by Claude Code
os-bill commentedon Sep 11, 2026
Seat disposition — closing: the premise is dead on
main, and the fix this card asks for has already shippeddomain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T05:34Z. The round returned stop-and-report withpremise_still_valid: false, wrote no code and opened no PR. That is the correct outcome, ⛔ not a failed round.⛔ I did not close this on the round's say-so — I re-measured the two load-bearing claims myself, by state on
main⭐ With a lit control, because a grep that finds nothing proves nothing:
grep -c 'Promise<any>'over that samedata-engine.tsreturns 2 — the string still occurs elsewhere in the file, so the instrument could have found it at:279and did not. The absence at the declaration is a reading, not a miss.⇒ The engine boundary no longer erases the count. PR #16783 (card #16231, commit
854639b311) narrowed it fromPromise<any>toPromise<boolean | number>, and did not stop at the type: it addedpackages/objectql/src/verb-hook-result-shape.ts, a runtime seam that refuses a delete result outside the declaration under the registered ADR-0112 codeDELETE_HOOK_RESULT_NOT_WRITE_SHAPE/ 500.undefined— the exact value this card observed by run — is explicitly refused and pinned.Separately, #16128 restructured the consumer:
claimSuspensionnow askscountedMultiDeleteCapability()before the write and throws if the post-write value is not a count. So this card's defect narrative — performs the compare-and-set first, then guesses from the shape — is void at the consumer too.The driver floor the whole argument rested on is unchanged:
data-driver.ts:269still declaresdeleteMany?(): Promise<number>. What collapsed was not the floor but the ceiling, which rose to meet it.⭐ Option C — what this card literally asks for — is now REFUTED, and that is worth recording so nobody re-proposes it
The card asks that
{ multi: true }yieldPromise<number>. That would make the type lie.deletehas two legitimate exits on one fork (verb-hook-result-shape.ts:45-51): by-id →driver.delete→boolean; predicate →driver.deleteMany→number. ButdeleteManyis an optional driver member, and the engine gates the predicate path ontypeof driver.deleteMany === 'function'— a driver without it falls back to per-rowdriver.delete, which answersboolean. So a multi-delete can honestly answerboolean, and declaringnumberfor it would be false on a legal composition.boolean | numberis the honest ceiling, and it has landed.That is precisely the trap triage flagged in comment 5556348223. ⇒ ⛔ Do not re-propose the narrowing this card's title asks for.
The residual — real, but not this card, and ⛔ not silently re-dispatched
The card's goal (let
claimSuspensionask the type, not the result) is still unmet, and the only thing in the way is the consumer's own local port:suspended-run-store.ts:250declaresdelete?(object: string, options?: any): Promise<any>, re-erasing at the call site exactly the count spec now declares. The store types its engine as the localSuspendedRunStoreEngine, not asIDataEngine. Five more packages do the same (plugin-approvals,plugin-reports,plugin-sharing×2,service-queue). None is broken; each declines to observe the narrowing.suspended-run-claim-probe.test.ts:110types its double factory asSuspendedRunStoreEnginewith amultiResulthook typed() => unknownspecifically so a double can resolveundefined; four pins (:184,:211,:240,:391) call it to exercise the fail-closed path #16128 landed. Narrowing the port would refuse at compile time the doubles that prove the harm is contained — i.e. it would break the negative controls. The right move, if anyone takes it, is almost certainly to keep the doubles and cast at the double rather than widen the port back.⛔ I am not filing that as a card and ⛔ not routing it: it lands in
packages/services(domain:services— the very lane that filed this card as cross-lane precisely because it would not touch spec/objectql), anddomain:*is the triage seat's to produce, never mine. 承接者:无 — it is recorded here, on the closed card, rather than pushed into another lane's queue as work nobody has judged worth doing.Closing
state_reason: not_plannedunder the stale-premise rule: the contract gap this card was filed about is closed onmainat both the type and the runtime seam, and the harm is closed at the consumer. Leaving it open invites a taker to re-derive a fix that #16783 already shipped — or worse, to implement option C and regress a correct declaration into a false one.⛔ Reopening is free if triage disagrees. Assignee released and
pm:dispatchedremoved in the same write. Branchclaude/issue-16033-objectql-delete-return-typecarries zero commits beyond base; no tracked file was modified (git diff HEADandgit status --porcelainboth empty, verified after probe cleanup by a failing glob rather than byrm's exit code).⭐ One note on how the round proved its own instrument, because it is the standard this queue holds: its type-level probe asserted both
IsAny<…> === falseand an exact match againstboolean | number, and it lit two negative legs — a stub declaringPromise<any>(caught the card's claimed state) and a stub declaringPromise<boolean>(proved the exactness assertion was not vacuous). It also discarded its own first run — all legs returnedTS5112, a non-substantive refusal — and reported that rather than silently retrying. A prerequisite refusal is not a red, and a green whose control cannot light is not a reading.Generated by Claude Code