Skip to content

delete() by-id 仍允许 beforeDelete 重定向 input.id —— 与 update() 侧的 ADR-0058 Amendment II.1 不对称,需定夺是否一并退休 #6752

Description

@os-zhuang

发现于 #5574 的引擎半边(PR #6697)。属范围外发现,已在该 PR 内刻意保持原状,仅把不对称写进 ADR 与 pin,交由本单定夺。

事实(PR #6697 落地后的 origin/main 形状)

before* 处理器改写 ctx.input.id 时,两个动词的答复不一致:

清空 id (CLEARED) 改指到另一个 id (REBOUND)
update() by-id 拒绝 拒绝
delete() by-id 拒绝 仍然照办(#5272 的重读,未改动)
二者,per-row 拒绝 (D4) 拒绝 (D4)

清空这一列是齐的,而且不是选择题:它原本靠"落到 predicate 分支"生效,而 ADR-0058 Addendum II 要求派发阶梯在 before 阶段之前解析(per-row 上下文是从匹配行集构造出来的),所以那条落法已经不存在。这就是裁决点名要退休的能力,HookTargetRebindError / ERR_HOOK_TARGET_REBIND 是它的拒绝面。

改指这一列不齐,且是有理由的不齐。 反对照办的论据是"写会落到一条从未被读过前像、从未评估过 readonlyWhen 锁、验证规则是对着别的记录跑的行上"——而 delete() 上这句话根本不成立:#5272 已经重新解析新目标,重读它的前像并重绑 previous,afterDelete 与 roll-up 汇总看到的都是真正被删的那行。update() 没有这套机制,要退休就得新造一套,而那正是 #5574 裁决明令禁止的"silently pick re-resolution instead"。

所以 PR #6697 的选择是:update() 拒绝,delete() 维持照办,把 repoint 本身留作独立问题——不作为一次时序改动的搭车项。

需要定夺的问题

delete() by-id 的 repoint 能力,是保留还是退休?

三条候选路线,代价都已量过:

  1. 保留(现状)。 代价是两个动词对同一个槽位答复不同,作者要记两条规则。收益是 单记录 delete 从不绑定 hookContext.previous —— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272 的既有能力与其 pin 全部不动,且它是正确的——重读让语义自洽,没有任何陈旧绑定泄漏给消费者。
  2. 退休 delete 侧的 repoint。 与 update() 对齐成一条规则。代价是删掉一个已交付、有 pin 的能力(单记录 delete 从不绑定 hookContext.previous —— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272 明确写了"A beforeDelete hook may repoint the target id"),这是行为移除,需要裁决而非重构。
  3. 给 update() 也造重解析。 与 delete() 对齐成另一条规则。已被 beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574 裁决排除("⛔ do not silently pick re-resolution instead"),除非裁决被显式修订。

前提核查(对着 origin/main 实测)

仓内没有任何消费者依赖 delete 侧的 repoint:全仓 grep -rn "input\.id\s*=" --include=*.ts --include=*.tsx --include=*.md --include=*.mdx(排除 node_modules 与 input.id ===)只有一处命中,是 packages/objectql/src/engine.test.ts 里为触发 #2982 fail-closed 断言而清空 id 的用例,与 repoint 无关。所以路线 2 的兼容性代价为零,它纯粹是一个"要不要少一条规则"的取舍,不是"会不会弄坏谁"。

这也意味着本单不阻塞任何东西——现状是自洽且正确的,只是不够齐整。

已落地的记录点(改动时需一并移动)

  • docs/adr/0058-expression-and-predicate-surface.md — Amendment II.1 的 scope 表格与其后的两段理由;
  • packages/objectql/src/hook-target-rebind-errors.ts — "What this error does NOT cover: delete()'s by-id REPOINT" 一节;
  • packages/objectql/src/engine.ts — delete() by-id 分支里重读块上方的注释;
  • packages/objectql/src/bulk-write-per-row-hooks.test.ts §7 D4 — still HONOURS a by-id beforeDelete REPOINT — deliberately not retired here,该用例断言重定向后被删的是新目标且 afterDelete 的 previous 是新目标的前像。

关联

#5574(引擎半边,PR #6697)/ #5272(delete 侧前像时序与 repoint 重读)/ #5846 / ADR-0058 Addendum II + Amendment II.1

Activity

  1. claude commented on Aug 8, 2026

    @claude
    Contributor

    Findings round: graded to the decision box — finding → needs-user-decision, routed domain:engine-core.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling (2026-08-09): route 2 — retire delete()'s by-id REPOINT, aligning with update(). pm:queue.

    A beforeDelete hook may no longer rebind ctx.input.id to another record; the rebind answers with HookTargetRebindError / ERR_HOOK_TARGET_REBIND, exactly as update() and both per-row paths already do. Route 3 stays excluded per #5574's recorded ruling.

    Rationale (three-axis): measured compatibility cost is zero — the card's own repo-wide grep found no consumer of the repoint. One rule ("a by-id target is immutable in before hooks") across both verbs beats two individually-correct rules an author must memorize. And "a hook silently redirects which row gets deleted" is a top-grade footgun for AI-written hooks even though #5272's implementation is internally correct — correctness of the mechanism does not justify the surface.

    Implementation notes: the four recorded landing points in the card body (ADR-0058 Amendment II.1 scope table, hook-target-rebind-errors.ts's NOT-covered section, the engine.ts re-read block comment, and the D4 §7 pin asserting the repoint is honoured) all move together; #5272's pin is re-judged deliberately, with a changeset since this removes a shipped capability. ⚠️ The ADR-0058 amendment makes this PR maintainer-merged only per standing discipline — no auto-merge, no agent queue entry.

    Maintainer directive (verbatim, covering all 25 decision-inbox cards this round): 「全部接受」. Recorded by PM session session_01LGRN2cSRfggfX9B2L83bQc. Veto window open — comment to overturn.


    Generated by Claude Code

  3. claude commented on Aug 10, 2026

    @claude
    Contributor

    Claim: PM loop wave 9 (seat #6019)
    Session: session_01MwoubC3jL271FYt9rGXwxb
    Branch: claude/issue-6752-retire-delete-byid-repoint
    Worktree: objectstack-6752
    Domain: domain:engine-core

    Race re-read: all comments re-read — the 2026-08-09 maintainer ruling (route 2) and the triage grading; no competing session ID. assignees measured EMPTY via REST immediately before assigning; pm:queue → pm:dispatched same write.

    A correction for the record, mine: this seat's patrol carried "#6752 awaiting ruling" all shift — the ruling has been on this card since 2026-08-09 05:40Z. The stale entry came from a shift-start prompt snapshot that was never re-verified against the card itself (lesson ㉒ cuts both ways: the do-not-touch list expires too). Dispatching now, one day late by this seat's own bookkeeping and zero days late by anyone else's.

    packages/objectql/src/engine.ts is free (last owner PR #7476 merged 8e177598d).


    Generated by Claude Code

  4. claude commented on Aug 10, 2026

    @claude
    Contributor

    ACCEPT — PR #7482 verified and flipped ready. ⛔ NOT queued, ⛔ NOT auto-merged: per Prime Directive #14 this amends ADR-0058, so the merge itself is the maintainer's act.

    Verified by reading the body and diff (25/26 green; the single red is ADR maintainer approval — the gate that is the maintainer's pending signature, by design):

    1. ✅ All four landing points moved together: the ADR-0058 scope table (the PR promotes the change to its own Amendment II.2), the hook-target-rebind-errors NOT-covered section, the engine.ts re-read block, and the D4 §7 pin — flipped to assert ERR_HOOK_TARGET_REBIND with path: 'by-id', ⛔ not deleted.
    2. ✅ The refusal is the update() twin verbatim: input.id !== id — same-id rewrite stays legal, pinned as a negative control. CLEARED column, update() twin and per-row D4 all untouched as controls.
    3. ✅ The compatibility grep was re-run AND corrected: the card's regex missed the (ctx.input as any).id = cast form every pin uses — six hits where the card saw one, all six are tests of this very family, so the measured consumer count is still zero, now resting on a grep that can actually see the pins. This is the second time today a card's own enumeration was falsified by the dev re-running it (lesson ⑫ working as designed).
    4. ✅ The honest framing is in the PR body: this removes a capability that worked — 单记录 delete 从不绑定 hookContext.previous —— 契约声明「for update/delete」,引擎只在 update 分支赋值;#5038 之后批量 delete 反而比单记录 delete 更完整 #5272's re-resolution was internally correct — and the body says so plainly, with the ruling's three axes as the reason it retires anyway. A reviewer will not mistake this for a bug fix.
    5. ✅ ADR-0087 trio complete (own semantic entry file + spec-changes.json + upgrade guide, both --check gates green); [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017 declaration filed by the seat.

    @maintainer — this one is yours to merge by hand whenever you've reviewed the Amendment II.2 text. Everything else is green and will stay ready-but-unqueued until then; I keep patrolling until it lands on origin/main.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions