Repository navigation
LocalManifestSource.read() 把「条目不存在」和「条目损坏」都答成 null,两个 HTTP 调用方因此只能回一句无因由的 500 #5426
Description
实施 #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):
-
:586安装路径的 ADR-0120 D5e posture gate ——previousEntry = this.ledger.read(manifestId)喂给evaluateGlobalUniqueGate()。损坏的既有条目读成null= 「没有既往 attestation」= gate 重新发问。这个方向是安全的(宁可重复一次一次性仪式,也不会跳过它),列在这里只为说明它已核验、不是隐患。 -
:929reseed 与:1009purge —— 两处都是先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. 与
LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 对齐,返回{ entry, failure }之类的结构化结果:契约一致,漏报编译期可见;要改 3 处调用方。 - B. 只让两处 500 带上因由:
read()不动,改在调用方 —— 但调用方拿不到 cause(它在read()里就没了),所以这条实际上要求read()至少把 cause 透出来,等价于 A 的弱化版。 - C. 保持现状,仅在
read()内补一行日志:与LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 分诊里否决 C 的理由相同(剥夺调用方报告权、该类不持有 logger)。
倾向 A(与 #5413 同一形状,两个方法的契约不至于分叉),但 read() 的 null 语义被三处调用方以不同方式消费,需要维护者拍板。
备注
- 已按关键词 + 文件路径搜过 open issues(
LocalManifestSource/installed-packages/manifest cache/ledger),无重复;唯一命中是LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 自身。 - 与
LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 无依赖关系,独立成单。不作为LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 的 sub-issue ——LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 的完成范围是list()的契约与它的三个消费者,read()与它的调用方不落在其中。
Activity
baozhoutao commentedon Aug 5, 2026
分诊(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 分诊同理否决。调用方口径::586D5e gate 行为不变(损坏视同无既往 attestation,fail-safe 已核验,不动);:929reseed /:1009purge:响应code不变,message 附 cause 原话(fix(cli):os doctor指名道姓报告非法OS_TENANCY_POSTURE并非零退出 (#5382) #5390 体例,操作者面 admin API),并补服务端日志一行 —— 500 从自我指涉变成指向那个文件。⛔ 串行约束(step 3 已知陷阱,解除前勿认领):与 PR #5424(#5413)完全同文件(
local-manifest-source.ts+marketplace-install-local-plugin.ts),须待其 MERGED 后基于新 main 派发,届时read()的改造直接复用 #5424 已落地的SkippedManifestEntry形状与测试 harness。Generated by Claude Code
baozhoutao commentedon Aug 5, 2026
认领:PM 循环第 4 轮(cli 车道)
会话:
session_016FNvXhtSdnEGEfLEsMmvxh分支:
claude/issue-5426-manifest-read-causeWorktree:
objectstack-issue-5426域:
domain:cli文件面:
packages/cloud-connection/src/local-manifest-source.ts、marketplace-install-local-plugin.ts+ 该包测试(串行约束已解除:PR #5424 已 MERGED7127b48,本单基于其后的 main 复用SkippedManifestEntry形状与测试 harness)执行口径沿 11:1xZ 分诊裁定:方案 A 对齐
list();:586gate 行为不变;reseed/purge 500 的code不变、message 附 cause 原话 + 服务端日志一行。Generated by Claude Code
baozhoutao commentedon Aug 5, 2026
验收(cli 车道 PM,
session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5439。read()返回InstalledManifestLookup(failure复用SkippedManifestEntry,cause 原对象不包装),三态表结构化区分「没装 / 装了读不出 / 读到了」;changeset 计 minor 携迁移说明。.entry行为不变,且合流决策在调用点公开化(注释写明 fail-safe 方向);reseed/purge 共享unreadableLedgerEntry(),code不变、message 指名文件 + 引 cause 原话 + 服务端 warn;has()/read()间隙的 race 兜底措辞诚实(无 cause 时不编造)。404 边界有防误伤 pin。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
fix(cloud-connection): `LocalManifestSource.read()` says WHICH of the…