Skip to content

LocalManifestSource.list() 静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413

Description

@baozhoutao

实施 #5412(拆开 os doctor 的 ledger 读取 catch)时核验发现;未在该 PR 里改 —— #5412 的判定面被 PM 限定为 packages/cli/src/commands/doctor.ts,本条在 packages/cloud-connection,且要动 list() 的返回契约(跨包)。

事实

packages/cloud-connection/src/local-manifest-source.ts(origin/main b4872a868),LocalManifestSource.list():

    /** Every valid entry in the ledger (corrupt files are skipped). */
    list(): InstalledManifestEntry[] {
        if (!existsSync(this.dir)) return [];
        const out: InstalledManifestEntry[] = [];
        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 */ }
        }
        return out;
    }

每个文件一个不带绑定的 catch。一个截断 / 不可读 / JSON 解析失败的 manifest 被就地丢掉,list() 成功返回一个短列表 —— 调用方无法把它和一个完整列表区分开:没有返回值上的差别,没有日志,没有计数。抛出的对象在 catch 处就被丢弃了。

实测(worktree 里跑真实实现,一好一坏两个条目):

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

后果(两个消费者,都是静默的错答案)

packages/cloud-connection/src/marketplace-install-local-plugin.ts:1279 readAll = () => this.ledger.list(),两处调用:

  1. rehydrate()(:212) —— boot 时把每个已缓存 manifest 重新注册进 kernel。条目损坏 = 那个已装应用不被注册,即从这个 runtime 里消失(app switcher 里没有、对象不存在),而日志里一个字都没有。函数自己对「拿不到 manifest service」是会 warn 的,唯独对「少读了几个条目」不会。

  2. handleList()(:702) —— 控制台「已安装应用」列表。损坏条目直接不在返回的 items 里,前端得到的是一份看起来完整的清单。

  3. 第三个消费者是 os doctor 的 ADR-0120 D5e unique-scope 建议(os doctor 的 installed-package ledger 读取 catch 把「损坏」和「没装」当成同一件事 —— D5e 建议因此静默少报,并打出「clean」 #5412)。os doctor 的 installed-package ledger 读取 catch 把「损坏」和「没装」当成同一件事 —— D5e 建议因此静默少报,并打出「clean」 #5412 修掉了目录级读取失败(✓ Unique scope 不再在 ledger 不可读时打印),但条目级损坏够不着那个 catch —— 因为它在这里就被吸收了。os doctor 的 installed-package ledger 读取 catch 把「损坏」和「没装」当成同一件事 —— D5e 建议因此静默少报,并打出「clean」 #5412 的 PR 用一条 ⚠ SCOPE BOUNDARY 测试把这个边界钉住了(doctor-ledger-read-failure.test.ts),本 issue 修好后那条测试会转红,这是预期的。

也属于 #4801 / cloud#1020「诊断面与运行时不一致」和 #5403 / #5412「catch 吞掉唯一能解释状况的那个对象」的家族。注释 /* skip corrupt files */ 说明跳过是有意的 —— 有意跳过是对的(一个坏文件不该让整个 runtime 起不来),有意不说才是缺陷。

复现路径

.objectstack/installed-packages/good.json     # 完整条目
.objectstack/installed-packages/broken.json   # 截断的 JSON

os serve 起一个装了 broken 那个包的 runtime:该应用不出现,日志无提示;GET 已装列表:少一项,success: true

决策点(不预设,留给分诊)

list() 的返回契约要改成什么,是本单真正的决策点,不是实现细节 —— 它是 @objectstack/cloud-connection 的公开 API,有 3 个消费者:

  • A. 返回结构化结果(如 { entries, skipped: Array of { file, cause } }):调用方各自决定怎么报;类型即契约,漏报变成编译期就看得见。破坏性,要改 3 处调用方。
  • B. 保持返回类型,加一个可选的 onSkip 回调 / 累加器:非破坏性,但「不传就还是静默」—— 默认仍然是错的那一边,和仓规「declared = enforced」相反。
  • C. list() 内部 logger.warn 一条:最小改动,但 LocalManifestSource 目前不持有 logger,且把「谁来报告」的决定从调用方手里拿走(doctor 要的是一行 HealthCheckResult,不是 stderr)。

倾向 A(契约优先:让「读了一半」在类型上无法被忽略),但 3 个调用方的改法需要维护者拍板。

备注

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions