Skip to content

fix(objectql): 同一 object 的多个 reap guard 按交集组合,后注册者不再静默顶掉前者 (#5535) - #5708

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5535-reap-guard-composition
Aug 6, 2026
Merged

fix(objectql): 同一 object 的多个 reap guard 按交集组合,后注册者不再静默顶掉前者 (#5535)#5708
os-zhuang merged 2 commits into
mainfrom
claude/issue-5535-reap-guard-composition

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5535

前提复核(先证后改)

issue 的两条前提在 origin/main(28ce594)上仍然成立,逐条核对过:

另外查证一点 issue 未展开的:ADR-0057 §3.3 及其 amendment 从未写过「一个 object 一个 guard / last wins」——全文搜不到这条规则。所以交集组合不是推翻已记录的决定(Prime Directive #13),是把 ADR 已经邀请的形态(「a guard is a domain callback」)在实现层补齐,ADR 正文无需修订。

为什么是交集,而不是 last-wins + warn + 探针

guard 的确认不是投票,是回执——「外部副作用已经做完,这行现在可以删了」。sys_file 的 guard 先 storage.delete(row.key) 再确认,而行是字节的唯一指针。单槽 last-wins 意味着第二个注册方把这段字节回收整体解除:行照删、字节泄漏、零日志,且新来者没有任何可读的东西能察觉。退一步的 last-wins + warn + 探针要求注册方遵守一条无法验证的约定,不取。

新契约:注册追加而非替换;只有全部 guard 确认的 id 才进删除集,任一 veto 即保留、下一轮 sweep 重试。与 registerRetentionFloor(#5195,每个注册方保留发言权、最严者胜)同构。

交集实现为"收窄流水线"而不是"各自表决再取交集",这是本 PR 唯一需要判断的语义细节:后一个 guard 只会被问到「前面的 guard 已确认」的行。理由还是回执语义——把 guard 1 已经 veto 的行递给 guard 2,会让 guard 2 为一行即将被保留的记录做不可逆清理(去索引化一行还活着的记录 → 它从搜索里静默消失,且没有任何东西会回来补索引)。删除集本身与注册顺序无关,这条有测试钉住。

guard 抛异常的处置:不变,对齐现状。异常上抛到 sweep() 的 per-object handler,report.errors 记一条,该 object 本轮一行不删。同批里更早的 guard 已做掉的清理由下一轮重试,而不会在无人完成确认的批次上兑现成删除。批量上限(500 行/批、20 批/sweep)不变。

顺带:重复注册同一个函数引用是 no-op(重跑 wiring,不是第二份意见)。与自己求交集不改变结果,却会让它的外部清理每批跑两遍。

未加只读探针:交集组合之后「察觉不到自己顶掉了谁」这个问题本身消失了(没有东西被顶掉),测试也不需要探针即可观察全部语义,故不为探针而探针。

反向验证(方向是事前预测的:红)

registerReapGuard 临时改回单槽 set()(guardedReap 的组合逻辑保留),预测组合类测试转红、单 guard 回归保持绿。实测完全吻合——7 failed | 57 passed:

  • 五条单 guard 回归全部保持绿(语义逐条未变)。
  • 七条组合测试转红,其中最要命的一条正是 issue 描述的形态:keeps byte reclaim running when a second consumer registers 断言残留行 expected [] to deeply equal [ 'f2' ] —— last-wins 下三行全被删掉,包括字节回收失败(应当 veto)的 f2。行没了、字节还在,即 issue 说的「行照删、字节泄漏」。
  • 两条组合测试在 last-wins 下依然绿,如实记录:registering the identical guard twice(单槽天然只留一个)与 multiple guards on an engine without find are skipped(一个 guard 就足以触发 fail-safe 跳过)。它们钉的是别的东西,不是组合。

验证

  • pnpm --filter @objectstack/objectql testTest Files 121 passed (121) / Tests 1979 passed (1979)
  • 目标文件单跑 → Tests 64 passed (64)(9 条新增组合测试 + 5 条原有单 guard 回归原样通过)
  • pnpm --filter @objectstack/objectql typecheck → 干净
  • pnpm check:query-options-erasureratchet holds: 84 unswept non-test site(s), none new
  • node scripts/check-nul-bytes.mjs → OK;并对两个改动文件做了越过该 gate 盲区的控制字符自扫(grep -naP,无命中)
  • pnpm --filter @objectstack/service-storage testTests 283 passed (283),该包零改动——现役注册方行为不变的证据

文件面

严格落在约束内:packages/objectql/src/lifecycle/lifecycle-service.ts、同目录 .test.ts、一个 changeset。未碰 engine.ts(#5504 在飞)、未碰 packages/services/**、未碰 packages/specsys_file 形态的字节回收回归 pin 用同形 stub 写在 objectql 测试内,不 import services 包。

(构建过程中 packages/spec/authorable-surface.base.jsongen:schema 重锚到本 HEAD 并带进 #5682scoping 键——与本单无关,已 checkout 还原,未进提交。)

本单合并即解除 #4672(知识插件走 reap-guard 去索引化,方案 C)的 Blocked-by:它可以直接在 sys_file 上追加注册,不必担心顶掉字节回收。

🤖 Generated with Claude Code

https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx


Generated by Claude Code

…ns (#5535)

`registerReapGuard` was a single-slot `set()` documented as "one guard per
object (last registration wins)", over a `private` registry with no probe and
no warn on overwrite. A second registrant on one object therefore silently
unhooked the first, and could not have noticed.

That is unsafe on this seam specifically, because a guard's confirmation is
not a vote but a receipt: "external cleanup is done, this row may go now".
`sys_file`'s guard reclaims storage bytes before confirming, and the row is
the only pointer to those bytes — so displacing it means rows deleted, bytes
leaked, zero log lines. ADR-0057 §3.3 explicitly invites a second domain
callback (#4672's de-indexing is one), which is what turns this from theory
into the next merge.

Guards now compose by intersection: registration appends, and only ids every
guard confirmed are deleted; one veto keeps the row for the next sweep. Same
shape as `registerRetentionFloor` one policy over, where every registrar keeps
its say and the strictest wins.

The intersection is a narrowing pipeline, not N verdicts unioned at the end: a
guard is asked only about rows the guards before it confirmed, so it never
performs irreversible cleanup for a row another guard is about to keep. The
delete set does not depend on registration order. A throwing guard still
aborts the object's reap with nothing deleted. Re-registering the identical
function is a no-op.

Single-guard behaviour is unchanged — the five existing guard tests pass
untouched, and service-storage's two registrars need no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 2:07am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants