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. 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

  2. self-assigned this
    on Aug 5, 2026
  3. 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

  4. 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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions