feat(client): environments.delete gains purge and documents the two-step delete - #17642
Conversation
…tep delete The hosted control plane's DELETE /api/v1/cloud/environments/:id archives a live environment and tears down only an archived one on ?purge=1 (cloud ADR-0014); ?force=1 is the production confirmation and never a purge. The SDK sent force only, so an SDK caller could archive but never purge. - opts.purge sends ?purge=1 and combines with force - the return type declares the route's two 200 answers, discriminated by deleted - the JSDoc describes the two-step semantics instead of a one-call cascade - organizations.delete's JSDoc no longer claims hooks tear environments down Claude-Session: https://claude.ai/code/session_c5c0ce54-bb9c-478c-9e5b-cf44b80d4569 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 14 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 d32fdebc5e2aa60480af156b6c308e0a0065c975 && git checkout d32fdebc5e2aa60480af156b6c308e0a0065c975
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 76c9fab30ca406b7b1f06b8ca3db286af9f2bf8b 0dc866618f114906d15c9a5a98d4affe15bf0f01 && git checkout -B drift-repro 76c9fab30ca406b7b1f06b8ca3db286af9f2bf8b && git merge --no-ff 0dc866618f114906d15c9a5a98d4affe15bf0f01
node scripts/docs-audit/affected-docs.mjs --json 76c9fab30ca406b7b1f06b8ca3db286af9f2bf8b
|
PM 复核(maintainer direct dispatch,Session
|
| 项 | 读数 | 结论 |
|---|---|---|
| 文件 | 3 个:changeset、packages/client/src/index.ts、新测试 |
与卡片范围一致 |
packages/spec/src/** / 受治理面 |
0 / 0 | Clause-②: no 成立 |
| changeset | @objectstack/client: minor(给已发布方法新增可选参数) |
非破坏性,定级合理 |
| 关闭关键字 | Fixes #17636(os-dev.md 的写法) |
与 Closes 等效,接受 |
请求构造
URLSearchParams 分别设置 force=1 / purge=1,两者同时传时为 ?force=1&purge=1,都不传时不带查询串。服务端 DELETE 路由以 isDeleteFlagSet(req.query?.force / purge) 读取(environment-lifecycle.ts:1940-1941)——对得上。
返回类型 vs 服务端——逐字段一致
| 分支 | 客户端判别联合 | 服务端 ok({...}) |
|---|---|---|
| 归档 | environmentId、deleted: false、archived: true、purgeDeferred、retentionDays、warnings、message |
environment-lifecycle.ts 归档分支:同七个键;warnings: [] 恒为空(服务端注释说明正是为本方法的类型声明而设) |
| 拆除 | environmentId、deleted: true、purged: true、warnings |
拆除分支:{ environmentId, deleted: true, purged: true, warnings } |
没有臆造字段,也没有遗漏——避开了本文件注释记录过的 credential 那类缺陷。
JSDoc
两步语义写全了:活跃环境一律归档、purge 只对已归档环境生效(对活跃环境为 purgeDeferred)、failed 一次拆除、force 是生产确认且永不等于 purge、拒绝以 reject 形式抛出(409 / 404 / 403)。
organizations.delete——只改文档,接受
方法体不变(仍 POST {auth}/organization/delete,走 better-auth)。原 JSDoc 声称组织插件钩子会拆除环境;两个仓里都不存在这样的钩子,改为如实说明「不删除任何环境,需先逐个删除环境」。席位同时指出我派发令里的前提有误(我以为它调的是 cloud 返回 409 的那条路由)——更正成立。
范围外发现的去向
- 已归档环境重复删除会重置
archived_at、契约注释却称 idempotent no-op → 已在 objectstack-ai/cloud#2190 第 ② 项,补充了契约原文与行号 - cloud 路由台账
cloud-route-ledger.ts:185「SDK 只发 force」在 pin 越过本 PR 后过时 → 已记到 objectstack-ai/cloud#2181(pin bump 时一并改) - SDK 缺归档 / 恢复方法:功能空缺而非缺陷,不立卡
下一步:CI 全部结束且无失败 → 转 ready → 入合并队列(以 timeline added_to_merge_queue 为准)。有红按日志回派。
Fixes #17636
Clause-②: no — an optional client option plus a doc and return-type correction; no
packages/spec/src/**file is touched and no contract accept/reject behaviour changes.What changed
@objectstack/client,packages/client/src/index.ts:environments.delete(id, opts)gainsopts.purge?: boolean, sent as?purge=1. It is independent offorce(?force=1) and both may be passed on one call. The query string is built withURLSearchParams, so every combination yields'',?force=1,?purge=1or?force=1&purge=1. A call with no options, or withforcealone, builds the same URL as before.retentionDays);purgetears down only an already-archived environment, and is deferred on a live one (purgeDeferred: true);forceis the production confirmation, required on both calls for a production environment, and never a purge;failedenvironment is torn down in one call;forceor a system environment, 404, 403.deleted. The three keys the old declaration named (deleted,environmentId,warnings) sit on both members, so existing reads still compile.organizations.deleteJSDoc no longer claims that hooks on the organization plugin tear down the organization's environments. No such hook exists (evidence under acceptance item 5). JSDoc only; no behaviour change.Also added: the test file
packages/client/src/environments-delete-two-step.test.ts, and the changeset.changeset/client-environments-delete-purge.md, gradedminor(an additive widening of a published method, per the "WHICH LEVEL" rule).Server truth —
objectstack-ai/cloudateeac7b22b(cloud#2188), read fromorigin/mainAll paths are under
packages/service-cloud/src/.routes/environment-lifecycle.ts:1930-1945. Bare line numbers below are in this file.isDeleteFlagSet(:1711-1712, acceptstrue,1,'true'and'1') readsreq.query.forceandreq.query.purge(:1940-1941).ok(), which is{ success: true, data }(cloud-artifact-helpers.ts:107). A failure goes throughfail(), which is{ success: false, error: { code, message, httpStatus } }(cloud-artifact-helpers.ts:141); the client'sfetchwrapper throws it.datafor a 200)failroutes/types.ts:200, reached from:1931fail:1933fail:1935organization_idfail:1326(gateassertEnvAdminOrCreator,:1312)fail:1329fail, same words as row 3:1338fail:1347fail:1769-1771forcefail:1773-1778fail:1799, from:639,:656,:660,:666purge{ environmentId, deleted: false, archived: true, purgeDeferred, retentionDays, warnings: [], message }:1822-1839fail:1843-1844purge, or afailedone{ environmentId, deleted: true, purged: true, warnings }:1886fail:1887-1891Notes on the table:
classifyEnvironmentType(plan-entitlements.ts:767), which uses the explicitenvironment_type, else the legacyis_default.purgeDeferredis thepurgeflag (:1818), andretentionDaysisENV_RETENTION_DAYS, which is 30 (environment-deletion-policy.ts:32).decideEnvironmentDeletionMode(environment-deletion-policy.ts:44). Afailedenvironment tears down (:49); anarchivedone tears down only withpurge(:53); anything else archives (:57).test/environment-delete-route-two-step.test.tslines 325, 343, 355, 384, 397, 403, 416, 429 and 444.Only rows 11 and 13 are 200s, so together they are the whole declared return type.
Acceptance, item by item
All runs are at
0dc866618.purgesends?purge=1, combines withforce, and every combination is tested.undefined,{}, the four true/false pairs, and the two single-flag spellings), plus an id-encoding case.pnpm --filter @objectstack/client exec vitest run --maxWorkers=2 --reporter=verbose src/environments-delete-two-step.test.tsgaveTests 14 passed (14).params.set('purge', '1')line gaveTests 4 failed | 10 passed (14)(the three purge cases and the encoding case). It was then restored withgit checkout HEAD --and proven byte-identical to the HEAD bloba2631376.environments.deletediff:retentionDays;purgeonly on an archived environment, deferred on a live one;forceis the production confirmation, never a purge;failedenvironment is torn down in one call.@ts-expect-erroronpurgedfor an archive;@ts-expect-erroronarchived,purgeDeferred,retentionDaysandmessagefor a teardown.tsconfig.test.jsonprogram:tsc -p tsconfig.test.json --noEmit --listFilesexits 0 and lists it once.retentionDays: number;from the archive member madetscexit 2, withTS2339atenvironments-delete-two-step.test.ts(55,42)and(170,23). It was then restored to the HEAD blob.grep -rn "environments.delete"overcontent/,apps/docs/,skills/andpackages/client/README.mdgives 0 hits.content/docs/api/client-sdk.mdxnames the environments namespace only in passing (lines 18 and 158).organizations.delete.POST /api/v1/auth/organization/delete, not cloud'sDELETE /api/v1/cloud/organizations/:id. The cloud route is the one that answers 409 while environments remain (routes/environment-crud.ts:872and:905), and it is ledgered as an SDKgap(cloud-route-ledger.ts:340).organizationHooks(packages/plugins/plugin-auth/src/auth-manager.ts:2951) declares none for delete, andgit grep "beforeDeleteOrganization\|afterDeleteOrganization"finds 0 hits in this repo'spackages/**/srcand in cloud'spackages/*/src.Gates
Declared unlocked mode.
scripts/pm/os-verify-lock.shfound no usableflockon this host (macOS), so every locked run below printedVERDICT command-exit N · UNLOCKED (declared): nothing was serialized.pnpm exec turbo run build --filter=@objectstack/client --concurrency=2Tasks: 33 successful, 33 total·VERDICT command-exit 0pnpm --filter @objectstack/client testTest Files 42 passed (42)·Tests 506 passed (506)·VERDICT command-exit 0pnpm --filter @objectstack/client typecheckcheck:test-typecheck: OK, 0 file(s) / 0 error(s) ·VERDICT command-exit 0pnpm check:nul-bytescheck-nul-bytes: OK (scanned 8386 text file(s), no raw control bytespnpm check:adr-0087-registrationthis PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)node scripts/check-changeset-no-major.mjsThis diff introduces no major bump; the level axis is not applicable locally (nopull_requestpayload)pnpm --filter @objectstack/spec run check:skill-examples, after building@objectstack/client-react258 prose examples type-check across 3 surface(s)node scripts/pm/dispatch-gates.mjs --commandsderived 60 families from the three changed paths. All 60 were run and recorded with exit codes. The--ranreconciliation reads:60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED.NOT MEASURED. Each gate below refused with its own
PREREQUISITE NOT MET, exit 3:pnpm check:dual-build-cjs-loads: it reads every workspace package'sdist/. 35 packages are unbuilt here, and building them is a whole-workspace build, which this dispatch excludes because the box is shared.pnpm check:type-check-debt:--re-measureneeds the wholepackages/*closure built (same exclusion). The client test-layer program was measured directly instead, and compiles with the new file in it (acceptance item 3).Narrowed lint. Repo-wide
pnpm lintbelongs to CI. The narrowed run waseslint --no-inline-config --format json packages/client/src/index.ts packages/client/src/environments-delete-two-step.test.ts, which exited 0.eslint.config.mjsignores neither.eslint.config.mjsenables no type-aware linting (noparserOptions.project, noprojectService), so this diff cannot move any untouched file's verdict.Acceptance notes
organizations.deleteto be the cloud route that answers 409. The SDK method is a different route, better-auth's. Its JSDoc was corrected for its own false claim, not for the 409 behaviour.DELETE /api/v1/cloud/environments/:id(cloud-route-ledger.ts:185) says the SDK method sendsforceonly. That goes stale once this lands and cloud pins past it. Carrier: the cloud pin bump that picks this up.purgere-runs the archive branch. It re-stampsarchived_at, which restarts the retention clock, and writes another audit row, whileenvironment-deletion-policy.tscalls that case an idempotent no-op. It is never destructive. Recorded for the cloud owner ofenvironment-lifecycle.ts.gap). Out of scope.Generated by Claude Code