feat(client): environments.create() declares the three response keys the control plane really sends (#12883) - #13379
Conversation
… really receives (#12883) `client.environments.create()` declared its unwrap shape as the single key `environment`, while `POST /api/v1/cloud/environments` answers 201 with `warnings`, `durationMs` and a conditional `hostnameAssignment` as well. `warnings` is the partial-degradation channel, so no SDK caller could learn what a provision failed to do without an `as any`. Per the 2026-08-29 maintainer ruling (verbatim 「同意」, option 甲) the three keys are declared as the INLINE WIRE SHAPE and are NOT bound to `@objectstack/spec/cloud`'s `ProvisionEnvironmentResponseSchema`, honouring the namespace docblock's recorded snake_case constraint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8ba81c5eba2fdb4d9e5e9f114dfbfc6018aae164 && git checkout 8ba81c5eba2fdb4d9e5e9f114dfbfc6018aae164
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f54efdf52c7c80a3ec242fa9a9e779a5873f852 5c8be5a65bddf47daa6ecdf370e9dfd65803cf90 && git checkout -B drift-repro 3f54efdf52c7c80a3ec242fa9a9e779a5873f852 && git merge --no-ff 5c8be5a65bddf47daa6ecdf370e9dfd65803cf90
node scripts/docs-audit/affected-docs.mjs --json 3f54efdf52c7c80a3ec242fa9a9e779a5873f852 |
PM 复核:ACCEPT-on-green。已 undraft,绿了就 arm。
裁决被逐字执行2026-08-29 裁决(verbatim 「同意」,option 甲)四项逐一对上: ⭐
|
Fixes #12883
client.environments.create()declared its unwrap shape as the single keyenvironment, whilePOST /api/v1/cloud/environmentsanswers 201 with threemore.
warningsin particular is the channel a partially-degraded provisionuses to report what it could not do, and no SDK caller could reach it without
an
as any.What changed
packages/client/src/index.ts— theenvironments.createunwrap now declares:environmentanyis deliberate (#11925 / #12036)warningsstring[], PRESENTdurationMsnumber, PRESENThostnameAssignmentrequestedHostname/assignedHostnamePer the maintainer ruling of 2026-08-29
(verbatim: 「同意」, option 甲) the three keys are typed as the inline wire
shape and are deliberately not bound to
@objectstack/spec/cloud'sProvisionEnvironmentResponseSchema. Those are camelCase row contracts, and the/api/v1/cloud/*control plane this namespace calls speaks snake_case — theconstraint already recorded on the namespace docblock. Binding them would
typecheck and be false.
The docblock at the declaration site used to say these keys were "deliberately
not declared here ... a separate decision, not part of this rename". That
decision has since been made, so the comment now records the ruling instead of
going on asserting a stance the repo no longer holds.
The request side is untouched — #3739 is a separate card and is not addressed
here. Related consumer-side card: objectstack-ai/objectui#6707.
Which readings are measured here, and which are inherited
objectstack-ai/cloudis not in this session's repository scope, so the handlerbody this declaration is written against —
packages/service-cloud/src/routes/environment-lifecycle.ts,POST /cloud/environments, spreadingenvironment/warnings/durationMsand a conditional
hostnameAssignment— is the card author's 2026-08-28measurement, relayed. Neither this PR nor any gate in this repo has read it, and
no gate here ever can. That is stated in the code comment too, so the next
reader does not have to rediscover it.
Measured in-repo and load-bearing for this change: the declaration site and its
docblock, the request-side snake_case parameters, the spec schema, and every
consumer of
environments.create()in this repo (swept below).packages/spec/src/cloud/environment.zod.ts:346markswarnings.optional()inside
ProvisionEnvironmentResponseSchema, while the ruling declares itPRESENT. The inherited handler quote spreads it unconditionally
(
warnings: result.warnings), which is consistent with "present" only ifresult.warningsis always an array — something only the cloud repo can settle.This PR implements the ruling: it is the authority, and the counter-evidence
is a defensive
.optional()in a schema this change is explicitly not binding.The divergence is recorded here rather than silently reconciled in either
direction. The same schema carries a second, heavier divergence (a REQUIRED
credentialthe handler quote does not send); it is unjudged, out of scope forthis card, and reported separately to the dispatching seat rather than touched
here.
Verification
All of the below ran on the final commit,
git rev-parse --short HEAD=5c8be5a65b.Tests
packages/client/src/client.environments-namespace.test.tsgains a compile-timepin (
createDeclaresTheWireResponseKeys) and three runtime relays. The type pincarries the teeth: presence is asserted by assigning into non-optional locals,
optionality by a
@ts-expect-errorthat only holds whileundefinedis in thetype, and
credentialis pinned as UNDECLARED so binding the spec schema goesred rather than landing quietly.
NOT-MEASURED guard for that green: the package's BUILD
tsconfig.jsonexcludes**/*.test.ts, sotsc --noEmitalone says nothing about the new pins.tsc --listFiles -p tsconfig.test.jsonwas used to confirm the program reallyreads both edited files — 1 hit for
packages/client/src/index.ts, 1 hit forclient.environments-namespace.test.ts, positive control 33 hits underpackages/client/src/.Consumer sweep
The widening is additive, and this proves it rather than asserting it.
environments.createrepo-wide (ts/tsx/mts/cts/js/mjs/cjs/md/mdx/json,node_modulesanddistexcluded)packages/cli/src/commands/environments/create.ts:91— plus 4 call sites in the client's own test file and 2 changeset mentionsenvironments.activatein the same population: 4 hits} = client.environments,environments['create'],= client.environments)client.environmentsin the same population: 25 hits, so the pattern family is reachableobjectuiobjectstack/clientthere: 97 hits, so that corpus is reachableEvery downstream consumer of
@objectstack/clienttypechecks:The filter direction is the PREFIX form (
...@objectstack/client) = dependents.Direction control: the suffix form (
@objectstack/client...) resolves to adifferent, 34-package upstream set, so the 7 above are consumers and not
dependencies. Each of the 7 echoed its own
typecheckscript in the run, sothis is not a zero-match filter passing silently.
Reverse verification
Direction predicted before running: RED, and red specifically in
check:test-typecheckrather than intsc --noEmit(the build config cannot seethe pins), as 8 errors on the reads of the three keys — 4 in the type pin,
4 in the runtime tests — with the
@ts-expect-errorlines staying suppressed andvitest staying green, because the pin's teeth are in the type layer only.
Ablation: revert only the widened unwrap generic back to the single
environmentkey, leaving the pins in place. No build or
distis involved — the pin importsthe mutated module relatively (
from './index'), source to source, so tsc readsthe edited bytes and not a stale declaration file.
Observed, matching the prediction exactly:
Honest limit on that reading: the gate reports a COUNT and a file, not TypeScript
error codes, so "all 8 are TS2339" is inferred from which sites can fail, not
quoted from the gate. The count and the file match the prediction exactly.
The restore used
git checkout HEAD -- ABSOLUTE_PATH(never the bare form, whichreads from a possibly-poisoned index), the mutation script carried an
EXIT INT TERMtrap restoring by absolute path, and both legs are proved by blobhash rather than by exit code.
Gates
Union derived from the real change set by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(3 pathsvs merge base
3ec8646f1; the script's own stale-tree note said nothing thisanswer derives from had changed). All named families were run. Each line below is
the gate's own verdict text, not an exit code read through a pipe:
Beyond the derived union,
pnpm --filter @objectstack/client check:exported-any-returnswas run because this change edits an exported callable's return type:
✅ no NEW exported callable of @objectstack/client resolves to 'any': 317 callables reached (52 caller-supplied generics, not counted as erasure), 42 ledgered site(s) still open.The build-dependent gates above were run after a full
turbo run build --filter=./packages/* --filter=./packages/*/*(70/70 successful),so none of them read a stale
dist.Repo-wide ESLint was run in full rather than narrowed:
eslint . --no-inline-config,5474 files judged by eslint's own config, 0 errors and 0 warnings, exit 0. (One
honest difference from
pnpm lint: that script prefixesnode --stack-size=4000;this invocation did not, and completed with zero fatal-parse messages.)
NOT MEASURED — recorded as neither green nor red, and not folded into the list above:
node scripts/check-test-completeness.mjs— refused:PREREQUISITE NOT MET — this gate grades a saved 'turbo run test' log, and no log was named(exit 3). Its owntext says the local reading is NOT MEASURED; CI tees the log and passes the path.
node scripts/pm/check-half-states.mjs— refused:PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential(exit 3). Nothing was swept.from this session.
Changeset
.changeset/environments-create-declares-wire-response-keys.md— additiveminoron@objectstack/client, as ruled. Nothing else in this diff publishes,so no other package is bumped.
Generated by Claude Code