Skip to content

fix(metadata-protocol): boot 重水合按行的真实 package_id 登记对象归属 (#4636 裁 B 收官) - #6261

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-4636-boot-package-binding
Aug 7, 2026
Merged

fix(metadata-protocol): boot 重水合按行的真实 package_id 登记对象归属 (#4636 裁 B 收官)#6261
baozhoutao merged 1 commit into
mainfrom
claude/issue-4636-boot-package-binding

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #4636 — 维护者 2026-08-07 裁决 B第二步:boot 半边,本单收官。PR1(#6219,已合入 c9bf94009)已把写路径切到真实 package id 并加上服务端强制盖章;本 PR 落地剩下的一行,两侧从此说同一句话。

问题

loadMetaFromDb 的 object 分支从 engine.find('sys_metadata', …) 返回的行上读 record.packageId。但这些行是数据库行,列名是 snake_case 的 package_id:sys_metadata 对象声明 package_id,SysMetadataRepositorypackage_id,getMetaItemsr.package_id,紧邻的非 object 分支三行之下就写着 (record as …).package_id。只有这一支拼成了 camelCase,于是表达式恒为 undefined || 'sys_metadata' —— 无论行有没有绑定包,每次 boot 都登记在哨兵下。

为什么这不只是个分类瑕疵

归属键就是包过滤键(getAllObjects(packageId) 匹配 contributor.packageId,runtime meta.ts 的侧边栏直接消费)。PR1 之后写路径记的是 app. 加真实 slug,所以残留缺陷正好是重启形状的:对象创建时在自己所属包的过滤里,重启之后就不见了。

更要紧的是重启后的第一次编辑。两侧键不一致时,这次保存会以另一个键重新认领归属,registerObjectalready owned by package "sys_metadata",而 applyObjectRegistryMutation 把它 catch 成 console.warn:保存返回 success: true、行也确实落盘,但内存 schema 停在 boot 时的版本 —— 这一笔编辑被静默丢弃,新字段在下一次重启前都不可用。

这是 cloud#970 在 PR1 之后的形态:不是 403(两侧都盖 _provenance: 'org',overlay 门是开的),而是一次被吞掉的 ownership 冲突。所以 pin 测试断言的不是「保存成功」,而是演进后的字段真的进了 schema

改了什么

  1. packages/metadata-protocol/src/protocol.ts —— 一行读法:record.packageId(record as { package_id?: string | null }).package_id。用 || 而非 ??,与写路径的 request.packageId || 'sys_metadata' 对称:空绑定就是「没有包」,哨兵只标记这一件事。该分支已经在盖 _provenance: 'org',本 PR 不动盖章。
  2. packages/objectql/src/registry.ts —— isTenantAuthored 契约注释终版:摘掉 PR1 加的「这半句描述的是契约,还不是代码」标注,改成「两侧都按真实 package id 登记」。注释的结论(_packageId !== 'sys_metadata' 无法回答 provenance)不变。

前提复核(在 origin/main 上)

前提 结论 证据
P1 PR #6219 已在 main c9bf94009 fix(metadata-protocol): 对象 overlay 写路径的 ownership 键切真实 package id …(#6219)
P2 该行仍是 record.packageId || 'sys_metadata' protocol.ts:10765(立单时 ~8958,派发词记 :10657,已再漂移;按内容定位)
P3 PR1 的 tripwire 用例原样在 protocol-writepath-object-ownership.test.ts:365 [tripwire] a package-bound rollback still 409s,本 PR 一字未动

测试

新增 packages/objectql/src/protocol-boot-object-package-binding.test.ts(5 例)。放在 objectql 而非被测代码旁,理由与 PR1 同:断言对象是 SchemaRegistry,而 objectql 依赖 metadata-protocol,只有这个方向能同时握住两半。

用例不手搓行,而是跑一遍真实写路径持久化、再把落盘的行交给一个全新的 registry + protocol —— 列名拼写正是本 PR 的全部主题,手搓 fixture 会让它与仓库实际写出的形状脱钩。

  • package-bound 行:落盘的行确实带 package_id(且没有 packageId)→ boot 后归属是 app.myapp;
  • _provenance 仍是 'org'(本 PR 不改盖章,显式钉住);
  • 侧边栏过滤:重启后 getAllObjects('app.myapp') 重新看得见,且不串到 app.otherapp;
  • 重启面:boot 之后的第一次编辑 → due_date 真的进内存 schema、归属仍是 app.myapp、落盘仍是一行且仍绑在包上;
  • package-less 行:哨兵照旧,_provenance: 'org',不混进任何包的过滤(回归)。
pnpm --workspace-concurrency=2 --filter @objectstack/metadata-protocol --filter @objectstack/objectql test

packages/metadata-protocol test:  Test Files  49 passed (49)
packages/metadata-protocol test:       Tests  502 passed (502)
packages/objectql test:  Test Files  138 passed (138)
packages/objectql test:       Tests  2254 passed (2254)

packages/objectql pnpm typecheck(tsc --noEmit)通过、零输出。metadata-protocol 没有 typecheck 脚本(在 type-check-coverage 的 DEBT ledger 里),与 PR1 相同。

门:

check-engine-double-contract: OK — 77 pinned, 133 in the DEBT ledger, 2 exempt.   (本 PR 前 75 pinned;新文件的两个 double 已 pinned)
check-nul-bytes: OK (scanned 5944 tracked text file(s); no raw ASCII control bytes)

反向验证(方向先写死,再实测)

⚠️ 同 PR1 的测量条件:objectql 的测试经 package exports 解析到 metadata-protocol 的 dist/,不是 src。下列结果都是改完重建 dist 之后的。

肢 A —— 还原那一行读法(回到 record.packageId)。 预测写在跑之前,逐条:

用例 预测 实测 符合?
真实 package id expected 'sys_metadata' to be 'app.myapp'
_provenance: 'org' 绿(本 PR 不动盖章) 绿
侧边栏过滤 expected [] to deeply equal [ 'myapp_invoice' ]
重启后第一次编辑 红,但不在 success —— saved.success 仍为 true(不会 403:两侧都盖 'org',门是开的),红在 due_date 断言,因为 registerObjectalready owned 被吞成 warn 红,正在 due_date:expected [ 'organization_id', …(8) ] to include 'due_date';success 断言先行通过;stderr 印出 [Protocol] registerObject failed for myapp_invoice: Object "myapp_invoice" is already owned by package "sys_metadata". Package "app.myapp" cannot claim ownership.
package-less 哨兵 绿 绿

Tests 3 failed | 2 passed (5) —— 逐例符合预测,范围与方向都没有偏差。特别记一句:这一支没有 403,和裁决前 dev 实测的「boot 单独修」形态不同,正是因为 PR1 已经把盖章补上;红的是归属、包过滤,以及被 console.warn 吞掉的那笔编辑。

必答项

#5079 终版定价 —— 成因分析不需要更新,removeRuntimeShadow 与本 PR 的结构不相交。 PR1 已实测的机制在 boot 侧原样成立:removeRuntimeShadow 扫的是 this.metadata.get(type) 里的复合键 pkg:name 兄弟项,判据 it._packageId !== 'sys_metadata' 也是对那个 Map 求值;而 object 这一片永远只有裸键 —— boot 的 object 分支和 applyObjectRegistryMutation 一样只调 registerObject(动的是 objectContributors),plugin.tstype === 'object' 显式 return(注释:objects are registered differently (ownership model)),全仓没有任何路径以 packageId 调 registerItem('object', …)。所以复合兄弟项从不存在,循环走不到那个判据。本 PR 搬的仍是 objectContributors 的键,是 removeRuntimeShadow 从不触碰的另一个结构。#5079 的成因分析(「object 分支本就无 shadow 可摘」)在 #4636 完整落地后依然成立,无需重写,也不应再被定价成「等 #4636」。

#6215 —— 零依赖确认。 本 PR 只动 loadMetaFromDb 的一行读法与一段注释,sys-metadata-repository.ts 一字未动,rollbackMetaItem / restoreVersion 一字未动。#6215 的成因(restoreVersionput 不传 packageId,putwhereFor(ref, state, opts.packageId ?? null),null 是谓词而非「任意包」)与 boot 读法无关;两个方向都不阻塞。

PR1 tripwire —— 原样未动确认。 protocol-writepath-object-ownership.test.ts 整个文件本 PR 未修改(git diff 不含该文件),[tripwire] a package-bound rollback still 409s 仍在 :365,仍绿。#6215 修好那天它照常翻红,把接手的人指向上一条用例里已经写好的 ownership 断言 —— 这条链路完好。


Generated by Claude Code

`loadMetaFromDb` 的 object 分支从 `engine.find` 的行上读 `record.packageId`,
而 `sys_metadata` 的列是 snake_case 的 `package_id`,该表达式恒为
`undefined || 'sys_metadata'` —— 每次重启都把绑定了包的对象 overlay 登记在
哨兵下。改读 `package_id`,与写路径(#4636 PR1)、`getMetaItems` 以及相邻的
非 object 分支一致。

归属键同时是包过滤键(`getAllObjects(packageId)`),所以此前对象在创建时
出现在自己所属包的侧边栏过滤里、重启后消失;更要紧的是重启后的第一次编辑:
boot 登记 `'sys_metadata'`、保存登记 `app.<slug>`,`registerObject` 抛
`already owned by package …` 被 `applyObjectRegistryMutation` 吞成 warn,
保存回 `success: true` 而内存 schema 停在重启时的版本,该笔编辑被静默丢弃
(cloud#970 的重启面)。

同时把 `objectql/src/registry.ts` `isTenantAuthored` 的契约注释收尾:摘掉
PR1 加的「这半句描述的是契约,还不是代码」标注 —— 两侧现已一致。

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

vercel Bot commented Aug 7, 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 7, 2026 12:28pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/objectql.

15 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, @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • 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.

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/m tests tooling

Projects

None yet

2 participants