发现于 #5259(unregister() 失效/删除顺序)实现期,不在该单范围内(改的是顺序与失败上报,不是 loader 契约),故单独立卡。当前无人踩到,按 observation-class 归档:挂 finding,不挂 pm:queue,留给分诊决定要不要动契约。
事实
packages/metadata/src/loaders/loader-interface.ts 的 MetadataLoader 接口声明了可选的
save?( type: string, name: string, data: any, options?: MetadataSaveOptions ): Promise< MetadataSaveResult >;
但完全没有 delete。于是 MetadataManager.unregister() 只能在调用点鸭子类型地探它(#5259 之前是两处 as any,之后是一个具名的 DeletableMetadataLoader 形状 + typeof … === 'function' 守卫,casts 收敛了,契约缺口没有变):
if (loader.contract.protocol !== 'datasource:' || !loader.contract.capabilities.write) continue;
if (typeof ( loader as DeletableMetadataLoader ).delete !== 'function') continue; // ← 静默跳过
也就是说:一个 loader 只要声明 protocol: 'datasource:' + capabilities.write: true,register() 就会往它写(save 有就写、没有就跳过),而 unregister() 在它没有 delete 方法时一声不吭地跳过——不是 warn,不是 #5259 新加的 error,什么都没有。随后 unregister() 照常删 registry、失效 listCache、publishRealtimeMetadataEvent('deleted') 并 notifyWatchers({type:'deleted'})。
后果与 #5259 处理的「delete 抛错」完全同类,而且更安静:
capabilities.write 的语义因此是分叉的:对 register() 它意味着「可写」,对 unregister() 它什么也不保证。declared ≠ enforced 的标准形状。
为什么现在没人踩到(所以是 observation,不是 P0)
全仓 protocol: 'datasource:' 的 loader 只有一个 —— packages/metadata/src/loaders/database-loader.ts —— 而它有 async delete(type: string, name: string): Promise< void >(第 1199 行)。所以今天任何用户路径都不会走到这个静默分支;它是给第三方/未来 loader 留的坑,以及给「AI 写一个新 loader」留的坑:接口没声明 delete,照着接口实现出来的 loader 天然就少这个方法,而它声明 write: true 时不会被任何东西拦下。
严重度不由我判:#5259 正文自己也说过「是否一并处理请分诊定夺」,而按 objectstack#4949 的纪律,填卡时判的严重度两个方向都不可靠。这里只把事实与范围写清楚。
可能的方向(不预设结论,涉及公共契约,应由维护者定)
MetadataLoader 声明 delete?( type: string, name: string ): Promise< void >,并在 unregister() 里把「声明了 write 却没有 delete」变成响的(一次性 error,或注册时就拒绝)。契约优先,作者期就能发现。
- 把删除能力搬进 contract:
capabilities 已经是声明面(read/write/watch/list),让 write 真正蕴含「可删」,并在 registerLoader() 落地时校验方法存在——declared = enforced,注册期就炸,而不是删除期静默。
- 维持现状,只补一条
registerLoader() 期的诊断。
方向 1/2 会动 MetadataLoader 这个公共接口(第三方 loader 的实现面),因此明确不在 #5259 内擅自决定。
发现于 #5259 实现期(会话 session_01Pbu27iNUfQCHeuS551Rqo7);未认领,留给分诊。
发现于 #5259(
unregister()失效/删除顺序)实现期,不在该单范围内(改的是顺序与失败上报,不是 loader 契约),故单独立卡。当前无人踩到,按 observation-class 归档:挂finding,不挂pm:queue,留给分诊决定要不要动契约。事实
packages/metadata/src/loaders/loader-interface.ts的MetadataLoader接口声明了可选的但完全没有
delete。于是MetadataManager.unregister()只能在调用点鸭子类型地探它(#5259 之前是两处as any,之后是一个具名的DeletableMetadataLoader形状 +typeof … === 'function'守卫,casts 收敛了,契约缺口没有变):也就是说:一个 loader 只要声明
protocol: 'datasource:'+capabilities.write: true,register()就会往它写(save有就写、没有就跳过),而unregister()在它没有delete方法时一声不吭地跳过——不是warn,不是 #5259 新加的error,什么都没有。随后unregister()照常删 registry、失效 listCache、publishRealtimeMetadataEvent('deleted')并notifyWatchers({type:'deleted'})。后果与 #5259 处理的「delete 抛错」完全同类,而且更安静:
list()/get()直接把它读回来(registry ∪ loaders 合并),重启后依旧;unregister()先失效 listCache 再删 loader:删除落库前到达的并发 list() 会把「已删项」重新缓存满 30s,之后没有任何东西再失效它 #5259 的区别只有一点:失败路径现在响(error,带后果与修复),而「压根没有 delete 方法」这条路径完全没有信号。capabilities.write的语义因此是分叉的:对register()它意味着「可写」,对unregister()它什么也不保证。declared ≠ enforced 的标准形状。为什么现在没人踩到(所以是 observation,不是 P0)
全仓
protocol: 'datasource:'的 loader 只有一个 ——packages/metadata/src/loaders/database-loader.ts—— 而它有async delete(type: string, name: string): Promise< void >(第 1199 行)。所以今天任何用户路径都不会走到这个静默分支;它是给第三方/未来 loader 留的坑,以及给「AI 写一个新 loader」留的坑:接口没声明delete,照着接口实现出来的 loader 天然就少这个方法,而它声明write: true时不会被任何东西拦下。严重度不由我判:#5259 正文自己也说过「是否一并处理请分诊定夺」,而按 objectstack#4949 的纪律,填卡时判的严重度两个方向都不可靠。这里只把事实与范围写清楚。
可能的方向(不预设结论,涉及公共契约,应由维护者定)
MetadataLoader声明delete?( type: string, name: string ): Promise< void >,并在unregister()里把「声明了write却没有delete」变成响的(一次性error,或注册时就拒绝)。契约优先,作者期就能发现。capabilities已经是声明面(read/write/watch/list),让write真正蕴含「可删」,并在registerLoader()落地时校验方法存在——declared = enforced,注册期就炸,而不是删除期静默。registerLoader()期的诊断。方向 1/2 会动
MetadataLoader这个公共接口(第三方 loader 的实现面),因此明确不在 #5259 内擅自决定。发现于 #5259 实现期(会话
session_01Pbu27iNUfQCHeuS551Rqo7);未认领,留给分诊。