fix(objectql,metadata-protocol): a deleted runtime-created overlay leaves the registry, so list/get/dispatch agree (#5079) - #6687
Conversation
…n delete (#5079) Deleting a runtime-CREATED metadata item removed its `sys_metadata` row and reported `reset: true`, while `GET /meta/<type>`, `GET /meta/<type>/<name>` and the ADR-0110 D3 declaration gate all kept serving it for the life of the process. #4521's write-through registers such an item under the SchemaRegistry's PLAIN key; the delete's heal (`removeRuntimeShadow`) only un-shadows a packaged artifact, and a runtime-created item has none — so nothing ever removed it. `restoreArtifactRegistryView` now walks the layers under the deleted overlay and stops at the first that can serve the name: composite-key artifact, then a MetadataService baseline, then — new — retire the plain-key entry through `SchemaRegistry.removeOverlayEntry`. The layer-2 read is diagnosed, so a degraded metadata plane stops the walk instead of retiring on an unread answer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
…ete-overlay-listing
…ete-overlay-listing
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31258757569 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue steward — requeue as-is (known signature, cross-repo generic ledger row). This PR was dropped from the merge queue by the Signature (read from the complete log archive, not the tail — SKILL note 7)
The second red job is the consequence, not an independent fault. Causality is taken from the aggregator's own declared inputs, not from log adjacency (SKILL note 7): Ledger basis (#5810): hits the cross-repo generic table's only row — GitHub Actions runner loss / npm registry 5xx / network timeout (infrastructure jitter, unrelated to the diff). The tarball transfer was truncated during toolchain provisioning, upstream of any repository code, so this PR's diff cannot reach it. It does not match any row of the objectstack table — in particular it is not a recurrence of the fixed Action: queue steward requeue as-is. Auto-merge re-enabled unchanged; no code, no ready/draft switch, no merge. Per SKILL note 2's annotation discipline this hit changes no fix scope, so no follow-up was added to any flaky issue. Related precedent: the same class hit Generated by Claude Code |
Fixes #5079
STEP ONE — 前提复核:残留仍在,且比 issue 报告的更宽
这张卡被刻意压在 #5086 / #5251 / #5259 之后,所以先在今天的
origin/main上真机复现,而不是照着 rc.3 的 repro 写修复。启动真实 showcase(pnpm dev -- --fresh -p 39517,env_local,better-sqlite3),用 admin 会话跑完整循环:premise_still_valid: true。补充证据:等待 60 秒以上后三面答案完全不变(不是 TTL,是进程内常驻);sqlite 直查sys_metadata中已无rg_clean行(sys_metadata: []),即行确实删掉了。两处对 issue 描述的实测订正(都写进本 PR 的动机)
①
GET /api/v1/meta/action/rg_clean也是陈旧的。 issue 只报告了 listing;实测单项读同样返回 200 和已删除的 body。所以受影响的不是「枚举视图」,而是全部元数据读取面。② 「dispatch correctly 404s」是巧合,不是正确失效。 注意上面两条 404 的文案不同:
has no declaration—— 来自domains/actions.ts的 ADR-0110 D3 分支(!actionDef)。on object 'showcase_task' not found—— 来自executeRegisteredAction返回dispatched: false后的 handler-miss 分支。也就是说 DELETE 之后声明仍然被解析出来了(
actionDef为真值),ADR-0066 D4 权限门仍在拿一个已不存在的声明做判定;404 只是因为ObjectQLPlugin.resyncAuthoredActions正确地把 handler 从注册表里摘掉了。dispatch 面并没有「同步失效」,它是被另一条独立的、恰好正确的路径救回来的。这一条把本卡从「listing 缓存没刷」改写成「删除只失效了 handler 注册表,元数据层一个都没失效」。根因:不在
getMetaItems,在删除侧的 registry healSchemaRegistry.removeRuntimeShadow的注释把它的保守性写得很清楚:在 #4521 之前这是对的 —— 平键位置只可能是 artifact 的影子。#4521 之后不再成立:
saveMetaItem现在会把 overlay 写穿进 registry 的平键,于是一个 runtime 新建项(底下没有任何 artifact)也住在平键上;而它的 DELETE 走restoreArtifactRegistryView→removeRuntimeShadow→ 因为找不到复合键 artifact 而直接放弃,此后没有任何代码路径会删掉这个条目。所以:getMetaItems的第一步registry.listItems(type)一直带着它 → LIST 陈旧;getMetaItem第 3 步registry.getItem(type, name)命中 → GET 陈旧;resolveRouteActionDeclaration的 source 2 命中 → 声明门陈旧。一个种子,三个面,而不是三个独立的缓存缝。
旁证:这个 bug 已经被消费者侧绕行过。
packages/plugins/plugin-security/src/permission-set-projection.ts的readDeclaredBody带着这样一段注释:同一条缝,
permission类型上的同一症状,在消费者侧打了补丁。按 Prime Directive #12,本 PR 在生产者侧修,不动那段绕行(它同时还挡 projection echo,属于别的事)。修复
restoreArtifactRegistryView改成沿被删 overlay 下面的层逐级下探,停在第一个能提供这个名字的层:removeRuntimeShadow,行为逐字未变,返回true即「shipped 值已重新可见」,直接结束;SchemaRegistry.removeOverlayEntry(type, name)退役平键条目。第 3 步等于让 registry 说出 #5927 已经让 receipt 说出的那句区分:
reset to artifact default(artifact-backed)对it no longer exists(runtime-only)。收据早就讲了真话,registry 没有。两条边界是刻意守住的:
removeOverlayEntry只删平键,永不触碰复合键;并且对「平键条目自己就是 artifact」的情况(_packageId有值、非sys_metadata补水哨兵、非 tenant-authored —— 与getArtifactItem自己的 bare-key fallback 同一谓词)直接拒绝。loadMetadataFromService会把 item 自己的_packageId透传给registerItem,所以 package-shipped item 确实可能落在平键上,把它注销比本 PR 要修的陈旧问题更严重。undefined授权第 3 步退役条目),所以它从裸get改走readItemFromMetadataService:降级读会中止下探、不退役任何东西。原来的[#5840]注释说这次读 "decides nothing" —— 当时为真,现在为假,注释已按实况改写,顺带复用同一个 helper 的单复数重试(而不是第三份手抄)。文件归属(认领申报面之外的说明)
packages/metadata-protocol/src/protocol.tsrestoreArtifactRegistryView(:7615一带),即deleteMetaItem的 registry heal helper。未触getMetaItems、updateData(#6479,:5743)、assertRuntimeAuthoringRules(#6285,:2405)—— 无区域冲突。packages/objectql/src/registry.tspackages/metadata*registry/cache invalidation files where the seam actually lives"。实测缝就在这里:SchemaRegistry住在packages/objectql,不在packages/metadata*。缺的原语(能删平键、又不误删 artifact)只能由持有那张 Map 的类提供;放在 protocol 侧就得从外面操作registry.metadata,那才是真正的越界。新增一个方法,removeRuntimeShadow逐字未动。packages/objectql/src/protocol-registry-shadow.test.tsremoveRuntimeShadow的 pin 就在这里),新原语的 pin 与它同处一室,避免两半语义分家漂移。packages/runtime/src/meta-overlay-read-your-writes.test.ts测试
新增 13 例(6 例 protocol 级 + 5 例 registry 级 + 既有 pin 未改)。
反向验证 —— 方向事先预测为「红」,实测为红。 抽掉两个源文件(
git checkout origin/main -- …,未用git stash)并重新 build 后:装回后两套件全绿。其余 4 例(artifact-backed reset、draft discard、跨类型同名、never-written 空删)是两侧都必须绿的边界 pin —— 它们红了才说明修复越界。
包级(merge
b230e5efd之后重跑):真机复跑(重新 build、换端口 39519、fresh DB):DELETE 之后
LIST total=70 / rg_fixed_present=false、GET → 404、POST → 404 "has no declaration"—— 与该项从未被创建时逐字相同的文案,即订正 ② 指出的那条错误 404 也一并消失。artifact-backed 非回归同样真机验过:
PUT showcase_mark_done(label=CUSTOMIZED)→LIST显示 CUSTOMIZED →DELETE(收据reset to artifact default)→LIST total=70, label=Mark Done,仍在列表里。门禁:按
.github/workflows/lint.yml逐条枚举执行(pnpm lint+ 全部check:*,共 56 步,OVERALL_FAIL=0;含check:engine-double-contract、check:route-envelope、check:error-code-casing、check:meta-type-normalized、check:nul-bytes、check:type-check-debt),merge 之后新增的check:kernel-hook-pairs单独补跑亦通过。本 PR 未引入任何新的 fake engine,所以不涉及assertEngineDeleteDispatch。与 #6602 的关系(明确回答,不假设)
不是同一条缝;本 PR 对 #6602 的影响是「更容易一点」,绝不更难,也不使它不必要。
environmentId, never on org #6602 讲的是applyRegistryWriteThrough与getMetaItems的 org-hydration 循环都只按environmentId设门、不按 org,导致 org A 的 overlay 泄进进程级 registry。本 PR 两处都没有触碰,diff 里没有applyRegistryWriteThrough,也没有那个 hydration 循环。environmentId, never on org #6602 的危害目前是一旦写进去就永久驻留(没有任何路径能移除那个平键条目);本 PR 之后,针对该(type, name)的一次 DELETE 至少能把它退役,泄漏窗口从「进程生命周期」缩短为「到下一次删除」。这缩小了 blast radius,但没有关闭 An org-scoped overlay row reaches the process-wide SchemaRegistry on unscoped kernels — both the write-through and the read hydration gate only onenvironmentId, never on org #6602 —— 一次没有对应删除的写入照样泄漏,所以 An org-scoped overlay row reaches the process-wide SchemaRegistry on unscoped kernels — both the write-through and the read hydration gate only onenvironmentId, never on org #6602 依然必须修,并且仍应按它自己说的「两个 seam 一起修」。environmentId, never on org #6602 修复者的观察(顺手说明,不在本 PR 修):restoreArtifactRegistryView连同removeRuntimeShadow/removeOverlayEntry都是按(type, name)寻址、对 org 无感的,所以一次 org-scoped DELETE 会退役那个共享平键条目,不问它是哪个 org 写的。这是平键模型本来就有的 org 盲区(removeRuntimeShadow一直如此),属于 An org-scoped overlay row reaches the process-wide SchemaRegistry on unscoped kernels — both the write-through and the read hydration gate only onenvironmentId, never on org #6602 完成范围里的第三个面;已在 An org-scoped overlay row reaches the process-wide SchemaRegistry on unscoped kernels — both the write-through and the read hydration gate only onenvironmentId, never on org #6602 上留言,不另开重复卡。未做的事
plugin-security那段消费者侧绕行(它还兼职挡 projection echo,拆它是另一件事)。getMetaItems:它没有 bug,它只是忠实地列出 registry 里还在的东西。Generated by Claude Code