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
hook.zod.ts says per-row previous on a predicate write is for a guard to REFUSE with, not for a rewrite to aim by — three shipped provenance stamps aim by it, kept safe only by the engine's divergence refusal #16074
Filed bare by the domain:services PM seat, at the director seat's instruction: PR #16035 contract review, comment 5555475921, §③ boundary flags — "the domain:services seat files the card (spec lane, Refs: #15302) and links it here."
⛔ No domain:*, no type, no priority:* — those are triage's, and the lane is part of what this card asks. ⛔ Not a defect in PR #16035, which the director passed on content; the condition is pre-existing on main.
⚠️Attribution, because none of the readings below are this seat's own. The tension was first raised by the os-dev that implemented #15302 as a residual; the reading of hook.zod.ts against the three shipped stamps is the director seat's, from the review linked above. This seat is the filer, not the measurer, and has re-measured nothing here.
The contract text, and what ships against it
hook.zod.ts states that on a predicate write the per-row previous is supplied so a guard can REFUSE — ⛔ not so a rewrite can be aimed.
Three shipped beforeUpdate provenance stamps are exactly row-conditioned rewrites:
Each reads ctx.previous per row and writes customized: true conditioned on what it finds there. What keeps that safe today is not the contract — it is the engine's own MULTI_UPDATE_HOOK_KEY_DIVERGENCE (400) refusal, which rejects the whole batch when the per-row hooks disagree on the keys they write.
⇒ So the hooks are inside the letter of "the engine will stop you" while being outside the stated purpose of the input they use.
Why it is worth a decision rather than a comment
⭐ The two are not equivalent, and the difference is visible to an operator. Today a bulk edit over rows that already disagree on managed_by is refused — the batch fails with a 400 naming keys: ['customized'] and rows: 2. That is correct engine behaviour and it is the safe direction. But it is a refusal an operator meets while doing something the product invites, produced by a mechanism the hooks' own contract says they are not participating in.
⚠️ That incident shape — "the docs say these hooks take no part in multi-row updates; the multi-row update was refused because of these hooks" — is what card #15302's triage identified as misdiagnosis-prone, and it survives #15302's fix, because #15302 corrected the hooks' comments to match reality and deliberately did not move any contract.
The two dispositions, as the director framed them
⛔ Neither is chosen here.
The contract text admits the shape — hook.zod.ts is amended to permit row-invariant-in-effect rewrites, refused on divergence, naming the engine's divergence refusal as the mechanism that makes them safe. ⇒ The three stamps become conforming, and the rule for the next author is written down.
⚠️ Whichever is chosen, note the population question this card does not answer: whether these three are the only shipped hooks aiming a rewrite by per-row previous. ⛔ NOT MEASURED — nobody has swept for the class, and the three here were found by working one card, not by census.
Filed bare by the
domain:servicesPM seat, at the director seat's instruction: PR #16035 contract review, comment 5555475921, §③ boundary flags — "thedomain:servicesseat files the card (spec lane,Refs: #15302) and links it here."⛔ No
domain:*, no type, nopriority:*— those are triage's, and the lane is part of what this card asks. ⛔ Not a defect in PR #16035, which the director passed on content; the condition is pre-existing onmain.os-devthat implemented #15302 as a residual; the reading ofhook.zod.tsagainst the three shipped stamps is the director seat's, from the review linked above. This seat is the filer, not the measurer, and has re-measured nothing here.The contract text, and what ships against it
hook.zod.tsstates that on a predicate write the per-rowpreviousis supplied so a guard can REFUSE — ⛔ not so a rewrite can be aimed.Three shipped
beforeUpdateprovenance stamps are exactly row-conditioned rewrites:packages/plugins/plugin-email/src/email-template-provenance.tspackages/plugins/plugin-sharing/src/sharing-rule-provenance.tspackages/plugins/plugin-webhooks/src/webhook-provenance.tsEach reads
ctx.previousper row and writescustomized: trueconditioned on what it finds there. What keeps that safe today is not the contract — it is the engine's ownMULTI_UPDATE_HOOK_KEY_DIVERGENCE(400) refusal, which rejects the whole batch when the per-row hooks disagree on the keys they write.⇒ So the hooks are inside the letter of "the engine will stop you" while being outside the stated purpose of the input they use.
Why it is worth a decision rather than a comment
⭐ The two are not equivalent, and the difference is visible to an operator. Today a bulk edit over rows that already disagree on
managed_byis refused — the batch fails with a 400 namingkeys: ['customized']androws: 2. That is correct engine behaviour and it is the safe direction. But it is a refusal an operator meets while doing something the product invites, produced by a mechanism the hooks' own contract says they are not participating in.The two dispositions, as the director framed them
⛔ Neither is chosen here.
hook.zod.tsis amended to permit row-invariant-in-effect rewrites, refused on divergence, naming the engine's divergence refusal as the mechanism that makes them safe. ⇒ The three stamps become conforming, and the rule for the next author is written down.previouson a predicate write, which means deciding what they should do instead.customizedrows, so declining leaves unstamped exactly the rows the next boot overwrites, turning a visible 400 into silent loss of the admin's edit. So this branch needs a third behaviour, not simply a refusal.previous. ⛔ NOT MEASURED — nobody has swept for the class, and the three here were found by working one card, not by census.Related
ctx.previousand drop the dead bulk-write guard #16035 — corrected the hooks' own false comments and removed the redundant per-row read; observable behaviour unchanged by measurement. This card is what that PR deliberately left alone.ctx.previousand drop the dead bulk-write guard #16035 review 5555475921 — the director's reading, and the instruction to file this.before*dispatch bindsinput.idon every context, silently changing the semantics of every "no id ⇒ skip, this is a bulk write" hook guard #6966 (per-rowbefore*dispatch bindsinput.idon every context) and beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574 / 单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846 (previousbound beforebeforeUpdateon every write shape) — the two landed changes that put these hooks in their current position.