Repository navigation
spec 同名双源:两个 MetadataWatchEvent 形状不同、分挂两个子路径入口,其中 kernel 版零消费方(ADR-0049 enforce-or-remove) #4411
Description
@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. */:1668—const legacyEvent: MetadataWatchEvent = {
任何按名字或按"哪个看起来更规范"来选的人(包括 AI 补全和自动 import)都会选反。 而且两者字段大量重叠(type 的三个规范值、name、path、data、timestamp 都在),选错未必立刻炸 —— 只在边缘值(add vs added)和必填性上翻车。
是怎么被发现的
#4404(#4251 B3)给 IMetadataService 补 subscribe? 时,我按 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 那份留为唯一来源。
删除后顺带:
packages/spec/src/contracts/metadata-service.ts里subscribe?的 import 处我留的那块"两个同名类型"的警示注释可以删掉(feat(spec,objectql): IObjectQLEngine —— objectql 槽的契约落地,类 implements 之,七个本地 stand-in 退场 (#4251 B3) #4404 加的)。api-surface快照会少两个./kernel导出 —— 这是 breaking(removed),check:api-surface会要求确认;走正常的 spec 变更流程即可。kernel/metadata-loader.test.ts里对应的 describe 块一并移除。
不建议顺手把 system 版收紧成 kernel 版的严格形状 —— 那会改变 metadata-manager 实际发出的事件(它确实发 add/change/unlink),属于独立的行为变更,不该混进一次命名去重。
已核实
packages/**:kernel版零运行时消费方(grep 全仓,仅自测命中)- 兄弟仓
cloud、objectui:均未引用MetadataWatchEvent(任一版本) - 两个 schema 均未被其他 schema 内嵌
- 两个 barrel 导出确认:
kernel/index.ts:27、system/index.ts:71(均为export *)
待裁决
kernel版是否真的可以直接删 —— 上面只覆盖了本仓 + 两个兄弟仓;@objectstack/spec是公开发布包,外部消费方无法用 grep 排除。若需保守,可先按弃用标记一个 release 再删。- 若确认删除,是否值得同时给 spec 加一条"跨 barrel 同名导出"的检查 —— 现在
api-surface.json里同名跨入口是可见但不报警的。这条检查能防住下一个同类问题;是否值当由维护者定。
关联:#4251(发现路径)、#4404(触发拒绝的 implements 检查)、#4393(同类的死影子,已删)、ADR-0049(enforce-or-remove)
Generated by Claude Code
Activity
refactor(spec)!: remove the kernel metadata-loader envelope family — …
feat(spec): ratchet cross-entry dual-source exports — same name, diff…
feat(spec)!: MetadataWatchEvent.type carries only the values the runt…
docs(releases): v17 upgrade checklist — the raw watcher values are go…
feat(spec)!: converge the dual-source MetadataFormat and CacheStrateg…
feat(spec)!: converge the 11 contracts-vs-domain dual-source type nam…
feat(spec)!: 双源 C1 收敛 — WebhookConfig / WebhookEvent 归 ./integration,…
feat(spec)!: 双源 C2 收敛 — MetadataEvent / MetadataBulkRegisterRequest 归…
feat(spec)!: 双源 C3 收敛 — 通知语汇归 ./api,./ui 与 ./system 侧死删 (objectstack-…
feat(spec)!: 双源 C4 收敛 — Session 归 ./api,./identity 侧死删 (objectstack-a…
feat(spec)!: 双源 C5 收敛 — ActivationEventSchema 归 ./kernel 结构化形状,./stud…
feat(spec)!: converge RetryPolicy onto one declaration (objectstack-a…
refactor(spec): converge HttpRequest onto one declaration (objectstac…
refactor(spec)!: rename connector-side RateLimitConfig (objectstack-a…
refactor(spec)!: FieldMapping no longer names three declarations (obj…
feat(spec)!: 移除 `./ui` 的 `HttpMethod` 导出 —— C14 双源清账,5 值子集用既有名 `HttpM…
feat(spec)!: 双源清账 C13+C15 —— 退役 L1「Simple Sync」整层,integration 冲突枚举改名 …
feat(spec)!: 改名 studio 侧 ActionLocationSchema → ActionContributionLoc…
feat(spec)!: 改名 kernel 侧 PackageDependencySchema → ResolvedPackageDep…
chore(spec): record the two #6674 exports in the #7090 export-origins…
fix(spec,lint): a formula field in searchableFields is refused loudly (…
refactor(spec): rename ./system ServiceStatus to KernelServiceStatus (o…
fix(spec): close entry-nameability on the four remaining leaking entr…