Skip to content

LocalManifestSource.read() 把「条目不存在」和「条目损坏」都答成 null,两个 HTTP 调用方因此只能回一句无因由的 500 #5426

Closed

Description

@baozhoutao

实施 #5413(给 list() 加上 skipped 返回契约)时在同一个文件里核验发现;未在该 PR 里改 —— #5413 的判定面被 PM 限定在 list() 的返回契约与它的三个消费者,本条是 read() 的返回契约,另一组调用方。

事实

packages/cloud-connection/src/local-manifest-source.ts(分支 claude/issue-5413-manifest-source-skipped-entries,read() 本身未被 #5413 改动):

    /** Read one entry; null when absent or unreadable. */
    read(manifestId: string): InstalledManifestEntry | null {
        const file = this.fileFor(manifestId);
        if (!existsSync(file)) return null;
        try {
            return JSON.parse(readFileSync(file, 'utf8'));
        } catch {
            return null;
        }
    }

和 #5413 修掉的 list() 是同一个形状的另一半:不带绑定的 catch,抛出的对象在原地丢弃。null 同时表示两个事实 —— 「这个 manifest 没装过」和「装过,但那个文件读不出来」。注释 null when absent or unreadable 说明这是有意的合并。

与 list() 的区别在于:read() 的 null 至少是一个信号(调用方会分支),不像 list() 的短列表那样完全不可察觉。所以这条比 #5413 轻。

后果

三个调用方(都在 packages/cloud-connection/src/marketplace-install-local-plugin.ts):

  1. :586 安装路径的 ADR-0120 D5e posture gate —— previousEntry = this.ledger.read(manifestId) 喂给 evaluateGlobalUniqueGate()。损坏的既有条目读成 null = 「没有既往 attestation」= gate 重新发问。这个方向是安全的(宁可重复一次一次性仪式,也不会跳过它),列在这里只为说明它已核验、不是隐患。

  2. :929 reseed 与 :1009 purge —— 两处都是先 has()(existsSync,为 true)再 read()(因损坏得到 null),于是回:

500 { code: 'MARKETPLACE_STORAGE_FAILED', message: 'Failed to read manifest cache.' }

has() 刚说文件在,read() 就说读不出来,而为什么读不出来(Unexpected end of JSON input / EACCES / EISDIR)在 catch 处已经被丢掉了。操作者拿到的是一句自己指向自己的话,没有任何一条能指向那个文件的线索;服务端日志里也没有补一行。属 #5403 / #5412 / #5413「catch 吞掉唯一能解释状况的那个对象」家族。

复现路径

.objectstack/installed-packages/com.acme.crm.json   # 截断的 JSON
POST /api/v1/marketplace/install-local/com.acme.crm/reseed-sample-data
→ 500 "Failed to read manifest cache."(无因由;日志无补充)

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

read() 的返回契约怎么改,和 #5413 是同一类决策,但结论不见得相同 —— read() 的 null 已经是一个可分支的信号,破坏性改造的收益比 list() 小:

倾向 A(与 #5413 同一形状,两个方法的契约不至于分叉),但 read() 的 null 语义被三处调用方以不同方式消费,需要维护者拍板。

备注

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。判级:真缺陷 —— 操作者面 500 无因由且自我指涉(has() 说在、read() 说读不出、为什么在 catch 处已丢),同 #5403/#5412/#5413 家族。

    方向裁定(留否决窗口 —— #5413 的 A 已裁定并落地,同类同文件的两个方法契约不分叉属一致性恢复,不上交):取 A,与 list() 对齐:read() 返回结构化结果(absent → 无 failure 的空结果;损坏 → 携 { cause }),B 是 A 的弱化(cause 必须先从 read() 透出)、C 按 #5413 分诊同理否决。调用方口径:

    ⛔ 串行约束(step 3 已知陷阱,解除前勿认领):与 PR #5424(#5413)完全同文件(local-manifest-source.ts + marketplace-install-local-plugin.ts),须待其 MERGED 后基于新 main 派发,届时 read() 的改造直接复用 #5424 已落地的 SkippedManifestEntry 形状与测试 harness。


    Generated by Claude Code

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

    @baozhoutao
    ContributorAuthor

    认领:PM 循环第 4 轮(cli 车道)
    会话:session_016FNvXhtSdnEGEfLEsMmvxh
    分支:claude/issue-5426-manifest-read-cause
    Worktree:objectstack-issue-5426
    域:domain:cli
    文件面:packages/cloud-connection/src/local-manifest-source.ts、marketplace-install-local-plugin.ts + 该包测试(串行约束已解除:PR #5424 已 MERGED 7127b48,本单基于其后的 main 复用 SkippedManifestEntry 形状与测试 harness)

    执行口径沿 11:1xZ 分诊裁定:方案 A 对齐 list();:586 gate 行为不变;reseed/purge 500 的 code 不变、message 附 cause 原话 + 服务端日志一行。


    Generated by Claude Code

  5. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

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

    • 方案 A 精确落地:read() 返回 InstalledManifestLookup(failure 复用 SkippedManifestEntry,cause 原对象不包装),三态表结构化区分「没装 / 装了读不出 / 读到了」;changeset 计 minor 携迁移说明。
    • 三调用方口径全兑现:D5e gate 读 .entry 行为不变,且合流决策在调用点公开化(注释写明 fail-safe 方向);reseed/purge 共享 unreadableLedgerEntry(),code 不变、message 指名文件 + 引 cause 原话 + 服务端 warn;has()/read() 间隙的 race 兜底措辞诚实(无 cause 时不编造)。404 边界有防误伤 pin。
    • 两处诚实申报值得记录:① 反向验证预判 4 红实测 3 红,偏差解释成立(第 4 条钉的是未动的 404 边界);② D5e fail-safe 测试在回退态不红并如实写明 —— 它钉的是未来方向(「装过就当已 attest」的一行之差会跳过 S10/S14 仪式),测试注释自述清楚,这正是钉方向而非钉现状的正确用法。
    • ⛔ 未触 packages/cli(doctor 只消费 list(),已核);cloud 仓仅 re-export。typecheck 缺口如实挂 [P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311(13 处既有错误,diff stash 前后同数)。

    CI 核后转 ready 挂 auto-merge,跟到 MERGED。


    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