Skip to content

fix(cli): os doctor 不再在 installed-package ledger 读不到时打出 ✓ Unique scope (#5412) - #5414

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5412-doctor-ledger-read-failure
Aug 5, 2026
Merged

fix(cli): os doctor 不再在 installed-package ledger 读不到时打出 ✓ Unique scope (#5412)#5414
baozhoutao merged 1 commit into
mainfrom
claude/issue-5412-doctor-ledger-read-failure

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5412

先说前提核验的结果:issue 的机制判定对了一半,复现路径是错的

按 origin/main (b4872a868) 核验,必须先读这一段再看实现。

issue 说 readInstalledPackageEntries() 的 catch 覆盖了两件事,其中第 2 件是
「目录存在但里面某个 manifest 损坏 / JSON 解析失败」。这条具体路径够不着那个 catch。

packages/cloud-connection/src/local-manifest-source.tsLocalManifestSource.list()
自己有一层 per-file catch:

        for (const name of readdirSync(this.dir)) {
            if (!name.endsWith('.json')) continue;
            try {
                const raw = readFileSync(join(this.dir, name), 'utf8');
                out.push(JSON.parse(raw));
            } catch { /* skip corrupt files */ }
        }

跑真实实现(一好一坏两条目)实测:

A) truncated-entry list() DID NOT THROW; returned 1 entries: [ 'good' ]

也就是说:损坏条目在生产方就被丢掉了,list() 成功返回一个短列表,doctor 那边
看到的是一次成功调用。issue 的复现路径(截断 JSON)按原样构造,✓ Unique scope
照样会打 —— 但原因不在 doctor 的 catch,而在上游。

issue 的核心判定仍然成立:那个 catch 确实把「可选包没装」和「ledger 读不到」
混为一谈,而且确实产出 false PASS。只是够得着它的是目录级失败(路径被文件占住 =
ENOTDIR、文件系统拒绝、读目录失败),不是条目级损坏。实测:

fs.existsSync(dir) = true
C) list() THREW: ENOTDIR | ENOTDIR: not a directory, scandir '…/.objectstack/installed-packages'

所以本 PR 修的是目录级那一半,并把条目级那一半明确钉成边界、另立 #5413,
而不是把测试改成能过的形状然后声称整单做完。PM 裁定的口径(拆两个 catch、warning
档、复用 renderHealthCheckResult() 不得打印)一字未改地适用于目录级路径;
只有 PM 要求的测试 ① 换了触发方式(ENOTDIR 而非截断 JSON),因为截断 JSON 在这一层
根本不产生任何信号。

改了什么

packages/cli/src/commands/doctor.ts:

  • readInstalledPackageEntries() 拆成两个 try。import('@objectstack/cloud-connection')
    保留自己的静默 catch —— 这一条注释写得对,doctor 必须能在没有该包的 checkout 里
    跑完。目录解析之后的读取单独一个 try,抛错时带着 cause 回到调用方。
    返回类型从 any[] 变为 { entries, failure? }:空数组本身是歧义的(「没装过东西」
    和「读不到」都产生它),第二种现在自带标记。
  • findUnscopedGlobalUniques() 相应返回 { advisories, ledgerFailure? }
  • 消费点:成功行是对建议两半(本项目 metadata + ledger 已装包)的断言,因此
    只有两半都跑过才允许打印。ledger 失败时改打一条常规 HealthCheckResult,经
    renderHealthCheckResult()(fix(cli): os doctor 说出配置载入失败的原因,不再只说一句「载入不了」(#5403) #5410 刚落地的那一个,没有发明第二套渲染),
    --verbose 展开免费获得。已跑过的那一半的 findings 照常上报 —— 一处静默省略
    不该换成另一处。
  • 新增 installedPackageLedgerFailureCheck(),warning 档,cause 按 fix(cli): os doctor 指名道姓报告非法 OS_TENANCY_POSTURE 并非零退出 (#5382) #5390 体例引
    上游原话不改写(ENOTDIR: not a directory, scandir '…' 本身就指名了挡路的那个
    文件,doctor 编不出更好的句子)。
  • 该行占用 Unique scope 名字列而非新起一个名字:重点是操作者扫报告时那一行
    、且不是 ;另起名字会让 Unique scope 干脆消失,那正是本单要治的沉默
    换了顶帽子。
  • 小重命名:configLoadHeadline / CONFIG_LOAD_HEADLINE_MAX 现在有了第二个消费者
    (ledger 的 cause 走同一个折行),名字改为 reportRowHeadline /
    REPORT_ROW_HEADLINE_MAX。纯内部,无外部引用。

反向验证(方向是先定后跑的)

预期方向:。把那个吞掉的 catch 恢复(第二个 try 的 catch 改回 return { entries: [] }),
预期只有钉缺陷的 3 条 e2e 转红,4 条无回归用例保持绿 —— 后者若也变绿/变红都说明它们
是因为「什么都没产出」而通过的。实跑结果与预期一致:

 × withholds the clean bill of health when the ledger cannot be read
 × expands the detail under --verbose, and only under --verbose
 × still reports this project’s own findings when only the ledger half failed
 ✓ an intact ledger with nothing to report still prints the clean bill — no regression
 ✓ an intact ledger WITH a global unique is still reported the way it always was
 ✓ says nothing about the ledger when there is no ledger at all
 ✓ ⚠ SCOPE BOUNDARY: a CORRUPT ENTRY is still absorbed by the producer
 ✓ prints no ledger row when the optional package cannot be loaded
 Tests  3 failed | 12 passed (15)

AssertionError: expected '…' not to contain 'No unconfirmed installation-wide uniq…'
AssertionError: expected '…' to contain 'Could not read the installed-package …'

恢复后 15/15 全绿。

测试

新增 packages/cli/src/commands/doctor-ledger-read-failure.test.ts(15 条,e2e 沿用
#5410 doctor-config-load-cause.test.ts 的 harness:temp cwd + node_modules 目录
陷阱,每条 }, 60_000))。覆盖 PM 点名的四项:

  1. ledger 读不到时 ✓ Unique scope 那行不出现;
  2. warning 行出现、含 cause,--verbose 才展开 cause:;
  3. 可选包缺失仍完全静默(用 vi.doMock + vi.resetModules() 让模块求值抛错来
    模拟不可解析;该场景下故意让 ledger 目录存在,这样若两个 catch 还共用,反方向
    的混淆会冒出一条假 warning);
  4. ledger 完好时 D5e 正常路径不受影响 —— 无 findings 照打 ,有 findings 照报
    installed package 'billing';另加「压根没有 ledger 目录」仍静默(不能把「从没装过」
    过度矫正成 warning)。

外加一条 ⚠ SCOPE BOUNDARY 用例,把「条目级损坏仍被生产方吸收」钉住并注明
#5413 落地时它应当转红

越界与另立单


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

`readInstalledPackageEntries()` 用一个不带绑定的 `catch` 罩住了两件性质完全
不同的事,并对两者都返回空数组:

  1. `@objectstack/cloud-connection` 解析不到 —— 可选包没装。这一条静默是
     对的,保持不变:`os doctor` 必须能在没有该包的 checkout 里跑完。
  2. ledger 目录**存在**(`fs.existsSync` 已经通过)但读取抛错。

第 2 种被当成第 1 种,于是它以「没有已装包」的身份抵达 ADR-0120 D5e 的
unique-scope 建议,建议无话可说,报告打出 `✓ Unique scope`。一个装了包、
ledger 却读不到的 isolated 环境拿到的是一张干净体检单 —— false PASS 比漏报
更糟,它让操作者停止查看。

两者现已拆开:`import()` 失败保留自己的静默 catch;读取失败带着 cause 回到
调用方,成功行被扣住(它是对建议**两半**的断言,只有两半都跑过才能打),
改为经 `renderHealthCheckResult()` 输出一条 warning 档 `HealthCheckResult`,
`--verbose` 展开随之免费获得。已跑过的那一半的 findings 仍照常上报。

不在本单范围:ledger 内**单个条目**损坏 —— `LocalManifestSource.list()` 在
自己的 per-file catch 里跳过它,调用成功且返回短列表,消费者无从分辨。
已另立 #5413,并由本 PR 的 SCOPE BOUNDARY 测试钉住。

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

vercel Bot commented Aug 5, 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 5, 2026 10:16am

Request Review

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

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

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.

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 10:19
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 10:19
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 9fad07f Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5412-doctor-ledger-read-failure branch August 5, 2026 10:24
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…edger entries it could not read (objectstack-ai#5413) (objectstack-ai#5424)

A truncated / unreadable / unparseable file under
`.objectstack/installed-packages/` was dropped in an un-bound per-file `catch`
and `list()` returned a bare array, so a short list was indistinguishable from a
complete one: no difference in the return value, no log, no count. All three
consumers gave a confidently wrong answer — `rehydrate()` left the installed app
unregistered (gone from the app switcher, its objects nonexistent) with nothing
in the log, `handleList()` served the console a list that looked whole with
`success: true`, and `os doctor` printed a clean `✓ Unique scope` over manifests
it had never parsed.

Skipping a corrupt file stays correct — one bad manifest must not stop a runtime
booting the packages that are fine. Skipping it SILENTLY was the defect.

`list()` now returns `{ entries, skipped }` (option A of the issue's decision
point): reporting is the caller's job, and "I read only half the ledger" becomes
a fact in the type rather than an absence. Enumerating the DIRECTORY still
throws — a different fact from "some files in it would not parse", and objectstack-ai#5412
already reports the two as separate rows.

Wiring, per triage:
- `rehydrate()` warns per skipped file, before the empty-entries early return
  (an all-corrupt ledger is the worst case, not the exempt one), naming the file,
  the consequence and the thrower's own words.
- `handleList()` logs the same; the WIRE SHAPE is deliberately unchanged —
  putting the skip in the response body is a separate schema decision.
- `os doctor` turns `skipped` into a `Unique scope` warning row and withholds
  the `✓` success line, alongside the directory-level row from objectstack-ai#5412.

objectstack-ai#5414's `⚠ SCOPE BOUNDARY` test went red exactly as its own comment predicted
and is rewritten as the positive assertion.


Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Co-authored-by: Claude <noreply@anthropic.com>
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