Skip to content

fix(cli): os doctor reads an unset NODE_ENV as development in the source checkout os dev serves - #22231

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22163-doctor-node-env-source-posture
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22163-doctor-node-env-source-posture

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 且无产物时应按开发语义判断。」

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:
      ✓ 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:
      ✓ 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 NODE_ENV 未设置时 /discovery 广播 environment=development,而 os start 默认 NODE_ENV=production、CLI doctor 也按 production 解析 #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

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 os doctor 不加载 .env*,读到的环境与 serve/dev/start 实际运行的不是同一份 —— 写在 .env 里的配置错误 doctor 一律看不见 #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

…rce checkout os dev serves

Beside an objectstack.config.ts that boots as itself (alone, or beside its
own dist/objectstack.json), the NODE_ENV row is now `ok` and names what
os dev and os start do, with no fix text. Everywhere else the #5673
warning and its fix are unchanged. The posture comes from
utils/artifact-precedence.ts (resolveArtifactBootSource + cwdConfigJoinsBoot),
read over the .env* cascade os dev loads; doctorNodeEnv() and the cascade
doctor reports are untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
@github-actions github-actions Bot added the size/m label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/automation/jobs.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/data-modeling/indexing.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/deployment/cli.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck), os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/deployment/environment-variables.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck), os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/deployment/production-readiness.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/deployment/self-hosting.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck))
  • content/docs/plugins/packages.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck))
  • content/docs/protocol/backward-compatibility.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/protocol/kernel/config-resolution.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck), os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/protocol/kernel/http-protocol.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck), os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/upgrading.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/releases/v17/17-0.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck))
  • content/docs/releases/v17/17-3.mdx (via NODE_ENV (literal, a string literal in nodeEnvCheck))
  • content/docs/releases/v17/17-5.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 35 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7b926f76007316ec13d2b17ec4b0a316b94e4d7e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c51a91ba889fbdf771d6ac397309349eb73f147b — the merge of head 12e2b1ed6b692fabb06fc4c732e4df1a2a29444b into base 7b926f76007316ec13d2b17ec4b0a316b94e4d7e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c51a91ba889fbdf771d6ac397309349eb73f147b && git checkout c51a91ba889fbdf771d6ac397309349eb73f147b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7b926f76007316ec13d2b17ec4b0a316b94e4d7e 12e2b1ed6b692fabb06fc4c732e4df1a2a29444b && git checkout -B drift-repro 7b926f76007316ec13d2b17ec4b0a316b94e4d7e && git merge --no-ff 12e2b1ed6b692fabb06fc4c732e4df1a2a29444b

node scripts/docs-audit/affected-docs.mjs --json 7b926f76007316ec13d2b17ec4b0a316b94e4d7e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7b926f76007316ec13d2b17ec4b0a316b94e4d7e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants