Skip to content

ADR-0006 D2 (SDK half): rename client.projects.* to client.environments.*, unwrap keys follow the wire, JSDoc names the endpoint (#12866) - #12885

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-12866-adr0006-d2-sdk-environments
Aug 28, 2026
Merged

ADR-0006 D2 (SDK half): rename client.projects.* to client.environments.*, unwrap keys follow the wire, JSDoc names the endpoint (#12866)#12885
os-zhuang merged 6 commits into
mainfrom
claude/issue-12866-adr0006-d2-sdk-environments

Conversation

@hotlong

@hotlong hotlong commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #12866
Fixes #12882
Part of #12865

⛔ LANDING CONSTRAINT — read before touching this PR

This PR must stay a DRAFT. Do not flip it ready, do not merge it, do not add it to the merge queue, do not arm auto-merge. It is one half of a coordinated cross-repo rename and it does not ship alone: shipping the SDK half by itself is ADR-0006 D3, permanently declined. The producer half is objectstack-ai/cloud PR #1692 (card cloud#1691), which draft-parks the same way. The maintainer opens the landing window and lands both halves plus the control-plane deploy together; see the epic parent for the window checklist.


What this is

The SDK half of ADR-0006 D2, ruled three times over: #12473 option 3, ADR-0006 D2 itself, and the maintainer's 2026-08-28 unpark ruling recorded on #12865. The projects method namespace on ObjectStackClient becomes environments, every declared unwrap key follows the wire rename, and the JSDoc stops asserting a server class that does not exist. No aliases — no client.projects getter, no fallback hedge in any consumer, no mapping layer (ADR-0006 D3 declined one with reasons).

Branch cut from dc75ba857 (origin/main, containing PR #12848). Verified head for every number below: cae0e248c.

The rename

client.projects.* becomes client.environments.* for all 16 methods including the nested packages block. URL paths are untouched — they were already on the environments spelling. The full before/after tables live in the changeset, which is the migration entry.

Declared unwrap keys, censused across the whole block rather than pattern-matched:

method before after
list projects + total environments + total
get project + database/credential/membership/organization environment + the same joined blocks
create project + database environment (see below)
update, activate, updateHostname, updateVisibility, retryProvisioning project environment
delete, rotateCredential, listRevisions, listBranches, renameBranch, deleteBranch, listDrivers, all packages.* no project-family key unchanged

Those match the producer half's post-rename table key for key.

Truth restorations beyond the pure rename — each one measured, each one declared

Three, all inside the declared file surface, all the same defect class as the card ("the declared shape must match the wire; the JSDoc must not assert what does not exist"):

  1. create never answered a project key at all. POST /api/v1/cloud/environments is served by cloud's packages/service-cloud/src/routes/environment-lifecycle.ts, which builds its body key by key and answers environment + warnings + durationMs + conditional hostnameAssignment. So the old declaration was not merely pre-rename, it was false against the running control plane — and os environments create read res.project.id through it, which means the default --activate has been silently doing nothing and the table output has been printing undefined. The rename fixes both.
  2. create no longer declares a database key. That route does not send one, and the key was declared NON-optional, so res.database.driver typechecked and threw at runtime — the exact failure a declared shape exists to prevent. get is the method that really answers a database block; it keeps it. The three keys create really does send are deliberately still undeclared: adding published surface is a separate decision, filed as environments.create() declares one response key; the control plane sends four (warnings / durationMs / conditional hostnameAssignment undeclared) #12883.
  3. A broken @see path, twice. docs/adr/0002-project-database-isolation.md does not exist; the file is docs/adr/0002-environment-database-isolation.md. Both occurrences in packages/client/src/index.ts are corrected. The "Active project id" prose next to the second one is left alone — that is the ScopedProjectClient noun lane, filed as ADR-0006 leftovers in @objectstack/client: client.project() / ScopedProjectClient / two test titles still speak the pre-v5.0 noun #12882.

The JSDoc

The create docblock said the server delegates to ProjectProvisioningService.provisionProject. Measured 2026-08-28 against the cloud repo's origin/main: zero hits for that spelling anywhere in packages/service-cloud/src (positive control — the routes files that produce these envelopes are right there and git grep finds them). The class the control plane really constructs today is EnvironmentProvisioningService and the method is provisionEnvironment.

Both docblocks — this SDK's and os environments create's — now name the endpoint rather than a server class, and say why. The class lives in a repo this one never compiles against, so a class name here rots with nothing to catch it; the endpoint is the one identifier the method itself builds and an in-repo reader can verify. Naming the currently-correct class would have re-created the exact rot in one edit.

The fourth surface — the environment-scoped sub-client (#12882)

Folded in by the same maintainer ruling of 2026-08-28 (「可以」), as its own commit
e1c8d20ea. ADR-0006's D1 census named three surfaces and missed this one; the
decision record on #12882 classes it as an oversight, not a deliberate retention,
so Prime Directive 13 is not engaged by renaming it.

before after
client.project(id) client.environment(id)
ScopedProjectClient (exported class) ScopedEnvironmentClient

No aliases, same as the namespace: an import of the old class name fails at the
import line, which is the loudest channel available. Behaviour is unchanged — the
scoped client still prefixes /api/v1/environments/:environmentId/... and exposes
the same data / meta / batch / packages shape; only the guard message moves,
to [ObjectStack] environment(id): environmentId is required.

Two consumers that would otherwise have been left asserting something false:
client-url-conformance.test.ts's NON_HTTP map is keyed by METHOD NAME, so its
'project' entry had to become 'environment' or the sweep's "methods neither
driven nor declared" assertion fails; and the two client.test.ts titles that said
/projects/:id while their assertions pinned /environments/:id are corrected —
that was the stale-title half of #12882.

Two hand-written docs asserted that client.project(id) survives as a compatibility
method name. Both are corrected rather than left to ship as documented falsehoods:
content/docs/api/environment-routing.mdx (a code sample that would no longer
compile, plus its migration-checklist line) and content/docs/concepts/north-star.mdx
(whose "Drift To Keep Shrinking" bullet for this exact method is replaced by the drift
that genuinely remains).

Deliberately NOT renamed, each being a different surface with its own decision
all measured, none silently skipped:

left alone why
setProjectId / getProjectId on the client getProjectId is a cross-package protocol contract: packages/runtime/src/domains/meta.ts duck-types it and packages/metadata-protocol/src/protocol.ts declares it. A coordinated rename, not a local one
enableProjectScoping / projectResolution live REST API config keys, read by packages/cli/src/commands/serve.ts. A different package's published surface. The docblocks naming them are worded so they stay true
packages/spec/src/migrations/** and docs/protocol-upgrade-guide.md ADR-0087 ledger prose about a past v17 change, plus the guide generated from it. packages/spec is off-limits to this card, and rewriting a historical migration record would falsify it
four other pending .changeset/*.md other authors' release notes, accurate when written. The changeset here names them so whoever compiles the release notes is not surprised
docs/audits/2026-07-dispatcher-client-route-coverage.md a dated audit record

Consumer census

Every consumer of the projects method namespace or of the project/projects unwrap keys, found by grepping this whole repo plus a read-only sweep of the sibling checkouts. Nothing outside the declared surface was edited.

In this PR (the declared surface):

file what it consumed
packages/client/src/index.ts the namespace, all 8 unwrap declarations carrying a project-family key, the block's JSDoc, and two cross-references to the "projects surface" precedent elsewhere in the file
packages/cli/src/commands/environments/list.ts projects.list, res.projects, the local bound from it
packages/cli/src/commands/environments/create.ts projects.create, projects.activate, three res.project reads, the stale provisioning-class docblock
packages/cli/src/commands/environments/show.ts projects.get, res.project, a docblock naming the method
packages/cli/src/commands/environments/switch.ts projects.get, projects.activate, lookup.project and the local bound from it
packages/cli/src/commands/environments/bind.ts projects.get, projects.update, two current.project.metadata reads
packages/client/src/client-url-conformance.test.ts CONTROL_PLANE_NAMESPACE, the literal string that bounds which namespace may reach /api/v1/cloud/ — a real dependency on the namespace NAME, not a comment
packages/client/src/return-type-precision.test.ts two docblock references to projects.get / client.projects.*

packages/cli/src/commands/environments/environments.test.ts was measured and is clean: it derives oclif command ids from file paths and asserts nothing about the SDK namespace or the envelope keys. Its os projects bind string is a deliberate reverse-verification fixture for a command-id rename and is unrelated.

Outside the declared surface — listed, not touched:

consumer disposition
client.project(id) / ScopedProjectClient / the environmentId option's "Active project id" prose the second project surface on the same class; a published-class rename needing its own coordinated window. Filed as #12882
packages/client/src/client.test.ts lines 1989 / 1999 two ScopedProjectClient test TITLES say /projects/:id while their assertions pin /environments/:id. Same card, #12882
content/docs/releases/v17.mdx line 868 release-owned, never edited in a code PR (CLAUDE.md). It is a historical note about a method deleted in #3702
packages/client/CHANGELOG.md, packages/cli/CHANGELOG.md, root CHANGELOG.md generated release history; renaming history would falsify it
client-url-conformance.test.ts line 154's projects.listTemplates mention history about a method that no longer exists under any name; left as written and annotated
cloud's projects-namespace-coverage.test.ts and cloud-route-ledger.ts they drive this SDK by namespace name, so they move with the producer half in the same window. Cloud's territory, not this PR's — the objectstack-side comment pointing at that filename is annotated to say so
../objectui measured read-only against its origin/main: zero consumers of the control-plane namespace or these keys
client.environments.create's request fields (project_type, slug, region, …) request side, not response side; already filed and open as #3739
create's undeclared warnings / durationMs / hostnameAssignment new published surface, a separate decision. Filed as #12883

Tests

New: packages/client/src/client.environments-namespace.test.ts — 7 runtime tests plus three exported compile-time pin functions.

The load-bearing pin is the absence of a compatibility layer, which no ordinary assertion observes: every test of client.environments stays green if someone later adds a projects getter beside it. So the file asserts 'projects' in client is false on the shape, drives each envelope through a recording fetch and reads the keys back, and carries one @ts-expect-error per retired key spelling. Those type pins are real, not phantom: packages/client/tsconfig.test.json includes src/**, the typecheck script names it, and tsc --listFiles -p tsconfig.test.json shows all three edited/added test files inside the program. An unused @ts-expect-error is itself an error, so a green typecheck proves every pinned property access really does not exist — the reverse verification is structural and needs no ablation.

Updated: client-url-conformance.test.ts (the namespace bound), return-type-precision.test.ts (two docblocks).


Verification — all at cae0e248c

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host: no usable flock. The shared verify lock is declared Linux-only (flock is util-linux, and a stock macOS does not ship it), so the commands below were run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for these runs, nor for any sibling agent in this container while they ran.

run verdict
pnpm --filter '@objectstack/cli...' build VERDICT command-exit 0 (70/70 turbo tasks on the later full build)
pnpm --filter @objectstack/client test Test Files 27 passed (27) · Tests 371 passed (371)
the new file alone, verbose Test Files 1 passed (1) · Tests 7 passed (7)
pnpm --filter @objectstack/client --filter @objectstack/cli typecheck both Done; check:test-typecheck: OK — @objectstack/client's test layer compiles under packages/client/tsconfig.test.json; 0 file(s) / 0 error(s)
pnpm lint (repo-wide eslint . --no-inline-config, no narrowing) exit 0, no output

Gate families derived mechanicallynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on the real change set (10 paths, three-dot merge-base semantics), then every derived family plus every convention-triggered one was run. 26 green, 1 red (declared below), 1 NOT MEASURED:

  • Green: nul-bytes, changeset-gate-self-tests, empty-changeset, changeset-no-major, objectui-changeset, pm-half-states, release-rehearsal-clone --self-test, cross-package-test-inputs (both spellings), ci-filter-parity, comment-mask-adoption, plugin-teardown-shape, objectql-double-limit, page-declaration-shape, published-files, slot-lookup, test-source-alias, type-source-resolution, query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, type-check-debt, i18n, i18n-coverage, spec check:skill-examples, client check:exported-any-returns, docs-audit/check-affected-docs.
  • Named verdict lines worth quoting: check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 63.3s, 1570 raw tsc error(s) total, none above its recorded number. · check-i18n-coverage: OK (12 config(s), 602 baselined untranslated string(s), none new).
  • NOT MEASURED, not green and not red: scripts/pm/check-half-states.mjs exits 3 — PREREQUISITE NOT MET — the anonymous API rate limit (60 req/h) is exhausted for this egress IP. It judges the PM board, not this diff, and it printed no finding line.
  • Three gates first reported COULD-NOT-MEASURE against an unbuilt worktree (i18n-coverage on an unbuilt @objectstack/connector-mcp, spec check:skill-examples on an unbuilt packages/client-react/dist, type-check-debt on the unbuilt closure). All three were re-run green after turbo run build across ./packages/* and ./packages/*/* — they were prerequisite states, not findings.

packages/client/exported-any-returns.json needed no edit: measured, it holds zero ObjectStackClient.projects.* entries, because every method in this block returns an any-CONTAINING type rather than bare any, which that gate deliberately does not list. check:exported-any-returns is green against the rebuilt dist.

@objectstack/cli suite — 5 files red, and they are red without this change

pnpm --filter @objectstack/cli test reports Test Files 5 failed | 194 passed (199) · Tests 3 failed | 2258 passed | 14 skipped. All five were then re-run on a clean worktree detached at the merge base dc75ba857, with none of this diff present and its own --filter '@objectstack/cli...' build: identical file set, identical failing-test set (Tests 3 failed | 28 passed | 14 skipped (45)). They are three serve-* port-drift e2e arms plus two login-NDJSON e2e files, all host-platform behaviour on macOS, none of them touching the renamed surface. Recorded as an observation in #12884, and the comparison worktree was removed afterwards.


Two declared deviations from the dispatch

  1. The changeset is minor, not major. The dispatch asked for major on @objectstack/client. Measured on this tree: scripts/check-changeset-no-major.mjs is a launch-window guard that refuses any diff introducing a major, because every publishable package is in the Changesets fixed group, so one major promotes all ~70 packages at once. There is no .changeset/pre.json, so the RC exemption does not apply, and --list reports 356 pending changeset(s), 0 declaring a major — nothing in the current stock does this. Sibling breaking changesets in the tree ship as minor and say so in words (manifest-kind-globs-retired.md, retire-auth-scim-provider-schema.md). The changeset therefore says BREAKING in prose and ships minor, matching the house convention. If this coordinated window is meant to BE the next major cut, the change is one word in the changeset plus the allow-major PR label — a release-cadence act reserved to the maintainer under Prime Directive Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15, deliberately not performed here. @objectstack/cli is minor for the same reason and on its own merits: its --format json payload keys move with the wire.

  2. check-adr-0087-registration is RED, deliberately, and the reason is recorded in the changeset itself. The honest disposition for this change is not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient) — no Zod schema, no packages/spec declaration, no authorable key, no stored representation, so objectstack migrate meta provably has nothing to rewrite (measured: zero projects envelope contracts in packages/spec/src; positive control, environments hits do exist there). Steps 1–3 of the gate's predicate pass. Step 4 refuses it because packages/spec/src/api/contract.zod.ts line 164 names ObjectStackClient in a JSDoc prose comment while neither declaring nor importing it, and the predicate does not strip comments. Filed as check-adr-0087-registration refuses a runtime-interface-only disposition on a symbol named only in a JSDoc comment inside a .zod.ts #12881.

    The claim was not swapped for not-required (no-migration-prescription), which would pass mechanically: this changeset ships a full before/after migration table, so that claim would be the self-contradiction the gate's own header documents as the ADR-0087's changeset disposition has no category for published runtime TS interfaces with no metadata surface — the #8277 exemption argument is correct, unverifiable, and will be re-litigated #8299 anti-pattern, held by a detector blind spot rather than a positive finding. registered is the other checkable option and needs an entry in packages/spec/src/migrations/registry.ts, which this card's ruling puts off-limits. Left stated and escalated rather than routed around. The gate runs in pr-automation.yml / cut-rc.yml, neither of which is in the six required contexts.

Findings filed (all unassigned, none fixed here)

#12881 · #12882 · #12883 · #12884. Request-side sibling #3739 was already open and is referenced, not duplicated.

Authored by Claude Code subagent agent-a1d45b69a14c2302b under session session_65d2faee-1ff4-4be8-be28-72b972c539d9 (durable copy of the attribution, since editing a PR body normalises the footer below).

Generated by Claude Code

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via ClientConfig (symbol), ObjectStackClient (symbol))
  • content/docs/api/environment-routing.mdx (via ObjectStackClient (symbol), /api/v1/cloud (route), /api/v1/cloud/environments (route))
  • content/docs/api/wire-format.mdx (via ObjectStackClient (symbol))
  • content/docs/concepts/north-star.mdx (via /api/v1/cloud (route), /api/v1/cloud/environments (route))
  • content/docs/deployment/cli.mdx (via os environments bind (command), os environments create (command), os environments list (command), os environments show (command), os environments switch (command))
  • content/docs/deployment/index.mdx (via /api/v1/cloud (route))
  • content/docs/deployment/publish-and-preview.mdx (via /api/v1/cloud (route))
  • content/docs/kernel/runtime-services/data-service.mdx (via ObjectStackClient (symbol))
  • content/docs/kernel/runtime-services/storage-service.mdx (via ObjectStackClient (symbol))
  • content/docs/permissions/authentication.mdx (via ObjectStackClient (symbol))
  • content/docs/plugins/packages.mdx (via ObjectStackClient (symbol))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via ObjectStackClient (symbol))

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

  • content/docs/releases/v17.mdx (via ObjectStackClient (symbol), os environments create (command), /api/v1/cloud (route))

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
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • 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.

Coarse fallback — 23 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 a77e4601007de2feac82c437415c216aff333e72packageMentionDocs.

Which tree this was computed on

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 28, 2026
…and client.project(id) to client.environment(id) (#12882)

The fourth project-spelled surface on the SDK client class. ADR-0006's D1
census named three surfaces and missed this one; the maintainer ruled on
2026-08-28 that it folds into the same D2 coordinated major.

No aliases, same as the namespace rename: neither the old class name nor the
old getter survives. Deliberately untouched, each being a different surface
with its own decision: setProjectId/getProjectId (getProjectId is a
cross-package protocol contract spoken by runtime and metadata-protocol) and
the REST API config keys enableProjectScoping/projectResolution.
@os-zhuang
os-zhuang self-requested a review August 28, 2026 14:47
@os-zhuang
os-zhuang marked this pull request as ready for review August 28, 2026 14:48
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/l tests tooling

Projects

None yet

2 participants