Skip to content

feat(objectql)!: 媒体值形态 strict 按部署生效 —— #3438 中标记真正覆盖的那一半 - #3681

Merged
os-zhuang merged 2 commits into
mainfrom
claude/platform-migration-gating-764pf2
Jul 27, 2026
Merged

feat(objectql)!: 媒体值形态 strict 按部署生效 —— #3438 中标记真正覆盖的那一半#3681
os-zhuang merged 2 commits into
mainfrom
claude/platform-migration-gating-764pf2

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

承接 #3617 落下的机制,做 #3438标记真正覆盖的那一半。前置 PR #3638 已合并。

变更

媒体字段(file/image/avatar/video/audio)的值形态校验,在本部署跑过 os migrate files-to-references --apply 且自检通过之后,从告警变为 invalid_type 拒绝。没迁移的部署完全不受影响。

理由是证据对得上:这里会被拒的遗留形态——inline {url, name, …} blob、裸 URL——正是那个迁移负责转换的东西。跑过并通过的部署已经被证明没有残留;没跑过的部署如果跟着版本号一起翻,升级瞬间每个 media 字段的 update 都会开始失败。

为什么只做媒体这一半

#3438 把三件事打包成一次翻转,而标记只覆盖其中一件。它断言的是「本部署的文件字段值已迁移并对过账」——对 lookup 的 id、location 的载荷是否规范什么都没说,对 D2 的动作参数更是毫无关系。把它们挂上去就是拿一个事实去给另一个不相干的事实背书,和 #3617 修掉的读解析器缺陷是同一个形状:让一个权威去回答没有问它的问题。

证据来源 本 PR
D1 媒体类型 adr-0104-file-references 标记 ✅ 按部署翻转
D1 引用 + 结构化 JSON 尚无对应证据 保持 warn-first 不动
D2 动作参数 与任何数据迁移无关 不动

后两行不是被机制卡住——机制已经在跑了;它们卡在「什么才算是这个部署的 location 值健全的证据」这个尚未有人回答的问题上。已在 #3438 留言说明。

环境变量

OS_DATA_VALUE_SHAPE_STRICT_ENABLED 语义未变(仍是全类别 opt-in,也仍能在未迁移的部署上强制 media 严格),所以不需要改名或兼容层——#3438 当初预期要改名,是因为假设了全体翻转。

新增逃生阀 OS_ALLOW_LAX_MEDIA_VALUES=1(按 Prime Directive #9OS_ALLOW_* 形式),在已验证的部署上重新放宽。两个都设时宽松的一方胜出:没人看的告警,代价小于停止写入的应用。

代价

事实存在数据库里,而校验器是同步且逐次写入的,所以 engine 读一次并 memoize——并且除非写入对象真的声明了 media 字段,否则连这一次都不读(沿用 file-reference-lifecycle.ts 已写明的休眠原则)。存文件的应用每进程一次查询,不存文件的应用零次。

实现过程中这一点是被测试逼出来的:我最初无条件读标记,4 个既有测试因为多出一次查询而失败——那不是测试太严,是我真的给每个 update 都加了一次无谓查询。

所有「不知道」的情形(没有存储服务、没有行、表读不了、行格式坏)一律答「未验证」:读不到证据的部署继续写入,而不是开始拒绝。

验证

  • 新增 8 条 validator 用例 + 8 条 engine 用例(含 memoize 只读一次、无 media 字段零查询、失败运行后回到宽松、逃生阀、opt-out 胜过 opt-in)。
  • 端到端在 showcase + SQLite 的同一个数据库上:同一次写入,迁移前放行 → 迁移后 invalid_type 拒绝 → 加 OS_ALLOW_LAX_MEDIA_VALUES 再次放行。
  • 全量 pnpm test 132/132 绿;spec 十道产物门、ESLint、example/downstream typecheck 本地全过。

文档

🤖 Generated with Claude Code

https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma


Generated by Claude Code

…file migration verified (#3438)

#3438 scheduled the strict flip for "a later minor once telemetry is quiet" —
our telemetry, deciding for their deployments. #3617 built the mechanism that
replaces that: a deployment's own migration flag. This lands the half that flag
actually covers.

A malformed media value now rejects with `invalid_type` instead of warning, but
only where `os migrate files-to-references --apply` has run and passed its
self-check. The legacy forms this rejects — inline `{url, name, …}` blobs, bare
URLs — are exactly what that migration converts, so a verified deployment has
been shown to hold none, while an unmigrated one is untouched rather than having
every media update start failing on upgrade.

## Only media, deliberately

#3438 packaged three things as one flip, and the flag covers one of them. It
asserts "this deployment's FILE values are migrated and reconciled". It says
nothing about whether a `lookup` id or a `location` payload is well formed, and
nothing at all about D2's action parameters. Gating those on it would be
borrowing evidence for a fact it does not cover — an authority answering a
question it was not asked, which is the same shape as the read-resolver defect
#3617 fixed one layer down. Reference and structured-JSON classes keep their
own warn-first rollout until something can vouch for them.

`OS_DATA_VALUE_SHAPE_STRICT_ENABLED` is unchanged (still opts every class in,
still forces media strict on an unmigrated deployment). New escape hatch
`OS_ALLOW_LAX_MEDIA_VALUES=1` re-opens media on a verified deployment; when both
are set the lenient one wins, because a warning nobody reads costs less than an
app that stops writing.

## Cost

The fact lives in the database while the validator is synchronous and per-write,
so the engine reads it once and memoizes — and stays dormant unless the written
object declares a media field, mirroring the rule the storage module already
states. One query per process for apps that store files, zero for those that
do not. Every way of not knowing (no storage service, no row, unreadable table,
malformed row) answers "not verified": a deployment whose evidence cannot be
read keeps writing rather than starting to reject.

Verified end-to-end on showcase + SQLite against one database: the same write
was accepted before the migration, rejected with `invalid_type` after it, and
accepted again under OS_ALLOW_LAX_MEDIA_VALUES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9uWWyKq6pNPQthwCXL8ma
@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 2:56pm

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 2 package(s): @objectstack/cli, @objectstack/objectql.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 14:56
@os-zhuang
os-zhuang merged commit 6169615 into main Jul 27, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/platform-migration-gating-764pf2 branch July 27, 2026 15:09
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… is gone (#3438) (#4213)

Executes the D2 half of ADR-0104's 2026-07-30 addendum: the warn-first window
closes in 17.0 rather than 18.0.

- actionParamsStrict() becomes laxActionParams(); enforcement is the default
  and OS_ALLOW_LAX_ACTION_PARAMS=1 is the only knob, spelled OS_ALLOW_* per
  PD #9 and ADR-0110 D6 — whose sole cited exception this removes.
  OS_ACTION_PARAMS_STRICT_ENABLED never reached `latest` (RC-only), so it is
  deleted outright rather than deprecated for a release.
- The opt-out path still warns once per action: opting out tolerates the
  drift rather than hiding it again.
- Dogfood duals inverted — the default path is now what the gate proves, and
  the hatch gets the test so the branch nobody sets cannot rot.

Also corrects docs the flip falsifies, plus two passages #3681 had left
self-contradictory in the v17 notes (hard rejection described as arriving only
via OS_DATA_VALUE_SHAPE_STRICT_ENABLED two paragraphs before saying media
enforces per verified deployment; the upgrade checklist prescribed that env var
as the route, which would opt in every value class including ones with no
migration behind them).
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… 第 2 项) (#4235)

媒体类已由 #3681 按部署翻转;引用(`lookup`/`master_detail`/`user`/`tree`)与结构化
JSON(`location`/`address`/`composite`/`repeater`/`record`/`vector`)此前只能借文件
迁移那个标记——而后者断言的是「文件值已迁移、归属已对账」,对 `lookup` id 或 `location`
载荷是否规范什么都没说。本命令给它们自己的门禁。

刻意没有回填:姊妹迁移转换遗留文件值,因为是平台收窄了那个存储形态、因此欠这笔转换;
而畸形的 `location` 是应用数据,正确值只有作者知道。所以这条命令只报告并给出处方(点名
对象、字段、类型、条数、样本 record id、parse issue),由运维修完重跑。由此得到一个
简化:没有可转换的东西,`--apply` 唯一的写入就是标记行,于是 #3617 的不变量平凡成立
——dry run 什么都不改,且「这次运行是否改变了本部署的姿态」从不取决于它发现了什么。

反漂移:`valueShapeViolation` / `isScannableValueShapeField` 从 `record-validator`
导出、被扫描器 import。两份「什么算畸形」的实现只要差一个子句,部署就会通过扫描却仍被
拒写——那等于标记在断言一个校验器不认的事实,正是 ADR 上一层禁止的「借来的证据」。一个
测试直接钉住这条属性:同一个值必须既被扫描器计数、又被 strict 写入路径拒绝。

分层:`scanValueShapes` 只读(它接受的 engine 类型没有写入面)。标记的读者用 spec 契约,
只有写者依赖 platform-objects——这是 `sys-migration.object.ts` 自己写下的设计意图;让
引擎去写标记会反转依赖方向,所以组合放在 CLI 命令里。

其余:`OS_ALLOW_LAX_VALUE_SHAPES` 逃生阀,优先级与媒体半边一致(opt-out > 全类 opt-in
> 标记),矛盾配置落在安全侧;引擎两个标记的读取重构成一个按 id 参数化的 helper,"所有
不知道的方式都答 false" 只写一遍;截断或有对象读不出来的扫描即使零违规也不开门——「我们
读到的那部分里没有」不是标记所断言的命题。

#4215 在本 PR 开着时落了 #3438 第 3 项,并顺带声明了同名的 `VALUE_SHAPES_MIGRATION_ID`;
合并保留一份声明加 main 的 `CREATION_ATTESTED_MIGRATION_IDS`。合并同时暴露的文档不自洽
一并修掉:新小节曾把 files-to-references 的收尾段与「写入规则」callout 劈开,挪回各自
命令名下;「其他值类保持 warn-first」改为指向现已存在的门禁;v17 发布说明补上本命令、
分开标记的理由、独立的升级清单条目,以及「从空创建」那节缺的 `OS_ALLOW_LAX_VALUE_SHAPES`。

顺带发现(另开 issue,未在本 PR 扩大范围):`sys_migration` 目前只由 service-storage
注册,这在只有存储一个消费者时说得通,现在有了第二个与存储无关的消费者。实际部署路径没
问题(`os serve` 会自动装配 storage),但账本是平台基础设施,不该由一个可选服务持有;
已在 `data-migration-plugins.ts` 注释中记录。

#3438 三项至此全部完成:第 1 项 698cbc2,第 3 项 #4215,本 PR 是第 2 项。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants