Skip to content

spec 同名双源:两个 MetadataWatchEvent 形状不同、分挂两个子路径入口,其中 kernel 版零消费方(ADR-0049 enforce-or-remove) #4411

Description

@os-zhuang

@objectstack/spec同一个名字导出了两个形状不同的 MetadataWatchEvent,分别挂在两个子路径入口上,靠 import 路径区分。其中一个是完全惰性的(零消费方,只有自己的测试在 parse 它),另一个是唯一在用的、且其消费方自己的注释管它叫 "legacy"。

import type { MetadataWatchEvent } from '@objectstack/spec/kernel';   // 一种形状
import type { MetadataWatchEvent } from '@objectstack/spec/system';   // 另一种形状

两份的实际差异

@objectstack/spec/kernel @objectstack/spec/system
定义 kernel/metadata-loader.zod.ts:298(type :536) system/metadata-persistence.zod.ts:270(type :338)
type 'added' | 'changed' | 'deleted' 'add' | 'change' | 'unlink' | 'added' | 'changed' | 'deleted'
metadataType string(必填) string?(可选)
name string(必填) string?(可选)
path string(必填) string(必填)
timestamp string(必填,datetime) string?(可选)
stats MetadataStats?
data unknown? unknown?
运行时消费方 0(仅 metadata-loader.test.ts 自测) 1:packages/metadata/src/metadata-manager.ts

即:system 版是 kernel 版的宽松超集 —— 多出 chokidar 原生的 add/change/unlink 三个值、多出 stats,并把 metadataType/name/timestamp 放宽为可选。两者不是两个概念,是同一概念的两个严格度

两个 schema 都没有被任何其他 schema 内嵌引用(唯一引用各自的就是它们自己那行 z.infer),所以拆解面很干净。

命名直觉是反的 —— 这是最容易踩的点

听起来更"正统"的那份(kernel,规范化枚举、字段必填)是死的;真正在用的是那份自称 legacy 的宽松超集。

metadata-manager.ts 自己的措辞:

  • :1646/** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
  • :1668const legacyEvent: MetadataWatchEvent = {

任何按名字或按"哪个看起来更规范"来选的人(包括 AI 补全和自动 import)都会选反。 而且两者字段大量重叠(type 的三个规范值、namepathdatatimestamp 都在),选错未必立刻炸 —— 只在边缘值(add vs added)和必填性上翻车。

是怎么被发现的

#4404(#4251 B3)给 IMetadataServicesubscribe? 时,我按 watch 的回调类型写了首稿。同一个 PR 刚给 ObjectQL 挂上的 implements 检查链条上,MetadataManager implements IMetadataService 当场拒绝了它 —— subscribe 转发的是 persistence 侧事件,watch 转发的是注册级事件。顺着这个拒绝才发现底下压着两份同名类型。

契约检查上线第一天,抓到的第一个错是新写的契约本身;这条 issue 是那次拒绝的副产品。

#4393 的死影子是同一病灶,但更危险

#4393 删掉的 packages/core/src/contracts/(IHttpServer 等)也是同名双源,但那份是死代码:零导入方、非子路径导出、非 tsup entry,只坑 grep 进去的人 —— 危害限于"读到过期契约后得出有依据的错误结论"(#4382 那条错误豁免正是这么来的)。

这一份不同:两份都在公开导出面上(api-surface.json./kernel./system 两个入口各列了一次 MetadataWatchEvent (type) + MetadataWatchEventSchema (const)),都可被外部 import,靠路径区分。

建议动作:退役,不是合并

kernel 那份符合 ADR-0049 declared-but-unenforced:声明了、导出了、有测试,但没有任何生产代码消费它。按 enforce-or-remove,应当删除,system 那份留为唯一来源。

删除后顺带:

不建议顺手把 system 版收紧成 kernel 版的严格形状 —— 那会改变 metadata-manager 实际发出的事件(它确实发 add/change/unlink),属于独立的行为变更,不该混进一次命名去重。

已核实

  • packages/**:kernel 版零运行时消费方(grep 全仓,仅自测命中)
  • 兄弟仓 cloudobjectui:均未引用 MetadataWatchEvent(任一版本)
  • 两个 schema 均未被其他 schema 内嵌
  • 两个 barrel 导出确认:kernel/index.ts:27system/index.ts:71(均为 export *)

待裁决

  1. kernel 版是否真的可以直接删 —— 上面只覆盖了本仓 + 两个兄弟仓;@objectstack/spec 是公开发布包,外部消费方无法用 grep 排除。若需保守,可先按弃用标记一个 release 再删。
  2. 若确认删除,是否值得同时给 spec 加一条"跨 barrel 同名导出"的检查 —— 现在 api-surface.json 里同名跨入口是可见但不报警的。这条检查能防住下一个同类问题;是否值当由维护者定。

关联:#4251(发现路径)、#4404(触发拒绝的 implements 检查)、#4393(同类的死影子,已删)、ADR-0049(enforce-or-remove)


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions