Skip to content

fix(metadata-protocol): 元数据保存的 422 保留 union 分支处方,Studio 重新拿得到字段名 (#5364) - #5596

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5364-savemeta-union-branches
Aug 5, 2026
Merged

fix(metadata-protocol): 元数据保存的 422 保留 union 分支处方,Studio 重新拿得到字段名 (#5364)#5596
os-zhuang merged 3 commits into
mainfrom
claude/issue-5364-savemeta-union-branches

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5364

> 排版说明:下文一律用 ⟨root⟩ 指代源码里那个 ASCII 尖括号字面量(小于号 + root + 大于号,见 i.path || ... 那行的 fallback)。GitHub 的正文消毒器会把小于号紧跟字母整段当 HTML 标签删掉,本 PR 初版正是这么被吃掉了一次,故改用这对括号写。

✅ 停放已解除(PM 维护,2026-08-05)

曾因 base 分支的 #5604 红(check-engine-double-contractaction-execution-calldata-not-found.test.ts)停放;#5615(2614aef)落地后本分支已同步 main(head 2cf9cc0),CI 23 项全绿,按 ACCEPT 流程转 ready 入队。

前提复核(在 origin/main e900015 上实测)

先按「issue 是线索不是规格」核了一遍。issue 引用的 protocol.ts:7128 已漂移,INVALID_METADATA 现在有两处:L7010 是「分层读信封当写体」的防御性拒绝(与本单无关),L7141 才是 saveMetaItem 的 spec-conformance 422。缺陷本身则完全成立,走真实 saveMetaItem 路径复现:

THROWN? true code= INVALID_METADATA status= 422
MESSAGE = [invalid_metadata] view/task_list failed spec validation: ⟨root⟩: Invalid input
ISSUES = [ { path: '', message: 'Invalid input', code: 'invalid_union' } ]
ROWS = 0

.map() 丢掉的 4 个分支里躺着的是:branch[1] 的 unrecognized_keys(点名 typecolumns,带 #4001 策展处方)、branch[2]/[3] 的 invalid_value(带槽位路径与合法枚举)。

改了什么

packages/metadata-protocol/src/protocol.ts 新增 zodIssuesToMetadataIssues,422 的 issues 由它产出。union 自己那条保留不动——展开是严格叠加的,今天读 issues[0] 的消费者不会少读到任何东西——后面跟上真正解释这次拒绝的分支,路径按绝对路径拼好(分支 issue 的 path 是相对于 union 的,#5014 为此付过学费)。422 的 message 摘要行随之变得可读。

分支选择策略与 main 上两份参考实现逐条一致:丢弃只报根部 kind 不匹配的分支;报得最少的分支胜出;unrecognized_keys 破平局;声明顺序决定其余;并列全出(上限 3);嵌套 union 递归(上限 3 层)。这是同一机制的第三份拷贝,形态不同而判决相同:spec 的 formatZodError(#4971)只导出字符串渲染器,rest 的 zodIssuesToFields(#5014)产出 ADR-0114 的 { field, code } 目录条目,本处信封是 { path, message, code }code 透传 zod 原码(按派发约束,不顺手对齐 ADR-0114 目录 —— 那需要单独裁决)。

修后实测

issue 那个 view 输入,issues 从 1 条变成 2 条,第二条 codeunrecognized_keys,message 点名 `type``columns` 并带 #4001 的完整处方;摘要行从 ... failed spec validation: ⟨root⟩: Invalid input 变成 ... failed spec validation: ⟨root⟩: Invalid input; ⟨root⟩: Unrecognized key(s) on this view container: ...

⚠️ 与派发词预期不一致的一点(如实报告,未按模板凑证据)

派发约束写的是「修后 issues 必须含带真实键名的 unrecognized_keys 条目type 路径的 invalid_value 条目」。用 issue 那个输入,后者不会出现,而且这不是实现缺陷:

  • branch1= 1 条 issue 且带 unrecognized_keys → rank [1, 0]
  • branch[2]/[3](扁平 list/form)= 各 2 条 issue → rank [2, 1]

「报得最少的分支胜出」是排序第 2 位,unrecognized_keys 破平局是第 3 位,所以只有 branch[1] 入选。要同时输出两者就必须改排序策略,而那会让四个消费者判决不一致——正是本单存在的理由。故按「语义必须与两份参考一致」执行,并把「带 type 路径、可高亮」的承重断言放到确实产出它的输入上(容器体 { list: { type: 'nope', ... } }pathlist.typecodeinvalid_value、message 含合法枚举)。

测试

新增 packages/metadata-protocol/src/protocol.save-union-issues.test.ts(14 例):

  • zod 确实把整次拒绝折叠成一条无路径 issue —— 缺陷本身钉死为「关于 zod 的事实」
  • issue 原样输入:真实键名到达作者;union 那条仍在 issues[0];摘要行可读
  • 容器体:list.type 绝对路径 + invalid_value + 合法枚举
  • 承重:合法 view 照常保存(不凭空造拒绝);非法 view 仍 422 且 rows.size === 0
  • 策略单测:非 union 原样透传;全是 kind 不匹配则输出不变;errors: [] 变体不加噪;最少胜出 + unrecognized_keys 破平局;并列全出且上限 3;逐层拼绝对路径;第 4 层 union 不再展开;同键同话去重一次;去重不跨顶层 issue;非数组入参返回空信封

反向验证(方向事先预判,结果与预判一致)

把调用点还原成旧的 .map(),预判「只有两条 save 路径的展开断言转红,其余 12 条(zod 事实钉、合法保存对照、10 条纯策略单测)保持绿」。实跑:

Tests  2 failed | 12 passed (14)
 x the issue's view body: real key names now reach the author instead of Invalid input
 x a container body localises the failure to a real path Studio can highlight

影响面(按规则消费半径,不按被改包)

err.issues 的下游 fixture 全扫了一遍:packages/objectqlprotocol-meta.test.ts / metadata-validation-sweep.test.ts 只断言 Array.isArray(issues) 与 code/status,packages/rest 的两个信封测试自建 issues 数组——都不钉条数或内容,故不受影响。最终同步圈(head 2cf9cc0,base 含 2614aef)三包实跑:

@objectstack/metadata-protocol   Test Files  43 passed (43)    Tests   402 passed (402)
@objectstack/objectql            Test Files 118 passed (118)   Tests  1911 passed (1911)
@objectstack/rest                Test Files  49 passed (49)    Tests   737 passed (737)

typecheck:@objectstack/metadata-protocoltypecheck 脚本(在 check-type-check-coverage.mjs 的 DEBT 台账里)。改用直跑 tsc 对比基线,63 → 63,未增一条;pnpm check:type-check-coveragenode scripts/check-nul-bytes.mjs 均 OK;check:engine-double-contract OK(26 pinned);#5600 新落的 query-options-erasure ratchet 绿且本 PR 未进其基线。

范围纪律

顺带发现(已另立 issue,不在本 PR 修)


🤖 Generated with Claude Code

https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

…#5364)

The spec-conformance 422's own comment promises "structured Zod issues so
the Studio form can highlight the offending field". A top-level `z.union`
broke that completely: zod folds every branch of a failed union into ONE
issue whose path is '' and whose message is the literal "Invalid input",
and `parsed.error.issues.map(...)` mapped exactly that. Since
`ViewMetadataSchema` IS a top-level union, EVERY failed view save arrived
at Studio as one rootless line with no field name in it at all.

`zodIssuesToMetadataIssues` expands the branches that explain the
rejection, resolving branch-relative paths against the union's own. The
union's own entry is kept, so the change is strictly additive.

Branch selection is byte-for-byte the ranking `formatZodError` (#4971,
spec) and `zodIssuesToFields` (#5014, rest) already use: drop
kind-mismatch-only branches, fewest issues wins, `unrecognized_keys`
breaks the tie, declaration order breaks the rest, ties all emitted
(cap 3), nested unions recursed (depth 3). Three copies, one verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
@vercel

vercel Bot commented Aug 5, 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 Aug 5, 2026 9:15pm

Request Review

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

github-actions Bot commented Aug 5, 2026

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.

Copy link
Copy Markdown
Contributor Author

CI 红说明(engine-core 车道 PM):ESLint job 的失败是 check:engine-double-contract 挂在 packages/runtime/src/action-execution-calldata-not-found.test.ts(lines 69/102)—— 该文件不在本 PR diff 内,失败条件在 origin/main 上齐备(#5584 与门收紧在飞交叉),属 base 分支自身的红,已由 #5604 立案并派发修复(cli 车道)。本 PR 不追此签名;#5604 落地后同步 main 重跑。若届时出现签名,再按新问题诊断。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 21:38
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit d275c10 Aug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5364-savemeta-union-branches branch August 5, 2026 21:49
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…ack-ai#5598) (objectstack-ai#5765)

computeMetadataDiagnostics 把 zod 的 error.issues 直接 .map() 成 _diagnostics
条目。zod 会把一个失败 z.union 的全部分支折叠成一条顶层 issue —— path 为空、
message 是字面量 "Invalid input" —— 而 ViewMetadataSchema 顶层本身就是 union,
所以库里每一个有缺陷的 view 读出来都退化成这一条没有字段名的记录,模块头承诺的
"内联字段错误"无处可标。

后果是同一份文档在两条路径上判决不一致:objectstack-ai#5364(PR objectstack-ai#5596)修好写路径之后,保存
一个有缺陷的 view 能看到出错的键名,打开同一份已存库的文档却仍然只有一条
Invalid input。

改法是复用而非再抄一份策略:读路径改调同包 objectstack-ai#5596 落地的
zodIssuesToMetadataIssues,分支选取口径由该函数单点定义,读写两路径按构造一致。
展开是纯增量 —— union 自己那条仍在 errors[0]。stripDiagnostics 一段未动。


Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

Co-authored-by: Claude <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.

元数据保存的 422 也丢掉 union 分支处方:一个 view 保存失败只回一条 path:"" message:"Invalid input",Studio 无字段可高亮

2 participants