Skip to content

观察:@objectstack/spec 每个 entry 各打一份 schema 实例 —— 跨 entry 的 === 恒为 false,registerMetadataTypeSchema 的可见范围也被这条边界圈死 #5379

Description

@baozhoutao

从 #5000 的核实工作里撞到的(写 CLI 侧 pin 测试时)。观察类:今天没有用户能碰到的故障,但它让一类看起来天经地义的断言/推理不成立,值得记下来。

事实

packages/spec 的 package.json 声明了 16 个 entry(. / ./kernel / ./ui / …),tsup 按 entry 分别打包,同一个 schema 在不同 entry 的产物里是不同的对象:

// 在已 build 的 workspace 里
const root = require('@objectstack/spec');
const kernel = require('@objectstack/spec/kernel');
const ui = require('@objectstack/spec/ui');

// root 的 ObjectStackDefinitionSchema.pages 的 element
const el = root.ObjectStackDefinitionSchema._zod.def.shape.pages._zod.def.innerType._zod.def.element;

el === kernel.getMetadataTypeSchema('page')   // false
ui.PageSchema === kernel.getMetadataTypeSchema('page')   // false

源码里它们是同一个模块绑定(stack.zod.ts 与 metadata-type-schemas.ts 都 import ui/page.zod.ts 的 PageSchema),只有从 src 直接跑(tsx / vitest 走源码)时 === 才成立;跨包消费的是 dist,就是各自一份。

为什么记下来

  1. 它把「同一道门」这件事变成不可断言。 objectstack build / validate 从不按 PageSchema 解析页面元数据:ADR-0089 D3a 早就该拒绝的键一路通过,#4001 的「三个示例应用 validate 全过」对 page 面是空证 #5000 问的正是「CLI 走的 schema 是不是写路径那一个」。最自然的证明是实例同一性;跨 entry 它恒为 false,所以只能退回到行为等价(同一个未声明键、两边同样拒绝)。我在 PR 里就是这么写的,并把原因写进了测试注释 —— 否则下一个人会先写一遍 identity 断言、看着它红、再怀疑是自己的改动。

  2. registerMetadataTypeSchema() 的可见范围被这条边界圈死。 运行时 overlay 存在 kernel bundle 里的 EXTRA_METADATA_TYPE_SCHEMAS。今天没事,因为注册表的读写 API 只从 ./kernel 导出,所有消费者拿的是同一份;但这是个巧合而不是保证 —— 哪天某个 entry 再导出一个内部会读注册表的 helper,插件注册的类型对它就是不存在的,而且这种「看得见声明、读不到注册」的失败非常难查。

  3. 顺带的:同一份 zod schema 在多个 entry 里各存一份,内存/体积上是重复的(analyze-bundle-size.ts 应该能量到)。

不建议顺手改

改动面(entry 拆分 / chunk 共享 / external 策略)属于 spec 打包契约,不是随手能定的;记在这里让后来者不必重新推一遍即可。

相关

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    分诊(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,2026-08-05):持有(留 finding)。为什么还留着:观察成立且有复现命令,但今日无用户可达故障;改动面(entry 拆分 / chunk 共享 / external 策略)属 packages/spec 打包契约,是 spec 单一所有者面 —— 若晋级应按跨车道协议转 spec 车道,而非本车道派发。下轮分诊轮复核。


    Generated by Claude Code

  2. added a commit that references this issue on Aug 6, 2026
  3. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮:持有(留 finding)+ 补域 domain:spec(前一轮判级由 cli 车道 PM 做出时未打域标签,本轮补齐 —— domain:* 单一生产者是分诊座位)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage — verdict: HOLD stands (keeps finding, domain:spec). Restart condition machine-checked this round rather than assumed.

    Restart condition ① re-measured on origin/main @ 1363084 — has not fired

    The condition: any entry re-exports a helper that reads the metadata-type registry — at which moment "declaration visible, registration unreadable" turns from a coincidence into a live, very hard-to-diagnose failure.

    Measured: getMetadataTypeSchema / registerMetadataTypeSchema / EXTRA_METADATA_TYPE_SCHEMAS are exported from exactly one place, packages/spec/src/kernel/index.ts:34 (export * from './metadata-type-schemas'). Every other reference in the package is either inside kernel/ itself or a test importing it by relative path (data/object-strictness-batch20.test.ts reaches into ../kernel/metadata-type-schemas) — tests run from source, so they are not a bundle boundary. No second entry among the 16 in package.json exports re-exports the registry API. ⇒ All registry consumers still share one instance; the safety remains real, and remains a coincidence rather than a guarantee, exactly as the body says.

    Why still held, not promoted: unchanged — no user-reachable failure today, and the fix surface (entry splitting / chunk sharing / external strategy) is the spec packaging contract, not something to change opportunistically. Under "shared contract surfaces have one owner" that surface belongs to the domain:spec seat, which the label already reflects.

    Restart conditions (unchanged):

    1. any entry re-exports a registry-reading helper ⇒ promote immediately;
    2. the spec packaging contract is reopened for another reason (entry consolidation, chunk sharing) ⇒ fold this in.

    Already-banked value, needing no action: the durable payoff here is that cross-entry === is documented as non-assertable, with the reason written into packages/cli/test/metadata-type-schema-gate.test.ts by #5000's PR. The next person writing an identity assertion reads it and stops, instead of watching it go red and suspecting their own change.

    Dedup: source #5000 is closed; the bundle-size duplication noted in the body has no separate issue and does not need one. No shadow in the sibling repos.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): closing, not planned. Documented build characteristic (per-entry schema instances); no consumer relies on cross-entry === identity, and the record stays searchable here. Reopen freely if a real consumer hits it — maintainer veto window applies.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions