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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 08:03
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 65159ae Aug 6, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5707-layered-overlay-outage branch August 6, 2026 08:19
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