Uh oh!
There was an error while loading. Please reload this page.
fix(cli): os doctor 指名道姓报告非法 OS_TENANCY_POSTURE 并非零退出 (#5382) - #5390
Conversation
doctor 的两处 posture 读取都在 config 分析那个很宽的 try 里,抛错被它的 catch 转述成「Could not load config for analysis」并只记 warning —— 一个 os serve 会拒绝启动的环境,os doctor 报「Environment is functional」+ exit 0, 全程不出现 OS_TENANCY_POSTURE 这个词。归因错了,严重级也错了。 现在在 run() 顶部、任何 try 之外解析一次,非法值产出一条普通的 error 体检项 (修法清单由 TENANCY_POSTURES 生成,cause 直接引用解析器原话),由 doctor 自己的错误汇总给出非零退出码 —— 不 process.exit,doctor 的语义是「报告」。 两处读取复用同一个已解析值。 顺带:那两处此前都在 if (configExists()) 之内,没有配置文件的环境从来没读过 posture;现在与是否存在配置文件无关。 残留(文案里如实写明,另记为 #5387):doctor 不加载 .env*,写在 .env 里的 非法 posture 仍到不了这份报告。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w
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:
|
baozhoutao
commented
Aug 5, 2026
合入 文件面无重叠(#5381 只动 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…ectstack-ai#5403) (objectstack-ai#5410) The config-analysis `catch` took no binding — `catch {` — so the error object was discarded at the point it was caught. A genuinely broken config produced `Could not load config for analysis (config checks skipped)` and nothing else, under any flag: the sentence came from a bare `printWarning`, not from a `HealthCheckResult`, so `--verbose` had no `fix` to expand. `os serve`, in the same directory, prints the error in full — the diagnostic command returned strictly less than the command it exists to diagnose. objectstack-ai#5382 / objectstack-ai#5387 / objectstack-ai#5397 fixed this sentence's ATTRIBUTION. This fixes what it SAYS now that it is finally saying it about the right thing. - `configLoadFailureCheck(err)` builds an ordinary `HealthCheckResult`, quoting the thrower verbatim (objectstack-ai#5390's `cause:` posture) rather than paraphrasing: the failure can come from user code, esbuild, Node's resolver, or `loadConfig()` itself. - `renderHealthCheckResult()` extracts the environment block's rendering so the finding travels the same path and the same `--verbose` rule as `Environment files` / `Tenancy posture`, deleting the bare-printWarning bypass that had no `fix` channel at all. - The row carries a whitespace-folded, bounded quote (esbuild's first line is its least informative one); `--verbose` carries the untruncated original. - Gauge unchanged: warning, rest of the report runs, exit 0. The recognizable sentence survives verbatim as the head of the row — sibling tests assert its absence to mean "the config loaded". Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh Co-authored-by: Claude <noreply@anthropic.com>
… two things its null meant (objectstack-ai#5426) (objectstack-ai#5439) `read()` answered `null` to two different questions at once — "this manifest was never installed" and "it was installed, but its ledger file cannot be read" — and its own comment (`null when absent or unreadable`) says the merge was deliberate. The consequence was not: two admin endpoints call `has()` first, so absence is already ruled out by the time they read, and both could only answer `500 MARKETPLACE_STORAGE_FAILED / "Failed to read manifest cache."` — a sentence whose only content is that the thing it just did failed, one line after `has()` said the file is there. `Unexpected end of JSON input` / `EACCES` / `EISDIR` had already been dropped in an un-bound `catch`, and nothing reached the log either. Option A of the decision point, aligned with `list()` (objectstack-ai#5413): `read()` returns `{ entry, failure }`, `failure` being the same `SkippedManifestEntry` shape with the thrown object carried unwrapped, and present ONLY when a file exists that would not parse. `failure === undefined` with `entry === null` now means "not installed" — the fact the merged null erased. Wiring, per triage: - the ADR-0120 D5e posture gate reads `.entry` and is behaviourally UNCHANGED: a corrupt entry still counts as "no attestation on record", so the one-time ceremony is asked again rather than skipped. Conflating the two nulls is the right call at that call site; it is now made there, in the open, instead of by the ledger for everyone. - reseed and purge keep `code: 'MARKETPLACE_STORAGE_FAILED'` — the same failure, newly explained, so a client branching on the code is unaffected — and their message now names the ledger file and quotes the cause (objectstack-ai#5390 house style), with a matching `warn` line on the server. `read()` still does not validate the parsed value's shape; `list()` still throws when the directory itself cannot be enumerated. Both deliberately unchanged. Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh Co-authored-by: Claude <noreply@anthropic.com>
Fixes#5382
前提核验:成立,而且比 issue 描述的更糟
先在
origin/main(ada0b84a3,#5380/#5385 已合入)上把 issue 的复现跑了一遍 —— 真机运行doctor命令,临时 cwd 里放一个能正常载入的objectstack.config.ts,OS_TENANCY_POSTURE=bogus:注意
✓ Platform spec那一行:配置是载入成功了的,checkSpecVersionGap都已经跑完并通过,之后才轮到doctor.ts:688的 posture 抛错被同一个try的 catch 接住。所以「Could not load config」这句话不只是归因不准,它是字面上的假话 —— config 载进来了。issue 的判断完全成立。顺带发现 issue 没提到的一半:那两处 posture 读取都在
if (configExists())之内。所以在没有objectstack.config.ts的环境里,doctor 连那句归因错误的 warning 都不会有 —— posture 从头到尾没被读过,零信息、exit 0:改了什么
按 PM 裁定的方向 1:在
run()顶部、任何try之外解析一次 posture,非法值报 error 并计入hasErrors,不process.exit(1)—— doctor 的语义是「报告」,报告照常跑完,由它自己的错误汇总给出退出码。修完之后,同一个环境(真机运行
bin/run.js doctor):@objectstack/spec/security的TENANCY_POSTURES生成,不是第二份字面量 —— 新增一个 posture 不会让这段建议悄悄过期(有测试钉住)。cause直接引用@objectstack/types解析器自己那句话,doctor 不维护会跟它跑偏的第二份措辞。doctor.ts:688的 D5e 闸门、findUnscopedGlobalUniques())现在复用同一个已解析值;findUnscopedGlobalUniques改为接收 posture 参数,resolveTenancyPosture()在整个文件里只剩包装器内那一处调用。与 PR #5381(serve)的关系:同形,不同裁决,没有复用它的代码
范本是 #5381 的
resolveTenancyPostureOrRefusal,本单是resolveTenancyPostureOrFinding,刻意分开:serve(#5359 / PR #5381)doctor(本单)process.exit(1)error体检项,报告跑完后由汇总退出os serverefuses to boot this environment」——doctor 自己不启动任何东西.envdotenvFlow.config()之后,所以明说「检查了每个 .env」.env*,所以明说「没看 .env」没有从
serve.tsimport,理由有二:文案在三处都必须不同(照搬会让 doctor 谎称自己检查过.env、谎称拒绝启动);且serve.ts是 2800 行、拖着整条启动依赖图的模块,让doctor为一个纯函数把它整个拉进来不划算。两份加起来约 40 行、共享同一个TENANCY_POSTURES来源。去重机会属实存在(一个utils/tenancy-posture.ts放共同的 verdict 形状 + hints 表,两边各自拼文案),但那要动serve.ts—— 本单文件面被限定在doctor.ts,按派发口径不扩面,记在这里供后续裁定。测试
新增
packages/cli/src/commands/doctor-tenancy-posture-report.test.ts(11 例)。此前packages/cli对 doctor 的该行为零测试。TENANCY_POSTURES里每个值、legacymulti⇒isolated、未设置时走OS_MULTI_ORG_ENABLED派生、空白值等同未设置。status必须是'error'(这个字段才是驱动hasErrors→ 非零退出的那个,写成'warning'就复现了缺陷)、指名变量与用户实际敲的值、修法清单覆盖每个 posture、cause引用原话、以及不出现「Could not load config」/「config checks skipped」。doctor命令的差分对照 —— 同一个临时 cwd、同一套检查,只改OS_TENANCY_POSTURE一个变量。关于那个端到端用例的两点自我约束:
node_modules/。否则 doctor 的Dependencies检查自己就是error、自己就会 exit 1 —— 那样「exit 1」这条断言会因为与本改动无关的理由通过,是个假绿。反向验证(方向在跑之前就定了:标准的 before-red / after-green)。 把
doctor.tsstash 回origin/main状态、只留测试,端到端用例如预期变红,红的方式正是缺陷本身:输出里OS_TENANCY_POSTURE一次都没出现、process.exit从未被调用、结尾是「Environment is functional」。上面「前提核验」那两段输出就是这次反向跑的原始输出。改动前的整包基线同样是 660 通过(除了新测试文件本身的 11 红),所以没有既有用例被这条新增的 error 行影响。另跑:
check:nul-bytesOK、check:durability-log-levelOK、check:startup-registry-verdictOK、check:role-wordOK、check:error-code-casingOK、check:wildcard-fallthroughOK、eslint 干净;控制字符自查(grep -naP扫\x00-\x08\x0b\x0c\x0e-\x1f)三个文件均无裸控制字节。顺带发现(已另开 issue,未在本 PR 修)
os doctor不加载.env*,读到的环境与serve/dev/start实际运行的不是同一份 —— 写在.env里的配置错误 doctor 一律看不见 #5387 ——os doctor不加载.env*,而serve/dev/start都用 dotenv-flow 加载。于是写在提交进仓库的.env里的非法 posture,doctor 报绿、serve 拒绝启动。本单修好的「doctor 与 serve 不一致」,在.env这条来源上仍然成立,只是从「归因错误」缩小成「完全看不见」。文案里已如实写明不冒充检查过。没有顺手改的原因写在那单里:它会改变 doctor 每一项(现在的和将来的)env 派生检查的输入,还得选一个 doctor 并没有对应旗标的node_env,属于诊断输出的契约变化。另外两处看过、判断为不需要动:
packages/cli/src/commands/migrate/plan.ts:104/118的resolveTenancyPosture()也在一个宽try里,但它的 catch 是printError(error.message)+this.exit(1)—— 消息里带着变量名、退出码是 1。缺处方,但不是本单这个「exit 0 + 归因错误」的缺陷,不构成 finding。fix文本会被打印两次(结果行一次、结尾汇总再一次),这是它对所有 error 行的既有行为(Node.js、pnpm、Dependencies都如此),只是本单的 fix 较长所以显眼。改它会连带改掉其它行的汇总提示,属于 doctor 输出契约的另一个决定,没有搭车。🤖 Generated with Claude Code
https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w
Generated by Claude Code