Skip to content

feat(spec)!: 媒体字段 accept/maxSize 声明并强制 + 存储形态收窄为引用 — ADR-0104 D3 wave 2 (PR-5a) - #3555

Merged
os-zhuang merged 2 commits into
mainfrom
d3w2/write-cutover
Jul 27, 2026
Merged

feat(spec)!: 媒体字段 accept/maxSize 声明并强制 + 存储形态收窄为引用 — ADR-0104 D3 wave 2 (PR-5a)#3555
os-zhuang merged 2 commits into
mainfrom
d3w2/write-cutover

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

wave 2 的写切换。配对的客户端采纳 objectui#2828 已合并

1. accept / maxSize:声明并强制

上传 widget 一直在读 field.acceptfield.maxSize,而 FieldSchema 从未声明它们——所以作者写了这两个键,会在 parse 时被 .strip 静默丢掉,约束根本不存在,而且没有任何反馈。

这正是 ADR-0104 要消灭的那类失败:声明在源码里被接受、在契约里被丢弃、全程无提示

现在平台拥有文件了,sys_file 带着权威的 MIME 类型和字节大小,所以记录写入会在约束真正生效的地方复核,而不只是在浏览器里。客户端检查是便利,不是控制——任何直接调 API 的调用方都能绕过。违反时抛 FileConstraintError,写入失败

两个实现细节:

  • 检查搭在已经在加载每个被引用 sys_file的那一趟上,不增加额外读取。
  • 只用文件真正报告的元数据来判定:没记录 MIME 的文件不可能失败 accept,没记录 size 的不可能失败 maxSize。"我们不知道"绝不能变成"不允许"——这个区分我第一版写错了,被测试抓了出来。

2. 存储形态收窄为 sys_file id

valueSchemaFor(field, 'stored') 对整个媒体家族现在给出 id;inline {url, name, size, …} blob 变成 'expanded' 读形态,并且展开形态同时仍接受未解析的 id(存储服务缺席、文件未 committed)——和未展开的 lookup id 依然有效完全同理。

两种遗留形态因此不再符合契约,都是故意的:

形态 为什么不再符合
inline blob 它不再是存储的东西,而是派生出来的读形态
外部 URL 它从来就不是受管文件。R7 把它导向显式的 url 字段——在 AI 写元数据的语境下这正是重点:让"受管文件"和"外部链接"不再是同一个声明

⚠️ 今天不是破坏性变更

值形态检查是 warn-first(ADR-0104 R1/R2):还没回填的行照样写得进去,作者拿到的是一条指名字段的告警。硬拒绝只在部署方主动打开 OS_DATA_VALUE_SHAPE_STRICT_ENABLED 时才发生——而那应该在跑完回填(#3535)并用 verifyFileReferences()(#3534)确认对账之后再做。

标题里的 ! 标的是为 v17 窗口而言的契约变更,不是升级即刻的运行时破坏。

测试

  • spec 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 不匹配 / */*)、元数据缺失不判违规、未声明约束的字段不受影响。
  • field-zoo 矩阵:5 个媒体类型的写入值改为 id,并注明"这些 id 匹配不到任何 sys_file 行,正是要验证无可展开时原样返回"。
  • 全量:spec 6842 ✅ · objectql 1082 ✅ · service-storage 168 ✅ · field-zoo dogfood 91 ✅ · build ✅ · lint ✅ · nul-bytes ✅ · check:api-surface ✅ · check:docs ✅

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

… 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
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 27, 2026 4:36am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, packages/qa, packages/services, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

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
os-zhuang marked this pull request as ready for review July 27, 2026 04:50
@os-zhuang
os-zhuang merged commit fe67e34 into main Jul 27, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the d3w2/write-cutover branch July 27, 2026 04:50
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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants