Skip to content

fix(cli): doctor's Environment files row names os serve / os start as its loaders in the source posture (#22249) - #22417

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22249-doctor-env-files-loader
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22249-doctor-env-files-loader

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22249
Clause-②: no

What changed

In the source checkout os dev serves, os doctor's Environment files row now says whose .env* cascade it resolved: os serve / os start. The source checkout is nodeEnvSourcePosture: an objectstack.config.ts that boots as itself, alone or beside its own dist/objectstack.json. This carries out triage route A (comment 6057410828) under #5387's standing ruling. Doctor still resolves the cascade exactly as os serve does, and only the row's wording in that posture changes.

  • environmentSourcesCheck(reading, env, opts) takes opts.sourcePosture, the same option shape as nodeEnvCheck. Its default is false, which gives the unchanged row.
  • run() now calls nodeEnvSourcePosture(cwd) once and hands the answer to both the sources row and the NODE_ENV row. Before, the NODE_ENV row computed it inline. It is still the one call site in doctor.ts, so the posture is not recomputed.
  • doctorNodeEnv(), readDotenvFiles() and the overlay are untouched, so every env-derived verdict is the same in every posture.
  • Naming os start is true for any NODE_ENV. start.ts loads .env* for NODE_ENV === 'test' ? 'test' : (NODE_ENV || 'production'), the same expression serve.ts uses without --dev and the one doctorNodeEnv() mirrors.

Measured: the rows, before and after

I ran the real Doctor command class from source with tsx and NODE_ENV unset, in five fixture directories. Before is base fdfdd7e76, with doctor.ts restored to it in the working tree. After is c76e46354.

Source posture, a freshly scaffolded project (config only, no .env*):

before  ✓ Environment files    No .env* files here (node_env=production) — environment read from this process only — no environment input set
after   ✓ Environment files    No .env* files here (node_env=production, the cascade `os serve` / `os start` load) — 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

Source posture with comment-only .env.production and .env.development files. The listed file shows the cascade did not move:

before  ✓ Environment files    .env.production (node_env=production), the cascade `os serve` loads — no environment input set
after   ✓ Environment files    .env.production (node_env=production), the cascade `os serve` / `os start` load — no environment input set

Controls: an artifact-only project (dist/objectstack.json, no config), an empty directory, and a config beside an OS_ARTIFACT_PATH naming another artifact. All three rows are byte-identical before and after (diff empty; sha256 of the three rows 7c5ce42e16f56430… on both sides):

✓ Environment files    No .env* files here (node_env=production) — environment read from this process only — no environment input set

Tests

New cases, all in packages/cli/src/commands/doctor-node-env-default.test.ts (unit tier):

  • Unit, environmentSourcesCheck, for both row shapes (no file, a .env file):
    • in the source posture the message names os serve and os start beside node_env=production;
    • control: { sourcePosture: false } equals the posture-less call field for field, and does not name os start;
    • the posture moves only the wording: status, fix and the closing provenance clause are equal across postures.
  • End to end (Doctor.run):
    • (f) the scaffold's sources row names os serve / os start beside node_env=production, directly above the os dev NODE_ENV row;
    • (g) with .env.production and .env.development present, the row lists the production file and not the development one;
    • controls (h), empty directory, artifact-only project: each row ends with exactly the posture-less environmentSourcesCheck(...) message and does not name os start.

H3 from the dispatch was measured partly false. The existing pins on this row (doctor-env-provenance.test.ts, doctor-config-env-overlay.test.ts) assert substrings such as No .env* files here and node_env=production, not the full row text. So the byte-identity control is carried by the new pins above and by the one-off measurement, not by those pins. All of them stay green.

Reverse verification. With the fix committed, I restored doctor.ts to base fdfdd7e76 in the working tree only (on-disk hash 14b8e959… equal to the base blob, marker count 0). Direction observed: red as expected. 4 of the 11 new cases failed, namely both unit source-posture cases plus e2e (f) and (g). All 7 controls stayed green (Tests 4 failed | 30 passed (34)). The restore leg was git checkout HEAD --: hash 01415add… equal to the HEAD blob, git diff HEAD empty, status clean. The subject resolves through source (./doctor.js relative import), not dist/, so no rebuild was needed for either leg.

Runs, all on c76e46354:

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --reporter=verbose src/commands/doctor-node-env-default.test.ts: Tests 34 passed (34).
  • The three files that read this row (doctor-node-env-default, doctor-env-provenance, doctor-config-env-overlay): Test Files 3 passed (3), Tests 68 passed (68).
  • The @objectstack/cli unit layer (vitest run --project unit --maxWorkers=2): 268 passed | 2 failed (270). The 2 failures were test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts, which threw packages/cli is not built (a prerequisite, not a measurement). Re-run after the workspace build, both passed (Tests 29 passed (29)).
  • pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK). tsc --listFiles -p tsconfig.json counts the edited test file in the program (1 of 1526 files).
  • Integration layer: declared to CI. The diff reaches no integration-tier file and no spawn entry.

Gates

All of these were run on c76e46354, the final commit, after the workspace build (pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2: 72 successful, 72 total).

  • Derived families: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, with no paths (change set taken from the merge base fdfdd7e76), gives 64 commands. That list is identical to the dispatch's lead list (sorted diff empty). All 64 exit 0. Reconciled with --ran (each line carrying its recorded exit): 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3).
    • A first pass ran before the workspace build. In it, check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity exited 3 (PREREQUISITE NOT MET), which is not a measurement. In the second pass, after the build, all four measured green. For example, check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
    • Some verdict lines: check-nul-bytes: OK (scanned 10382 text file(s) … no raw ASCII control bytes); check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen); No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
    • Locally, check-changeset-no-major --base origin/main reports its clause-② level axis as NOT APPLICABLE because there is no pull_request payload. CI reads this body's Clause-②: no line.
  • pnpm lint (eslint . --no-inline-config, whole repository, not narrowed): exit 0, no findings.
  • pnpm --filter @objectstack/cli typecheck: exit 0.

Acceptance notes

  • Claim file surface. The claim (6073648874) places the doctor pins under packages/cli/test/, but they live in packages/cli/src/commands/. The new cases were added to the file the claim names, doctor-node-env-default.test.ts, where it actually is.
  • Noted, not filed. In the source posture with only .env.development present (for example OS_TENANCY_POSTURE=isolated in it), the row still opens No .env* files here (node_env=production, the cascade …). It also says no environment input set. That is correct under os doctor 不加载 .env*,读到的环境与 serve/dev/start 实际运行的不是同一份 —— 写在 .env 里的配置错误 doctor 一律看不见 #5387 for the production cascade, and the new parenthetical now says which cascade, but the opening words read literally false beside a .env.development. Rewording No .env* files here is outside route A's single clause. No carrier.
  • #22163's pending changeset still says the row "still reports the .env* cascade for node_env=production". That stays true; this PR's changeset adds the loader wording and does not edit that file.

Generated by Claude Code

… its loaders in the source posture

In the source checkout `os dev` serves, the `Environment files` row now
says whose `.env*` cascade it resolved: `os serve` / `os start`. The
cascade itself (doctorNodeEnv, NODE_ENV || 'production') and every
env-derived verdict are unchanged; outside that posture the row is the
same bytes as before. The posture is asked once in run() and handed to
both the sources row and the NODE_ENV row.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

9 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 os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/deployment/environment-variables.mdx (via 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/protocol/backward-compatibility.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/protocol/kernel/config-resolution.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))
  • content/docs/protocol/kernel/http-protocol.mdx (via os doctor (command, read off packages/cli/src/commands/doctor.ts))

⛔ 2 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-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 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 abd254508b861da8ed1e96c2a813541b3274fb40 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 83c52023a36bb51eab09006505d03fde0eeaf14c — the merge of head c76e4635441bb51f7b30eff2d8a85c9a00493f15 into base abd254508b861da8ed1e96c2a813541b3274fb40, 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 83c52023a36bb51eab09006505d03fde0eeaf14c && git checkout 83c52023a36bb51eab09006505d03fde0eeaf14c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abd254508b861da8ed1e96c2a813541b3274fb40 c76e4635441bb51f7b30eff2d8a85c9a00493f15 && git checkout -B drift-repro abd254508b861da8ed1e96c2a813541b3274fb40 && git merge --no-ff c76e4635441bb51f7b30eff2d8a85c9a00493f15

node scripts/docs-audit/affected-docs.mjs --json abd254508b861da8ed1e96c2a813541b3274fb40

⚠️ 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 abd254508b861da8ed1e96c2a813541b3274fb40 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 05:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 05:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37886957837 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   unit  src/utils/collect-docs.package-docs.test.ts > collectDocsFromSrc reads src/<pkg>/docs/ for a resolvable package > attributes a directory named by the last segment 
      ↳ 失败原因: @objectstack/cli:test: Error: Hook timed out in 10000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/utils/collect-docs.package-docs.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 6 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Queue ejection — seat disposition (re-queued once). Session: session_01BmsuLyUeuG5CNpZFMH1jzS, seat domain:cli#1.

Signature (new; no queue-flake anchor exists for this file key, and the triage comment above counts this PR as the only one to hit it in 24h):

  • Build: 37886957837, job Test Core (1/6)
  • packages/cli/src/utils/collect-docs.package-docs.test.ts > collectDocsFromSrc reads src/<pkg>/docs/ for a resolvable package > attributes a directory named by the last segment of the package id, with its pedigree
  • Error: Hook timed out in 10000ms. at the file's beforeEach (line 45: a mkdtempSync + writeFileSync + mkdirSync). It is the only failure: 1 failed, 4951 passed. The shard ran 1469s with 712s spent on imports, so the runner was heavily loaded.

Initial read: this is load or timing, not a regression from this PR. All three facts that allow one re-queue hold:

  1. The import closure does not intersect the diff. The failing file's local import closure is 4 files, and neither packages/cli/src/commands/doctor.ts nor doctor-node-env-default.test.ts is among them. This PR changes only those two files plus a changeset.
  2. The queue base is green on the same shard. Base 27a8b33de got there through merge group 37885892627, and Test Core (1/6) succeeded there (job 113675779436).
  3. The first error is a timeout, not an assertion.

Pre-landing checks re-run on the unchanged head c76e463: 34/34 latest checks green. check-governed-merges reports NOT governed (208 lines). A fresh merge-tree against origin/main 27a8b33de is clean.

The PR is re-queued once, as is. If the same signature ejects it again, the seat stops re-queuing and hands it to a fresh diagnosis. It is not added to any flake table; this is only a flag for that ledger.


Generated by Claude Code

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