Commit 5ced825
committed
fix(spec,metadata-protocol,runtime): one place decides the unset-NODE_ENV discovery environment (#5936)
Fixes #5936. Direction 1 of the maintainer's 2026-08-07 ruling (decision-inbox
round 2), which returned the issue to the queue as a placement question after
#5673 had already settled the semantics on 2026-08-06.
## Premise, re-verified against origin/main (3a1d9c7)
Every line the issue cites is where it says, and the defect reproduces:
- `resolveDiscoveryEnvironment` at `packages/spec/src/api/discovery.zod.ts:341`
returned `development` for a non-string input — so `undefined` in.
- `packages/metadata-protocol/src/protocol.ts:2900` (`getDiscovery`) passed
`process.env.NODE_ENV` in raw, with no default of its own. Served by
`packages/rest`.
- `packages/runtime/src/http-dispatcher.ts:1331` carried the #5673 default at
its own call site as `getEnv('NODE_ENV', 'production')`, with a comment
stating this exact residue and naming #5936 as the follow-up.
- The mapping table's `unset / anything else -> development` row at
`discovery.zod.ts:314` was still there, and had been false for the runtime
caller since #5673.
Net effect on main: a production deployment whose operator forgot `NODE_ENV`
was advertised `production` on the dispatcher's `/discovery` and `development`
on the one served through `@objectstack/rest`. Same question, two answers.
## What lands
The default moves INTO the shared mapper: an absent value resolves to
`production`, and both producers pass the operator's value through as read.
The dispatcher's local default is deleted; metadata-protocol never had one to
delete and simply inherits the corrected mapper. #4828's rule is untouched and
still points the other way — a set-but-unrecognised spelling (`qa`, `preview`)
degrades to `development`, because absence is the host declining to answer
while an unrecognised spelling is a guess.
One judgement call the ruling did not spell out, decided and recorded rather
than left implicit: **"unset" includes a blank value**. `NODE_ENV=` exports an
empty string; the runtime's `getEnv` has always folded that into its default
(it tests with `||`), and #5673's own pin already asserts the dispatcher answers
`production` for it. Had the mapper treated blank as "anything else", the two
producers would have gone on disagreeing about exactly that input — the drift
this consolidation exists to end — so blank is unset. Stated in the TSDoc, in
the changeset, and pinned on both producers.
Direction 2 (patch metadata-protocol locally) was the alternative and is what
the ruling rejected: it would have left a second copy of the default, which is
what the shared table was created to prevent (#4828).
## Documentation, both faces of it
- `discovery.zod.ts`: the `unset / anything else` row splits into two rows,
one per rule, each with its reason, plus the note that the unset default
moved here in #5936 and why blank counts as unset.
- `http-protocol.mdx` already carried the corrected table from #5673 — but it
describes the dispatcher, and until now the table was true of that producer
only. It now states that the mapping and the default are shared by every
`/discovery` producer, and names the behaviour change for anyone reading
`environment` from a REST-served response.
- `http-dispatcher.ts`: the comment block that explained *why the default sits
at this call site* is replaced by one explaining why it no longer does, and
telling the next author not to re-add one.
- `protocol.ts`: a short comment at the call site making the same point from
the other side — this producer adds no default; the mapper decides.
## Reverse verification — two revert shapes, opposite outcomes, both predicted first
Predicted, then measured:
- **Full revert to main's shape** (mapper back to `development` for a
non-string AND the dispatcher back to `getEnv('NODE_ENV', 'production')`):
metadata-protocol's two unset rows go RED, runtime stays **23/23 GREEN**.
That asymmetry IS the bug this issue reports, reproduced on demand.
Measured: `Tests 2 failed | 17 passed (19)` in metadata-protocol,
`Tests 23 passed (23)` in runtime, `Tests 1 failed | 77 passed (78)` in
spec's mapper suite.
- **Mapper-only revert** (dispatcher keeps its local default deleted): BOTH
producers go red — measured `2 failed | 17 passed (19)` and
`2 failed | 21 passed (23)`. That is the consolidation stated as a test
outcome: the dispatcher no longer has a second place to be right.
The unrecognised-spelling rows stayed green under both reverts, as predicted —
they are #4828's rule and this change does not touch it. Both source files were
restored and verified byte-identical with `diff` afterwards.
## Tests
- `packages/spec/src/api/discovery.test.ts`: the single case "never CLAIMS
production for an unset or unrecognized value" split into two, because the
two rules now point opposite ways. The unset case covers `undefined`, `null`,
`''` and `' '`.
- `packages/metadata-protocol/src/discovery-schema-conformance.test.ts`: a new
block driving the REAL producer — unset and blank advertise `production`,
unrecognised spellings still `development`, and the shared table
(`test`/`staging`/`production`) reproduced end-to-end so the two producers'
suites assert the same facts.
- `packages/runtime/src/discovery-schema-conformance.test.ts`: pins unchanged
and still green (this producer's behaviour does not move); its comment is
rewritten, since the sentence explaining that a spec-side test "cannot see"
the default stopped being true the moment the default moved there.
Two end-to-end pins rather than one mapper test on purpose: a local default
re-added at either call site would satisfy the mapper's own test and still be
the drift #5936 removes. Only a producer-level assertion separates them.
## Verification (all foreground, real readings)
- `pnpm --filter @objectstack/spec test` -> 340 files, **8727 passed**
- `pnpm --filter @objectstack/metadata-protocol test` -> 55 files, **571 passed**
- `pnpm --filter @objectstack/runtime test` -> 110 files, **1606 passed**
- `pnpm --filter @objectstack/rest test` -> 64 files, **884 passed**
- `pnpm --filter @objectstack/objectql test` -> 146 files, **2448 passed**
- `pnpm --filter @objectstack/client test` -> 21 files, **263 passed**
- `pnpm --filter @objectstack/hono test` -> 2 files, **73 passed**
- `turbo run typecheck --filter=./packages/* --filter=./packages/*/*` ->
**119 successful, 119 total**
- `pnpm --filter @objectstack/spec check:generated` -> **10/10 up to date**
(nothing regenerated: no `.describe()`, no export and no authorable key moved)
- `pnpm --filter @objectstack/spec gen:openapi` -> ran (ungated generator);
writes only the gitignored `json-schema/openapi.json`, no tracked diff
- `pnpm lint` -> clean
- `check:nul-bytes` / `check:empty-changeset` / `check:adr-0087-registration` /
`check:route-envelope` / `check:error-code-casing` /
`check:wildcard-fallthrough` / `check:adr-anchors` / `check:doc-authoring` /
`check:docs-audit-scope` / `check:quick-reference-counts` /
`check:startup-registry-verdict` / `check:service-providers` /
`check:spec-parsed-alias` -> all green
`check:adr-0087-registration` confirms no declared-breaking changeset, so no
disposition marker is owed. The changeset covers all three packages at `patch`
and states the behaviour change an operator can observe.
One first-run red that was NOT this change: `@objectstack/client` reported
3 failed suites with "Failed to resolve entry for package
`@objectstack/runtime`" — a fresh worktree with runtime's `dist` unbuilt
(AGENTS.md §9's stale-artefact trap in mirror image). `pnpm --filter
'@objectstack/client...' build` fixed it and the suite went 21/21.
## Ruling mapping
| Ruling clause | Where it lands |
|:---|:---|
| unset -> `production`, folded into `resolveDiscoveryEnvironment` | `discovery.zod.ts` — the mapper body |
| producers drop their local defaults | `http-dispatcher.ts` (deleted); `protocol.ts` had none and now says so |
| correct the stale mapping-table comment | `discovery.zod.ts` — one row becomes two, each with its rule |
| pin both producers under unset `NODE_ENV` | new block in metadata-protocol's conformance suite; runtime's #5673 pins kept and re-commented |
| one PR across spec + metadata-protocol + runtime | this commit |
| changeset covering the three packages, patch | `.changeset/discovery-environment-single-default.md` |
## Out-of-scope findings (recorded, not fixed)
1. `packages/rest` carries its own `discovery-schema-conformance.test.ts` and is
the SERVER for the metadata-protocol producer, but it has no NODE_ENV block
of its own — it composes over `getDiscovery()`, so the new pins do cover the
behaviour it serves. Worth knowing if a third shape ever grows there.
2. `getEnv` (`packages/core/src/utils/env.ts:15`) collapses an empty string into
its default via `||`, which is right for `NODE_ENV` but is a repo-wide
policy applied to every variable — an `OS_*` value that is legitimately the
empty string cannot be expressed. Not touched here; noted because this change
depends on that behaviour at one call site.
## 交付摘要(PM 开 PR 素材)
- **前提**:对 origin/main 3a1d9c7 逐条复核成立 —— `discovery.zod.ts:341` 非字符串返回 `development`、`protocol.ts:2900` 原样递传、`http-dispatcher.ts:1331` 本地默认连同「本条残留待 #5936」的注释俱在、:314 那行过时表格行仍在。缺陷可复现:同一台忘设 NODE_ENV 的部署,两个生产者给出两个答案。
- **落地**(裁决方向 1):缺省判定收进共享映射(unset → `production`),runtime 删本地默认,metadata-protocol 本无默认、随映射自动纠正;#4828 的「不在猜测上宣称 production」不动,两条规则拆成两行各述其理。
- **裁决未明说而必须定的一点**:空串算 unset。`NODE_ENV=` 导出空串,runtime 的 `getEnv` 一向用 `||` 把它折进默认,#5673 的 pin 也已断言该情形为 production;若映射把空串归入「anything else」,两个生产者就会在这一个输入上继续分歧 —— 正是本次收敛要消灭的漂移。已写进 TSDoc、changeset 并在两侧各 pin 一条。
- **反向验证**:两种回退形态,方向先判后跑,结果相反且都命中 —— 整体回退到 main 形态:metadata-protocol 红 2、runtime 23/23 全绿(**这就是 #5936 报告的不对称本身**);仅回退映射:两侧各红 2(说明 runtime 已无第二处可依赖)。未识别拼写行在两种回退下均保持绿。两个源文件事后 diff 验证逐字节还原。
- **文档面**:`discovery.zod.ts` 表格行订正;`http-protocol.mdx` 原表自 #5673 起就是对的,但只对 dispatcher 成立 —— 现补明该表对**所有** `/discovery` 生产者成立,并写清经 `@objectstack/rest` 读 `environment` 的调用方会看到的行为变化。
- **验证读数**:spec 8727、metadata-protocol 571、runtime 1606、rest 884、objectql 2448、client 263、hono 73 全绿;`turbo typecheck` 119/119;生成物 10/10 且本次无一需要重生成;lint 与门禁族全绿。client 首跑 3 个 suite 红是新 worktree 未建 runtime dist(AGENTS §9 陈旧产物陷阱的镜像),补建后 21/21,与本改动无关。
- **界外发现**两条见上,均未夹带。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011btrhv6sHn6JkN93YRtGQp1 parent 3a1d9c7 commit 5ced825
8 files changed
Lines changed: 250 additions & 53 deletions
File tree
- .changeset
- content/docs/protocol/kernel
- packages
- metadata-protocol/src
- runtime/src
- spec/src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
190 | 199 | | |
191 | 200 | | |
192 | 201 | | |
| |||
Lines changed: 82 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
197 | 197 | | |
198 | 198 | | |
199 | 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 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
200 | 281 | | |
201 | 282 | | |
202 | 283 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2897 | 2897 | | |
2898 | 2898 | | |
2899 | 2899 | | |
| 2900 | + | |
| 2901 | + | |
| 2902 | + | |
| 2903 | + | |
| 2904 | + | |
| 2905 | + | |
| 2906 | + | |
| 2907 | + | |
| 2908 | + | |
2900 | 2909 | | |
2901 | 2910 | | |
2902 | 2911 | | |
| |||
Lines changed: 28 additions & 14 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
275 | 275 | | |
276 | 276 | | |
277 | 277 | | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
283 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
284 | 282 | | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
290 | 302 | | |
291 | 303 | | |
292 | | - | |
293 | | - | |
294 | | - | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
295 | 309 | | |
296 | 310 | | |
297 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1293 | 1293 | | |
1294 | 1294 | | |
1295 | 1295 | | |
1296 | | - | |
1297 | | - | |
1298 | | - | |
1299 | | - | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
| 1299 | + | |
| 1300 | + | |
| 1301 | + | |
| 1302 | + | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
1300 | 1309 | | |
1301 | | - | |
1302 | | - | |
1303 | | - | |
1304 | | - | |
1305 | | - | |
1306 | | - | |
1307 | | - | |
1308 | | - | |
1309 | | - | |
1310 | | - | |
1311 | | - | |
1312 | | - | |
1313 | | - | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
1314 | 1321 | | |
1315 | 1322 | | |
1316 | 1323 | | |
1317 | 1324 | | |
1318 | 1325 | | |
1319 | 1326 | | |
1320 | 1327 | | |
1321 | | - | |
1322 | | - | |
1323 | | - | |
1324 | | - | |
1325 | | - | |
1326 | | - | |
1327 | | - | |
1328 | | - | |
1329 | | - | |
1330 | | - | |
1331 | | - | |
| 1328 | + | |
1332 | 1329 | | |
1333 | 1330 | | |
1334 | 1331 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1181 | 1181 | | |
1182 | 1182 | | |
1183 | 1183 | | |
1184 | | - | |
1185 | | - | |
1186 | | - | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
1187 | 1203 | | |
1188 | 1204 | | |
1189 | 1205 | | |
| |||
0 commit comments