fix(cli): os doctor tells a broken cloud-connection install from an absent one (#5644) - #5658
Conversation
… absent one (#5644) `readInstalledPackageEntries()` reached the installed-package ledger through a dynamic `import('@objectstack/cloud-connection')` whose `catch` meant "the optional package is not installed". That covered two states with opposite remedies: a specifier that does not resolve (genuinely absent — silence is correct and stays), and a package that IS installed and will not load (unbuilt or pruned `dist/`, interrupted install, an artefact that throws while it evaluates). The second was answered with the first one's silence, so the ADR-0120 D5e advisory saw "no installed packages" and printed `✓ Unique scope` over a ledger nobody read — the false PASS #5412 removed at the `readdir` boundary and #5413 at the entry boundary, one boundary up. The two are separated by resolution, not by the `import()` having thrown: `isModuleNotFoundError()` first (an error that is not module-not-found came from the package itself), then `import.meta.resolve()`, which answers "is the package there" without stating its entry file. New `utils/optional-package.ts` owns the classification and the measurements behind it; doctor renders the present-but-broken state as a `Unique scope` warning row through the same renderer its two siblings use, and withholds the success line. 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 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
|
CI 状态说明(交报告时点):快速门全绿—— 本地等价证据: Generated by Claude Code Generated by Claude Code |
Fixes #5644
按分诊 21:56Z 裁定的方案 A(先解析再加载)实现。B(用 ledger 目录在场做代理)、C(维持现状写成已知取舍)均未采用。
前提复核:成立(先证后改)
在已构建的工作树里,把
@objectstack/cloud-connection置为「在场但求值抛错」,并放一份声明了 installation-wideunique的 ledger 条目,OS_TENANCY_POSTURE=isolated跑os doctor --verbose:invoice.code这条本该出现的 finding 一行都没有,连--verbose也没有。issue 正文描述的假 PASS 在origin/main上逐字复现。判别机制:为什么是
import.meta.resolve,不是createRequire().resolve()Node 22.22 上对真包实测的四态(见
packages/cli/src/utils/optional-package.ts头注释):import.meta.resolve()createRequire().resolve()import()dist/缺失(未构建/被裁剪)第二行是关键,也是不能用 CJS 解析的两个理由:它会 stat 入口文件,于是这一行塌回「没装」——而这恰是 issue 正文那条复现路径(
mv packages/cloud-connection/dist /tmp/x)所在的态;它还按require条件解析,回答的是另一个产物(index.cjs),而import()加载的是index.js。import.meta.resolve回答同一个产物,且不 stat,所以「包在不在」与「入口能不能加载」保持两个问题。判别分两步,两步都复用仓内既有的所有者:
isModuleNotFoundError()(@objectstack/types,framework#3265 指定的单一所有者,serve.ts同样读它)。解析只在
import()已经失败之后才调用:happy path 零成本,且判定「加载是否成功」的始终是加载本身,不是解析器。无import.meta.resolve的运行时(本包 engines 是 Node 22+,不会发生)只保留第 1 步的确定性,含混态一律归absent——宁可退回改动前的沉默,也绝不在从未装过该包的 checkout 里误报。这条硬约束原样保留。报法
新增第三个同族行,与 #5412 目录级行、#5413 条目级行同档:
Unique scope名列、warning、退出码不变、错误原文入--verbose详情,走renderHealthCheckResult(#5410)。措辞与 #5412 那行刻意区分(
load对read,且不是其超串),因为事实与药方都不同:修的是@objectstack/cloud-connection的安装,不是 ledger。这一行也刻意不以.objectstack/installed-packages/是否存在为条件——ledger 目录名是那个加载不了的包自己的导出常量,doctor 无从断言「没有 ledger」;拿目录在场当条件正是被否决的方案 B。代价说清楚:未构建
packages/cloud-connection的仓内工作树,isolated下跑 doctor 会多这一条 warning。而这个态的沉默,正是 #5612 追查一个从未回退的报告面的起点。测试(三态各就各位)
packages/cli/src/utils/optional-package.test.ts(6 例,全真机制、零 mock):真不可解析的 specifier →absent;求值抛错 →broken;可解析但文件不在 →broken(未构建dist/的形态,单看错误码会误判成absent的那一例);包自身依赖缺失 →broken;健康 →loaded;外加真 workspace 包的对照例。doctor-ledger-read-failure.test.ts:17 → 24 例。新增 5 例渲染单测 + 2 例 e2e。LocalManifestSource.list()静默丢弃损坏的 ledger 条目 —— 已装应用在 boot 时消失、在控制台列表里缺席,且没有任何一条日志 #5413 用过的「换断言不换复现」处置);而「真没装 → 沉默 + 照打 clean bill」这条契约移到自己的 describe,通过 loader 接缝表达(该包是本包的声明依赖,module mock 改不了它的可解析性,再用旧写法就是钉一个不存在的态)。分层是刻意的:分类本身对着真运行时钉在optional-package.test.ts,渲染对着 doctor 钉在这里。beforeAll前置断言未回退,并保留在它仍有判别力的位置:「在场且坏」那个 describe 里,未构建的工作树会因为同一个原因产出同一行,前置断言正是把模拟与巧合分开的那层。新增的 absent describe 不加它,并写明理由——那里根本不读真包(接缝已 mock),没有「因巧合而绿」可防;这是doctor-ledger-read-failure.test.ts整块 7 条在 origin/main 上就是红的 ——os doctor对坏 ledger 一行都不报,#5413/#5424 的报告面测不出来 #5612 同一条推理的应用,不是放宽。反向验证(方向先判后跑,两处都是 before-green/after-red)
optional-package.ts的解析限支删掉(所有 module-not-found 一律判absent)。预测:两条判别用例红,其余绿。实测:broken分支还原成改动前的合并(broken 也当 absent)。预测:两条 e2e 红,absent 那条仍绿(它走接缝)。实测:验收
pnpm --filter @objectstack/cli test:83 passed (83)/825 passed (825)(改动前为 82 文件 / 812 例)30 passed (30)pnpm --filter @objectstack/cli typecheck绿;改动文件 eslint 零告警node scripts/check-nul-bytes.mjsOK;改动文件另做控制字节自扫,无命中@objectstack/clipatch(行为变化:doctor 对坏包新增 warning 行)边界
未动
packages/cloud-connection本体、未动serve.ts(它对同一个包的点名告警只是仓内对照读法)、未动packages/client/**(#5638 在飞)。#5429(posture 门禁触发条件)现象不同,未收敛、未触碰。一处界外观察,未新立单:在干净工作树里跑一次构建会重写
packages/spec/authorable-surface.base.json(baseRev + 三个EmailServiceConfig键)。已按关键词查重,命中既有 open 单 #5358 / #5370,同一现象,不另开;本 PR 已把该文件还原,不含在改动里。🤖 Generated with Claude Code
https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
Generated by Claude Code