Skip to content

fix(metadata-protocol): read decorations stop round-tripping into persisted metadata bodies (#4326) - #4334

Merged
os-zhuang merged 2 commits into
mainfrom
claude/read-decorations-roundtrip-4326
Jul 31, 2026
Merged

fix(metadata-protocol): read decorations stop round-tripping into persisted metadata bodies (#4326)#4334
os-zhuang merged 2 commits into
mainfrom
claude/read-decorations-roundtrip-4326

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4326。这是 #4317(#3903)的两个收尾之一,顺带做完了当时留下的残留核查。

问题

getMetaItem/getMetaItems 给每个被服务的文档盖上 _diagnostics(预览读还加 _draft),而写入端按设计 verbatim 持久化 request body(ADR-0005 §Validation——用 parsed.data 会把 Studio 专有的辅助字段剥掉)。中间没有任何一处剥离装饰,于是最标准的设计器往返:

GET(带装饰)→ 改一个字段 → PUT 整个 body → 逐字节入库

读取时刻的过期判定写进了 sys_metadata.metadata、它的 checksum、以及此后每一条 history diff。

为什么一直没人发现:读取端每次都重算,decorateMetadataItem{ ...item, _diagnostics } 展开,新判定盖住旧的——用户永远看不到错误的诊断。坏掉的是存储的字节:一次纯装饰的重存会让内容 checksum 漂移,history diff 里出现作者从没写过的诊断噪音。

也正因为不可见,这里需要的是盯住存储字节的钉子测试而不是行为测试。我用禁用修复的方式验证过:三条往返测试全部变红,恢复后全绿——测试确实咬得住。

改动

saveMetaItem所有读取 body 的步骤之前剥离装饰(破坏性变更 diff、schema 网关、authoring 网关、持久化),新增 stripReadDecorations(导出供测试)。

几个刻意的设计选择:

  • 静默剥离,不同于旁边那个 layered-envelope 的拒绝:那是调用方发错了文档、必须让他改;而这两个键是我们自己的装饰,骑在一份"除此之外正是作者所编辑的"文档上——拒绝标准往返是敌意的。剥离恢复的是"往返后行体逐字节相同"这条不变式。
  • 不碰 ADR-0010 保护信封(_lock/_lockReason/_provenance)与 _packageId:那些是写入端合法承载并合并的信封状态(mergeArtifactProtection),不是读取装饰。误剥会放松打包件的锁。
  • 无可剥时返回同一引用,常见路径零分配(沿用 graftNormalizedOperators 的纪律)。
  • _diagnostics 其实有两个生产者:除 decorateMetadataItem 外,视图容器展开的命名冲突告警(stampRenameWarning,spec/ui/view.zod.ts)也盖同一个键。两者都是从文档派生的,所以都不该待在文档里——按键剥离对两者都成立。

顺带:#3903 的残留核查已闭环

我在 #4317 里把 SysMetadataRepository 判定为"版本层、刻意不转换",但没穷尽验证有没有服务路径绕过 protocol 的已转换缝隙。这次把 get() 的全部调用方过了一遍,结论写进了方法文档:

调用方 用途 判定
saveMetaItem / revertCommit / deleteMetaItem 只取 hash(父版本谱系、存在性探测) verbatim 正确——转换会改变 hash 所依据的 body,破坏配对
diffMeta current.bodysys_metadata_history 的行体对比 verbatim 载重——history 按设计 verbatim,只转换一侧会把"转换本身"渲染成用户改动
publishPackageDrafts(seed) draft body → applySeedBodies 唯一结构上的服务路径;今天是空的(没有任何转换针对 seed/dataset 面),已在文档里点名为"若将来有,要接的缝隙"

getByHash 无外部消费者、listDrafts 不返回 body、history 按设计 verbatim。边界成立。

测试

新增 protocol.read-decorations.test.ts(8 条):

  • 单元:剥两个键、无可剥返回同引用、非对象直通、不改动入参、不碰保护信封与 _packageId;
  • 集成(stub 引擎,断言存储字节):GET→编辑→PUT 后行体无 _diagnostics 且除被编辑键外与首存逐字节相同;预览读的 _draft 不入库(draft 态仍在 state 列);纯装饰重存 checksum 不漂移

合并 main 后重跑:metadata-protocol 136、rest 523、runtime 951、plugin-security 677、metadata 281、objectql 1374,全绿。ESLint 干净(sys-metadata-repository.ts 那条 rule not found 在 main 上同样存在,与本改动无关,未触碰)。

🤖 Generated with Claude Code

os-zhuang and others added 2 commits July 31, 2026 14:31
…sisted metadata bodies (#4326)

`getMetaItem`/`getMetaItems` decorate every served document with
`_diagnostics` (and `_draft` on preview reads), while the write path persists
the request body verbatim by design (ADR-0005 §Validation). Nothing stripped
the decorations in between, so the standard designer round-trip — GET the
served document, edit a field, PUT the whole body back — baked a stale
read-time verdict into sys_metadata.metadata, its checksum, and every history
diff.

Never user-visible (reads recompute and the fresh verdict shadows the stale
one), which is why it needed a pin on the stored bytes rather than a behaviour
test: verified by disabling the strip and watching all three round-trip tests
go red.

`saveMetaItem` now strips the decorations before the destructive-change diff,
the schema gate, the authoring gate and persistence. A silent strip, unlike
the neighbouring layered-envelope rejection: these are our own decorations
riding on a document that is otherwise exactly what the author edited, so
rejecting the round-trip would be hostile. The ADR-0010 protection envelope
(`_lock`/`_lockReason`/`_provenance`) and `_packageId` are deliberately left
alone — envelope state the write path legitimately carries.

Also closes out the #3903 residual verification: every `SysMetadataRepository.get`
caller was audited and the verbatim boundary documented on the method — three
sites read only `hash` (parent-version lineage, existence probes; converting
would break the body↔hash pairing), `diffMeta` compares against equally-verbatim
history rows (converting one side would render the conversion as a user edit),
and the seed-publish path is the one structurally-serving caller — vacuous
today since no conversion targets the seed surface, named as the seam to wire
if one lands.

Closes #4326.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 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 31, 2026 6:34am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling and removed size/m labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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 merged commit 3245174 into main Jul 31, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/read-decorations-roundtrip-4326 branch July 31, 2026 06:55
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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] Read decorations (_diagnostics, _draft) round-trip into persisted sys_metadata bodies

1 participant