把 ADR-0104 D3 wave 2 收尾时暴露出来的同一类问题合并成一条。核心是一个设计前提错了,而它同时卡住了两件事:file-as-reference 的回收(#3459 PR-5b)和 D1/D2 的 strict 默认值翻转(#3438)。
一、错在哪
wave 2 给回收定的验收门是这样的:
在真实租户上跑回填 → verifyFileReferences 连续 ≥7 天零阻断项 → 才允许开启回收。
这是一份 SaaS 形态的运维 runbook,隐含前提是"我们运营那个租户、我们能观察它、我们来翻开关"。
但 ObjectStack 是开发平台。 第三方开发者在上面开发元数据应用,版本交付出去之后,每个部署自己决定何时升级;他们的数据我们看不到,也不该看到。根本不存在一个"我们的观察窗口"可以让所有人等。
二、正确的结论:证据要求不能取消,只能换地方
R4 的本质没变——一本账在被证明与现实一致之前,不能授权删除字节。改变的是产生证据的主体:从"我们"变成"每个部署自己"。
所以需要的不是一段一次性脚本(跑完可以无视输出,等于没有门禁),而是:
一个带自检门禁的数据迁移步骤——回收开关由「这个部署自己跑过并通过了」来打开,而不是由版本号打开。
形状
os migrate files-to-references
1. 回填(默认 dry-run,--apply 才写)
2. verifyFileReferences
3. 仅当零阻断项 → 写入部署级标记
{ id: 'adr-0104-file-references', verified_at, blocking: 0 }
4. reap guard / strict 开关读这个标记,而不是读全局 flag
由此得到的性质
- 升级到新版本不会自动打开回收。 装了新版 ≠ 开始删字节。
- 第三方开发者不需要理解 R4。跑
os migrate,绿了就完事;红了报告直接指出哪条记录持有一个没人拥有的文件。
- 没跑 / 没通过 → 文件继续永生。 浪费存储,零数据丢失——"fail toward retention" 这条默认值在部署维度同样成立。
- 叠加已有的 30 天墓碑窗口,每个部署有自己独立的 30 天回头路,不需要共享任何人的观察期。真正不可逆的时刻是各自开启后的第 30 天,不是升级那天。
三、⚠️ 同一个错误在 #3438 也埋着
D1/D2 的 strict 默认值翻转有一模一样的问题。
我们不能替所有第三方部署决定「从现在起拒绝遗留 inline blob」。一个部署如果还没回填完,strict 一开,任何改写 media 字段的 update 都会被拒——正常工作的应用会挂,而且是我们替他们做的决定。
所以 #3438 也该走同一个机制:这个部署回填完、对账干净了,strict 才对它生效,而不是某个版本号一到就全体翻转。
两件事应当共用同一个「本部署已完成 file-as-reference 迁移」标记。 这也是把它们合并成一条 issue 的原因——分开做会做出两套不兼容的门禁。
四、现实的缺口(真正的工作量在这)
查过当前 main(d2a8695):
| 现状 |
结论 |
os migrate = plan.ts / apply.ts / meta.ts,围绕 ChangeSet |
纯 DDL(加字段/改字段/建对象/执行 SQL) |
packages/spec/src/system/migration.zod.ts |
只有 schema 操作类型,没有「带校验门禁的数据迁移」概念 |
| 运行时迁移状态 |
不存在(sys_migration 只有 spec 层类型,没有运行时表) |
backfillFileReferences / verifyFileReferences |
已实现(#3535 / #3534),但只是库函数,没有命令行入口,运维跑不了 |
五、工作项与顺序
| # |
事项 |
规模 |
备注 |
| 1 |
os migrate files-to-references 命令(包装两个已有库函数,dry-run 只读) |
小 |
建议先做:零风险,第三方下个版本就能跑,立刻拿到真实数据规模和外部 URL 清单 |
| 2 |
数据迁移 + 门禁基础设施(部署级标记的持久化与读取) |
比 PR-5b 本身大 |
#3438 也要用 |
| 3 |
PR-5b 回收:改读标记而非全局 flag |
小 |
两半必须同一 change,见 #3459 |
| 4 |
#3438 strict 翻转:改读同一个标记 |
小 |
|
之前 #3459 里把讨论集中在第 3 项,顺序是反的——真正卡住第三方的是第 1、2 项。
六、有一类东西自动化不了,但不阻断
回填里的外部 URL(https://cdn…)要不要迁到显式的 url 字段,是建模决定,只能由应用作者做。好在它们天然不阻断:外部 URL 根本不是 sys_file,进不了 GC。所以报告里单列为 advisory 即可(PR-6 已经是这么设计的:external_url_skipped 是 advisory 不是 blocking),不卡住回收门禁。
相关
🤖 Generated with Claude Code
把 ADR-0104 D3 wave 2 收尾时暴露出来的同一类问题合并成一条。核心是一个设计前提错了,而它同时卡住了两件事:file-as-reference 的回收(#3459 PR-5b)和 D1/D2 的 strict 默认值翻转(#3438)。
一、错在哪
wave 2 给回收定的验收门是这样的:
这是一份 SaaS 形态的运维 runbook,隐含前提是"我们运营那个租户、我们能观察它、我们来翻开关"。
但 ObjectStack 是开发平台。 第三方开发者在上面开发元数据应用,版本交付出去之后,每个部署自己决定何时升级;他们的数据我们看不到,也不该看到。根本不存在一个"我们的观察窗口"可以让所有人等。
二、正确的结论:证据要求不能取消,只能换地方
R4 的本质没变——一本账在被证明与现实一致之前,不能授权删除字节。改变的是产生证据的主体:从"我们"变成"每个部署自己"。
所以需要的不是一段一次性脚本(跑完可以无视输出,等于没有门禁),而是:
形状
由此得到的性质
os migrate,绿了就完事;红了报告直接指出哪条记录持有一个没人拥有的文件。三、⚠️ 同一个错误在 #3438 也埋着
D1/D2 的 strict 默认值翻转有一模一样的问题。
我们不能替所有第三方部署决定「从现在起拒绝遗留 inline blob」。一个部署如果还没回填完,strict 一开,任何改写 media 字段的 update 都会被拒——正常工作的应用会挂,而且是我们替他们做的决定。
所以 #3438 也该走同一个机制:这个部署回填完、对账干净了,strict 才对它生效,而不是某个版本号一到就全体翻转。
两件事应当共用同一个「本部署已完成 file-as-reference 迁移」标记。 这也是把它们合并成一条 issue 的原因——分开做会做出两套不兼容的门禁。
四、现实的缺口(真正的工作量在这)
查过当前 main(
d2a8695):os migrate=plan.ts/apply.ts/meta.ts,围绕ChangeSetpackages/spec/src/system/migration.zod.tssys_migration只有 spec 层类型,没有运行时表)backfillFileReferences/verifyFileReferences五、工作项与顺序
os migrate files-to-references命令(包装两个已有库函数,dry-run 只读)之前 #3459 里把讨论集中在第 3 项,顺序是反的——真正卡住第三方的是第 1、2 项。
六、有一类东西自动化不了,但不阻断
回填里的外部 URL(
https://cdn…)要不要迁到显式的url字段,是建模决定,只能由应用作者做。好在它们天然不阻断:外部 URL 根本不是sys_file,进不了 GC。所以报告里单列为 advisory 即可(PR-6 已经是这么设计的:external_url_skipped是 advisory 不是 blocking),不卡住回收门禁。相关
🤖 Generated with Claude Code