Skip to content

fix(metadata-protocol,objectql): loadMetaFromDb 用返回值表达「没读到存储」,boot 侧不再把 outage 记成空库 (#5897) - #5998

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5897-hydration-outage-diagnostic
Aug 6, 2026
Merged

fix(metadata-protocol,objectql): loadMetaFromDb 用返回值表达「没读到存储」,boot 侧不再把 outage 记成空库 (#5897)#5998
baozhoutao merged 2 commits into
mainfrom
claude/issue-5897-hydration-outage-diagnostic

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5897

前提复核(裁决要求,开工第一件事)

PM 裁 A 带两条前提,均在 origin/main 上复核成立:

前提 1 —— 返回值的生产消费方仍只有 restoreMetadataFromDb 成立。
git grep -n loadMetaFromDb 全仓命中中,读返回值的非测试代码只有
packages/objectql/src/plugin.ts:1140。其余源码命中全部是注释
(registry.ts:1444metadata-core/src/objects/index.ts:14
plugin-security/src/permission-set-projection.ts:264
driver-sql/src/sql-driver.ts:1796metadata-protocol/src/plugin.ts:39),
测试侧 5 个文件调用它但都是断言,不构成生产消费方。

前提 2 —— ProtocolWithDbRestore 不在 packages/spec 公开契约面。 成立。
git grep -ln ProtocolWithDbRestore 全仓只命中一个文件
packages/objectql/src/plugin.ts;packages/spec 零命中。该接口是消费侧自己
声明的结构化(鸭子)类型,不是 wire contract。

改了什么

loadMetaFromDb 的返回值 { loaded, errors, invalid } 没有任何字段能表达
「这次水合根本没读到存储」—— 读不到的数据库与真正的空库都答 loaded: 0
唯一消费方因此无从分支:它唯一的分支只是在两条日志之间做选择,而「什么都没
回来」那一侧是 debug 级的 No persisted metadata found in database。于是
一个字都没读到持久化元数据的 kernel,在 debug 级上宣称「本来就没有」,然后
照常报告 ready。

代价不是假设,写在 plugin.ts 自己的 Phase 2 注释里:registry 为空时
registry.getObject 把「读不到」答成「没声明」—— unknown-column 查询守卫、
hooks、relationships 静默降级,overlay 对象既不建表也不桥接。ADR-0110 D3
(outage ≠ miss)在 boot 侧的落地,继 #5108 / #5089 / #5532 / #5707 之后。

生产端(packages/metadata-protocol/src/protocol.ts):返回值加
storeUnavailable: boolean,只在已经打印 [Protocol] DB hydration skipped
的那条分支上置位 —— 即 isMissingTableError 判为非良性的读失败。#5841 刚落地的
「未建表」良性分支不置位,那里 loaded: 0 确是事实。

消费端(packages/objectql/src/plugin.ts):ProtocolWithDbRestore 声明该
字段并读它,outage 打 error 级,按 AGENTS「Degradation log levels」交付两样
东西 —— 后果(什么都没恢复、kernel 仍报健康、哪些能力静默降级)与修法(查
sys_metadata 背后的数据源:连接、凭据、表是否存在,然后重启)。可读的空库照旧
debug。分支顺序刻意把 outage 放最前:一次没发生的读,它的计数不该压过「读没成功」
这件事。

刻意没做

对鸭子类型实现者的影响:零破坏(取舍如实说明)

ObjectQLPlugin 用结构化匹配拿 protocol 服务,新字段在消费侧声明为
optional
(与既有的 invalid 同例),旧 shim 照常通过类型检查,并被读成「不是
outage」—— 正是它此前唯一能表达的判定,行为逐字不变(有专门用例钉住)。

取舍值得点名:optional 意味着无法强迫第三方 shim 开始上报 outage,这类 shim
会和今天一样沉默。改成 required 可以让这件事无法忽略,代价是为一个目前只有一个
仓内生产者会置位的位,破坏所有外部实现者。仓内生产者
(ObjectStackProtocolImplementation)自己声明并返回 required,所以每个真实
ObjectStack kernel 实际走的那条路是全覆盖的。

测试与反向验证(方向先预测,后运行)

生产端扩展 #5841 的相邻文件 protocol.load-meta-hydration-benign.test.ts(而非
另开孤立文件):其中「事实 2 等价性」用例被翻面 —— 它原本断言 outage 与空库
不可分辨,并留了「契约长出表达方式时这条断言 EXPECTED 要翻,别删」的注记;现在断言
两者可分辨,且只由该位分辨(所有计数仍逐字相等,downstream 无法从
loaded/errors/invalid 重建这个区别、长出第二套弱问法)。

消费端新建 packages/objectql/src/plugin-restore-metadata-outage.test.ts(跨包,
无相邻文件可扩)。协议替身只声明 loadMetaFromDb —— 既是类型守卫探测的全部面,
也是该方法调用的全部面;不声明任何写动词,故没有 delete/update dispatch 需要
check:engine-double-contract 扫描,也没有守卫可手抄。

反向验证做了两肢,方向均在运行前写死在文件头,结果与预测逐条相符:

  • 肢 A —— 删生产端 storeUnavailable = true: 生产端 4 红(ECONNREFUSED、
    非 Error 拒绝、未识别措辞、outage-vs-空库),13 绿(全部良性措辞 + 工作存储
    对照 + stored-conversions)。
  • 肢 B —— 删消费端 if (storeUnavailable) 分支: 消费端 4 红,5 绿。

⚠️ 一条与模板预设不符、如实记下:肢 A 不会让消费端变红。消费端用例喂的是
协议替身(直接投喂返回契约),所以只有肢 B 能翻它。要证消费端读了这个位,
必须删消费端的读,而不是删生产端的写 —— 两肢各证一半,合起来才是完整链路。

命令与真实输出

pnpm --filter @objectstack/metadata-protocol test
  Test Files  49 passed (49)        Tests  487 passed (487)

pnpm --filter @objectstack/objectql test
  Test Files  129 passed (129)      Tests  2140 passed (2140)

pnpm --filter @objectstack/objectql typecheck
  tsc --noEmit → Done

pnpm --filter @objectstack/runtime test      (boot 路径联测)
  Test Files  102 passed (102)      Tests  1474 passed (1474)

node scripts/check-durability-degradation-log-level.mjs
  OK — 24 durability-critical catch seam(s), all loud, rethrowing or propagating
node scripts/check-startup-registry-verdict.mjs
  OK — 40 seam(s) across 1455 file(s), none recording a contradictable verdict
node scripts/check-nul-bytes.mjs
  OK (5754 tracked text files; no raw ASCII control bytes)
pnpm --filter @objectstack/spec check:generated
  All 10 generated artifacts are up to date.

typecheck 第一轮真红过一次,如实记:Logger 契约声明的是
error(message, error?: Error, meta?),两参写法只在 ObjectLogger 上过得去
(它额外容忍 meta 占 error 槽),对声明契约是越界。已按契约把计数放第三槽、
error 槽留空(这里确实没有 Error 可传:驱动原文已由生产端在 warn 上打过),
并在用例里按位置钉住。

推送前已 git merge origin/main(7 个提交,无冲突,⛔ 未 rebase),重装、重建
spec 与 objectql 依赖链后全部重跑 —— 以上数字即合并后的结果。


🤖 Generated with Claude Code

https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We


Generated by Claude Code

claude added 2 commits August 6, 2026 13:49
…再把 outage 记成空库 (#5897)

`loadMetaFromDb` 的返回值 `{ loaded, errors, invalid }` 没有任何字段能表达
「这次水合根本没读到存储」—— 读不到的数据库与真正的空库都答 `loaded: 0`。

其唯一生产消费方 `ObjectQLPlugin.restoreMetadataFromDb` 因此无从分支:它唯一
的分支只是在两条日志之间做选择,而「什么都没回来」那一侧是 debug 级的
`No persisted metadata found in database`。于是一个一个字都没读到持久化元数据
的 kernel,在 debug 级上宣称「本来就没有」,然后照常报告 ready。

代价写在 plugin.ts Phase 2 注释里:registry 为空时 `registry.getObject` 把
「读不到」答成「没声明」—— unknown-column 查询守卫、hooks、relationships 静默
降级,overlay 对象既不建表也不桥接。这是 ADR-0110 D3(outage ≠ miss)在 boot
侧的落地,继 #5108 / #5089 / #5532 / #5707 之后。

- 生产端:返回值加 `storeUnavailable: boolean`,只在已经打印
  `[Protocol] DB hydration skipped` 的那条分支上置位 —— 即 `isMissingTableError`
  判为非良性的读失败。未建表的首次启动(#5841)不置位,那里 `loaded: 0` 确是事实。
- 消费端:读该位并打 **error** 级日志,按 AGENTS「Degradation log levels」写清
  后果(什么都没恢复、kernel 仍报健康、哪些能力静默降级)与修法(查 sys_metadata
  背后的数据源:连接、凭据、表是否存在,然后重启)。可读的空库照旧 debug。

⛔ 不改控制流:boot 继续降级运行 —— 对着读不到的 overlay 存储拒绝启动,会把一次
瞬时故障变成彻底停机。变的只是「降级」不再被当成「健康」。

对 `ProtocolWithDbRestore` 鸭子类型实现者零破坏:新字段在消费侧声明为 optional
(与既有的 `invalid` 同例),旧 shim 照常通过类型检查并被读成「不是 outage」——
正是它此前唯一能表达的判定。

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 2:09pm

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/objectql.

15 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, @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • 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/l tests tooling

Projects

None yet

2 participants