feat(migrate): 平台形态的迁移门禁 —— 带自检的数据迁移 + 部署级标记 (#3617)#3638
Merged
Conversation
…ot per release (#3617) The ADR-0104 D3 wave 2 acceptance gate was written as an operations runbook — run the backfill on the tenant, watch `verifyFileReferences` stay clean for seven days, then flip the switch. Every clause assumes we run the tenant, we observe it, and we flip. ObjectStack is a development platform. Third-party deployments upgrade on their own schedule and their data is not visible to us, so there is no observation window of ours that anyone else's data can wait inside. A gate phrased as one is not strict, merely unlocatable. R4's substance is unchanged — a ledger may not be given the power to delete bytes until it has been shown to agree with reality. What changes is who produces the evidence: each deployment, for itself. os migrate files-to-references # dry run: reports, writes nothing os migrate files-to-references --apply # converts, verifies, records the flag An apply run whose reconciliation reports zero blocking discrepancies records `sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }`. That row — never the platform version — is what may later open released-file collection (#3459 PR-5b) and strict media value-shape enforcement (#3438); both read the same flag through one spec-level predicate so the two gates cannot disagree about the same fact. Not run, or not passed, leaves files retained forever: storage cost, zero data loss. A later failing run clears the verified state, so a database that has drifted closes its own gate. A dry run writes nothing at all — not the conversions, and not the flag either, even when the self-check would pass — so whether a run changed the deployment's posture never depends on what it happened to find. The command also refuses to run when no app metadata is loaded: an empty scan is indistinguishable from a clean one, and this verdict authorises irreversible behaviour. Also fixes a defect the end-to-end run surfaced: on a live kernel the engine's read resolver expanded stored ids to their `{ id, url, … }` form before the scan saw them, so the reconciliation reported every held reference as absent — noisy `stale_owner` findings, and a missed `unowned_reference` would have been a false pass of the very gate this work builds. Reads may now opt out via the spec's `RAW_FILE_VALUES_CONTEXT_KEY`; the storage service's scan and bookkeeping reads do. Verified end-to-end against the showcase app on SQLite: 10 legacy values converted and claimed, reconciliation clean, flag recorded; a dry run against the same database left both the data and the flag untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 115 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…Flag The generated `content/docs/references/system/migration.mdx` is derived from `migration.zod.ts` and CI checks the two agree — adding the schema without regenerating left them out of sync. The page's intro is the file's first JSDoc block, and `migration.zod.ts` had none, so the regenerate lifted the `DATA_MIGRATION_FLAG_OBJECT` constant's TSDoc into that slot. Given the module now carries both kinds of migration, a real file-level docblock is what the page wants anyway: it says why the two are separate — a ChangeSet reshapes the database from metadata, while whether a data migration has run is a fact about one deployment's rows that no release can assert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma
The snapshot gate diffs the built export surface against the committed file so a removed or renamed export cannot silently break a pinned consumer. This change adds six exports and removes none: RAW_FILE_VALUES_CONTEXT_KEY, DATA_MIGRATION_FLAG_OBJECT, FILE_REFERENCES_MIGRATION_ID, DataMigrationFlagSchema, DataMigrationFlag, isDataMigrationFlagVerified Ran every gate in the type-check job locally this time — the spec's ten check:* scripts, the example-app and downstream-contract typechecks — rather than fixing them one CI round at a time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma
os-zhuang
marked this pull request as ready for review
July 27, 2026 13:13
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3617 的第 1、2 项(命令 + 门禁基础设施)。第 3、4 项(#3459 PR-5b 回收、#3438 strict 翻转)改读本 PR 落的标记,留给各自的 change。
错在哪,改了什么
wave 2 给回收定的验收门是「在真实租户上跑回填 →
verifyFileReferences连续 ≥7 天零阻断项」。这句话的每个成分都假设我们运营那个租户、我们观察它、我们翻开关。但 ObjectStack 是开发平台:第三方部署自己决定何时升级,数据我们看不到,也不该看到。不存在一个我们的观察窗口能让别人的数据在里面等——这样表述的门禁不是严格,而是无处可满足。R4 的实质没变:一本账在被证明与现实一致之前,不能被授予删除字节的权力。变的是证据由谁产生:从我们一次,变成每个部署为自己产生。
apply 且对账零阻断项 → 写入
sys_migration { id: 'adr-0104-file-references', verified_at, blocking: 0 }。由此得到的性质
verified_at被清空——数据漂移的部署自己关掉门禁。--apply是唯一的写入模式,所以「这次运行有没有改变我的部署姿态」永远不取决于它碰巧发现了什么。isDataMigrationFlagVerified谓词读它,两个门禁不可能对同一个事实产生分歧。sys_file,进不了 GC;要不要改建模成url字段是应用作者的决定(R7),不卡门禁。顺带修掉的一个缺陷
端到端跑的时候暴露的:活体 kernel 上,engine 的读解析器会在扫描看到之前就把存储的 id 展开成
{ id, url, … },于是对账把每一条持有中的引用都报告为不存在——制造一堆stale_owner噪音,而且一条被漏掉的unowned_reference会让这个门禁假通过,正是本 PR 要建立的那个门禁。现在读路径可以通过 spec 的RAW_FILE_VALUES_CONTEXT_KEY退出解析,storage 服务的扫描与记账读取都带上了它。改了哪些包
specDataMigrationFlagSchema、FILE_REFERENCES_MIGRATION_ID、isDataMigrationFlagVerified(两个消费方共用的唯一谓词)、RAW_FILE_VALUES_CONTEXT_KEYplatform-objectssys_migration对象 +readDataMigrationFlag/isDataMigrationVerified/recordDataMigrationRun。读取一律倒向"未验证":读不到证据的门禁必须保持关闭service-storagerunFilesToReferencesMigration(回填 → 对账 → 记标记);注册sys_migrationobjectqlclios migrate files-to-references验证
migration-flag.test.ts(9 例,含"读失败必须关门"、"失败运行清空 verified_at")、files-to-references-migration.test.ts(7 例,含"dry-run 什么都不写"、"截断的扫描即使零阻断项也不通过"、"回退后重新关门")、engine 的 raw-marker 回归例。pnpm test绿(132/132 任务)。sys_file.ref_*正确写入,对账干净,标记落库;对同一个数据库跑 dry-run 则数据与标记都原样未动。文档
content/docs/deployment/cli.mdx新增「Data migrations」一节。🤖 Generated with Claude Code
https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma
Generated by Claude Code