Repository navigation
Commit b9e6e5c
fix(cli): os doctor reads an unset NODE_ENV as development in the source checkout os dev serves (#22231)
Fixes #22163
Clause-②: no
## What changed
`os doctor`'s `NODE_ENV` row now depends on the directory it runs in.
With `NODE_ENV` unset, it picks one of two rows. Doctor's exit code does
not change in either case.
| Where doctor runs | Row |
|---|---|
| The source checkout that `os dev` serves: an `objectstack.config.ts`
that boots as itself, either alone or beside its own
`dist/objectstack.json` | `✓ NODE_ENV Not set — os dev runs this project
as development; os start forces production`, with no fix text |
| Anywhere else: a config beside `OS_ARTIFACT_URL`, a config beside an
`OS_ARTIFACT_PATH` naming another artifact, an artifact with no config,
or an empty directory | The #5673 `⚠ … treated as production` warning
and its fix. Both are byte-identical to what they were before this
change |
A `NODE_ENV` that is set still prints no row in either case.
The maintainer's direction, quoted in the card: 「`os doctor` 在开发项目里警告
"NODE_ENV not set — treated as production"。 刚脚手架完就被告知在跑生产,令人困惑;旁边有
`objectstack.config.ts` 且无产物时应按开发语义判断。」
- `packages/cli/src/commands/doctor.ts`:
- `nodeEnvCheck(env, opts)` gains `opts.sourcePosture`, which defaults
to `false`. A caller that does not pass it gets the #5673 row, so every
existing caller and pin compiles and behaves as before.
- The new `nodeEnvSourcePosture(cwd, env)` decides the posture by
calling `utils/artifact-precedence.ts`'s own `resolveArtifactBootSource`
and then `cwdConfigJoinsBoot`. It does not copy the precedence order,
which that module's header forbids.
- The docblock now sets the #22163 direction beside #5673's argument. I
also rewrote the `run()` call-site comment, which the change made false.
- Unchanged:
- `doctorNodeEnv()` and the `.env*` cascade doctor resolves. The
`Environment files` row still says `node_env=production`.
- The `/discovery` default.
- How `os serve`, `os start` and `os dev` resolve the mode.
- No file under `serve.ts`, `start.ts`, `dev.ts`, `packages/spec`,
`content/docs` or `docs/qa` is touched.
## The PM readings, measured
- **H1: no artifact does not survive the first `os dev`. Confirmed.**
- Setup: I scaffolded with the published `create-objectstack@17.7.0
h1-app --template blank` (packages installed), then ran the published
`os doctor`.
- Then I ran the compile `os dev` runs (`os compile --output
$PWD/dist/objectstack.json`, which wrote `dist/objectstack.json`, 2.4
KB), and ran doctor again.
- Both runs printed the same two rows:
```text
✓ Environment files No .env* files here (node_env=production) —
environment read from this process only — no environment input set
⚠ NODE_ENV Not set — this environment is being treated as production
```
- So the card's literal test ("no `dist/objectstack.json`") would turn
this very project back to the production row after its first `os dev`.
The posture therefore accepts the config's own compiled output at the
conventional path.
- With this branch's CLI, the same project prints the following, both
before and after `dist/` exists:
```text
✓ Environment files No .env* files here (node_env=production) —
environment read from this process only — no environment input set
✓ NODE_ENV Not set — os dev runs this project as development; os start
forces production
```
- Under `--verbose`, no fix line follows the `NODE_ENV` row. Exit code
0.
- **H2: the posture comes from `artifact-precedence.ts`. Held, with one
deliberate difference from `os dev`.**
- `os dev` reads `OS_ARTIFACT_URL` / `OS_ARTIFACT_PATH` after a `.env*`
load. `dev.ts:330`, `dotenvFlow.config({ node_env: 'development',
silent: true })`, runs before `resolveArtifactBootSource` at `:348`.
- The posture mirrors that order: this process's environment over the
node_env=development cascade, with the shell winning. This is a second,
private read of the files. The cascade the `Environment files` row
reports does not change.
- Not mirrored: the `configCompiledTo` that `dev.ts:366` passes. Under a
local `OS_ARTIFACT_PATH`, `os dev` compiles the config into that path
and counts the file as the config's own. With that input,
`cwdConfigJoinsBoot` answers `true` for "a config plus an
`OS_ARTIFACT_PATH` naming another artifact", which contradicts both the
triage direction ("everywhere else #5673's production default stands")
and pin (c).
- So the posture passes no `configCompiledTo` and no `homeDir`. Its
table is the PM's six rows exactly.
- **H3:** the row is `ok`, has the card's message, and carries no `fix`.
`HealthCheckResult.status` is not widened.
- **H4:** the two rows a freshly scaffolded project prints are quoted
under H1. My reading is in the Acceptance notes.
- **H5:** `nodeEnvCheck` and `nodeEnvSourcePosture` cannot be reached
from any `@objectstack/cli` entry export. The built `dist/index.d.ts`
re-exports only `default as DoctorCommand` from `commands/doctor.js`.
`console.d.ts` and `hook-body.d.ts` do not mention doctor. So `Clause-②:
no` stands.
- **H6:** done, as described above.
## Pins
- **Unit pins, in `doctor-node-env-default.test.ts`:**
- **Source posture:** `ok`, the message names `os dev` and `os start`,
the production sentence is absent, and `fix` is undefined.
- **Explicit `sourcePosture: false`:** `toEqual` the default call. The
default call is the row that the unchanged #5673 cases in the same file
pin.
- **A set `NODE_ENV`:** no row in either posture.
- **The posture table** (`nodeEnvSourcePosture`):
- Source posture: a config alone; a config beside its own `dist/`.
- Not source:
- a config beside an `OS_ARTIFACT_PATH` naming another artifact, or
beside `OS_ARTIFACT_URL`;
- an artifact with no config;
- an empty directory.
- The cascade it reads:
- `.env.development` naming an artifact moves the posture to not-source.
- `.env.production` does not, because `os dev` never loads it.
- **Run level** (`Doctor.run` in a temp cwd, in the same harness as the
#5673 cases). `NODE_ENV` is unset in (a) to (d), and doctor exits 0 in
all five:
- (a) A config alone gives the `✓` row naming `os dev` and `os start`,
with no `treated as production`.
- (b) A config plus `dist/objectstack.json` gives the same row.
- (c) A config plus `OS_ARTIFACT_PATH` naming another artifact gives the
`⚠ … treated as production` row.
- (d) An empty directory: the existing #5673 case `prints the row when
NODE_ENV is unset`, kept as it was.
- (e) `NODE_ENV=development` and `NODE_ENV=production` in the source
posture: no `NODE_ENV` row.
- **The config's own load does not decide the row.** The run-level
config is the scaffold's first line (`import { defineStack } from
'@objectstack/spec'`), and nothing in the temp cwd can resolve it. Case
(a) asserts that `Could not load config for analysis` is printed in the
same report as the development row.
- **The drift guard in `doctor-env-provenance.test.ts`.** The docblock
names `OS_ARTIFACT_URL` and `OS_ARTIFACT_PATH`, and the guard requires
every `OS_*` name in `doctor.ts` to be declared in `DOCTOR_ENV_INPUTS`.
I did not declare them there:
- The `Environment files` row attributes those inputs against the
node_env=production cascade, but these two are read through the
development one. A line for them in that row would name the wrong file,
and H4 rules out rewording that row.
- Setting either variable can only send the row back to its #5673 text.
- They sit in a named `POSTURE_ONLY` allowance with that reason. A
second assertion requires both names to still appear in `doctor.ts`, so
the allowance cannot outlive its reader.
## Ablation (one-time proof, not kept)
- Committed `12e2b1ed` first.
- `node scripts/ablation-replace.mjs` replaced the call-site anchor
`nodeEnvCheck(process.env, { sourcePosture: nodeEnvSourcePosture(cwd)
})` with `sourcePosture: false`:
- anchor count 1 → 0, replacement count 0 → 1;
- blob `e38feeb4a35e` → `f875089c53b6`.
- No build or `dist/` was involved: the test imports `./doctor.js`,
which resolves to `src`.
- Predicted: (a) and (b) turn red and everything else stays green.
Observed: exactly that.
- `× (a) … expected ' ⚠ NODE_ENV Not set — th…' to contain '✓'`
- `× (b) … expected ' ⚠ NODE_ENV …' to contain 'os dev'`
- `Tests 2 failed | 21 passed (23)`
- Restore: blob after restore `e38feeb4a35e` equals the blob at HEAD,
and `git diff HEAD` is empty.
## Byte identity outside the source posture
- In an artifact-only directory, `doctor --verbose` printed the
`NODE_ENV` row and its 11 fix lines byte-identical (`cmp`) from this
branch's CLI and from the published 17.7.0 CLI.
- The `doctor.ts` diff removes no line of the warning's literal.
## Verification
All of the following ran on HEAD `12e2b1ed`.
- **Build.** `pnpm turbo run build --filter=@objectstack/cli...
--concurrency=2`: 59 tasks, 57 cached, exit 0.
- **The two pin files.** `vitest run --project unit` on the two pin
files: `Test Files 2 passed (2)`, `Tests 47 passed (47)`.
- **`@objectstack/cli` unit tier.** `Test Files 1 failed | 262 passed
(263)`, `Tests 1 failed | 3886 passed (3887)`.
- The one failure is `test/hook-timeout-override-refusal.test.ts › ⭐
CONTROL — a run naming no override passes and says nothing`, with `Test
timed out in 5000ms`. That file does not touch doctor.
- The run held the shared box for 10m47s while the gate battery ran
beside it.
- Rerun alone: `Test Files 1 passed (1)`, `Tests 4 passed (4)`.
- **The integration tier** is declared to CI: the diff touches no
integration-tier file and no spawn entry.
- **`@objectstack/cli` typecheck:** exit 0.
- `tsc --noEmit --listFiles` counts both pin files in the program.
- `check:test-typecheck: OK`.
- **Gates.** The PM's 64 commands match the list re-derived by
`dispatch-gates.mjs` on `12e2b1ed` line for line. All 64 exited 0.
- `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3
(PREREQUISITE NOT MET: packages unbuilt in this worktree). After a full
`turbo run build` (72 tasks, 71 cached) both exited 0:
- `✓ check:dual-build-cjs-loads — 106 published require entry point(s)
across 66 package(s) load`
- `check-i18n-coverage: OK (13 config(s), 621 baselined untranslated
string(s), none new)`
- `dispatch-gates --ran`: `64 derived famil(ies) accounted for — 64 run,
0 NOT-MEASURED`.
- **Full `pnpm lint`** (`eslint . --no-inline-config`): exit 0, with no
findings printed.
## Acceptance notes
- **H4, the two rows side by side.** They do not contradict on the
facts. `Environment files` reports the cascade doctor resolved, which is
the one `os serve` / `os start` load with `NODE_ENV` unset. The
`NODE_ENV` row now says what `os dev` does. A newcomer still reads
`node_env=production` one line above "runs this project as development",
so a milder form of this card's confusion remains on that row.
- **The cascade gap behind it.** In a source checkout, doctor attributes
`OS_*` inputs against the production cascade, while `os dev` loads the
development one. That is the boundary the PM ruling on #5387 drew when
it declined a `--dev` mode for doctor. It is not changed here. Following
up is the `domain:cli` seat's call. Carrier: `domain:cli` seat.
- **`OS_ARTIFACT_PATH` set to the conventional path.**
`OS_ARTIFACT_PATH=./dist/objectstack.json` beside a config counts as the
source posture. That is the shared predicate's answer (the artifact is
the config's own compiled output), not a separate rule here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0b7ba4d commit b9e6e5c
4 files changed
Lines changed: 329 additions & 12 deletions
File tree
- .changeset
- packages/cli/src/commands
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
327 | 327 | | |
328 | 328 | | |
329 | 329 | | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
330 | 339 | | |
331 | 340 | | |
332 | | - | |
| 341 | + | |
333 | 342 | | |
334 | 343 | | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
335 | 347 | | |
336 | 348 | | |
337 | 349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
39 | 50 | | |
40 | 51 | | |
41 | 52 | | |
| |||
44 | 55 | | |
45 | 56 | | |
46 | 57 | | |
47 | | - | |
| 58 | + | |
48 | 59 | | |
49 | 60 | | |
50 | 61 | | |
| |||
127 | 138 | | |
128 | 139 | | |
129 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
130 | 233 | | |
131 | 234 | | |
132 | 235 | | |
| |||
224 | 327 | | |
225 | 328 | | |
226 | 329 | | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
227 | 417 | | |
0 commit comments