观察类发现,在做 #6011 台账半边(PR #6138)的反向验证时测出来的。不影响任何用户今天能碰到的行为,故只打 finding、不入 pm:queue,交 PM 分诊定级。
事实
ADR-0087 台账(packages/spec/src/migrations/registry.ts)有两道门禁:
check:spec-changes —— 比对 packages/spec/spec-changes.json
check:upgrade-guide —— 比对 docs/protocol-upgrade-guide.md
两道钉的都是台账 ↔ 生成物同步,即「registry 改了但没重生生成物」。实测方向(PR #6138 的反向验证,事前判定为红、结果一致):撤掉一条 registry 条目、生成物保持原样,两道立刻 exit 1 并报 stale。
但没有任何门禁钉「一次已经发生的退役必须有台账条目」。 因为生成物是 registry 的纯投影,当条目从一开始就不存在时,registry 与生成物是互相一致的 —— 两道门禁全绿,全仓其余门禁也全绿。
PR #6048(2026-08-07 合并)删除了 ActorUser 上的 roles 别名(ctx.user.roles / req.user.roles),退役已经发生。而 ADR-0087 台账里这一面一条都没有,同一次 ADR-0090 改名的另外三张面却都在册(data.hookContext.session.roles / ui.actionSession.roles / CEL/formula: current_user.roles)。
这个缺口存在期间 CI 全绿。它是分诊座位人工比对发现并立单的(#6011 正文那张对照表),不是任何门禁报出来的;补登记又要另派一轮(PR #6138)。也就是说:目前「退役有没有进台账」这件事,唯一的检测手段是人眼。
为什么值得记一笔
后果不是运行时错误,而是升级路径上的静默缺失:台账条目是 objectstack migrate meta / spec-changes.json / 生成的升级指南三条渠道的唯一数据源。对于本来就没有 spec schema 的面(如 ctx.user,只有 runtime TS interface),既没有 retiredKey() tombstone 也没有 schema 拒绝,台账条目就是唯一的告知渠道 —— 漏登记等于该退役对升级者完全不可见,而 CI 不会有任何声音。
明确不主张具体做法
怎么补(或要不要补)是个设计问题,不是本条要裁的:候选思路各有代价 —— 例如把 changeset 的 major/breaking 与台账条目做交叉核对、或给 retiredKey() / 退役 PR 加一道清单式提示 —— 都要先回答「什么算一次需要登记的退役」,而跨包退役(本次退役发生在 packages/runtime,台账在 packages/spec)恰恰是最难自动判定的一类。按 startup focus,先把事实记在案,由维护者判断是否值得建这道门禁。
发现于 PR #6138(#6011 台账半边),该 PR 正文也如实写明了这一点、并刻意没有顺手新造门禁。
Generated by Claude Code
观察类发现,在做 #6011 台账半边(PR #6138)的反向验证时测出来的。不影响任何用户今天能碰到的行为,故只打
finding、不入pm:queue,交 PM 分诊定级。事实
ADR-0087 台账(
packages/spec/src/migrations/registry.ts)有两道门禁:check:spec-changes—— 比对packages/spec/spec-changes.jsoncheck:upgrade-guide—— 比对docs/protocol-upgrade-guide.md两道钉的都是台账 ↔ 生成物同步,即「registry 改了但没重生生成物」。实测方向(PR #6138 的反向验证,事前判定为红、结果一致):撤掉一条 registry 条目、生成物保持原样,两道立刻 exit 1 并报 stale。
但没有任何门禁钉「一次已经发生的退役必须有台账条目」。 因为生成物是 registry 的纯投影,当条目从一开始就不存在时,registry 与生成物是互相一致的 —— 两道门禁全绿,全仓其余门禁也全绿。
实证:#6011 就是这个形状
PR #6048(2026-08-07 合并)删除了
ActorUser上的roles别名(ctx.user.roles/req.user.roles),退役已经发生。而 ADR-0087 台账里这一面一条都没有,同一次 ADR-0090 改名的另外三张面却都在册(data.hookContext.session.roles/ui.actionSession.roles/CEL/formula: current_user.roles)。这个缺口存在期间 CI 全绿。它是分诊座位人工比对发现并立单的(#6011 正文那张对照表),不是任何门禁报出来的;补登记又要另派一轮(PR #6138)。也就是说:目前「退役有没有进台账」这件事,唯一的检测手段是人眼。
为什么值得记一笔
后果不是运行时错误,而是升级路径上的静默缺失:台账条目是
objectstack migrate meta/spec-changes.json/ 生成的升级指南三条渠道的唯一数据源。对于本来就没有 spec schema 的面(如ctx.user,只有 runtime TS interface),既没有retiredKey()tombstone 也没有 schema 拒绝,台账条目就是唯一的告知渠道 —— 漏登记等于该退役对升级者完全不可见,而 CI 不会有任何声音。明确不主张具体做法
怎么补(或要不要补)是个设计问题,不是本条要裁的:候选思路各有代价 —— 例如把 changeset 的 major/breaking 与台账条目做交叉核对、或给
retiredKey()/ 退役 PR 加一道清单式提示 —— 都要先回答「什么算一次需要登记的退役」,而跨包退役(本次退役发生在packages/runtime,台账在packages/spec)恰恰是最难自动判定的一类。按 startup focus,先把事实记在案,由维护者判断是否值得建这道门禁。发现于 PR #6138(#6011 台账半边),该 PR 正文也如实写明了这一点、并刻意没有顺手新造门禁。
Generated by Claude Code