Skip to content

fix(metadata-protocol): 分层读的 overlay 读失败不再被画成「这一项没有定制」(#5707) - #5843

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5707-layered-overlay-outage
Aug 6, 2026
Merged

fix(metadata-protocol): 分层读的 overlay 读失败不再被画成「这一项没有定制」(#5707)#5843
baozhoutao merged 1 commit into
mainfrom
claude/issue-5707-layered-overlay-outage

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5707

getMetaItemLayeredsys_metadata overlay 读裹着一个裸 catch,故障期把「读不到定制」画成「没有定制」。本 PR 复用 PR #5705 引入的 rethrowUnlessMetadataStoreUnprovisioned,与该 PR 的四处 overlay 读、#5706 的锁闸读同语义。没有新增返回形状——issue 正文提到的「给 overlay 加第三态」那条路没有走(那是新契约,需维护者拍板),下文「裁定」一节说明为什么不需要。


一、第一步是测量:谁消费三层视图,故障期各自怎么处置

分诊晋级评论要求先测量再选修向。逐个消费方核实结果如下(objectstack 与 objectui 均以各自 origin/main 为准)。

1. 故障期今天到底发回什么(实测,不是推断)

packages/metadata-protocol 用「所有读都 reject connect ECONNREFUSED 10.0.0.5:5432」的引擎跑修复前的代码,getMetaItemLayered resolve 出这个信封(测试断言失败信息里逐字打印):

{"type":"object","name":"acct","code":{"name":"acct","label":"Account (packaged)"},
 "overlay":null,"overlayScope":null,"effective":{"name":"acct","label":"Account (packaged)"},
 "_diagnostics":{...},"lock":"none","editable":true,"deletable":true,"resettable":false}

HTTP 200,三个正面断言同时成立:overlay: null(从没被定制过)、overlayScope: null(两个作用域都没有行)、effective === code(生效的就是打包件原样)。没有任何字段能让消费方看出这是一次失败的读。

另一条实测:org 作用域读失败时,env 行即使完全可读也一并被吞——两次 lookup 在同一个 try 里,org 先跑,抛出即跳过 env。

2. REST:GET /meta/:type/:name?layers=true

packages/rest/src/rest-server.ts 的 layered 分支与普通读共用同一个 handler 的 try/catchhandleRouteError,#5532 已由 packages/rest/src/rest-meta-outage-vs-miss.test.ts 钉住该信封落地为 503 + SERVICE_UNAVAILABLE(文案按 5xx 规则 withheld,驱动错误经 cause 进日志)。所以 REST 侧零改动、零新分支。

3. objectui / Studio:有直接消费,且早已有错误通道

PM 预扫「未见直接命中」需要更正:objectui 消费得相当广。packages/data-objectstack/src/metadata-client.tsMetadataClient.layered() 拼的正是 ?layers=true,而它 对非 2xx 一律 throw parseError(res),只有 404 被映射成空信封。调用点:

调用点 故障期处置
metadata-admin/ResourceEditPage.tsx(4 处) 加载 try/catchsetError / setLoadFailed,渲染错误态
metadata-admin/EmbeddedItemEditor.tsx 自身 try/catch
metadata-admin/PermissionMatrixEditor.tsx .catch(() => null) 降级
studio-design/PackageOwdOverviewPanel.tsx .catch(() => null) 降级
studio-design/StudioDesignSurface.tsx(4 处) 走各自 surface 的加载错误路径

关键结论:503 不引入任何新形态。这些分支今天就要处理网络错、502、以及 #5532 之后普通读的 503;改动只是让「后端读不到」从谎报 200 变成走那条本来就存在的错误通道。反过来,前提里担心的「整体空白」恰恰是现状的风险面更大:今天作者看到的是一个渲染完整、内容为假的 diff 视图。

4. plugin-security:三个消费点,其中一个没有 try/catch —— 实测它

packages/plugins/plugin-security/src/permission-set-projection.ts 有三处:

  • effectiveBodyForRow(~618):有 try/catch,回落 permissionSetBodyFromRow(row);
  • reconcilePermissionSetProjection(~912):有 try/catch,layered = null;而它的外层在 security-plugin.ts(~1927)又整体裹了 try/catch + warn;
  • projectPermissionMutation(~480):没有 try/catch —— PM 点名要测的就是这处。

实测方式:把上面实测到的故障期信封,喂进真实的 projectPermissionMutation(临时 harness,已删除),记录它对 sys_permission_set 的写:

场景 结果
A. 改前 + 代码里有 declared body(打包集 + 定制 overlay) 不抛;UPDATE sys_permission_set {"id":"ps_1","label":"Contributor","customized":false} —— 定制被静默改回打包基线,customized 标记被抹掉
B. 改前 + 代码里没有 declared body(Studio 自建集) 不抛;DELETE sys_permission_set {"id":"ps_1"} + registry.unregisterItem(permission,...) —— 一次读失败把权限集记录直接退役
C. 改后 + declared 503 SERVICE_UNAVAILABLE,写次数 0
D. 改后 + undeclared 503 SERVICE_UNAVAILABLE,写次数 0

也就是说:这个安全面消费者在「静默 null」下的行为不是「退回 code 基线」这么温和——无 declared body 时它会删记录。503 化把一次破坏性写换成一次拒绝。

那这个抛会不会「把权限投影整体打挂」?测量三个入口:

  1. 协议的投影缝:runMutationProjector(protocol.ts)的契约就是 never throws —— 捕获后返回 { success: false, error },由 saveMetaItem / publishMetaItem / deleteMetaItem 作为 projectionApplied 附在响应上。既有测试已钉:packages/metadata-protocol/src/mutation-listeners.test.ts 的 "a throwing projector is surfaced as { success:false, error }, never thrown"。故写路径不会因此 500,只会如实报告投影没做成。
  2. boot 收敛:security-plugin.tsreconcilePermissionSetProjection 整体 try/catch + warn,启动不会被打断;况且它先读 sys_metadata 才进循环,读不到时循环根本不跑。
  3. 数据门 write-through:projectAndFetch(insert/update 路径)会把 503 上抛给调用者。这正是应该的:该路径此前的静默结果是「saveMetaItem 成功了,记录却被按 A/B 两种方式写坏或删掉,并返回一个残缺 record」。

前提核对结论:PM 给的前提成立 —— 503 化不会让任何测量到的消费面整体空白,也不会让 permission-set projection 比「静默退回基线」更糟;测量显示恰恰相反。

5. protocol.ts 里那句 "3-state diagnostic shape"(~7467)

那句话在 saveMetaItem 的防御分支里,指的是 { code, overlay, overlayScope, effective }三层信封(用于识别有人把读信封原样 PUT 回来),不是「每层三态」。本 PR 不动信封形状,所以该注释无需校准,措辞与改动一致。为免后人再把两者混读,getMetaItemLayered 的 TSDoc 里补了一句:层是 3-LAYER 不是 3-VALUE,「读不到」没有对应的层值,所以照失败上报。


二、裁定:复用 helper 走 503,不发明第三态

按预裁执行,前提已由上面的测量证实。补一条支持理由(不代维护者拍板,仅记录为什么第三态在这里也非必要):三层信封里的 null 已经被占用为「没有定制」,再往同一个字段塞「未知」需要新的取值词汇 + 全部消费方跟进;而所有实测到的消费方都已经有错误通道,拿不到收益。真需要「部分可用的诊断视图」时,那是新契约,应由维护者立单。

改动本身是一处 catch:

} catch (error) {
    // ...(注释见 diff)
    this.rethrowUnlessMetadataStoreUnprovisioned(error);
}

isMissingTableError(表尚未建 → 确实没有 overlay 行)良性放行,首次启动照旧只渲染 code 层;其余抛 status: 503 / code: SERVICE_UNAVAILABLE,驱动错误挂 cause

一个次级事实,顺带记录、本 PR 不处理:ADR-0010 的 lock / editable / deletable 三元组是从同一批层里算的(lockSource = code ?? overlay ?? {}),所以「只有 overlay、没有 code」的项在故障期还会额外报出 lock: 'none'。那是读侧展示,写侧闸门已由 #5706 fail-closed;503 化之后这个展示也一并不再产生。(resettable 只由 artifactBacked 决定,与 overlay 无关,不在此列。)


三、测试落点与反向验证

覆盖并入同族文件 packages/metadata-protocol/src/protocol.metadata-store-outage.test.ts(#5705getMetaItems / getMetaItem 建立的那批),不新开孤立文件:将来谁把其中一处 catch 重新放宽,会是同一个文件里的 diff,且共用 expectStoreUnavailable,信封不会按方法各自漂移。新增 6 例:

  • 故障 → 抛出与其他四处同一个信封(503 / SERVICE_UNAVAILABLE / ErrorCode 可解析 / 文案含 "unknown" 不含 "not found" / cause 是驱动错误本体);
  • 「两种失败此前渲染得一模一样,现在被分开」:同 registry、同请求、同引擎,只有错误类型不同 —— benign 仍 resolve 且 effective === code,outage 变 503;
  • org 作用域读失败时不再静默降级到那行「本来读得到」的 env overlay;
  • benign:未建表仍渲染 code 层(overlay/overlayScope 为 null、effective === code);
  • benign:未建表且哪都没有 → 仍是全 null 信封,不是 503;
  • healthy:正常存储照旧报出 overlay 与 overlayScope: 'env'

反向验证(方向在跑之前先写下:普通红)。} catch { /* DB unavailable — overlay stays null */ } 放回去,预测「3 红 / 3 绿」——3 个故障用例转红(resolve 而非 throw),3 个 benign/healthy 保持绿,以此证明改动是「故障与良性的分流」而不是「layered 现在会抛」。实测与预测一致:

× throws the same 503 envelope instead of resolving a fabricated 3-layer view
   → expected a rejection, but the call resolved with {"type":"object",...,"overlay":null,"overlayScope":null,"effective":{"name":"acct","label":"Account (packaged)"},...}
× the two failures that used to render IDENTICALLY are now told apart
× a failed ORG-scope read does not silently demote to the env row it never got to
✓ an unprovisioned sys_metadata still renders the code layer (benign, unchanged)
✓ an unprovisioned sys_metadata + nothing anywhere is still an all-null envelope, not a 503
✓ a healthy store still reports the overlay layer and its scope

上面第一条的失败文本,就是本 PR 第一节引用的那个「故障期实测信封」的来源。

既有的 packages/objectql/src/protocol-layered-get.test.ts(layered 的行为测试)全部照旧通过——它们用的引擎都能正常应答,不依赖这处吞异常,无需 fixture 分诊。


四、验证

pnpm --filter @objectstack/metadata-protocol test
  → Test Files 47 passed (47) / Tests 457 passed (457)

pnpm --filter @objectstack/metadata-protocol exec vitest run protocol.metadata-store-outage
  → Test Files 1 passed (1) / Tests 18 passed (18)   (12 既有 + 6 新增)

pnpm --filter @objectstack/objectql exec vitest run protocol-layered-get protocol-meta protocol-save-meta-repo-path
  → Test Files 6 passed (6) / Tests 119 passed (119)   (对重新构建的真实协议跑)

npx tsc --noEmit  (packages/metadata-protocol,该包按台账无 typecheck 脚本、记 28 条 DEBT)
  → 输出里 protocol.ts 与 protocol.metadata-store-outage.test.ts 均 0 条,未新增类型错误

node scripts/check-nul-bytes.mjs → OK (5699 files)
pnpm check:error-code-casing / check:durability-log-level / check:engine-double-contract / check:adr-anchors → 全绿

已加 changeset(@objectstack/metadata-protocol: patch)。


五、给后续同文件单的定价回答(PM 必答项)

影响 依据
#5265(保存回执一律自称 customization overlay) 完全无影响 落点是 saveMetaItem 的两处成功 message 模板(写路径),与本 PR 的读路径 catch 无共享代码、无共享测试。仅同文件,merge 面不重叠。
#5079(deleted overlay 仍出现在 /meta listing) 完全无影响(诊断上略微更可信) 其因在 delete 后枚举视图/缓存未失效,落点是 delete 路径与 getMetaItems 的 registry 视图。唯一间接关系:该单的排查要对照 listing / layered / dispatch 三面,而故障期 layered 不再谎报,只会让对照更可信;工作量不变。
#4636(loadMetaFromDb object 分支 record.packageId 恒 undefined) 完全无影响 另一个方法(loadMetaFromDb)、另一类 bug(snake_case 键名读错),需要的是 boot-hydrate pin 测试,与本 PR 的 outage 覆盖不相交。注意同文件串行即可。

六、范围外发现(已单独立单,未在本 PR 修)

两条都按 Prime Directive #10 立单、unassigned,严重度交 PM 分诊。


Generated by Claude Code

…lay instead of "no overlay" (#5707)

The layered read's `sys_metadata` overlay lookup was wrapped in a bare
`catch` that fell through to `overlay: null`. That is not a neutral
placeholder: the envelope then makes three positive claims at once, with
HTTP 200 — `overlay: null` ("never customised"), `overlayScope: null`
("no scope holds a row") and `effective === code` ("what runs today is
the packaged artifact, verbatim"). The Studio diff tab exists to answer
"what did I change?"; during an outage it answered "nothing".

Same defect and same discrimination as #5532 / PR #5705, on the one
overlay read that PR deliberately did not reach (scope = the issue):
`rethrowUnlessMetadataStoreUnprovisioned` lets `isMissingTableError`
through (an unprovisioned `sys_metadata` genuinely holds no overlay row,
so first boot still renders the code layer) and turns every other error
into `503` / `SERVICE_UNAVAILABLE` with the driver error on `cause`.

No new return shape: the envelope stays a 3-LAYER shape, not a 3-VALUE
one. Coverage joins the same-family outage file rather than a new
isolated one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 7:47am

Request Review

@github-actions github-actions Bot added the size/m label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」

2 participants