Skip to content

哨兵拆掉之后:#4441 的 readonly 收窄与 #4551 的巡检跳过还需要吗?——注释已过期,跳过范围值得收窄 #4743

Description

@os-zhuang

发现自 #4556 的实现过程(PR #4742)。未认领。

#4556 把 sys_metadata_history.recorded_by 的哨兵字符串 'system' 改成了 NULL。那个哨兵正是 #4441 的写入面收窄和 #4551 的巡检跳过在文档里点名的动机样例。哨兵没了,两处豁免的处境不再相同,需要分别看。

PR #4742 按 PM 约束一行未动这两处,只把判断留在这里。

事实一:#4441 的收窄该保留,但注释已经过期并且会误导

packages/objectql/src/engine.ts(assertReferencesResolve 里的 if (fields[name]?.readonly === true) continue;)。

收窄本身应当保留:它的成立理由是「只有调用方提供的值才由调用方负责」——非系统调用方写 readonly 字段的值在写入前已被 stripReadonlyFields / stripReadonlyForInsert 剥掉,留下的必然是平台自己写的,本就在这个检查的自述范围之外。这个论证独立于 recorded_by。

问题在紧挨着它的那段注释,现在描述的是一个已经不存在的状态:

Found by the dogfood gate rather than by reasoning: sys_metadata_history.recorded_by is Field.lookup('sys_user', { readonly: true }) that the metadata repository fills with actor ?? 'system' — a SENTINEL STRING, not a user id … The sentinel-in-a-lookup is a real modelling wart and is filed separately

actor ?? 'system' 已经不存在了。下一个读到这段的人会得到一个错误印象:这条收窄是为了绕开一个 bug 的临时补丁,那个 bug 既然修好了,收窄就可以拆。而拆掉它会重新拒绝掉平台自己的写入。按 AGENTS.md Prime Directive #13 的推论(「实现某个决定时把它的 id 留在代码里」),这里要的是把注释改成陈述那条独立成立的原则,并把哨兵作为历史而非现状来写。

事实二:#4551 的巡检跳过值得重新评估,这才是真问题

packages/objectql/src/integrity/dangling-reference-audit.ts:所有 readonly 引用字段整体跳过。

它自己的文档写明了跳过的两条理由,其中一条现在作废了:

  • readonly reference fields are skipped … including the audit-provenance family (created_by / updated_by / organization_id, all readonly: true from applySystemFields) and the recorded_by sentinel above.

剔掉 recorded_by 之后,被豁免的 readonly 引用字段就只剩审计溯源那一族。而它们是货真价实的 id,并且真的会悬空:删掉一个用户,他创建过的每一行 created_by 就指向一个不存在的行;删掉一个组织同理。这恰恰是一个悬空引用巡检最该报告的一类——「谁创建的」解析不出来,正是审计场景关心的。

也就是说:#4556 消除了「平台会往 readonly 引用列写非 id」这一整类情形,而那是「整体跳过」当初唯一超出「调用方不负责」之外的额外理由。

为什么需要维护者决策,不适合顺手改

收窄这个跳过影响面不小:任何删过用户的库,一次巡检会瞬间亮起大片 created_by / updated_by 悬空。这不是 bug 被发现,而是一个既有的、可能被接受的状态被第一次报出来。需要先定:

  • A. 维持现状(继续整体跳过 readonly)。成本:巡检对审计溯源族永久失明,而这一族现在是它唯一还在豁免的东西——豁免的理由已经从「平台会写非 id」退化成「量大、吵」,那是一个应当明说的取舍,不是一条技术判断。
  • B. 纳入巡检,但单独分类(例如报到一个 provenance 分组或 undetermined 之外的新桶里),让「用户被删过」和「业务外键断了」在报告里可区分。表达力最好,改动最大。
  • C. 纳入巡检,与普通引用同等对待。最简单,但一次报出的量可能淹没真正的业务发现,反而降低巡检的可信度——这正是 dangling-reference-audit.ts 自己在「Unknown and absent are DIFFERENT answers」一节里最在意的失效模式。

倾向 B:这个巡检的既有设计通篇在强调「不同的答案不要混成一个」(undetermined / unreadableObjects / truncatedObjects 都是为此而生),把「引用的是一个被删掉的用户」和「引用的是一个从未存在的业务记录」混在一起,与它自身的设计立场矛盾。但 B 的成本落在报告结构上,且会改变消费者读到的形状,所以由维护者定。

边界

Refs #4441(PR #4511)、#4551(PR #4555)、#4556(PR #4742)。

Activity

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