fix(cloud-connection,cli): LocalManifestSource.list() 报告读不出来的 ledger 条目 (#5413) - #5424
Conversation
…edger entries it could not read (#5413) A truncated / unreadable / unparseable file under `.objectstack/installed-packages/` was dropped in an un-bound per-file `catch` and `list()` returned a bare array, so a short list was indistinguishable from a complete one: no difference in the return value, no log, no count. All three consumers gave a confidently wrong answer — `rehydrate()` left the installed app unregistered (gone from the app switcher, its objects nonexistent) with nothing in the log, `handleList()` served the console a list that looked whole with `success: true`, and `os doctor` printed a clean `✓ Unique scope` over manifests it had never parsed. Skipping a corrupt file stays correct — one bad manifest must not stop a runtime booting the packages that are fine. Skipping it SILENTLY was the defect. `list()` now returns `{ entries, skipped }` (option A of the issue's decision point): reporting is the caller's job, and "I read only half the ledger" becomes a fact in the type rather than an absence. Enumerating the DIRECTORY still throws — a different fact from "some files in it would not parse", and #5412 already reports the two as separate rows. Wiring, per triage: - `rehydrate()` warns per skipped file, before the empty-entries early return (an all-corrupt ledger is the worst case, not the exempt one), naming the file, the consequence and the thrower's own words. - `handleList()` logs the same; the WIRE SHAPE is deliberately unchanged — putting the skip in the response body is a separate schema decision. - `os doctor` turns `skipped` into a `Unique scope` warning row and withholds the `✓` success line, alongside the directory-level row from #5412. #5414's `⚠ SCOPE BOUNDARY` test went red exactly as its own comment predicted and is rewritten as the positive assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 22 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
范围外发现(Prime Directive #10,均已另开 issue,未在本 PR 修)实施过程中在同一份文件 / 同一条报告路径上核验到两条,都不落在本单的完成范围内:
两条均已按关键词 + 文件路径查重( Generated by Claude Code |
Fixes #5413
缺陷
.objectstack/installed-packages/下一个截断 / 不可读 / 解析失败的文件,被list()的每文件不带绑定的catch就地丢掉,而list()返回一个裸数组 —— 短列表和完整列表在调用方看来完全一样:返回值上没有差别,没有日志,没有计数。抛出的对象在 catch 处就被丢弃了。三个消费者因此都给出了「静默的错答案」:rehydrate():那个已装应用不被注册进 kernel —— 从这个 runtime 里消失(app switcher 里没有、对象不存在),日志里一个字都没有;handleList():控制台「已安装应用」清单少一项,success: true;os doctor(ADR-0120 D5e):对一份自己从没解析过的 manifest 打出✓ Unique scope。跳过是对的(一个坏文件不该让整个 runtime 起不来),有意不说才是缺陷 —— issue 的定性照搬到这里仍然成立。
改法:分诊裁定的方案 A(契约优先)
list()改为返回{ entries, skipped }。skipped是Array< { file: string; cause: unknown } >:文件基名 + 原样的抛出对象(不重新包装、不字符串化)。为什么是 A 而不是内部
logger.warn:报告是调用方的事。boot 要一条warn,HTTP handler 要在不动 wire 形状的前提下记一行,os doctor要的是一行HealthCheckResult而不是 stderr。把事实返回出去(而不是持有 logger,也不是一个默认静默的可选onSkip回调),才让「我只读了半个 ledger」成为类型上的事实:消费者想忽略它,必须显式写出来。ENOENT/EACCES/Unexpected end of JSON input是三种不同的运维事实、三种不同的修法,所以 cause 一路透传,由消费者引原话(#5390 体例)。目录级枚举失败仍然 throw,没有被顺手「补全」成不抛 —— 那是「什么都没读到」,和「读到了一部分」是两个事实,#5412 已经把它们报成两行了。producer 侧也补了一条测试钉住这点。
三个消费者的接线
rehydrate()warn,放在entries.length === 0提前返回之前 —— 全部条目都损坏是本 bug 的最坏情况(每个已装应用都消失),不能反而是唯一沉默的那条路径。行内含文件名、后果、原始 cause、以及要修/删的绝对路径handleList()data的键集不变)os doctorskipped产出Unique scopewarning 行(复用renderHealthCheckResult()),并抑制✓;与 #5412 的目录级行并存、各报各的日志档位取
warn,与rehydrate()既有的「拿不到 manifest service」同档:按 AGENTS.md 的判据,这是功能性降级(ledger 文件仍在盘上,没有任何声称已持久化的东西没落地),系统只是可见地比 ledger 说的小一圈。反向验证(方向先判后跑)
预判:#5414 的
⚠ SCOPE BOUNDARY用例会转红,且只红在not.toContain('broken')这一条 —— 新行的 headline 与LEDGER_HEADLINE是两串不同的字,所以那条断言应当仍绿。实跑与预判一致,红在
doctor-ledger-read-failure.test.ts:349,其余 742 条全绿。doctor 真实输出:该用例按其自身注释改写为正向断言(损坏条目 → 报告出现且指名、
✓不出现),并未删除 —— repro 一字未改,只是判定反了过来。验证
消费者普查
仓内
list()的生产消费者只有两处(marketplace-install-local-plugin.ts的readAll、doctor.ts),没有第 4 个。cloud仓packages/objectos-runtime/src/index.ts只是再导出LocalManifestSource,不调用list(),不受返回类型变更影响。changeset
@objectstack/cloud-connectionminor(公开契约变更,body 里写了 FROM → TO 的一行迁移)、@objectstack/clipatch。Generated with Claude Code
Generated by Claude Code