Skip to content

MetadataLoader 不声明 delete?:一个 capabilities.write 的 datasource loader 若没有 delete 方法,unregister() 会静默跳过它并照常宣告「已删除」 #5276

Closed

Description

@os-zhuang

发现于 #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 的纪律,填卡时判的严重度两个方向都不可靠。这里只把事实与范围写清楚。

可能的方向(不预设结论,涉及公共契约,应由维护者定)

  1. MetadataLoader 声明 delete?( type: string, name: string ): Promise< void >,并在 unregister() 里把「声明了 write 却没有 delete」变成响的(一次性 error,或注册时就拒绝)。契约优先,作者期就能发现。
  2. 把删除能力搬进 contract:capabilities 已经是声明面(read/write/watch/list),让 write 真正蕴含「可删」,并在 registerLoader() 落地时校验方法存在——declared = enforced,注册期就炸,而不是删除期静默。
  3. 维持现状,只补一条 registerLoader() 期的诊断。

方向 1/2 会动 MetadataLoader 这个公共接口(第三方 loader 的实现面),因此明确不在 #5259 内擅自决定。

发现于 #5259 实现期(会话 session_01Pbu27iNUfQCHeuS551Rqo7);未认领,留给分诊。

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 4, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 5, 2026
  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    发现分诊轮判级(session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):晋级 pm:queue。前提核验见 #4604 01:57Z(对 b4ad98435 成立:loader-interface.ts 有 save? 无 delete)。理由:loader 无 delete 方法时 unregister() 静默跳过却照常宣告「已删除」—— 声明与执行脱钩的静默容错,正是两轴都要杀的形状。维护者可否决。


    Generated by Claude Code

  4. self-assigned this
    on Aug 5, 2026
  5. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮(engine-core 车道)
    会话:session_01V7WetGmnfoXNn8cLieKKmx
    分支:claude/issue-5276-loader-delete-contract
    Worktree:objectstack-issue-5276
    域:domain:engine-core
    文件面:packages/metadata/src/loaders/loader-interface.ts、packages/metadata/src/metadata-manager.ts(registerLoader 校验 + unregister 守卫)、metadata 包内测试、.changeset/*.md。

    方向裁决(PM 拍板,维护者可否决——依据:declared = enforced 是既有仓规范 PD #10,restore-invariant 类不升级):取方向 1+2 合并——MetadataLoader 接口声明 delete?(type, name): Promise<void>;registerLoader() 落地时校验「protocol: 'datasource:' 且 capabilities.write: true ⇒ 必须有 delete 方法」,不满足即响亮拒绝注册(作者期就炸);unregister() 现有守卫保留(对已注册 loader 由构造保证不可达,防御性存在)。方向 3(仅补诊断)被否:静默容错正是两轴要杀的形状。存量影响为零(全仓唯一 datasource loader database-loader.ts 已有 delete),对第三方是收紧,属「声明了能力就必须兑现」的契约兑现,非能力扩张。


    Generated by Claude Code

  6. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    复核通过,ACCEPT(engine-core 车道 PM,第 2 轮):PR #5652,CI 绿后转 ready 入队。

    交付按裁决(方向 1+2 合并)落地且证据完整:接口声明 delete?(DeletableMetadataLoader 临时形状连同两处 cast 退休);registerLoader() 门禁 assertWritableLoaderCanDelete 响亮拒绝,错误信息单一来源、写明后果与两条修法;registerLoader() 为 this.loaders 唯一写入口已核实(构造函数入口也走它);unregister 守卫留作防御性代码并注明不可达性来源。11 例新测试 + 反向验证(摘门禁 → 恰 4 条拒绝用例红、7 条范围/正例/回归绿,与预测一致);存量唯一受影响 stub 按意图补 delete,顺带删掉的三处 as any 使接口改动真正承重;tsc 前后 92→92 净零新增;消费半径(rest/NodeMetadataManager/cloud)逐一核过均不在门禁范围。

    顺带发现 #5654(save 侧同形状缺口 —— #5652 之后 delete 侧 declared=enforced 而 save 侧仍脱钩,不对称)已核实立单为 finding,归发现分诊轮定级;其修法(同一校验函数把 save 列为必需)已在卡内写明,届时是便宜活。


    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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions