Skip to content

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

Closed

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

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 5, 2026
  2. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    分诊(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,2026-08-05):入队 pm:queue,domain:cli。判级:真缺陷,且比 #5412 的 doctor 面更重 —— 已装应用 boot 时静默消失、控制台清单少项、零日志,「有意跳过是对的,有意不说才是缺陷」的定性准确。查重记录(dev 已做)确认无重复。

    方向裁定(PM 按两轴拍板,留否决窗口 —— 决策点落在仓规已成文的范围内,不上交):取 A(结构化返回 { entries, skipped: [{ file, cause }] }),B、C 否决。

    • 长远轴:A 是唯一契约先行的形态 —— 「读了一半」成为类型上无法忽略的事实;B 的「不传回调就还是静默」把默认值留在错的那一边,与 declared = enforced 相反;C 把报告权从调用方手里拿走(doctor 要的是 HealthCheckResult 行,不是 stderr),且 LocalManifestSource 不持有 logger。
    • 防 AI 轴:A 让漏报在编译期就看得见,消费者想忽略必须显式写出来;B 是 AI 批量犯错被掩盖的温床(默认静默)。
    • 三个消费者的接线口径:rehydrate() 对每个 skipped 条目 warn(指名文件与 cause,与它对 manifest service 缺失的既有 warn 同档);handleList() 保持 wire 响应形状不变(⛔ 改响应 schema 属另一个决策,不在本单),skipped 以日志报告;doctor 消费 skipped 产出 HealthCheckResult(fix(cli): os doctor 不再在 installed-package ledger 读不到时打出 ✓ Unique scope (#5412) #5414 的 ⚠ SCOPE BOUNDARY 测试届时转红并改写为正向断言 —— 这是预期,该测试注释已写明)。

    ⛔ 串行约束(step 3 已知陷阱):doctor 侧接线与 PR #5414 同文件,须待 #5414 合并后基于新 main 派发。

    域注记:packages/cloud-connection 未在 domain 表中,本次按锚定规则(消费者为 cli serve / doctor / runtime boot)首次分类为 domain:cli;表的补录另行走 PR(规程要求 taxonomy 变更走 PR,不随认领)。


    Generated by Claude Code

  3. self-assigned this
    on Aug 5, 2026
  4. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    认领:PM 循环第 2 轮(cli 车道)
    会话:session_016FNvXhtSdnEGEfLEsMmvxh
    分支:claude/issue-5413-manifest-source-skipped-entries
    Worktree:objectstack-issue-5413
    域:domain:cli(packages/cloud-connection 按 09:1xZ 于 #5095 记录的首次分类归 cli)
    文件面:packages/cloud-connection/src/local-manifest-source.ts、marketplace-install-local-plugin.ts + packages/cli/src/commands/doctor.ts(消费接线 + 翻正 #5414 的 SCOPE BOUNDARY 测试)+ 两包对应测试(与在飞 #5415 packages/lint 不相交;队列中 PR #5416 触 cli/utils/** 与 lint/index.ts,亦不相交)

    串行约束已解除:PR #5414 已合并(10:24Z)。执行口径沿 10:19Z 分诊裁定:方案 A(list() 返回 { entries, skipped: [{ file, cause }] }),rehydrate() 逐条 warn,handleList() wire 形状不变仅日志,doctor 消费 skipped 产出 HealthCheckResult,⚠ SCOPE BOUNDARY 测试转红改写为正向断言。


    Generated by Claude Code

  5. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    验收(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5424。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions