fix(metadata-protocol): 元数据存储读不到不再被讲成「这一项不存在」 (#5532) - #5705
Merged
Conversation
…a missing item (#5532) The customization-overlay reads in `getMetaItems`/`getMetaItem` each wrapped their sys_metadata access in a bare `catch {}` and answered with their own empty value, so a metadata store the protocol could not reach was indistinguishable from an item nobody ever customised. The emptiness then travelled the read chain and each consumer named it differently and wrongly: `getMetaItemCached` as `Metadata item <type>/<name> not found`, the `state='draft'` read as `NO_DRAFT`/404, `getMetaItems` as `items: []`. ADR-0110 D3: a miss and an outage are different facts with opposite meanings. #5108 fixed this in DatabaseLoader's plural read and #5089 in listForIndex; this is the same rule on the protocol's own overlay reads. Discrimination is by error TYPE through `isMissingTableError` — the predicate DatabaseLoader (#5108) and SysMetadataRepository (#4867) already ask, so a driver quirk is taught to the platform once. The one benign reason (the table is not provisioned yet) still falls through to the registry; everything else throws 503 + SERVICE_UNAVAILABLE with the driver error as `cause`, which the REST boundary's existing #5437/#5464 sanitising and logging already handle. The terminal miss in `getMetaItemCached` is structured too: 404 + RESOURCE_NOT_FOUND, so a plain miss stops falling out of `mapDataError`'s catch-all as an unattributable 500 (and, pre-#5489, as a 400 shipping the internal wording verbatim). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
…meta-outage-vs-miss
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 02:05
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…jectstack-ai#5706) (objectstack-ai#5736) `getEffectiveLock` is the only source of truth for the ADR-0010 §3.3 lock gate, and both callers are write-path admission — `assertLockAllowsWrite` (save/publish/rollback) and `assertLockAllowsDelete`. Its overlay read was wrapped in a bare `catch` that fell through to `lock: 'none'`. `'none'` is not a neutral placeholder there: it is the verdict "the author declared no protection", which `evaluateLockForWrite` / `evaluateLockForDelete` turn straight into "allow". A `sys_metadata` read that FAILED therefore became a write that was PERFORMED on an item whose overlay row declared it protected. Measured on origin/main, with the row carrying `_lock` and only the gate's own read rejecting: `saveMetaItem` resolved `success: true` after `update:sys_metadata` on a `no-overlay` item, and `deleteMetaItem` the same on a `no-delete` one — while the same rows read successfully produce 403 ITEM_LOCKED. The audit trail did not compensate: the allowed path writes its ordinary `outcome: 'allowed'` row. Reuses `rethrowUnlessMetadataStoreUnprovisioned` (objectstack-ai#5705) rather than inventing a second predicate — an unprovisioned `sys_metadata` genuinely has no overlay row, so `'none'` is the truth and first boot still saves; every other error becomes 503/SERVICE_UNAVAILABLE with the driver error as `cause`. Wire-visible, and deliberate: refusing one uncertain write beats performing one that had to be refused. Unaffected and pinned by regression tests: artifact-level locks (answered from the in-memory registry before the overlay read), a genuine miss on a healthy store, and control-plane kernels. Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5532
这条修的是什么
sys_metadata读不到的时候,协议层会把「读不到」讲成「这一项不存在」。四处customization-overlay 读各自裹着一个裸
catch {},注释自己就写着 "DB notavailable",然后照 miss 处理;空值一路穿过读链,每个消费方给它起了一个不同却
同样错的名字:
getMetaItemCachedMetadata item object/acct not foundgetMetaItem?state=draftNO_DRAFT/ 404「没有待发布的草稿」getMetaItem?preview=draftgetMetaItemsitems: []「这个环境一个都没声明」对客户端的后果就是 issue 描述的那个:Studio / Setup 在元数据库故障期把每一个对象
显示成「不存在」,处置方向和真相完全相反。
ADR-0110 D3 已经为这件事立过规矩:miss 与 outage 是两个不同的事实、安全含义相反。
#5108 按这条修掉了
DatabaseLoader的复数读,#5089 修掉了listForIndex;本 PR 是同一条规矩在协议自己的 overlay 读上,单数与复数、active 与 draft 一并覆盖。
前提复核(issue 是线索,不是规格)
按 issue 的 harness 在合并后的
origin/main上实跑(真ObjectStackProtocolImplementation,驱动每个读方法都 reject):
根因前提成立,并且 issue 列为「未验证」的两处同型点(draft 分支、复数读)实测
确属同病,同 PR 一并修。
次生事实已漂移,如实记录:issue 写的 wire 结果是「400 无 code,内部措辞逐字上线」。
那是 #5489 落地前的形状。今天
mapDataError的终末兜底已经是UNCLASSIFIED_FAULT(500+INTERNAL_ERROR,措辞 withheld),实测:所以本 PR 的 wire 翻转不是 PM 分诊预设的
400 -> 404,而是500 -> 404。方向不变(把错误答案改成对的答案),但值不同,changeset 与测试都按实测写。顺带一提:这也让
方向 C 比立单时更值得做 —— 一个真的 miss 现在被当成服务器故障回 500。
改了什么
1. 判别按错误类型,不按异常猜(方向 A)
唯一良性的读失败是「
sys_metadata还没建表」:那时确实没有 overlay 行,落回registry 就是真相,首次启动不该爆炸。判定走
isMissingTableError——DatabaseLoader(#5108)与本包
SysMetadataRepository(#4867)问的同一个谓词,一个驱动怪癖只教给平台一次。其余一律不良性(保守方向和该谓词自身一致:认不出的错误 不是 良性)。
新增一个私有方法
rethrowUnlessMetadataStoreUnprovisioned,四处 overlay 读共用。2. 故障照实上报
抛
status: 503/code: SERVICE_UNAVAILABLE,驱动原始错误挂cause。503而不是500:请求本身没有任何问题,这是可能自愈的依赖故障,调用方/代理应该 重试 —— 和
mapDataError给ERR_DATASOURCE_UNAVAILABLE的裁决一致。SERVICE_UNAVAILABLE是标准目录里 503 自己的码(HttpStatusErrorCodeMap[503],ADR-0112),不是新造的字符串;没有 往 error-code ledger 里加新词汇 —— 今天没有
任何被测量到的消费方需要比「503 元数据面不可用」更细的粒度,按创业期聚焦原则,
词汇跟着需要它的实现一起回来。
写侧两个 500 正是当初拼进去、原样上线的标本)。运维照样拿得到 —— 驱动错误在
cause上,logWithheldServerFault会把整个对象打出来。3. 终末 not found 结构化(方向 C)
getMetaItemCached的终末 miss 现在带status: 404/code: RESOURCE_NOT_FOUND(标准目录的 404 floor,也正是这条路由 app 可见性闸门已经在发的那个拼写)。
⛔
packages/rest生产码一行未改,packages/metadata也没碰:接住方(#5437 / #5464 / #5489)本来就是对的,缺陷在产出方。rest 侧只加了断言测试。
wire 可见变化
500+INTERNAL_ERROR(#5489 前:400逐字上线)503+SERVICE_UNAVAILABLE?state=draft)404+NO_DRAFT503+SERVICE_UNAVAILABLE200 {"items":[]}503+SERVICE_UNAVAILABLE500+INTERNAL_ERROR404+RESOURCE_NOT_FOUNDsys_metadata尚未建表反向验证(方向在跑之前先定)
两半都预判为「普通红」,并且 红得不一样,这正是要点:
} catch { /* DB not available */ }还原 → 7 红 / 5 绿,而且红成 issue报告的形状(单数与 preview 读 RESOLVE 出无 item,复数读 resolve
{items: []},draft 读抛 404)。预判是 6(六个 outage 用例);第七个是 miss/outage 对照用例,它的
outage 那一半 就是那六个之一 —— 如实记下来,不四舍五入。
throw new Error('Metadata item …/… not found')还原 →3 红 / 9 绿:两个「真 miss 是结构化 404」用例,加上首次启动的良性 miss 用例,
全部红在
status/code是undefined上;而每一个 503 用例保持 绿。这个分离是刻意的 —— 它证明 404 是方向 C 自己的贡献,不是 outage 拆分的副产品。
「良性 / 健康」那个 describe 是反向的护栏:它专门抓过度收紧 —— 把首次启动、或者一个
本来就不存在的条目,也讲成故障。
Fixture 分诊(消费半径,不是编辑包)
改动在
packages/metadata-protocol,踩红的 fixture 在packages/objectql:protocol-meta.test.ts两个用例把旧行为钉死了 ——"should handle DB errors gracefully and return undefined item" / "…return empty items",
喂
new Error('DB down')然后断言答案是空值。按三分诊里的 整条替换:它们的断言之所以通过,正是因为故障被吞掉了。各自换成「不可读 = 上报 503」并 补上 良性对照
(「未建表仍然优雅降级」)—— 老用例里对的那一半保留下来了。
测试
新增:
packages/metadata-protocol/src/protocol.metadata-store-outage.test.ts(12 用例)packages/rest/src/rest-meta-outage-vs-miss.test.ts(6 用例,只加断言、不动 rest 生产码)改写:
packages/objectql/src/protocol-meta.test.ts(2 个旧 fixture → 4 个用例)跑过(合并
origin/main之后重跑一遍,含 #5682):范围外发现
同文件里还有两处同族的裸 catch,不在本 PR 范围,已另开 issue,按 Prime
Directive #10 不夹带修:
getEffectiveLock的 overlay 读 —— 故障时 lock 落成'none',即保护闸门 fail-open;getMetaItemLayered的 overlay 读 —— 故障时 overlay 静默为null,三层视图把「读不到」画成「没有 overlay」。
🤖 Generated with Claude Code
https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
Generated by Claude Code