Skip to content

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

Closed

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

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 3, 2026
  2. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    裁决(PM 代决,维护者可否决):两个事实拆两步执行 ——

    1. 事实一(直接做):engine.ts 中 assertReferencesResolve 的 readonly 收窄保留,紧邻注释按 PD [WIP] Add Chinese version of the documentation #13 改写为陈述独立原则(非调用方提供的值不归调用方负责),'system' 哨兵只作历史引用——现在的注释描述一个已不存在的机制,会误导后人把收窄当可拆的临时补丁。
    2. 事实二(选 B):溯源族(created_by/updated_by/organization_id)纳入悬空引用巡检,单独 provenance 分组。理由:失明比噪音贵——这些是真 id、真会悬空(删用户/删组织),永久豁免的理由已随哨兵拆除而失效;单独分组恰好把「删过用户的库」的存量噪音与真正的业务发现隔离,与该文件「不同答案不混报」的既有设计立场一致。方案 C(同等对待)会淹没业务发现,方案 A(维持失明)违背 declared=enforced 的巡检承诺。

    可拆两个 PR,事实一纯文档性无风险先行。已入队待派。


    Generated by Claude Code

  3. 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 3, 2026
  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    排程记录(engine-core 车道 PM,会话 session_01V7WetGmnfoXNn8cLieKKmx):本轮不派,两条已知约束预记:(1) 事实一(注释过期)落 packages/objectql/src/engine.ts,与在飞 PR #5627(#5503,同文件写路径)同文件串行,须待其合并后基于新 main 核对;(2) 事实二(巡检跳过收窄)自带 A/B/C 三向抉择且影响巡检报告消费形状,issue 作者倾向 B —— 派发前 PM 将按升级门槛判定是否需维护者拍板,倾向按 B 派发并留否决窗(该巡检自身「不同答案不混桶」的设计立场支持 B)。两事实可拆两 PR,事实一为零风险文档改动。


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 6, 2026
  6. self-assigned this
    on Aug 6, 2026
  7. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮追加批(engine-core 车道,维护者指令并发提至 5)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-4743-provenance-audit-bucket
    Worktree:objectstack-issue-4743
    域:domain:engine-core
    范围:仅事实二(巡检跳过收窄)。文件面:packages/objectql/src/integrity/dangling-reference-audit.ts + 其测试、.changeset/*.md。⛔ 不碰 packages/objectql/src/engine.ts(#5504 在飞;事实一的注释校准留待后续随 engine.ts 改动顺带)。
    方向裁决(PM 拍板,维护者可否决):取 B —— readonly 引用字段纳入巡检但单独分桶(如 provenance),使「引用了被删用户」与「业务外键断裂」在报告中可区分。依据:该巡检自身的设计立场就是「不同的答案不混桶」(undetermined/unreadableObjects/truncatedObjects 皆为此而生);A(维持整体跳过)让巡检对审计溯源族永久失明且理由已退化为「量大、吵」;C(同桶)会淹没业务发现。


    Generated by Claude Code

  8. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 2 轮追加批):事实二交付于 PR #5719("Part of",本 issue 不随其关闭)。CI 绿后转 ready 入队。

    按裁决 B 落地且三处派生判断各有锚点:provenance / provenanceUndetermined 两个追加键(optional 的理由是消费方手写字面量替身的类型约束,非风格);provenance 单独不触发 summary warn(每次都响的告警正是 #4747 拆掉的坏形状 —— 计数随行、明细常驻);provenanceUndetermined 与 undetermined 分离保住既有断言原义;prioritise 加第三档保预算花在业务发现上。反向验证 8/9 红,唯一双绿用例是结构性断言且已把另一条「沉默用例」改造成必须「查过且解析成功」。消费半径六方逐一核过零改动。

    事实一(engine.ts 注释校准)仍开放:等 #5698(#5504)合并解锁 engine.ts 后,连同本轮新记的 engine.ts:2876 @link 文案漂移("readonly skip" → "readonly split")一并做 —— 已记入车道待办,将随下一个 engine.ts 位派发或顺带。顺带发现 #5718(预算耗尽后剩余对象无桶可记)已核实,等分诊定级。


    Generated by Claude Code

  9. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    越界发现,记在这里而不是另开孪生单 —— 它落在事实一(engine.ts 注释扫尾)的完成范围内,不是独立缺陷。来自 #5718 的实现过程(PR #5760,只动 dangling-reference-audit.ts,按约束一行未碰 engine.ts)。

    PR #5719 落地事实二之后,engine.ts 里第二处关于本巡检的注释也过期了 —— 不是 assertReferencesResolve 那处,是 inspectDanglingReferences 的文档块结尾(origin/main packages/objectql/src/engine.ts:2997):

    See {@link auditDanglingReferences} for the judgments (readonly skip, empty values, unknown ≠ absent) and the bounded-scan honesty of the report.

    readonly skip 已经不存在了:#5719 把 readonly 引用从「整体跳过」改成「照读、findings 进 provenance 桶」。下一个读到这行的人会以为跳过还在,而这正是事实一担心的那种误导(把一条现存判断读成一条已被删掉的判断,或反过来)。

    观察级,不是用户今天会踩到的东西 —— 单纯的注释漂移,行为由 #5719 的测试钉住。建议在做事实一的那一版里顺手扫掉这一行(同文件同主题,分开两个 PR 反而制造 engine.ts 的冲突面)。

    顺带一提,bounded-scan honesty of the report 那半句在 #5718 合入后会更准确:对象级预算耗尽现在有了自己的桶(unscannedObjects),不再是唯一没有对应桶的不完整路径。


    Generated by Claude Code

  10. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    排程记录(engine-core 车道新任 PM,会话 session_019Q7oc7ASjh8yxyS3Yz78We):事实一按前任排程搭车派出 —— 随 #5284 的派发(分支 claude/issue-5284-per-object-prior-gate,同文件 rider)一并执行:assertReferencesResolve 紧邻注释按 2026-08-03 裁决改写、~2876 @link 文案漂移、以及 2026-08-06 04:06Z 追记的 ~2997 inspectDanglingReferences docblock「readonly skip」漂移。该 PR 将以 Closes #4743 收尾本单(事实二已由 PR #5719 交付)。


    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