fix(metadata,metadata-protocol,objectql): getDiagnosed —— get() 不再把 loader outage 答成「这一项没声明」 (#5840) - #6051
Conversation
…adata-get-degraded
…oader outage 答成「没声明」
MetadataManager.loadDiagnosed 算出的 ADR-0110 D3 判定,在两跳内被丢掉:
load() 只取 .data,get() 再把 null 变 undefined。六个消费点因此对
「读不到」与「这一项没声明」拿到同一个 undefined。
新增 getDiagnosed(type, name) -> { data, degraded, errors },即 loadDiagnosed
的 registry-first 对应物,并在 IMetadataService 上声明为可选成员。get() 本体
逐字不变(含 register() 观察者依赖的 microtask 时序),零破坏。
本车道内被测量为 gating 的消费点按各自语境处置:
- getMetaItem / getMetaItemCached:degraded 且 registry 也无 -> 503,不再落到 404
- getMetaItemLayered 的 code 层:与它 overlay 层同规矩(code: null 会派生
lockSource,outage 可把 _lock:'full' 渲染成 editable)
- ObjectQLPlugin 的 object 事件重读:warn(写已落地、只是重读失败)而非 error
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…adata-get-degraded
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31137943091 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
队列管家:原样重投(命中 #5810 台账 objectstack 表「已知 flaky」行,本签名今夜第 4 次命中) 踢出事实(两读数): 完整签名(完整 job 归档,⛔ 未看 tail): 台账四条件逐条核过:①签名逐字吻合(文件+用例+5000ms+行号 已核让行:处置前读本 PR 最近 30 分钟评论,仅 01:36:24Z 的 已执行:重新挂 auto-merge(GraphQL),⛔ 未改代码、未 rebase、未 force、未重跑。按 notes 2,纯计数命中不追记 #6044。
Generated by Claude Code |
Fixes #5840
前提复核(开工第一件事,
origin/main)单上「判定被算出来、然后在两跳之内被丢掉」的链条原样成立,行号已漂、内容定位:
metadata-manager.tsasync get(type, name)—— registry 命中直返,否则const result = await this.load(type, name); return result ?? undefined;load()——return (await this.loadDiagnosed< T >(type, name, options)).data;loadDiagnosed()——return { data: null, degraded: errors.length > 0, errors };loadDiagnosed的 TSDoc 正是为这件事写的(ADR-0110 D3,正文已引),而它算出的degraded到get()的调用方那里已不可达。第一步:6 个消费点逐点定性(裁决要求,实测)
单上表格的行号全部漂了;按内容重新定位,并多测出一个单上没列的(第 7 行)。
metadata-protocolgetMetaItem第 2 步(MetadataService 层)protocol.ts:3719 一带metadata-protocolgetMetaItemLayeredcode 层protocol.ts:3932 一带code: null派生lockSourcemetadata-protocolrestoreArtifactRegistryViewprotocol.ts:7349 一带objectqlsubscribeToMetadataEventsplugin.ts:712 一带warn(含后果与修法)plugin-securitysyncEvaluatorRegistrypermission-set-projection.ts:398mcpagent promptmcp-server-runtime.ts:443service-datasource的getDatasource/getObjectplugin.ts:82permission-set-projection:398的定性值得单独说,因为它与单上的预判相反、且结论更省事:它读的是自己刚写进内存 registry 的 projection echo(isProjectionEcho(current))。而get()是 registry 优先 —— echo 若在,根本走不到 loader,degraded不可能为真;degraded为真则意味着 registry 里没有 echo,此时「提前 return、什么都不治」本就是正确动作。所以这一点对 outage/miss 之分结构上不敏感,不需要跟着改。单上说它「最值得先测」,测下来是这个结果,如实记。mcp:443与service-datasource:82都是真误述(outage 被答成 "Agent X not found" / 数据源不存在),但都 fail-closed、不放宽任何访问,且分属 cli / services 车道 —— 按跨座位协议交 PM 转席。生产端:
getDiagnosedpackages/metadata/src/metadata-manager.ts新增即
loadDiagnosed的 registry-first 对应物。这个「registry 优先」不是细节,而是消费点不能直接改用loadDiagnosed的原因:后者只走 loader,换过去会跳过内存 registry、解析出不同的条目。registry 命中一律degraded: false(它没问过任何 loader);干净 miss 也一律degraded: false。措辞用既有的
degraded,⛔ 未抄 #5897 的storeUnavailable—— 后者是刻意收窄的单存储读措辞,这里是 loader 集合语义("至少一个 loader 抛了且没人答出这一项"),两者不是一回事。#5897 的 ACCEPT 评论已把这条区分记在案。同时在
packages/spec/src/contracts/metadata-service.ts的IMetadataService上把它声明为可选成员,与loadDiagnosed同例(#4127 batch 4 的先例:调用点与实现早已一致,缺的只是契约)。get()本体零破坏 —— 而且是被一条用例真实拦下来才做对的get()逐字未改,签名未改。最初的实现是return (await this.getDiagnosed(type, name)).data;—— 语义完全等价、更整洁 —— 它让register-notifies-watchers.test.ts的「订阅者重读看得见新 body」变红:notifyWatchersLocal用void callback(event)派发、从不 await,于是那条用例能成立靠的是get()内部的 microtask 跳数;多一个 async 帧就翻。处置:放弃委托写法,保留三行重复,把原因常驻进
get()的 TSDoc;重复的风险从另一侧钉住(新用例断言get()与getDiagnosed().data在每个 case 上一致)。那条用例本身的脆弱性是本 PR 范围外的发现,已按 PD #10 单独立案 #6043(finding,未入队)。⛔ 方向 (b)(
get()在 degraded 时抛)未走:改公共契约,且 boot 侧禁抄的理由已由 #5897 常驻在objectql/plugin.ts的 TSDoc 里。诊断是提供给调用方的,不是强加的。消费端处置:⛔ 不一刀切,逐点论证
(1)
getMetaItem/getMetaItemCached→ 503。 这半边有一句 main 上自己写下、但当时只有四分之三为真的注释:getMetaItemCached的 404 旁写着「reaching here now means a real miss ——getMetaItemthrows 503 rather than answeringundefinedwhen the store could not be read」。那对 overlay 读成立(失败以 throw 到达),对 MetadataService 读不成立(失败被MetadataManagerwarn 掉、以普通undefined到达)。本 PR 让这句话成为真的,并把这段因果写进了那条注释。刻意窄:放在 registry 兜底之后才判。registry 命中是一份真实声明,答案里没有任何无依据的断言,照旧原样服务;只有整条链什么都没解析出来、即答案本会是「这个不存在」时,degraded 才改变结果。
(2)
getMetaItemLayered的 code 层 → 503,与它 overlay 层对称。 这是最锋利的一处:code: null不是耸肩,是这个方法正面声明「不存在打包/代码层定义」,并且响应从它派生 ——lockSource = code ?? overlay ?? {}喂给resolveLockState,于是 code 层声明了_lock: 'full'的条目,在「本该找到那个锁的读失败了」时被渲染成editable: true, deletable: true。可用性故障放宽授权面,正是 ADR-0110 D3 点名要禁的事,而这个方法的 overlay 半边早已拒绝这么做(#5707)—— 两半不对称,只是因为 loader 失败在这一侧看不见。(3)
restoreArtifactRegistryView→ 不改。 返回 void,undefined不产生任何面向调用方的答案,方法自身的契约就写着 best-effort、下次 reload 自愈。给它加日志只会往一条已声明为静默的路径上添噪 —— 那是 AGENTS「Degradation log levels」明确警告的过度套用。就地留了注记,免得下一位读者把「没改」当成「漏了」。(4)
objectql事件重读 →warn,不是error。 这一处没有照抄旁边restoreMetadataFromDb的error(#5897),级别是按 AGENTS 那一节自己的判定问句诚实问出来的:「有没有本代码声称已持久化的东西没落地,而系统看起来照常?」没有 —— 写入早已落进 metadata store(事件正是它的通告),失败的是一次重读,registry 继续服务它已持有的定义。这是功能性降级(本 kernel 的副本落后),不是持久性降级;升成 error 就是那条规则点名的镜像错误,而且它会在 outage 期间每个事件打一次,而 boot 那条每进程只打一次。级别归级别,代价不能不说:新的
warn交付后果(registry 保留上一版定义、无人重试、读取继续服务陈旧 schema 直到后续事件成功或进程重启)与修法,这两样它替换掉的那句 debug 一样都没有。与 #5998(#5897)的关系:互补,非取边
派发时 #5998 尚在合并队列,故先做 metadata-manager 半边与 metadata-protocol 消费点;它合入后
git merge origin/main(⛔ 未 rebase,两次,main中途又动过一次)再动objectql/plugin.ts。无冲突,两份改动在同一文件里各占其位:storeUnavailable—— boot 期loadMetaFromDb的单一存储读(protocol.ts:10581 /plugin.ts:1171)degraded—— 运行期 metadata 服务的 loader 集合读测试与反向验证(方向先预测,后运行)
生产端新建
packages/metadata/src/metadata-manager-get-diagnosed.test.ts;消费端扩展相邻文件protocol.metadata-store-outage.test.ts(#5532/#5707 的同一份覆盖,第三处读加入同一条规矩);objectql 侧新建plugin-metadata-event-outage.test.ts,与 #5998 的plugin-restore-metadata-outage.test.ts并列,并在文件头写明为什么级别不同。测试替身只声明subscribe/get/getDiagnosed,不含任何引擎写动词,故无delete/updatedispatch 需要check:engine-double-contract扫描、也无守卫可手抄。三肢反向验证,方向均在运行前写死,结果逐条相符:
getDiagnosed改回走load()丢判定:预测 4 红 / 5 绿,实测 4 红 / 5 绿,且正是点名的那四条。绿的五条全是关于 miss 的断言 —— miss 从来不是坏掉的那半边,这正是 ADR-0110 D3 的形状。if (… degraded)分支:预测 5 红 / 3 绿,实测 5 红 / 3 绿,上方 18 条 getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532/getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」 #5707 用例全绿。metadataService.get(...):预测 3 红 / 3 绿,实测 3 红 / 3 绿。命令与真实输出(合并后重跑的结果)
推送前
git merge origin/main做了两次(⛔ 全程未 rebase);两侧packages/spec都动过,故按 AGENTS §10 重装、重建 spec 与三条依赖链后完整重跑,上列数字即最终合并态的结果。必答项
record.packageIdfrom a snake_case row — always undefined, every object overlay registers under the 'sys_metadata' sentinel at boot #4636(决策箱在途,loadMetaFromDb的 object 分支packageId登记):无交叠、无影响。本 PR 一行未碰loadMetaFromDb;getMetaItemLayered的 code 层与 loadMetaFromDb object branch readsrecord.packageIdfrom a snake_case row — always undefined, every object overlay registers under the 'sys_metadata' sentinel at boot #4636 的 snake_case 行读是两个不同轴(前者是「读没读到」,后者是「读到的行怎么取 packageId」)。loadMetaFromDb object branch readsrecord.packageIdfrom a snake_case row — always undefined, every object overlay registers under the 'sys_metadata' sentinel at boot #4636 若落地,落点在loadMetaFromDb内部,与本 PR 的诊断读法不相邻。check:durability-log-level结构性看不见「读接缝把故障答成空值」这一类 —— #4825 / #5108 全家都在闸门盲区里 #5186(check:durability-log-level对这一类盲区的门禁侧登记):变简单,且已部分被它接住。派发时它还是needs-user-decision;本 PR 开工期间其门禁侧已随 feat(scripts): durability 闸门新增「读接缝编造空值」规则 —— #4728/#4825/#5108 这一族终于有闸门了 (#5186) #5986 落地为check-durability-degradation-log-level.mjs里的 read-seam invention 规则,本 PR 在它下面跑绿(64 seam,none invents an unreported empty answer)。变简单的地方是定价:这一类盲区的两端(生产端要能表达判定、消费端要读它)本 PR 各给了一个可照抄的样本,且 objectql 那一处示范了「同一族、不同级别」的判定怎么论证 —— 门禁若要把get()这类registry-first 读纳入扫描面,现在有了一个getDiagnosed形状可以对齐,不必先发明一个。⛔ 未实现它(只读参照)。Generated by Claude Code