feat(spec)!: 媒体字段 accept/maxSize 声明并强制 + 存储形态收窄为引用 — ADR-0104 D3 wave 2 (PR-5a) - #3555
Merged
Conversation
… a reference — ADR-0104 D3 wave 2 (PR-5a)
accept and maxSize are now declared on FieldSchema, and enforced on the server.
Both were already READ by the upload widgets — field.accept, field.maxSize —
while the spec did not declare them, so an author who wrote them had the keys
silently stripped at parse and the constraint simply never existed. That is
exactly the ADR-0104 failure class this ADR exists to remove: a declaration
accepted in source, dropped from the contract, with no feedback anywhere.
Now that the platform owns the file, sys_file carries the authoritative MIME
type and byte size, so a record write is re-checked where the constraint
actually binds rather than only in the browser — a client-side check is a
convenience, not a control, since any caller talking to the API directly
bypasses it. Violations raise FileConstraintError and fail the write. The check
rides the pass that already loads each referenced sys_file row, so it costs no
extra read, and an entry is only judged against metadata the file actually
reports: no recorded MIME type cannot fail an accept test, no recorded size
cannot fail maxSize. "We don't know" must not become "not permitted" — a test
caught that distinction being got wrong.
The stored form of a media field narrows to an opaque sys_file id.
valueSchemaFor(field, 'stored') now yields an id for the whole media family;
the inline {url, name, size, …} blob becomes the 'expanded' read form, which
also still admits an unresolved id (storage service absent, file not committed)
exactly as an unexpanded lookup id stays valid. Two legacy forms stop
conforming, both deliberately: the inline blob, which is no longer stored but
derived; and an external URL, which was never a managed file — R7 retires those
toward an explicit `url` field, and under AI authoring that IS the point, since
it stops "managed file" and "external link" being the same declaration.
Not a breaking change today. Value-shape checking is warn-first (R1/R2): a
not-yet-backfilled row still writes and the author gets a warning naming the
field. Hard rejection arrives only under OS_DATA_VALUE_SHAPE_STRICT_ENABLED,
which a deployment opts into after running the backfill and confirming
reconciliation. The `!` marks the contract change for the v17 window, not a
runtime break on upgrade.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The spec-property liveness gate (ADR-0049, declared != enforced) flagged both new properties as UNCLASSIFIED — which is the gate doing exactly its job, since the whole reason these two are being added is that they were read by widgets while nothing in the contract declared or enforced them. Both are registered live, with the server-side enforcement site as evidence rather than the widget that merely offers them to the file picker: a client-side check is bypassed by any caller talking to the API directly, so it is not what makes the property live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
os-zhuang
marked this pull request as ready for review
July 27, 2026 04:50
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.
wave 2 的写切换。配对的客户端采纳 objectui#2828 已合并。
1.
accept/maxSize:声明并强制上传 widget 一直在读
field.accept和field.maxSize,而FieldSchema从未声明它们——所以作者写了这两个键,会在 parse 时被.strip静默丢掉,约束根本不存在,而且没有任何反馈。这正是 ADR-0104 要消灭的那类失败:声明在源码里被接受、在契约里被丢弃、全程无提示。
现在平台拥有文件了,
sys_file带着权威的 MIME 类型和字节大小,所以记录写入会在约束真正生效的地方复核,而不只是在浏览器里。客户端检查是便利,不是控制——任何直接调 API 的调用方都能绕过。违反时抛FileConstraintError,写入失败。两个实现细节:
sys_file行的那一趟上,不增加额外读取。accept,没记录 size 的不可能失败maxSize。"我们不知道"绝不能变成"不允许"——这个区分我第一版写错了,被测试抓了出来。2. 存储形态收窄为
sys_fileidvalueSchemaFor(field, 'stored')对整个媒体家族现在给出 id;inline{url, name, size, …}blob 变成'expanded'读形态,并且展开形态同时仍接受未解析的 id(存储服务缺席、文件未 committed)——和未展开的 lookup id 依然有效完全同理。两种遗留形态因此不再符合契约,都是故意的:
url字段——在 AI 写元数据的语境下这正是重点:让"受管文件"和"外部链接"不再是同一个声明值形态检查是 warn-first(ADR-0104 R1/R2):还没回填的行照样写得进去,作者拿到的是一条指名字段的告警。硬拒绝只在部署方主动打开
OS_DATA_VALUE_SHAPE_STRICT_ENABLED时才发生——而那应该在跑完回填(#3535)并用verifyFileReferences()(#3534)确认对账之后再做。标题里的
!标的是为 v17 窗口而言的契约变更,不是升级即刻的运行时破坏。测试
field-value.test.ts:重写媒体契约断言——存储形态接受 id 与 uuid、拒绝 inline blob / 外部 URL / resolver URL /data:URI;展开形态接受解析后的对象和未解析的 id;wave-1 的"对象必须有 url"收紧迁到展开形态。record-validator.test.ts:符合契约的值改为 id;新增一条锁定迁移路径的用例——遗留 inline blob 在默认模式下仍可写入,只有 strict 才拒绝。file-reference-lifecycle.test.ts+10:超 maxSize 拒绝(且未被认领)、MIME 不在 accept 内拒绝、两者都满足则通过、6 组 accept 匹配矩阵(精确 MIME /image/*/.ext/.ext不匹配 / MIME 不匹配 /*/*)、元数据缺失不判违规、未声明约束的字段不受影响。wave 2 剩余
只剩 PR-5b 开启回收(🔒 gated、不可逆),前置条件按 #3459:回填 +
verifyFileReferences在真实租户数据上连续 ≥7 天零阻断项,且放松护栏与扩展 reap guard 复核必须同一个 change。🤖 Generated with Claude Code
https://claude.ai/code/session_01SHpGw3GBA9aFpfwVArRWfd
Generated by Claude Code