Skip to content

feat(storage): 遗留文件值回填 — ADR-0104 D3 wave 2 (PR-6) - #3535

Merged
os-zhuang merged 3 commits into
mainfrom
d3w2/backfill
Jul 27, 2026
Merged

feat(storage): 遗留文件值回填 — ADR-0104 D3 wave 2 (PR-6)#3535
os-zhuang merged 3 commits into
mainfrom
d3w2/backfill

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

承接 #3527(PR-3 独占所有权)。把 cutover 之前存在字段里的遗留形态,转换成引用形态。

转换什么、不转换什么

file/image/avatar/video/audio 字段在 cutover 前可能持有 inline 元数据 blob({url, name, size, …})或裸 URL 字符串。backfillFileReferences() 把它们转成引用形态——一个由该记录字段拥有的不透明 sys_file id。

遇到的值 处理 是否搬字节
指向本平台 resolver 的 URL(…/storage/files/:id 它本来就已经指明了一个 sys_file,字段改写为裸 id ❌ 不搬
data: URI 字节就在值里,上传 → 建 sys_file → 字段改写为新 id
外部 URLhttps://cdn.example.com/… 只报告,绝不转换

为什么外部 URL 不转换:那意味着去抓取第三方内容并悄悄地重新托管它——这是带宽、许可和隐私上的决定,不是一次迁移该替用户做的。ADR-0104 R7 的方向是把它们迁到显式的 url 字段,而在 AI 写元数据的语境下这正是重点:它让"受管文件"和"外部链接"不再是同一个声明,AI 就没法把两者搞混。

安全性质

  • 默认 dry run:不加 apply 什么都不写,而且 dry-run 报告和实际执行的报告同构,所以计划可以先审、事后可以对差异。
  • 幂等:已经是引用形态的值记为 already_id 并原样保留,所以跑到一半中断可以安全重跑。
  • 绝不自己写 ref_*:它只改写记录,由 PR-3 的 claim hook 观察到这次写入并记录所有权。只有一条认领路径,也就不存在两条路径互相矛盾的可能——有一条测试专门盯着这点,一旦回填开始直接碰 sys_file 就会失败。

测试(11 项)

本地 resolver URL 改写(裸串 + 包在 inline blob 里两种)、data: URI 上传并正确解出 mime/size/name、外部 URL 既不改也不上传、multiple:true 逐元素处理(三种值混在一个数组里)、dry run 零写入、幂等(第二次 converted=0)、无存储服务时 data: 值保持原样、无 file 字段的对象跳过、分页 500 条 + 截断标记,以及上面那条"绝不写 ref_*"的断言。

全量:service-storage 136 ✅ · nul-bytes ✅ · lint ✅

位置

按 ADR 2026-07-27 addendum 的重新排序,回填必须先于开启回收:一本账在被回填并对账证明与现实一致之前,不该被授予对不可逆删除的权力。跑完这个之后应当运行 verifyFileReferences()#3534)确认两边一致。

🤖 Generated with Claude Code

https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd


Generated by Claude Code

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack Ready Ready Preview, Comment Jul 27, 2026 5:13am
spec Building Building Preview, Comment Jul 27, 2026 5:13am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): packages/services.

6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/plugins/packages.mdx (via packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/services/service-storage/src/backfill-file-references.ts Fixed
claude added 3 commits July 27, 2026 03:12
backfillFileReferences() converts the pre-reference forms a file/image/avatar/
video/audio field may hold — an inline metadata blob ({url, name, size, …}) or
a bare URL string — into the reference form: an opaque sys_file id, owned by the
record's field.

What it will and will not convert:

  - A URL naming this platform's own resolver (…/storage/files/:id) already
    identifies a sys_file; the field is rewritten to the bare id, no bytes move.
  - A data: URI carries its bytes inline; they are uploaded, a sys_file is
    registered, and the field is rewritten to its id.
  - An external URL is REPORTED, NEVER CONVERTED. Re-hosting third-party
    content is a bandwidth, licensing and privacy decision that is not a
    migration's to make. ADR-0104 R7 retires these toward an explicit `url`
    field, which under AI authoring is the point: it stops "managed file" and
    "external link" being the same declaration.

Dry run by default — nothing is written unless apply is set, and the dry-run
report has the same shape as the applied one so the plan can be reviewed and
diffed against what actually happened. Idempotent — a value already in
reference form is recorded as already_id and left alone, so a partially
completed run is safe to repeat.

The backfill never writes the ref_* columns itself: it rewrites the record, and
the claim hooks observe that write and record ownership. One claiming path, so
there is nothing that can disagree with itself — asserted by a test that fails
if the backfill ever starts touching sys_file directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
The 2026-07-27 addendum sequenced the write cutover before the backfill. That is
wrong for the same reason the backfill must precede collection, one step further
back: narrowing the accepted stored form while records still hold legacy values
would reject any update that rewrites such a field, breaking working apps until
the backfill catches up. Backfilling first leaves the cutover nothing legacy to
reject.

Also states plainly why the last two steps are held to different standards. The
cutover breaks loudly and recoverably (a rejected write); enabling collection
breaks silently and permanently (deleted bytes). Only the second earns the
reconciliation evidence requirement, and neither it nor the migration run should
happen without an explicit human decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
…tracking regex

CodeQL js/polynomial-redos: the resolver-URL pattern was anchored only at the
tail, so the engine retried from every start position. The values it scans come
straight out of tenant records, so a value repeating the matched prefix turns
that into a polynomial blowup on attacker-influenced data.

Replaced with slicing and splitting — linear in the URL's length whatever it
contains, and clearer about what it actually accepts. Covered by a timing
regression on an adversarial value plus a table of accepted shapes and
near-misses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants