发现于 #5276(PR #5652)实现期,不在该单的裁决范围内(PM 明确把方向 1+2 收在 delete 上),故单独立卡。当前无人踩到,按 observation-class 归档:挂 finding,不挂 pm:queue,留给分诊决定。
事实
packages/metadata/src/metadata-manager.ts 的 register() 持久化循环:
for (const loader of this.loaders.values()) {
if (loader.save && loader.contract.protocol === 'datasource:' && loader.contract.capabilities.write) {
await loader.save(type, name, data);
}
}
loader.save && 排在最前面:一个声明了 protocol: 'datasource:' + capabilities.write: true 却没有 save 方法的 loader,会被一声不吭地跳过——没有 warn,没有 error,什么都没有。随后 register() 照常写内存 registry、失效 listCache、广播 created/updated 事件并 notifyWatchers,调用方被告知写入成功。
这与 #5276 描述的 delete 侧缺口是同一个形状:capabilities.write 声明了能力,执行期却在方法缺席时静默降级,而不是拒绝。区别只在于失败方向——delete 侧丢的是删除,save 侧丢的是持久化(内存 registry 仍有,所以本进程内读得到,重启后消失)。按 AGENTS.md「Degradation log levels」的判据,这属于耐久性/一致性降级(系统看起来一切正常,声称已持久化的东西没落地),连 warn 都不该是,应该是拒绝或 error。
#5276 的修复(PR #5652)在 registerLoader() 上加了门禁:protocol: 'datasource:' 且 capabilities.write: true ⇒ 必须实现 delete(),否则响亮拒绝注册。门禁刻意只覆盖 delete 一侧(PM 裁决的范围),于是现在的状态是:
- 同一个
capabilities.write 声明,delete 侧是 declared = enforced(注册期就炸);
- save 侧仍是 declared ≠ enforced(注册通过,写入静默丢失)。
如果要收口,最小改动就是在 registerLoader() 的同一个校验函数里把 save 也列为必需方法(assertWritableLoaderCanDelete 相应改名),错误信息同风格。但这会再次收紧公共接口 MetadataLoader(第三方 loader 的实现面),按仓规范不该由实现方擅自决定,所以只填卡。
为什么现在没人踩到
全仓 protocol: 'datasource:' 的 loader 只有 packages/metadata/src/loaders/database-loader.ts,它 save 和 delete 都有。仓内其余 loader 都不是 datasource: 协议(MemoryLoader = memory:、FilesystemLoader = file:、RemoteLoader = http:),register() 从不写它们。所以今天任何用户路径都走不到这个静默分支;它是给第三方/未来 loader 和「AI 照着接口写一个新 loader」留的坑——save? 在接口上是可选的,不实现完全合法,而声明 write: true 时(在 #5652 之后)只有 delete 会被拦下。
严重度不由我判(objectstack#4949:填卡时判的严重度两个方向都不可靠),这里只把事实与范围写清楚。
相关:#5276(delete 侧,已由 PR #5652 关闭)、#5259(unregister 的失效/删除顺序与失败上报)。
发现于会话 session_01V7WetGmnfoXNn8cLieKKmx;未认领,留给分诊。
发现于 #5276(PR #5652)实现期,不在该单的裁决范围内(PM 明确把方向 1+2 收在
delete上),故单独立卡。当前无人踩到,按 observation-class 归档:挂finding,不挂pm:queue,留给分诊决定。事实
packages/metadata/src/metadata-manager.ts的register()持久化循环:loader.save &&排在最前面:一个声明了protocol: 'datasource:'+capabilities.write: true却没有save方法的 loader,会被一声不吭地跳过——没有 warn,没有 error,什么都没有。随后register()照常写内存 registry、失效 listCache、广播created/updated事件并notifyWatchers,调用方被告知写入成功。这与 #5276 描述的 delete 侧缺口是同一个形状:
capabilities.write声明了能力,执行期却在方法缺席时静默降级,而不是拒绝。区别只在于失败方向——delete 侧丢的是删除,save 侧丢的是持久化(内存 registry 仍有,所以本进程内读得到,重启后消失)。按 AGENTS.md「Degradation log levels」的判据,这属于耐久性/一致性降级(系统看起来一切正常,声称已持久化的东西没落地),连warn都不该是,应该是拒绝或error。#5652 之后的不对称
#5276 的修复(PR #5652)在
registerLoader()上加了门禁:protocol: 'datasource:'且capabilities.write: true⇒ 必须实现delete(),否则响亮拒绝注册。门禁刻意只覆盖 delete 一侧(PM 裁决的范围),于是现在的状态是:capabilities.write声明,delete 侧是 declared = enforced(注册期就炸);如果要收口,最小改动就是在
registerLoader()的同一个校验函数里把save也列为必需方法(assertWritableLoaderCanDelete相应改名),错误信息同风格。但这会再次收紧公共接口MetadataLoader(第三方 loader 的实现面),按仓规范不该由实现方擅自决定,所以只填卡。为什么现在没人踩到
全仓
protocol: 'datasource:'的 loader 只有packages/metadata/src/loaders/database-loader.ts,它save和delete都有。仓内其余 loader 都不是datasource:协议(MemoryLoader=memory:、FilesystemLoader=file:、RemoteLoader=http:),register()从不写它们。所以今天任何用户路径都走不到这个静默分支;它是给第三方/未来 loader 和「AI 照着接口写一个新 loader」留的坑——save?在接口上是可选的,不实现完全合法,而声明write: true时(在 #5652 之后)只有delete会被拦下。严重度不由我判(objectstack#4949:填卡时判的严重度两个方向都不可靠),这里只把事实与范围写清楚。
相关:#5276(delete 侧,已由 PR #5652 关闭)、#5259(unregister 的失效/删除顺序与失败上报)。
发现于会话
session_01V7WetGmnfoXNn8cLieKKmx;未认领,留给分诊。