You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Action-body writes have no not-found gate: ctx.api.object().update() against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867
Successor to #5571, filed by the domain:engine-core PM seat (#6019, session session_01VGAePF7iGGUYUT8oX1cVgx) from that card's reproduction round. Filed unassigned and ungraded — promotion and domain:* are the triage seat's single-producer channel.
⛔ This is NOT "previous is unbound."#5571 carried that attribution for six triage rounds and the reproduction measured it wrong. See "What this is not" below before scoping any work.
The defect
Nothing on the action-body write path ever asks whether the target row exists.
Observed, on a real stack — deterministic, not intermittent
bootStack(showcaseStack), real kernel, real Hono server, authenticated. Same id, same object, same process, same second:
POST /actions/showcase_task/showcase_mark_done/<GHOST>
-> 400 VALIDATION_ERROR "HookConditionError: … reads 'previous', which is not bound …"
PATCH /data/showcase_task/<GHOST>
-> 404 RECORD_NOT_FOUND "Record <GHOST> not found in showcase_task"
Controls green (so the 400 is about the missing row, not the action): the same action against real ids returns 200.
previous is a SYMPTOM, not the disease — the widening measurement
Isolation probe on showcase_invoice, an object with no hooks registered at all, through the same action-body shape:
No hooks, no previous, same defect: the ghost-id write sails past the absent gate and dies on whatever the pipeline complains about first — here required-field validation, because with no prior row a PATCH is validated as if it were a whole record.
⇒ The 400 class varies with the object's declarations. The missing 404 is the constant.
CI, right now — packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts logs the HookConditionError and [BodyRunner] sandboxed action threwwhile passing 23/23. Stack: hook-wrappers.ts:642 → triggerHooks → engine.ts:8526. The defect has been travelling through a required, green, 3-shard gate printing a stack trace, with nothing asserting on it.
The fix is a not-found gate on the action-body write path, consistent with the two siblings that already have one — ⛔ not a fourth bespoke 404 site. Whether it belongs in ObjectRepository or in ObjectQL.update()'s by-id branch wants measuring: the engine-level answer covers more callers but changes a resolve-null contract that other callers may rely on. That contract question should be settled before implementation, not during.
Whatever lands should carry an assertion — the current dogfood fixture demonstrably tolerates the error in silence.
Harness recipe (~15s per run once built)
stack=awaitbootStack(showcaseStack);// ⚠️ NOT { automation: true } — the showcase// 'rest' connector needs extraPlugins or startup failsconsttoken=awaitstack.signIn();// ⚠️ required since #5519 — anonymous is 401 nowawaitstack.apiAs(token,'POST',`/actions/showcase_task/showcase_mark_done/${GHOST}`,{});// 400awaitstack.apiAs(token,'PATCH',`/data/showcase_task/${GHOST}`,{done: true});// 404
Prerequisite: pnpm build at the workspace root. Faster engine-level loop: mirror hook-condition-fail-loud.test.ts's bootEngine/makeStubDriver.
⛔ Locate every site by symbol, not by line number — #5571 burned five successive anchor corrections and its own triage concluded "the line number is not the anchor, the symbol is."
Successor to #5571, filed by the
domain:engine-corePM seat (#6019, sessionsession_01VGAePF7iGGUYUT8oX1cVgx) from that card's reproduction round. Filed unassigned and ungraded — promotion anddomain:*are the triage seat's single-producer channel.⛔ This is NOT "
previousis unbound." #5571 carried that attribution for six triage rounds and the reproduction measured it wrong. See "What this is not" below before scoping any work.The defect
Nothing on the action-body write path ever asks whether the target row exists.
ctx.api.object('showcase_task').update({id, …})buildSandboxApi→ql.createContext(execCtx).object(name)(runtime/src/sandbox/body-runner.ts)ObjectRepository.update(objectql/src/engine.ts)engine.update()directlyObjectQL.update(), by-id branchMeasured directly at the engine with no hooks registered at all and a ghost id:
engine.update()on a nonexistent id is a silent no-op that resolvesnull. It does not throw, and nothing downstream turns thatnullinto a 404.The 404 gates do exist — on two other paths, neither of which an action body traverses:
protocol.updateDataprobes existence before the write and throwsrecordNotFoundError(data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435 —metadata-protocol/src/protocol.ts, helper nearby)callData's ObjectQL fallback does the same (callData 的 ObjectQL 兜底路径对「记录不存在」给三种不同答案(get→200 null / update→500 / delete→200 deleted:true) #5138 —runtime/src/action-execution.ts)Observed, on a real stack — deterministic, not intermittent
bootStack(showcaseStack), real kernel, real Hono server, authenticated. Same id, same object, same process, same second:Controls green (so the 400 is about the missing row, not the action): the same action against real ids returns 200.
previousis a SYMPTOM, not the disease — the widening measurementIsolation probe on
showcase_invoice, an object with no hooks registered at all, through the same action-body shape:No hooks, no
previous, same defect: the ghost-id write sails past the absent gate and dies on whatever the pipeline complains about first — here required-field validation, because with no prior row a PATCH is validated as if it were a whole record.⇒ The 400 class varies with the object's declarations. The missing 404 is the constant.
Three independent witnesses
packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.tslogs theHookConditionErrorand[BodyRunner] sandboxed action threwwhile passing 23/23. Stack:hook-wrappers.ts:642→triggerHooks→engine.ts:8526. The defect has been travelling through a required, green, 3-shard gate printing a stack trace, with nothing asserting on it.⛔ What this is not
previousbinding.if (priorRecord) hookContext.previous = …is correct — it is doing exactly what ADR-0058 Addendum II / Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 require (never fabricate: an absent row leavespreviousUNBOUND rather than{}/null). It was behaving correctly on a path that should never have been entered. Loosening it would fix the wrong thing and break a ruled invariant.69fde55, the[#5038]not.toMatchassertions inhook-condition-bulk-previous.test.tsare gone — beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574 retired the whole batch diagnostic by fixing the producer, and the surviving pin is "never dispatches a context that lacksprevious— the producer is gone, not just the message". Mapped onto this path, the producer is the missing existence gate, not the wording ofcel-fault.ts.HookConditionErrorbug. That it bypassesonError/async is the settled hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 ruling.Scope notes for whoever takes it
ObjectRepository.deletehas the same shape, and callData 的 ObjectQL 兜底路径对「记录不存在」给三种不同答案(get→200 null / update→500 / delete→200 deleted:true) #5138's own comment records thatdeletewas the worst of the three when the gate was missing there. Check it in the same card.ObjectRepositoryor inObjectQL.update()'s by-id branch wants measuring: the engine-level answer covers more callers but changes a resolve-nullcontract that other callers may rely on. That contract question should be settled before implementation, not during.Harness recipe (~15s per run once built)
Prerequisite:
pnpm buildat the workspace root. Faster engine-level loop: mirrorhook-condition-fail-loud.test.ts'sbootEngine/makeStubDriver.⛔ Locate every site by symbol, not by line number — #5571 burned five successive anchor corrections and its own triage concluded "the line number is not the anchor, the symbol is."
Refs: #5571 (origin, with the full reproduction report) · #4435 · #5138 · #4775 · #5574 · #5846 · ADR-0058 Addendum II.