Skip to content

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

Closed

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. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  2. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  3. 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

  4. 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

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 9, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 10, 2026
  7. 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

  8. 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 · seat 1 — ⏳ vacant #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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions