Skip to content

知识库 object source 需要对账通道:事件管新鲜度,对账管正确性(批量写后索引会陈旧) #4672

Description

@os-zhuang

#4639 的裁决中拆出,unassigned。

2026-08-05 范围改判(维护者批准方案 C,见评论):本单收窄为「reap guard 去索引化」—— 知识插件注册 lifecycle reap guard,在行删除前按 id 去索引,零新增契约面、不碰 packages/objectql、不建对账子系统;应用层谓词写(updateMany/deleteMany)的部分诚实不覆盖(文档 + 既有 warn 说明),待真实 object 源出现再按 #4606 的 enforced-先行原则回看。原正文保留作背景。

Blocked-by: #5535(reap guard 需先获得交集组合语义,否则第二注册方静默顶掉 sys_file 字节回收)

背景

#4639 给谓词写(multi: trueIDataDriver.updateMany/deleteMany)加了诚实的聚合事件 data.records.updated / data.records.deleted,载荷是 { id, type, object, matched, userId?, timestamp }

这修好了 webhook 通路(新增 opt-in 的 bulk_update / bulk_delete 触发器),但修不好知识同步,而且这不是 #4639 的实现留下的缺口——是驱动契约的性质:

  • 知识索引是逐记录投影;
  • matched: 40 不指代任何一条记录,KnowledgeService.handleRecordUpsert / handleRecordDelete 都需要记录体或 id,适配器也不接受谓词。

所以谓词写之后,该 object 的知识索引会陈旧,且这条订阅链路无法自愈。#4639 的实现里已经让它出声(knowledge-service-plugin.tsdata.records.* 打 warn 指明索引可能陈旧),但出声不是修复。

现实影响

最主要的陈旧来源是 LifecycleService 的保留期回收(packages/objectql/src/lifecycle/lifecycle-service.tsreapWhere),它按谓词删记录。注意不能把它改成逐记录扇出:ADR-0057 §3.3 有硬规则——

the LifecycleService's own deletes/rotations are not audited or activity-logged (otherwise cleanup re-feeds the tables it is draining — the same self-audit trap ADR-0052 already guards). Aggregate one summary row at most.

逐记录扇出正是该规则禁止的形态(N 条事件重新灌回正在清理的表)。所以这条路封死,对账是唯一出路。

⚠️ 2026-08-05 核对:上句「对账是唯一出路」已被推翻 —— ADR-0057 amendment 的 reap guard 是被认可的 domain callback 形态,见核对评论。

处置方向(原文,历史背景)

给 object source 加周期性/可触发的对账:枚举索引条目,与源对象比对,删除源已不存在的条目(并补上索引缺失的行)。设计要点:

  • 触发方式:定时 + 收到 data.records.* 后标记该 object "需要对账"(避免全量轮询);
  • LifecycleService 的关系:对账是知识服务自己的职责,不要塞进 lifecycle sweep(ADR-0057 §3.3 "no bespoke sweepers" 的对偶——反过来也不该把别人的清理逻辑塞进 lifecycle);
  • 成本控制:分批、可中断,不能在大对象上把索引后端打满。

参考

Blocked-by: #5535

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions