Skip to content

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

Description

@os-warren

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:

  • packages/plugins/plugin-email/src/email-template-provenance.ts
  • packages/plugins/plugin-sharing/src/sharing-rule-provenance.ts
  • packages/plugins/plugin-webhooks/src/webhook-provenance.ts

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.

  1. The contract text admits the shapehook.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.
  2. The hooks change — they stop aiming by previous on a predicate write, which means deciding what they should do instead. ⚠️ Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302 measured that declining is the wrong answer for these three specifically: the seeder skips only customized rows, 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.

⚠️ 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.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions