From af3fe67043cd1740121547cf5fe242116d6c5539 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 10:26:53 +0100 Subject: [PATCH 1/4] docs: show how to pass a node address to platform commands Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/guides/deploying.md | 16 ++++++++++++++++ skills/prisma-composer-core-concepts/SKILL.md | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index 09065ddf0..1d7785563 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -328,6 +328,22 @@ array of `{ address, kind, platformId, url }`, with `null` for a missing value. `prisma deploy list` needs a `prisma` CLI release that includes it; 8.0.0-rc.26 does not. +### Using an address with platform commands + +A platform command whose subject is a project, service, database or bucket +takes `@:
` in place of the platform ID. Without an +environment, the address means production: + +```bash +prisma service logs @pr-42:storefront-auth.auth.service +prisma postgres delete @storefront-auth.auth.database --confirm @storefront-auth.auth.database +``` + +The command finds the row whose `logicalId` is the address, then acts exactly +as it does when given the ID. It never changes the deploy state, so the next +deploy recreates anything deleted this way. This needs a `prisma` CLI release +newer than 8.0.0-rc.28. + ## Removing an app or an environment To remove an app with every environment, delete its Project. Its id is in the diff --git a/skills/prisma-composer-core-concepts/SKILL.md b/skills/prisma-composer-core-concepts/SKILL.md index f7bab7cb2..0141a1b3b 100644 --- a/skills/prisma-composer-core-concepts/SKILL.md +++ b/skills/prisma-composer-core-concepts/SKILL.md @@ -412,6 +412,13 @@ It finds the app as `deploy` does (`--name`, `--entry`) and fails with address, kind, platform ID, URL per line; `--json`: `[{ address, kind, platformId, url }]`. It needs a `prisma` CLI release that includes it; 8.0.0-rc.26 does not. +**Using an address with platform commands.** A `prisma project|service|postgres|bucket` +command takes `@:
` in place of the platform ID +(`prisma service logs @pr-42:shop.api`); no environment means production. It +acts on that one row and never changes the deploy state, so the next deploy +recreates anything deleted this way. It needs a `prisma` CLI release newer than +8.0.0-rc.28. + **Removing an app** with every environment is a platform command: find the Project id with `prisma project list`, then `prisma project delete --confirm `, and check `prisma project list` again to confirm it is gone. It works while the From e1e71889cd662569cb0e097a31cf5b095003f805 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 10:26:53 +0100 Subject: [PATCH 2/4] drive: record node-addressing final retro and deferrals Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/PLAN.md | 17 +++++++++++++++++ .drive/PROJECT.md | 12 ++++++++++++ .drive/deferred.md | 7 ++++++- .drive/projects/node-addressing/retros.md | 12 ++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 .drive/PLAN.md create mode 100644 .drive/PROJECT.md create mode 100644 .drive/projects/node-addressing/retros.md diff --git a/.drive/PLAN.md b/.drive/PLAN.md new file mode 100644 index 000000000..0db452f30 --- /dev/null +++ b/.drive/PLAN.md @@ -0,0 +1,17 @@ +# Planning failure modes + +Checks to run when decomposing a project into slices. + +- **Every slice serves the project's purpose.** Before the plan is final, + read each slice against the spec's purpose sentence. A slice that came out + of the same design discussion but solves a different problem is its own + project or a deferred item, not a slice. Added 2026-10-28: the + node-addressing project carried a cascading Branch delete and + `prisma branch delete` because the discussion that started it was about + teardown. Neither was about addresses; Will canceled both. +- **Read the owning team's recorded decisions before planning a slice in + their repository.** A slice in another team's repository (pdp-control-plane, + prisma-cli) needs that repository's specs and decision records read first. + Added 2026-10-28: the cascading Branch delete was planned without reading + pdp-control-plane's Branch spec, which records the platform team's decision + against a cascade. The conflict surfaced only when the slice started. diff --git a/.drive/PROJECT.md b/.drive/PROJECT.md new file mode 100644 index 000000000..6e898f730 --- /dev/null +++ b/.drive/PROJECT.md @@ -0,0 +1,12 @@ +# Project conventions + +- **Long-lived docs** go into the existing design docs, not a per-project + folder: decisions into `docs/design/90-decisions/` as new or amended ADRs, + terms into `docs/design/03-domain-model/glossary.md`, user-facing behaviour + into `docs/guides/` and `skills/`. Each slice lands its own docs, so + close-out usually migrates nothing. +- **Transient** at close-out: everything under `.drive/projects//`, + including spikes' findings notes, QA records and review files. Their + evidence belongs in the slice's PR description. +- **Tracking:** Will tracks Composer work outside Linear. Close-out PRs + reference no Linear Project. diff --git a/.drive/deferred.md b/.drive/deferred.md index f323a5e2a..3dd2fb262 100644 --- a/.drive/deferred.md +++ b/.drive/deferred.md @@ -245,4 +245,9 @@ ADR-0053 (the commands). What we deliberately didn't do: # Node addressing — deferred (2026-10-22) -- **`prisma deploy delete @:
`: tear down a subtree.** Not built: `project delete`, `branch delete` and platform deletes by address cover every teardown scenario we found. Its only own jobs are emptying a stage while keeping its Branch (equivalent: `branch delete`, then redeploy) and resetting one module (equivalent: a few platform deletes, then redeploy). Design if it is ever needed: follow containment (the node and everything its modules provisioned), not dependency edges; delete compute before data and wait until each instance can be deleted; list nodes outside the subtree that depend on it and warn without blocking; leave the stored topology and deploy state alone so the next deploy rebuilds it; a subcommand of `deploy`; production must be named explicitly. Origin: node-addressing design discussion. +- **`prisma deploy delete @:
`: tear down a subtree.** Not built: `project delete`, the `destroy` operation and platform deletes by address cover every teardown scenario we found. Its only own jobs are emptying a stage while keeping its Branch (equivalent: `destroy`, then redeploy) and resetting one module (equivalent: a few platform deletes, then redeploy). Design if it is ever needed: follow containment (the node and everything its modules provisioned), not dependency edges; delete compute before data and wait until each instance can be deleted; list nodes outside the subtree that depend on it and warn without blocking; leave the stored topology and deploy state alone so the next deploy rebuilds it; a subcommand of `deploy`; production must be named explicitly. Origin: node-addressing design discussion. + +# Node addressing — deferred at project close (2026-10-28) + +- **Stage removal through the platform: a cascading Branch delete in the Management API and `prisma branch delete`.** Canceled by Will, 2026-10-27: it is not about addresses. pdp-control-plane's Branch spec also records a decision against a cascading Branch delete. Removing one environment stays the `destroy` operation. Origin: node-addressing plan. +- **Remove CI's `deleteProjectDeep` workaround** in `scripts/ci-cleanup-utils.ts`. It retries `project delete` after a 409 caused by live compute, which the platform no longer returns: `project delete` succeeded while services ran in the node-addressing QA. Not done because it is CI cleanup, not addressing. Origin: node-addressing plan. diff --git a/.drive/projects/node-addressing/retros.md b/.drive/projects/node-addressing/retros.md new file mode 100644 index 000000000..b6f444a15 --- /dev/null +++ b/.drive/projects/node-addressing/retros.md @@ -0,0 +1,12 @@ +# Retros + +## 2026-10-28 — final retro at project close + +**Outcome.** Track A (full addresses with automatic migration, entry in the config, `prisma deploy list`) shipped in Composer 0.35.0 and prisma 8.0.0-rc.28. B1 (`@:
` in platform commands) merged in prisma-cli. The asks app upgraded in production with no data lost. B2 and B3 were canceled. + +**What went wrong.** +1. Stage removal (B2, B3) was planned as part of this project. Root cause: the design discussion started as "should Composer have a `destroy` command?", and the plan kept every outcome of that discussion instead of checking each slice against the purpose. Landed: `.drive/PLAN.md`, "Every slice serves the project's purpose". +2. B2 contradicted pdp-control-plane's recorded decision against a cascading Branch delete. Root cause: the plan was written without reading that repository's specs. Landed: `.drive/PLAN.md`, "Read the owning team's recorded decisions". +3. Close-out had no `.drive/PROJECT.md` saying where long-lived docs go. Landed: `.drive/PROJECT.md`. + +**What went right.** The spike checked the three upstream Alchemy assumptions on the real platform before A1 began, and A1's real-platform QA found the platform's bucket-on-new-Branch bug, which the platform team fixed. From 1a67e0a90e72c0554d04681aa51e232a2d18adec Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 10:26:53 +0100 Subject: [PATCH 3/4] chore: delete .drive/projects/node-addressing Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../assets/spike-alchemy-renames.md | 45 ----- .../projects/node-addressing/design-notes.md | 94 --------- .drive/projects/node-addressing/handover.md | 72 ------- .drive/projects/node-addressing/plan.md | 85 -------- .drive/projects/node-addressing/retros.md | 12 -- .../a1-fully-qualified-identity/plan.md | 72 ------- .../slices/a1-fully-qualified-identity/qa.md | 137 ------------- .../reviews/code-review.md | 182 ------------------ .../reviews/system-design-review.md | 92 --------- .../a1-fully-qualified-identity/spec.md | 66 ------- .../upgrade-effects.md | 46 ----- .../slices/a2-deploy-list/plan.md | 25 --- .../slices/a2-deploy-list/qa.md | 129 ------------- .../a2-deploy-list/reviews/code-review.md | 106 ---------- .../reviews/system-design-review.md | 52 ----- .../slices/a2-deploy-list/spec.md | 60 ------ .../slices/a3-entry-in-config/plan.md | 37 ---- .../slices/a3-entry-in-config/qa.md | 48 ----- .../a3-entry-in-config/reviews/code-review.md | 134 ------------- .../reviews/system-design-review.md | 60 ------ .../slices/a3-entry-in-config/spec.md | 69 ------- .drive/projects/node-addressing/spec.md | 84 -------- 22 files changed, 1707 deletions(-) delete mode 100644 .drive/projects/node-addressing/assets/spike-alchemy-renames.md delete mode 100644 .drive/projects/node-addressing/design-notes.md delete mode 100644 .drive/projects/node-addressing/handover.md delete mode 100644 .drive/projects/node-addressing/plan.md delete mode 100644 .drive/projects/node-addressing/retros.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/plan.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/qa.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/code-review.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/system-design-review.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/spec.md delete mode 100644 .drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md delete mode 100644 .drive/projects/node-addressing/slices/a2-deploy-list/plan.md delete mode 100644 .drive/projects/node-addressing/slices/a2-deploy-list/qa.md delete mode 100644 .drive/projects/node-addressing/slices/a2-deploy-list/reviews/code-review.md delete mode 100644 .drive/projects/node-addressing/slices/a2-deploy-list/reviews/system-design-review.md delete mode 100644 .drive/projects/node-addressing/slices/a2-deploy-list/spec.md delete mode 100644 .drive/projects/node-addressing/slices/a3-entry-in-config/plan.md delete mode 100644 .drive/projects/node-addressing/slices/a3-entry-in-config/qa.md delete mode 100644 .drive/projects/node-addressing/slices/a3-entry-in-config/reviews/code-review.md delete mode 100644 .drive/projects/node-addressing/slices/a3-entry-in-config/reviews/system-design-review.md delete mode 100644 .drive/projects/node-addressing/slices/a3-entry-in-config/spec.md delete mode 100644 .drive/projects/node-addressing/spec.md diff --git a/.drive/projects/node-addressing/assets/spike-alchemy-renames.md b/.drive/projects/node-addressing/assets/spike-alchemy-renames.md deleted file mode 100644 index 943ffe289..000000000 --- a/.drive/projects/node-addressing/assets/spike-alchemy-renames.md +++ /dev/null @@ -1,45 +0,0 @@ -# Spike: Alchemy renames for dotted resource IDs - -Run 2026-10-23 against the real platform with `alchemy@2.0.0-beta.83`, `Prisma.providers()`, and Composer's hosted state layer (`prismaStateLayer` from `@internal/lowering/state`). Throwaway Project `spike-renames-891e50` (`proj_q02oduck65rs8yeu5n3s0iq0`), now deleted. Scripts and logs: `wip/spike-alchemy-renames/`. - -## The API to use - -```ts -import { renamedFrom } from 'alchemy/Rename'; - -const db = yield* Prisma.Database('shop.catalog', { ...props, logicalId: 'shop.catalog' }) - .pipe(asOwnRow, renamedFrom('catalog-db')); -const conn = yield* Prisma.Connection('shop.catalog.conn', { database: db, name: 'catalog' }) - .pipe(renamedFrom('catalog-conn')); -``` - -`renamedFrom(...formerIds)` takes one or more former IDs, most recent first. A bare string resolves against the current namespace; `{ fqn }` is an absolute state key. Composer uses no namespaces, so bare strings are right. Swapping two IDs in one deploy fails the plan. A filtered (selected-resource) deploy refuses to migrate a rename. - -## 1. A rename keeps the resource: yes - -Deployed `catalog-db`, `catalog-conn`, `api-svc`, wrote a row, then redeployed as `shop.catalog`, `shop.catalog.conn`, `shop.api` with renames. Plan output: `[shop.catalog] update (renamed from catalog-db)`, and the same for the other two. Nothing was created or deleted. - -- Database `db_cexzg9ehp6t6n473g7e3j4kd` and Connection `con_zvnfetb9svadtlskzhdt1on4` kept their ids before and after. App `cps_k3dz06azn3ik2sg0asgzxf6i` kept its id too. -- `select * from spike` still returned `1 | written before rename`. -- The hosted state rows moved to the new keys and kept the same `instanceId`. - -Caveat: Composer passes no `name` on platform deploys, so the database display name comes from the resource ID. It changed from `spike-catalog-db-q1-oawhghear3vmnftc` to `spike-shop-catalog-q1-oawhghear3vmnftc`. This is an in-place update. A Connection is **replaced** if its `name` prop changes, so keep passing the same `name` (`catalog`, not `shop.catalog`). - -## 2. Rename declarations on a fresh Branch: yes, it deploys cleanly - -Empty state, new IDs, renames declared: `Plan: 3 to create`, `Done: 3 succeeded`. A rename that matches no state row is ignored. Leftover declarations are also harmless on later deploys: an unchanged redeploy planned `no changes`. - -## 3. Partial failure, then rerun: clean - -- **Failing resource:** I ran five renames plus a new Database with an invalid region. The broken create failed. The other five updates still finished, and the run reported `Failed: 5 succeeded, 1 failed`. After I removed the bad resource, the rerun planned five `noop`s and one `delete` (the failed row). All ids stayed the same. -- **Hard kill:** I sent `kill -9` to the process at its first `updating` line. By then all five rows had already moved to the new keys, and the old rows were gone. Three were left with status `updating`. The rerun did five in-place updates, and all ids stayed the same (`db_bqaaomu7…`, `db_l9exb214…`, `con_wmv1szgv…`, `con_kisu3u33…`, `cps_lb0bg1uz…`). -- **Not hit live:** the moment when both the old and new rows exist. Upstream writes the new row before it deletes the old one, and treats two rows with the same `instanceId` as an unfinished migration. -- **Surprise:** after the kill, the dead process's deploy lease blocked every state call for about 60 seconds, until its time limit ran out. A user who retries at once will get the lease-held error. - -## 4. Dotted IDs in hosted state: yes - -`shop.catalog.conn` was created (by migration and by fresh create), read back on the next deploy as a no-op, and fetched directly over HTTP. A `destroy` on the fresh Branch deleted all three rows (state list `[]`) and their platform resources. The HTTP store runs keys through `encodeURIComponent`, which leaves dots as they are, and the API handled them fine. - -## 5. Changing only `logicalId` updates in place: yes - -Changing `logicalId` from `catalog` to `shop.catalog` (and from `api` to `shop.api`) planned `update`. Database `db_cexzg9ehp6t6n473g7e3j4kd` and App `cps_k3dz06azn3ik2sg0asgzxf6i` kept their ids. The platform showed the new `logicalId`, and the row was still there. The Connection also showed `update` because its database's output changed, but it kept its id. diff --git a/.drive/projects/node-addressing/design-notes.md b/.drive/projects/node-addressing/design-notes.md deleted file mode 100644 index 6eed499b0..000000000 --- a/.drive/projects/node-addressing/design-notes.md +++ /dev/null @@ -1,94 +0,0 @@ -# Node addressing — design notes - -Decisions from the design discussion with Will, 2026-10-16 to 2026-10-22. Each has its reason and the alternatives rejected. - -## 1. A node's address is fully qualified, and it is the node's only identity - -The address is the path of provision IDs starting at the root: `shop`, `shop.catalog`, `shop.auth.api`. The root's address is the app name. The platform `logicalId` of a node's row, and of its node in the application topology, is that address, byte for byte. - -**Why.** Today children leave the app name out (`auth.api`), so a reference to a node needs a second piece of context to say which app. With the app name in the address, one string names a node everywhere: in the CLI, in errors, in logs, on the platform. The definition has no special case for the root. If a Branch ever holds more than one app, which the platform topology spec deliberately allows, short addresses would collide and fully qualified ones do not. - -**Assumes.** Addresses stay unique within a graph. Provision IDs below the root stay letters and digits only. - -**Rejected.** -- Keep children unqualified and resolve the app from context (the linked project, the directory). Needs a second input for every reference. -- Two terms, a full "address" and a shorter "path" for code and internal names. Will rejected any second form of identity. - -## 2. Alchemy resource IDs are built from the address with dots - -A node's main resource uses the address itself (`shop.catalog`). Its other pieces add a role (`shop.catalog.connection`, `shop.api.deployment`). Today they are `catalog-db`, `catalog-conn`, `api-svc`. - -**Why.** The `logicalId` names a node; a resource ID names one of the pieces the node becomes. They do the same job at different sizes, so they should read the same way. There is no value in telling them apart by their characters, which is the only reason `-` was used. - -**Why it cannot collide.** Every address is either a module, which has children but no Alchemy resources, or a service or resource, which has resources but no children. So `X.role` is never also a node's address. - -**Rejected.** Keep `-` suffixes so a resource ID can be recognised on sight. It only guarded against an old bug where upstream Alchemy wrote its resource ID as the `logicalId`. - -## 3. Environment variable keys are built from the full address - -`COMPOSER_` plus the address segments, upper-cased, with `-` replaced by `_`: `my-shop.api` gives `COMPOSER_MY_SHOP_API_...`. - -**Why.** App names may contain `-`, which is not valid in a POSIX variable name. Replacing it cannot cause a collision, because one app has one root name, and below the root segments are letters and digits only. - -## 4. `serviceOrigin` takes the fully qualified address - -`serviceOrigin('auth-example.api')`. - -**Why.** One form of address everywhere. If the deployed app name differs from the one in the code (`--name`), the deploy fails at Load with an unknown address. That is a loud, data-safe failure the author fixes in one line. - -**Rejected.** Resolve the string relative to the module the call is written in. That is a second way to write an address. - -## 5. Existing deployments migrate automatically on their next deploy - -Every Alchemy resource Composer declares carries a rename declaration from its old ID, which Composer computes from the address: the address without the app name, then `-` and the role. Upstream Alchemy (`2.0.0-beta.83`, `Rename.ts`) moves the state entry instead of deleting and recreating. The `logicalId` change is an in-place update in upstream Alchemy: it sets the field to null, then to the new value, as the Management API requires. - -**Why.** Without it, the first deploy after upgrading would create an empty database, delete the old one with its data, regenerate secrets and rotate RPC keys. - -**Assumes, to verify first.** -- A rename declaration whose old ID is not in the state, on a new stage, is a no-op. -- A deploy that fails partway through the renames can be run again and finishes them. -- Alchemy and the platform's hosted state accept dots in resource IDs. The ID appears in the state URL path. - -**Verified** by the spike on the real platform ([assets/spike-alchemy-renames.md](assets/spike-alchemy-renames.md)): renames keep platform ids and data; renames on a new stage are ignored; a deploy that fails or is killed partway reruns cleanly; dotted IDs work in the hosted state; a `logicalId` change alone updates in place. The API is `renamedFrom('catalog-db')` from `alchemy/Rename`, piped onto the resource. - -**Every platform name is the full address.** Will, 2026-10-23: "WE HAVE A SINGLE FUCKING METHOD OF IDENTIFYING A NODE." The App display name, the local Database name, and the Connection, Bucket and bucket access key names are all the full address (`shop.catalog`), on new stages and on the upgrade deploy. App, Database and Bucket names update in place. Upstream replaces a Connection or an access key when its `name` changes, so the upgrade gives each database a new password and each bucket new S3 credentials; data and objects are kept, and Composer's services get the new values in the same deploy. Will accepted this. Name validation uses each platform kind's real limit (Connection 65, Bucket 100, App 256) and fails naming the address; it never shortens a name. - -**Rejected.** Keeping the short name below the app on these platform objects, to avoid recreating Connections and access keys. That is a second form of a node's name. - -**Accepted.** An older Composer deploying a stage after it migrated deletes and recreates resources. Will: not a concern. - -**Local dev.** The per-directory record of emulator instances is keyed by address. Composer rewrites its keys on the first `prisma dev` after upgrading, so local databases keep their data. - -## 6. Deploy's entry file lives in the config - -`composer({ entry: './module.ts', extensions, state })` in `prisma.config.ts`, overridable with `--entry `. The positional entry argument is removed from `deploy` and `dev`. - -**Why.** It is an input to the tool, like the ORM section's schema path. It is not an application fact, which stay in code. It also stops every command needing the entry, and stops a subcommand of `deploy` from being mistaken for a file name. - -**Changes in the recorded design.** The deploy-from-the-root-node decision says the config carries "no app reference"; the config-section decision lists the allowed fields; the commands decision gives the shape `prisma deploy `. Each is amended. - -## 7. Platform commands accept a node address as their subject - -`prisma service logs @pr-42:shop.api`, `prisma postgres delete @shop.catalog --confirm …`. The part before `:` is the stage. With no stage, the address means production. The command finds the row by `logicalId`, then does exactly what it does when given an ID. It never changes the stored topology or the deploy state; the next deploy recreates anything deleted this way. - -**Why.** Administering Composer apps through the platform commands needs no second set of commands. The address is positional because the CLI's command principles say a command's subject is positional and flags never name it. Production when no stage is given matches both `prisma deploy` without `--stage` and platform commands without `--branch`. - -**API.** No change needed to resolve addresses: the Project, service, database and bucket list endpoints already filter by `logicalId`, and the resource lists accept `branchGitName`. PRO-225 makes the API reject `:` in Branch names, so the address can be split at the first `:`. - -**Rejected.** An explicit `--node` flag: flags never name a command's subject. - -## 8. `prisma deploy list` lists what is deployed on a stage - -`prisma deploy list [--stage ]` prints each node's address, kind, platform ID and URL, from the stage's stored topology joined to its rows by `logicalId`. - -**Why.** Without it, users and agents can only learn addresses by reading code. `list`, not `show`, because it displays a collection. The stage is a flag because it narrows the scope; it is not the subject. - -## 9. Teardown is done with platform commands - -- Remove an app and every environment: `prisma project delete --confirm `. It already works while compute is live: the platform soft-deletes the services and a scheduled job deletes their running versions. -- Remove one stage: `prisma branch delete --confirm `. New. The Management API gets a cascading Branch delete that removes the Branch's services, databases and buckets first; today it refuses while they exist. The cascade belongs in the API, not the CLI, so scripts and agents calling the API get it too and it cannot stop halfway. -- Remove one instance: a platform delete with a node address. - -**Why.** An unattended agent tried three times to tear down a Composer app and wrote its own scripts against the `destroy` operation. Everything it needed is `project delete` plus documentation. Our CI's workaround for a 409 on live compute is out of date. - -**Rejected.** A Composer `destroy` command (a second destructive verb next to `delete`), and `deploy delete` for subtrees (see `deferred.md`). diff --git a/.drive/projects/node-addressing/handover.md b/.drive/projects/node-addressing/handover.md deleted file mode 100644 index f26a371e8..000000000 --- a/.drive/projects/node-addressing/handover.md +++ /dev/null @@ -1,72 +0,0 @@ -# Handover: node-addressing project - -Written 2026-10-23 by agent brunhilde-44 for the next agent. Read this first, then the files it links. You work for Will Madden (`madden@prisma.io`), who makes design decisions; you execute with the Drive process (`/drive-process`) and ask him as rarely as possible. Follow `~/.claude/CLAUDE.md` and the repo's `CLAUDE.md` exactly: name yourself first, read the design docs before any design opinion, write plain English, act as the `wmadden-electric` bot for git and GitHub, turn on Auto-fix for every PR you open and follow it to merge. - -## Where to find the full context - -- **Previous session's transcript** (the whole design discussion, every decision and correction): `/Users/will/.claude/projects/-Users-will-Projects-prisma-composer--claude-worktrees-composer-destroy-discussion-56c939/c88b7a0c-7a0a-48cf-823d-46cdaa0a9ad8.jsonl`. It is large; search it rather than reading it whole. The session in the app: `claude://claude.ai/epitaxy/local_d8cff239-c134-4d67-8d00-e695a9dfc40e`. -- **Project files on the branch** `drive/fully-qualified-node-addresses` (pushed to `prisma/composer`): - - [spec.md](spec.md): purpose, non-goals, project done conditions. - - [design-notes.md](design-notes.md): every decision with its reason and rejected alternatives. Treat it as settled. - - [plan.md](plan.md): the slices and their order. - - [assets/spike-alchemy-renames.md](assets/spike-alchemy-renames.md): real-platform evidence that Alchemy renames keep data. - - `slices/a1-fully-qualified-identity/`: slice A1's spec, dispatch plan and the two review files under `reviews/`. - - `.drive/deferred.md`: the deferred `deploy delete` design. -- **Will's memory notes**: `/Users/will/.claude/projects/-Users-will-Projects-prisma-composer/memory/project-node-addressing-teardown.md` and the feedback files indexed in that folder's `MEMORY.md`. - -## What the project is - -It started as "should Composer have a `destroy` command?" and became this: - -1. **A node's address is fully qualified and is its only identity.** `shop`, `shop.catalog`, `shop.auth.api`; the root's address is the deployed app name. The platform `logicalId` is that string. Alchemy resource IDs are `
` or `
.`. `COMPOSER_` env keys come from the full address with `-` turned into `_`. `serviceOrigin()` takes the full address. Will rejected any second form of identity ("address" vs "path"); do not reintroduce one. -2. **Existing apps migrate on their next deploy** through Alchemy `renamedFrom` declarations on every resource, with no data loss. Will does not care about an older Composer deploying after the migration. -3. **Deploy's entry file moves into `prisma.config.ts`** (`composer({ entry })`), with `--entry` overriding it; the positional argument goes away. -4. **Platform commands accept a node address as their subject**: `prisma service logs @pr-42:shop.api`. Positional only; no `--node` flag. No stage means production. The API already supports the lookups. -5. **`prisma deploy list [--stage]`** lists a stage's nodes with address, kind, platform ID and URL. -6. **Teardown uses platform commands, not a Composer command.** `prisma project delete` already works with live compute (checked in pdp-control-plane code). A new cascading Branch delete in the Management API plus `prisma branch delete --confirm ` covers one stage. `deploy delete` is deferred, design kept. - -`--stage` keeps its name: it is Alchemy's target-neutral term. Do not propose renaming it. - -## State of each slice (see plan.md) - -| Slice | Repo | State | -| --- | --- | --- | -| Spike | composer | Done. All answers yes. | -| A1 Fully qualified identity + migration | composer | See "A1 status" below. | -| A2 `prisma deploy list` | composer | Not started. Starts after A1 merges. | -| A3 Entry in the config | composer | Not started. A proposed session card exists in the app ("Node addressing A3: deploy entry in the config"). | -| B1 `@stage:address` in platform commands | prisma/prisma-cli | Not started. Will has not yet said whether to start it. | -| B2 Cascading Branch delete + reject `:` in Branch names (PRO-225) | prisma/pdp-control-plane | Not started. Same. | -| B3 `prisma branch delete` | prisma/prisma-cli | Not started; needs B2 deployed. | -| Docs direct change | composer | Mostly done inside A1 (guides now say `project delete`). Still to do: remove CI's out-of-date `deleteProjectDeep` 409 workaround in `scripts/ci-cleanup-utils.ts`; document `branch delete` once B3 ships. | - -PRO-225 (Product team, Linear) asks the platform to reject `:` in Branch names. TML-3605 was canceled; Will tracks Composer work outside Linear, so no Linear issues for slices. - -## A1 status - -Branch `drive/fully-qualified-node-addresses`, pushed to `prisma/composer` (remote `bot`). No PR yet. The slice's spec and dispatch plan are in `slices/a1-fully-qualified-identity/`. - -**Done and committed:** -- D1: ADR-0057 replaces ADR-0051; principles, glossary and domain docs updated. -- D2: full addresses in Load (root = deployed app name on both the CLI pipeline and the deploy lowering), `logicalId`s, topology, `COMPOSER_` keys (`-` to `_`), `serviceOrigin`. -- D3: dotted resource IDs (`target/src/resource-ids.ts`) and `renamedFrom` on every resource, with old IDs built in `target/src/legacy-resource-ids.ts`. Environment variable rows are replaced with identical values, because their keys change; this is accepted. -- D4: local dev record migration (`local-target/src/legacy-address-keys.ts`, `formatVersion: 2` in `emulator-instances.json`); bucket data folders keep their existing names. -- D5: examples, guides, agent skill and READMEs use full addresses; guides say to remove an app with `prisma project delete`. -- Review (`slices/a1-fully-qualified-identity/reviews/`) and all fixes. Notable decisions taken in the fixes: - - The app name must be letters, digits, `-` and `_` (`assertAppName` in core). A name with `.` would have produced wrong old IDs and lost data. - - Every platform `name`/`displayName` Composer writes is the full address (Will, 2026-10-23: one form of a node's name). The upgrade recreates Composer's Connections and bucket access keys with new credentials; see `slices/a1-fully-qualified-identity/upgrade-effects.md`. - - Role names are nouns: `connection`, `accesskey`, `deployment`, `warmup`, `migration`, `env`, `generated`, `servicekey`, `streamkey`. -- Tests: typecheck passes. core, cli, local-target, lowering and test/integration pass. In the target package, 6 local-Postgres tests failed with "No space left on device" because about 31 leaked test Postgres servers (`prisma-composer-pn-pg-*`) were holding shared memory. They are not caused by this branch. Some may belong to other sessions, so they were not killed. Re-run the target tests once they are gone. - -**Remaining for A1:** -1. **QA (D6) restarts from scratch** (superseded 2026-10-23 by victoria-52). Follow D6 in `slices/a1-fully-qualified-identity/plan.md`, including the planted state outside the graph. The earlier test Project `qa-a1-qsgn6n` (`proj_lbs6tif5hbmz29ge7btc3wax`) recorded no data and an incomplete snapshot; delete it. Write results to `slices/a1-fully-qualified-identity/qa.md`. -2. Open the PR. Follow `~/.claude/CLAUDE.md`'s PR rules: grounding example first, `Agent: `, Auto-fix on. Every platform name is now the full address (Will decided 2026-10-23); the PR lists that the upgrade recreates Composer's Connections and access keys with new credentials, and that local bucket folders keep their names. -3. The asks app's production deploy is the project's final production test, after A1 is released. - -## How to work - -- Clone sibling repos fresh into this worktree's `wip/` (git-ignored) before citing them. Never cite `~/Projects/prisma/*` checkouts; they are stale. -- QA credentials: `~/.config/prisma-compose/deploy.env` (PDP token and workspace). Use them; do not ask Will. Name test Projects so they are obviously yours and delete them afterwards. -- Implementer and reviewer subagents: always pass `model: opus` explicitly. -- Commit with `git commit -s --trailer "Signed-off-by: Will Madden "`; push with `git push bot `. -- PRs: lead with a grounding example, then a narrative, alternatives last; put `Agent: ` above the attribution line; turn on Auto-fix; once Will approves, turn on squash auto-merge and watch until merged. diff --git a/.drive/projects/node-addressing/plan.md b/.drive/projects/node-addressing/plan.md deleted file mode 100644 index 0b7d35116..000000000 --- a/.drive/projects/node-addressing/plan.md +++ /dev/null @@ -1,85 +0,0 @@ -# Node addressing — plan - -**Spec:** [spec.md](spec.md) · **Decisions:** [design-notes.md](design-notes.md) - -Six slices in two independent tracks, plus one spike and one small docs change. Six is above the usual limit of four. The tracks live in different repositories and share no code, so they run in parallel with no coordination beyond the address format, which is already settled. Splitting them into two projects would add bookkeeping without reducing coordination. - -Tracking: Will tracks this work outside Linear, so no Linear issues are created. PRO-225 already exists for the platform's `:` rule. - -## Spike: verify upstream Alchemy behaviour - -Before slice A1. Deploy a fabricated test app with QA credentials and answer, with evidence: - -1. Does a rename declaration whose old ID is not in the state (a new stage) deploy cleanly? -2. If a deploy fails partway through the renames, does the next deploy finish them? -3. Do Alchemy and the platform's hosted state accept dots in resource IDs? - -**Hands to A1:** a short findings note in `assets/`. If any answer is no, return to design discussion before A1 starts. - -## Track A — Composer (prisma/composer) - -### A1. Fully qualified node identity, with automatic migration - -Addresses include the app name. `logicalId`s, topology nodes, Alchemy resource IDs and `COMPOSER_` keys are built from them as the design notes describe. Every resource declares a rename from its old ID. `serviceOrigin` and `dev url` take the full address. Local dev's record is rewritten on first run. The topology root uses the deployed app name (fixes the `--name` mismatch). The identity decision record is replaced, and the principles, glossary and domain docs are updated in the same PR. - -- **Builds on:** the spike. -- **Hands to:** every deployed stage names its nodes by fully qualified address, on its rows and in its topology. -- **Done when:** a test app deployed with the current release and redeployed with this branch keeps its database rows, bucket objects, generated secrets and RPC keys; a new stage deploys from scratch; the asks app's production deploy upgrades with its data intact. - -### A2. `prisma deploy list` - -Lists every node on a stage with its address, kind, platform ID and URL, from the stored topology joined to rows by `logicalId`. `--stage` narrows it; without it, production. `--json` output. - -- **Builds on:** A1, so it prints the final address format. -- **Done when:** against a real stage it lists every node, and its end-to-end test passes. - -### A3. Deploy's entry in the config - -`composer({ entry })` in `prisma.config.ts`, `--entry` override, positional argument removed from `deploy` and `dev`. The three affected decision records are amended in the same PR. - -- **Builds on:** nothing. Runs in parallel with the spike and A1. -- **Done when:** `prisma deploy` and `prisma dev` run from the config's entry, `--entry` overrides it, and a missing entry fails with a message that names both. - -## Track B — platform commands (prisma/prisma-cli, prisma/pdp-control-plane) - -### B1. Platform commands accept `@stage:address` - -In prisma-cli, every command whose subject is a project, service, database or bucket accepts `@:
` in place of the ID. No stage means production. It is resolved through the existing `logicalId` and `branchGitName` filters, with a clear error when nothing matches or the row is the wrong kind. - -- **Builds on:** nothing. It matches `logicalId`s exactly, whatever format a stage was deployed with. -- **Done when:** `prisma service logs @:
` and `prisma postgres delete @:
--confirm …` work against a real deployed app, with end-to-end tests. - -### B2. Cascading Branch delete in the Management API - -In pdp-control-plane, deleting a Branch first deletes its services, databases and buckets, then the Branch, in one server-side operation. The production Branch is still never deleted. The same PR rejects `:` in Branch names (PRO-225). - -- **Builds on:** nothing. -- **Hands to B3:** an API call that removes a stage with live members. - -### B3. `prisma branch delete` - -In prisma-cli: `prisma branch delete --confirm `, calling the cascading delete. - -- **Builds on:** B2 deployed. -- **Done when:** it removes a stage whose services are running, with an end-to-end test. - -## Direct change: teardown documentation - -Not part of a track. The Composer guides and agent skill say how to remove an app (`prisma project delete`, then confirm with `prisma project list`) and, once B3 ships, one stage. CI's `deleteProjectDeep` 409 workaround in `scripts/ci-cleanup-utils.ts` is removed, because `project delete` no longer refuses live compute. - -## Order - -``` -spike ──▶ A1 ──▶ A2 -A3 (parallel) -B1 (parallel) -B2 ──▶ B3 (parallel) -docs direct change (any time; stage removal docs after B3) -``` - -## Close-out - -- [ ] Verify every Project DoD condition in [spec.md](spec.md). -- [ ] Long-lived content is already in `docs/design/` from each slice; confirm nothing in this folder is still needed. -- [ ] Remove references to `.drive/projects/node-addressing/` elsewhere in the repo. -- [ ] Delete `.drive/projects/node-addressing/`. diff --git a/.drive/projects/node-addressing/retros.md b/.drive/projects/node-addressing/retros.md deleted file mode 100644 index b6f444a15..000000000 --- a/.drive/projects/node-addressing/retros.md +++ /dev/null @@ -1,12 +0,0 @@ -# Retros - -## 2026-10-28 — final retro at project close - -**Outcome.** Track A (full addresses with automatic migration, entry in the config, `prisma deploy list`) shipped in Composer 0.35.0 and prisma 8.0.0-rc.28. B1 (`@:
` in platform commands) merged in prisma-cli. The asks app upgraded in production with no data lost. B2 and B3 were canceled. - -**What went wrong.** -1. Stage removal (B2, B3) was planned as part of this project. Root cause: the design discussion started as "should Composer have a `destroy` command?", and the plan kept every outcome of that discussion instead of checking each slice against the purpose. Landed: `.drive/PLAN.md`, "Every slice serves the project's purpose". -2. B2 contradicted pdp-control-plane's recorded decision against a cascading Branch delete. Root cause: the plan was written without reading that repository's specs. Landed: `.drive/PLAN.md`, "Read the owning team's recorded decisions". -3. Close-out had no `.drive/PROJECT.md` saying where long-lived docs go. Landed: `.drive/PROJECT.md`. - -**What went right.** The spike checked the three upstream Alchemy assumptions on the real platform before A1 began, and A1's real-platform QA found the platform's bucket-on-new-Branch bug, which the platform team fixed. diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/plan.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/plan.md deleted file mode 100644 index 11c278baa..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/plan.md +++ /dev/null @@ -1,72 +0,0 @@ -# A1 — dispatch plan - -Spec: [spec.md](spec.md). Dispatches run in order on branch `drive/fully-qualified-node-addresses`. Commits stay on the branch; the PR opens after review and QA. - -### D1. Design records describe the new identity - -- **Outcome:** a new decision record replaces the node-identity decision, and the principle, glossary and the four domain docs listed in the spec describe fully qualified addresses, dotted resource IDs, the env key rule and the automatic migration. The ADR index marks the old record replaced. -- **Builds on:** design notes and spike findings. -- **Hands to:** the written contract the code dispatches implement. Docs-only; `pnpm lint` for markdown, if any, passes. - -### D2. Addresses are fully qualified everywhere they are read or written - -- **Outcome:** Load gives every node its full address, using the deployed app name for the root on both the CLI pipeline graph and the deploy lowering. `logicalIdProp` and the topology builder write full addresses. `COMPOSER_` keys are built from the full address with `-` replaced by `_`, in the serializer and its local-dev copies. `serviceOrigin`, `dev url`, reports, log labels and errors use the full address. Unit tests are updated to the new form. -- **Builds on:** D1. -- **Hands to:** a graph whose addresses are fully qualified. Resource IDs are still built by the old string pattern on top of the new address, so they are temporarily wrong; D3 fixes them. Typecheck and unit tests pass. - -### D3. Resource IDs are dotted, and every resource renames from its current ID - -- **Outcome:** every Alchemy resource Composer declares has ID `
` (main resource) or `
.`, and carries `renamedFrom()`. Generated values, service keys and stream keys are renamed, not recreated. Connections keep their current `name` prop. Unit tests assert both the new IDs and the declared old IDs for every node kind. -- **Builds on:** D2. -- **Hands to:** a deploy that migrates an existing stage in place. Typecheck and unit tests pass. - -### D4. Local dev keeps its instances and data across the upgrade - -- **Outcome:** on the first `prisma dev` after upgrading, `emulator-instances.json` keys are rewritten to full addresses, and bucket data directories and local database lookups follow, so existing local instances, ports and data are reused. A test starts from a record written in today's format. -- **Builds on:** D2 and D3. -- **Hands to:** local dev that survives the upgrade. Tests pass. - -### D5. Examples, guides, skill and READMEs use full addresses - -- **Outcome:** every user-facing example of an address (`serviceOrigin(...)`, `dev url ...`, reports) in examples, guides, the agent skill and package READMEs uses the full form, and the examples' own tests pass. -- **Builds on:** D2. -- **Hands to:** a branch ready for review. - -### Review - -`/drive-code-review` on the branch (no walkthrough); fix findings in follow-up dispatches. - -### D7. Merge main and renumber the identity decision - -- **Outcome:** `origin/main` (v0.34.0 and the dev-server decision record numbered 0056) is merged in. The branch's identity decision record becomes ADR-0057, and every reference to it on the branch says 0057; main's ADR-0056 keeps its number and meaning. The three conflicting docs (ADR-0053, the ADR index, `docs/guides/deploying.md`) keep both sides' content. -- **Builds on:** the review fixes. -- **Hands to:** a branch current with main. -- **Gate:** `pnpm typecheck`, `pnpm lint`; no reference to the identity record as ADR-0056 remains. - -### D8. Every platform name is the full address - -- **Outcome:** the App `displayName`, the local Database `name`, and the Connection, Bucket and bucket access key `name` are the full address. `addressBelowApp` survives only for building legacy resource IDs. Name validation checks each kind against the platform's real limit (Connection 65, Bucket 100, App 256, access key none) and names the address on failure. ADR-0057, the glossary, the principle and `alchemy-lowering.md` say every platform name is the address, and that the upgrade recreates Connections and access keys with new credentials. Unit tests assert the full names. -- **Builds on:** D7. -- **Hands to:** a deploy that writes one form of name everywhere. -- **Gate:** `pnpm typecheck`; tests for core, cli, lowering, local-target, target; `test/integration`. - -### D9. Review round 2 fixes - -- **Outcome:** the findings in `reviews/code-review.md` and `reviews/system-design-review.md` are fixed. The env key clash is caught before anything changes, by a preflight check that computes every new `COMPOSER_` key from the graph and fails naming both node addresses and the variable. The documented fix loses nothing: delete that one old `COMPOSER_` variable, then deploy again. The ADR no longer says to rename a node, and no longer says keys cannot collide during the move. `upgrade-effects.md` states when a Database update can rotate its default connection. -- **Builds on:** D8. -- **Gate:** typecheck, lint, tests for core, cli, lowering, local-target, target. - -### D6. QA on the real platform - -QA starts from scratch with a new test Project. The Project from the earlier attempt, `qa-a1-qsgn6n`, is deleted. Before the upgrade deploy, QA plants state the Composer graph doesn't represent, and checks after the upgrade that each item is unchanged: -- a custom domain on a service; -- an environment variable set by hand; -- a Connection made by hand on the database, which must still connect afterwards; -- a bucket access key made by hand, which must still read objects afterwards; -- the database's default connection. - -QA also checks what the spike didn't: that the App, Bucket and access key name changes apply, that each service's endpoint domain is unchanged, and that the names are dotted. - -- **Outcome:** the slice's done conditions are checked with evidence on a fabricated app: deploy with the current published release, write rows, objects, and record the generated secret and RPC key; redeploy from this branch; confirm data, objects, the generated secret and RPC key are unchanged, that the Connection and access key were recreated under full-address names and the services work with their new credentials, and that the state and rows use the new form; deploy the same app to a new stage from scratch; run `prisma dev` on a directory last used with the current release and confirm local data survived. Every test Project is deleted afterwards. -- **Builds on:** review fixes. -- **Hands to:** evidence for the PR description. diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/qa.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/qa.md deleted file mode 100644 index 1ac04ec90..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/qa.md +++ /dev/null @@ -1,137 +0,0 @@ -# A1 QA: upgrading from v0.34.0 on the real platform - -Run on 2026-10-23 against `https://api.prisma.io/v1`, branch `a1-full-platform-names` at `6274ac6e`. Evidence (logs, snapshots, scripts) is in `wip/qa-a1/` of this worktree, which git ignores. No secret values are recorded here. - -**Verdict:** the upgrade from v0.34.0 lost no data and touched nothing outside the Composer graph. Every upgrade check passed. One requested step, deploying to a new stage, fails on v0.34.0 and on this branch alike, because the platform now creates a bucket on every new Branch. It passed after I deleted that bucket. Details are in step 8. - -## Step 0. Delete the earlier attempt's Project - -| Check | Result | Evidence | -|---|---|---| -| `qa-a1-qsgn6n` deleted | PASS | `prisma project delete proj_lbs6tif5hbmz29ge7btc3wax --confirm …` printed "The project, its databases, and its apps were deleted". `GET /v1/projects/proj_lbs6tif5hbmz29ge7btc3wax` returns `resource-not-found`. | - -## Step 1. The fabricated app - -The app is `qa-a1-190492`. It has a rawPostgres `catalog`, a bucket `files`, an RPC service `quotes`, and a service `api`. `api` depends on all three, and has a `generatedParam()` input `token` and a `serviceOrigin` input `origin`. `api` exposes `/write` and `/put` to add a row and an object, and `/rows`, `/objects` and `/health` to read them with its current credentials. `/hashes` returns sha256 prefixes of the generated token and of every `COMPOSER_` variable. `quotes` exposes `/env-hash`. - -Packages from npm: `@prisma/composer`, `@prisma/composer-cli` and `@prisma/composer-prisma-cloud` at `0.34.0`, `prisma@8.0.0-rc.27`, `alchemy@2.0.0-beta.83`. `npm ls` shows one `@prisma/composer-cli@0.34.0`, which the `prisma` host shares. - -I first used `prisma@8.0.0-rc.26`. It carries its own `@prisma/composer-cli@0.33.0`, so that deploy was not purely v0.34.0. I deleted that Project (`qa-a1-mzg279`, `proj_d8adm2yjsu72ag2fr5uhlzis`) and started again with rc.27, which depends on `composer-cli@0.34.0`. - -## Step 2. Deploy with v0.34.0 and write data - -| Check | Result | Evidence | -|---|---|---| -| Deploy to production | PASS | "Deployed qa-a1-190492 to production", 27 resources created. Project `proj_bksk92tih782r3rv71zj9x3h`. | -| Rows and objects written | PASS | 3 rows in table `qa`, 3 objects of 99, 56 and 89 bytes. `/health` returns `{"rows":3,"objects":3}`. | - -A second v0.34.0 deploy, with a one-line code change, gave the services a Deployment that has the hand-set variable from step 3. - -## Step 3. State planted outside the graph - -| Item | Result | Evidence | -|---|---|---| -| Custom domain on `api` | PASS | The API refuses a hostname whose DNS does not point at the platform. `qa-a1-190492.66-135-7-198.sslip.io` resolves to the switchboard's address, so the platform accepted it: `dom_01M5MJ74K2KF6781NB1D1ESAZ4`, status `active`. It served `api`'s `/health`. | -| Hand-set variable | PASS | `QA_HAND_SET=hand-set-qa-a1-190492`, production class, `envvar_q7crwo5b4kljm885egy2dh7z`. The API never returns values, so `api` reports it from its own environment. | -| Hand-made Connection | PASS | `con_s1cunpoutq0p1xvcoeucrrwf` (`qa-a1-190492-handmade`). It read all 3 rows directly. | -| Hand-made access key | PASS | `bkey_k3y6j4rb996thu0xtw6avu38` (`qa-a1-190492-handmade`, role `read`). It listed all 3 objects and read one. The API requires a `role`. | - -The platform also made state of its own that is not in the graph: the default connection `con_tx0l18de1k2hxvazlnxufefv` ("Prisma Postgres API Key"), and `DATABASE_URL` and `DATABASE_URL_POOLED` variables. I checked them too. - -## Step 4. Snapshot before - -`wip/qa-a1/before.txt`. It has ids, names and `logicalId`s, connections and keys, endpoint domains, custom domains, every variable's key, id and `updatedAt`, the topology, the deploy state's resource IDs, the hashes, the rows and objects, and hashes of the default connection's credentials from the deploy state. It also records that those credentials connect. - -## Step 5. Upgrade - -I built the repo (`pnpm build`), packed the three `packages/9-public/*` packages with `pnpm pack`, and installed the tarballs. The lockfile shows each package resolved from `file:../tarballs/…`. I changed `serviceOrigin('api')` to `serviceOrigin('qa-a1-190492.api')`, as the upgrade guide says. Then I deployed to production. Full log: `wip/qa-a1/deploy-upgrade.log`. - -``` -Plan: 7 to update, 19 to replace, 1 tasks -[qa-a1-190492.catalog] update (renamed from catalog-db) -[qa-a1-190492.catalog.connection] replace (renamed from catalog-conn) -[qa-a1-190492.files] update (renamed from files-bucket) -[qa-a1-190492.files.accesskey] replace (renamed from files-key) -[qa-a1-190492.api] update (renamed from api-svc) -[qa-a1-190492.api.generated.token] update (renamed from COMPOSER_API_INPUT:token-generated) -[qa-a1-190492.api.servicekey.quotes] update (renamed from servicekey-api.quotes) -… -Done: 27 succeeded (41.5s) -``` - -The 7 updates are the Database, Bucket, two Apps, warm-up, generated value and service key. The 19 replaces are the Connection, the access key, the two Deployments, and 15 `COMPOSER_` variables. Nothing else was replaced or deleted. - -## Step 6. Snapshot after and compare - -`wip/qa-a1/after.txt`. Run `diff before.txt after.txt` to see every difference. - -| Check | Result | Evidence | -|---|---|---| -| Database, Bucket and App ids unchanged | PASS | `db_dhmp6sefsgr3qirf4awbq11c`, `bkt_gvwwghnfu99x8im06tpknuqu`, `cps_efn6g905qra9txjwssu4hwex`, `cps_nqklgifjhfkdjk46a1qxcd82`. Project and Branch ids are also unchanged. | -| Rows and objects unchanged | PASS | The same 3 rows (ids and notes) and the same 3 objects (keys and sizes), read through `api`, the hand-made Connection and the hand-made key. | -| Generated value unchanged | PASS | Token hash `567caf9babb8eb4c` before and after. | -| RPC key unchanged | PASS | `api`'s service key hash `2454e742d7a4558f` and `quotes`' accepted keys hash `7c8d34971f2305f5` before and after. `/quote` over the RPC edge returns `{"quote":"ok"}`. | -| Endpoint domains unchanged | PASS | `https://efn6g905qra9txjwssu4hwex.ewr.prisma.build` and `https://nqklgifjhfkdjk46a1qxcd82.ewr.prisma.build`. | -| Custom domain unchanged | PASS | Same id, hostname and `active` status. It still serves `/health`. | -| Hand-set variable unchanged | PASS | Same id and `updatedAt`. The new Deployment's environment has `QA_HAND_SET=hand-set-qa-a1-190492`. | -| Hand-made Connection unchanged and working | PASS | `con_s1cunpoutq0p1xvcoeucrrwf` is still listed, and its credentials from before the upgrade read all 3 rows. | -| Hand-made access key unchanged and working | PASS | `bkey_k3y6j4rb996thu0xtw6avu38` is still listed, and its credentials from before the upgrade list and read the objects. | -| Default connection unchanged | PASS | `defaultConnectionId` is still `con_tx0l18de1k2hxvazlnxufefv`, with the same `createdAt`. The stored connection-string hashes are unchanged (direct `cd39904006c344f4`, pooled `65b4f2ec252d5006`, accelerate `68ba9ed9feb0eb1b`). The connection string from before the upgrade still connects and counts 3 rows. The platform's `DATABASE_URL` and `DATABASE_URL_POOLED` variables keep their ids and `updatedAt`. Nothing was rotated. | -| Composer's Connection and key recreated under full-address names | PASS | `con_szd4f2llo0afreayqas8ipgm` (`catalog-99d500d6c983`) is gone, and `con_q8e8pmhcezwspl35gdmczmw8` (`qa-a1-190492.catalog-256e3bd08ebe`) replaces it. `bkey_bbtiq63iqnqz3dpulfztj9l1` (`files-a2c1b420a232`) is gone, and `bkey_oyo38878a1yhtcirjeusiary` (`qa-a1-190492.files-1ee547ad648e`) replaces it. The upstream token suffix is expected. | -| Services read through the new credentials | PASS | `api`'s database URL and access key hashes changed to the new ones (`95ace3a7…`, `2a86cbad…`). `/health` returns `{"rows":3,"objects":3}`. | -| App, Bucket and access key names are the full address | PASS | Apps `qa-a1-190492.api` and `qa-a1-190492.quotes`, Bucket `qa-a1-190492.files`, key `qa-a1-190492.files-`. | -| Every `logicalId` is the full address | PASS | Database `qa-a1-190492.catalog`, Bucket `qa-a1-190492.files`, Apps `qa-a1-190492.api` and `qa-a1-190492.quotes`. Project `qa-a1-190492`. Every topology node and parent uses the full address. | -| Deploy state holds only dotted IDs | PASS | 26 IDs, all starting with `qa-a1-190492.`. No `-db`, `-conn`, `-svc`, `-var`, `servicekey-` or `-generated` remains. The 27th entry, `composer-deployment-report`, is the per-deploy report task. It is not a node's resource, and both releases name it the same. | -| No old `COMPOSER_` variables remain | PASS | All 15 rows now start with `COMPOSER_QA_A1_190492_`. None of the old `COMPOSER_API_…` or `COMPOSER_QUOTES_…` rows remain. | - -Observations, none of which loses data: - -- The Deployment created during the upgrade still has the old `COMPOSER_API_…` keys in its process environment, with the old values. The platform copies variables into a Deployment when it creates it. The new Deployment is created before the old rows are deleted at cleanup. The app reads only the new keys. The old values point at the deleted Connection and key, so they no longer work. Later Deployments are created after the old rows are gone. I did not check that they lack the old keys. -- The Database's generated platform name changed from `qa-a1-190492-catalog-db-br-…` to `qa-a1-190492-qa-a1-190492-catalog-br-…`, which contains the app name twice. Upstream builds it from the stack name and the resource ID. It is only a label, as `upgrade-effects.md` says. - -## Step 7. Redeploy unchanged - -| Check | Result | Evidence | -|---|---|---| -| Unchanged redeploy plans only expected churn | PASS | `Plan: 16 to update, 1 to replace`. The replace is `qa-a1-190492.api.deployment`. The updates are the 15 `COMPOSER_` rows and `qa-a1-190492.quotes.deployment`. Nothing else changes. | - -Why this churn happens, and why it is not new: - -- Upstream `EnvironmentVariable` plans `update` whenever it has a resolved value. The API never returns values, so upstream rewrites each one to repair drift (`alchemy/src/Prisma/EnvironmentVariable.ts`, `diff`). -- `api`'s Deployment is replaced because upstream `Deployment` replaces when it cannot prove that its triggers are unchanged. I did not trace which of `api`'s trigger values is unresolved at plan time. -- I deployed a control app, `qa-a1-190492-ctl`, with v0.34.0 and redeployed it unchanged. It planned exactly the same: 16 updates and 1 replace of `api-deploy`. This slice does not add the churn. - -## Step 8. Deploy from scratch to a new stage - -| Check | Result | Evidence | -|---|---|---| -| `--stage qa-fresh` deploys from scratch | FAIL on the first try; the same failure happens on v0.34.0 | Before the deploy started, the platform made bucket `bkt_93fa9a7733ea111ed30cda2cd7b47285` (name `qa-a1-190492.files`, `logicalId` null, its own S3 bucket) on the new Branch, 4 seconds after creating the Branch. Composer's check for an empty deploy state then stopped the deploy: "has no deploy state, but its branch … already holds 1 resource(s): bucket "qa-a1-190492.files"". Composer creates the Branch with only `{ gitName }`. The v0.34.0 control app failed the same way (`bucket "files"`). | -| After deleting that bucket | PASS | Deleted `bkt_93fa…`, then deployed again: `Plan: 26 to create`, "Deployed qa-a1-190492 to qa-fresh". It holds full-address names, `logicalId`s, state IDs and env keys (`wip/qa-a1/fresh.txt`). A row and an object were written and read back. `/quote` works. | -| Second deploy to the stage | PASS, with the same churn as step 7 | `Plan: 16 to update, 1 to replace`. These are the same resources as in step 7. | - -**Needs a decision outside this slice:** a first deploy to a new stage fails for any app with a bucket, on v0.34.0 and on this branch. The platform appears to create a bucket on every new Branch, copying the default Branch's bucket name. I did not check whether it copies objects. - -## Step 9. Local dev - -I used a fresh copy of the app named `qa-a1-190492-dev`, in `wip/qa-a1/app-dev`. - -| Check | Result | Evidence | -|---|---|---| -| `prisma dev --detach` with v0.34.0 | PASS | 26 resources created. Wrote 3 rows and 3 objects. Token hash `11b58395fe92cc00`. The record has no `formatVersion` and uses keys `catalog`, `files`, `quotes`, `api`. | -| `prisma dev --detach` with the branch's packages | PASS | 26 updated, each "renamed from" its old ID. Nothing was created or deleted. | -| Rows and objects survived | PASS | The same 3 rows and 3 object keys. | -| Generated value survived | PASS | Token hash `11b58395fe92cc00`. | -| Record rewritten | PASS | `formatVersion: 2`. Keys are `qa-a1-190492-dev.catalog` and so on. Instance ids and ports are unchanged (database `db_1465a41667e4dfa1` on port 51941, api 3075, quotes 3074). `prisma dev url qa-a1-190492-dev.catalog` works. | -| Nothing left running | PASS | `prisma dev stop` after each run. Port 3075 refuses connections afterwards. I stopped no other process. | - -## Step 10. Teardown - -| Check | Result | Evidence | -|---|---|---| -| Delete while services run | PASS | `api` answered `/health` right before. `prisma project delete --confirm ` deleted `proj_bksk92tih782r3rv71zj9x3h` (`qa-a1-190492`, both Branches) and `proj_d9ds5b8fyflqi1qwtf8pcn39` (`qa-a1-190492-ctl`). | -| Every QA Project is gone | PASS | `prisma project list` shows no `qa-a1` Project. `GET /v1/projects/` returns `resource-not-found` for `qa-a1-qsgn6n`, `qa-a1-mzg279`, `qa-a1-190492` and `qa-a1-190492-ctl`. | - -## Not tested - -- Whether later Deployments drop the old `COMPOSER_` keys that the upgrade Deployment still carries (step 6). -- Which trigger makes `api`'s Deployment replace on every deploy (step 7). Both releases behave the same. diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/code-review.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/code-review.md deleted file mode 100644 index b22aadab2..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/code-review.md +++ /dev/null @@ -1,182 +0,0 @@ -# Code review — A1, fully qualified node identity (round 2) - -Branch `a1-full-platform-names` (pushed as `drive/fully-qualified-node-addresses`), range `origin/main...HEAD`, 33 commits, 109 files. Reviewed against the slice [spec](../spec.md), [plan](../plan.md) and [upgrade-effects.md](../upgrade-effects.md), the project [spec](../../../spec.md) and [design notes](../../../design-notes.md), the [spike findings](../../../assets/spike-alchemy-renames.md) and ADR-0057. Upstream source read in `node_modules/alchemy/src` (`alchemy@2.0.0-beta.83`, the same version v0.34.0 pins). Platform limits read in `wip/pdp-control-plane` (management-api routes and models). - -Settled and not reopened: one form of identity, the full address, used for every platform name; the upgrade recreates Composer's own Connections and access keys; an older Composer deploying after the upgrade; `serviceOrigin` with the full address breaking `--name`; `--stage` keeps its name. - -## Summary - -I rebuilt every old resource ID from `git show v0.34.0:`, not from the branch's helpers. Every declared former ID matches what v0.34.0 built, and the list of resources is complete. By reading the upstream code, no step of the upgrade deletes a database, a bucket, an object or a custom domain, and every replace deletes only the one object Composer stored, after checking its identity. So the critical condition holds by code reading. It is not yet shown on the platform, because QA has not run. - -Two things need fixing before QA. First, the documented way out of the environment-variable key clash tells the user to rename a node, which deletes that node's rows, and for a module holding a database or bucket, its data (F01). Second, the QA plan does not plant the out-of-graph state the critical condition requires, and does not check the behaviours the spike never covered (F02). The rest are small accuracy fixes. - -## What looks solid - -- **Every former ID matches v0.34.0.** At v0.34.0 a node's lowering `id` equalled its address, which left out the app name (`core/src/control/deploy.ts` lines 673–675, 731; `load-module.ts` line 528). `addressBelowApp` removes the known `${app}.` prefix, so each former ID is v0.34.0's string: - - App `${id}-svc`, Deployment `${id}-deploy`, Database `${id}-db`, Connection `${id}-conn`, PgWarm `${id}-warm`, OrmMigration `${id}-migrate`, Bucket `${id}-bucket`, BucketAccessKey `${id}-key`, S3Credentials `${id}-creds`. - - Environment rows `${configKey(address, d)}-var`, `${inputRow.key}-var`, `${leaf.varName}-var` and the provider row `${configKey(address, {owner:'service', name})}-var`, rebuilt through the same serializer functions on the old address. The `-` to `_` change cannot alter an old key: old addresses never contained the app name, and child provision IDs were already letters and digits at v0.34.0. - - GeneratedParam `${inputKey}:${path}-generated`, ServiceKey `servicekey-${edge.to}.${edge.input}`, streams key `streamskey-${edge.from}`. -- **The resource list is complete.** A grep of v0.34.0 for every resource constructor finds exactly the 17 call sites above. Project and Branch are resolved outside Alchemy and are unchanged. No Alchemy namespaces are used, so bare former IDs resolve correctly. -- **No old ID can equal a new ID.** Every new ID starts with `.` and ends in a segment without `-`; every old ID ends in `-` or starts with `servicekey-`/`streamskey-`/`COMPOSER_`. So the rename shift and cycle rules in `Rename.ts` never apply. -- **Upstream replace and delete paths are as `upgrade-effects.md` says.** I read each `diff` and `delete`: - - App, Database and Bucket replace only on project (Database also on region, default flag or source). `logicalId`, `displayName` and `name` changes are updates. The Bucket update API documents its display name as a label that does not change the S3 bucket name (`pdp-control-plane` `routes/v1/buckets.ts` around line 640). - - Connection replaces on `name`; its `delete` loads the stored `connectionId` and refuses if its database or physical name differ from the stored ones. - - BucketAccessKey replaces on `name`; its `delete` removes `{bucketId, keyId}` from state only. Upstream keeps no list for it, so a hand-made key is never in state. - - EnvironmentVariable `delete` checks project, branch, class and key (`ensureVariableIdentity`) and skips system-managed rows. - - Deployment `delete` checks the deployment belongs to the stored `appId`. - - Replacement is create-first, and old generations are deleted only in the garbage-collection phase after every resource has converged (`Apply.ts` line 247, lines 1102–1110). A run that fails partway deletes nothing. -- **The 52-character Connection limit is right.** `physicalInstanceName` (`Prisma/Internal/EnvName.ts`) cuts the name to 52 and appends `-` and 12 characters; the API caps Connection names at 65 (`routes/v1/connections.ts` line 223). Bucket 100 (`routes/v1/buckets.ts` line 307) and App 256 (`models/v1/compute.ts` line 55) match; the access key name has no maximum. No name kind restricts characters, so dots are accepted by the API schema. -- **`validatePlatformName` runs while the stack is declared**, before Alchemy applies anything, and the tests assert the exact message and that no Database or Bucket was recorded (`control-lowering.test.ts` lines 2718–2800). -- **App name check.** `assertAppName` runs in Load for every module root and in `resolveAppIdentity`, so a name with `.` can no longer produce wrong former IDs. -- **Local dev.** The record carries `formatVersion`; a record without it gets the app name in front of every key, without guessing. The emulators hook and the local providers both key by `resourceId(address)`. The bucket folder keeps its old path. -- **Tests pin literal former IDs** for every node kind (`control-lowering.test.ts` lines 3025–3330), including an app name with `-`. - -## Findings - -### F01 — The documented way out of the env key clash deletes data, and the clash is wider than described - -- **Location:** `docs/design/90-decisions/ADR-0057-a-nodes-address-is-its-full-path-from-the-app-name.md` line 136; `.drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md` line 37; `docs/guides/deploying.md` lines 458–495 (says nothing). -- **Issue:** The ADR says to "rename one of the two nodes before upgrading". Renaming a node makes it a new entity, and the same ADR (line 127) says the deploy then deletes the old entity's rows. If the user renames the module named like the app, every database and bucket inside it is deleted with its data. If they rename the service, its App is deleted with its custom domains, and its generated secrets are minted again. Either breaks the critical done condition. - - The condition is also wider than "a module named like the app". Keys are upper-cased and `-` becomes `_`, so a new key of node N equals an old key of node M whenever M's old key starts with the app name in key form. That happens with: - - a module or a service directly below the root whose ID is the app name in any letter case (`Shop` in the app `shop`). A service `shop.shop` with a dependency input `x` had the old key `COMPOSER_SHOP_X_P`, which is the new key of service `shop.x`'s own param `p`; - - an app name with `-` or `_` and modules that spell it (`my-shop` with `my.shop.x`). - - The user sees upstream's message "appeared after the adoption check... rerun with adoption enabled", which names neither node and suggests the wrong fix. Every rerun fails the same way. -- **Suggestion:** Detect it before Alchemy applies anything. From the graph, compute every new key that equals another node's old key; preflight already queries variables by key (`target/src/preflight.ts` `listEnvVars`), so check whether a row with that key exists on the stage. If one does, fail naming both addresses and the key. Give a remedy that loses nothing: delete that old `COMPOSER_` variable on the Branch (it is Composer's own superseded row, and the old Deployment already holds a copy), then deploy again; garbage collection then finds it gone and continues. Write the precise condition and this remedy in the ADR, `upgrade-effects.md` and the upgrade guide, and add a unit test for the detection. - -### F02 — The QA plan does not cover the critical done condition - -- **Location:** `.drive/projects/node-addressing/slices/a1-fully-qualified-identity/plan.md` D6 (lines 55–59); `.drive/projects/node-addressing/handover.md` lines 62–69. -- **Issue:** The slice spec requires QA to plant state the graph does not represent before the upgrade (a custom domain on a service, a hand-set environment variable, a hand-made Connection) and show it is unchanged after. D6 and the handover's steps do not mention it, and step 1 on `qa-a1-qsgn6n` already ran without it. The spike also never changed an App `displayName`, a Bucket `name` or an access key `name`; it kept the short Connection name. -- **Suggestion:** Add to D6 and the handover, before step 2: plant a custom domain on a service, a hand-set environment variable, a hand-made Connection on the database and a hand-made access key on the bucket, and record their ids. After the redeploy, check that each is unchanged, that the App id, endpoint domain and custom domain binding are unchanged, that the Bucket id and S3 bucket name are unchanged, that the database's default connection was not rotated (F03), and that the platform accepted the dotted names. Plant these on the existing QA Project before step 2, or start over. - -### F03 — `upgrade-effects.md` says a Database update changes nothing, but upstream can rotate its default connection - -- **Location:** `.drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md` line 16 (Database row); upstream `node_modules/alchemy/src/Prisma/Database.ts` lines 916–933 and `Prisma/Internal/DatabaseSecrets.ts` lines 105–165. -- **Issue:** Every upgrade updates every Database, because its `logicalId` and derived name change. On update, upstream calls `recoverDatabaseConnectionSecrets`, which rotates the database's default connection when the deploy state holds no connection secrets for it. The default connection is not in the Composer graph, and anything that copied its URL from Console would stop working. State that upstream created holds the secrets, so this should not happen for most v0.34.0 stages, but the document should not say "Nothing" without the condition. -- **Suggestion:** State the condition in the Database row and the verdict. Add the check to QA (F02). - -### F04 — The core-model sketch still says the Connection keeps an unchanged label - -- **Location:** `docs/design/10-domains/core-model.md`, the descriptor sketch (`Prisma.Connection(`${address}.connection`, { database: db, name: /* unchanged label */ })`). -- **Issue:** The Connection `name` is now the address. -- **Suggestion:** Write `name: address`. - -### F05 — `Load`'s doc comment now documents the `APP_NAME` regex - -- **Location:** `packages/0-framework/1-core/core/src/graph.ts` lines 19–28. -- **Issue:** `const APP_NAME` was inserted between `Load`'s doc comment and `Load`, so the comment attaches to the constant and `Load` has none. -- **Suggestion:** Move `APP_NAME` and `assertAppName` above the comment. - -### F06 — Two code comments cite the superseded ADR-0051 for the adoption rule - -- **Location:** `packages/1-prisma-cloud/1-extensions/target/src/descriptors/shared.ts` line 175; `packages/1-prisma-cloud/0-lowering/lowering/src/state/empty-scope.ts` line 81. -- **Issue:** The rule now lives in ADR-0057, which supersedes ADR-0051. -- **Suggestion:** Cite ADR-0057. - -### F07 — `prisma emulator list` shows short addresses for instances made before the upgrade - -- **Location:** `packages/1-prisma-cloud/0-lowering/local-target/src/emulator-instances.ts` line 224; `packages/9-public/composer-cli/src/family/emulator-list.ts` lines 29 and 101; `docs/guides/running-locally.md` line 173. -- **Issue:** Carried over from round 1 (F08), not addressed. Old instances keep the label `composer.address: 'api'`, while the guide says the list shows the node's address. One form of identity means the listing should not show two. -- **Suggestion:** Rewrite the label when an existing instance is reused, or say in the upgrade note that old instances show the short address until `--fresh`. - -## Deferred (out of scope) - -- **`LowerContext.id` and `address` are always equal** (round 1, S06). A cleanup across core; not this slice. -- **Rename declarations under a filtered deploy.** Upstream refuses them; Composer has no filtered deploy path. -- **A state row in the oldest format that also needs a rename.** Should compose; low priority. -- **Temporary duplicate `COMPOSER_` rows and Connections during the upgrade deploy.** Only matters near a platform count limit; a failure there is loud and deletes nothing. - -## Already addressed (round 1) - -| Round-1 finding | Fixed by | -| --- | --- | -| F01 app name with `.` gives wrong former IDs | `058dd4dd` (`assertAppName`); `addressBelowApp` removes the known prefix | -| F02 app name characters invalid in env keys | `058dd4dd` | -| F03 local record guessed old keys | `a3c3e9a3` (`formatVersion`) | -| F04 length limit on the full address | `25d9b0d7`, `f8f46c16` (per-kind limits, decided by Will) | -| F05 "before anything changes" | `4b480705` (ADR and guide now say the topology is replaced) | -| F06 two helpers for "address without the app name" | `c4b46087` (`addressBelowApp` only) | -| F07 hook and providers keyed by different helpers | `c4b46087` (both use `resourceId`) | -| S02/S04 short labels on some platform names | `25d9b0d7` (every name is the address) | -| S07 `serviceOrigin` with `--name` | `4b480705` | -| S08 role names | `c4b46087` (nouns) | -| S09 "streams module" comment | `c4b46087` | - -## Acceptance-criteria verification - -| AC | Verdict | Detail | -| --- | --- | --- | -| **Critical:** upgrading from v0.34.0 loses no application data and touches no out-of-graph state; only Composer's Connections and access keys are recreated | **NOT VERIFIED** | By code reading it holds: former IDs match v0.34.0, no App/Database/Bucket is replaced, every replace deletes one stored object after an identity check, and deletes happen only after a fully converged run. Open: F01 (the documented remedy deletes data), F03 (default connection rotation when state lacks secrets). QA has not run and its plan omits the planted state (F02). | -| A fabricated app deployed with v0.34.0 then redeployed keeps rows, objects, the secret and the RPC key; rows carry full `logicalId`s; the state has only new IDs | **NOT VERIFIED** | QA step 2 not run. Unit tests prove the declarations; the providers of GeneratedParam, ServiceKey and S3Credentials return their stored output, so a moved row keeps its value. | -| The same app deploys from scratch to a new stage | **NOT VERIFIED** | The spike showed unmatched renames are ignored; QA step 3 not run. | -| `prisma dev` on a directory last run with v0.34.0 keeps its local database's data | **WEAK** | `legacy-address-keys.test.ts` lines 261–300 runs real emulators from an old-format record and asserts instances, ports and the bucket folder are reused. It does not write and read database rows, and does not start from a directory v0.34.0 itself wrote. QA step 4 covers it. | -| Addresses start with the deployed app name on the CLI graph and the lowering | **PASS** | `load-module.ts` line 526; `pipeline.ts` lines 129–138 pass the name to Load; `lowering()` passes `opts.name`. Tests in `load.test.ts` and `lowering.test.ts`. | -| Resource IDs are `
` or `
.`, each with `renamedFrom()` | **PASS** | Every constructor checked; literal former IDs asserted in `control-lowering.test.ts` lines 3025–3330. | -| Generated values, RPC keys and stream keys are renamed, not recreated | **PASS** (unit) | Former IDs match v0.34.0; reconcile returns stored output. Live check in QA. | -| Every platform name is the full address; validation per kind, never shortened | **PASS** | `descriptors/shared.ts` lines 96–118; test "every platform name Composer writes is the address" and the limit tests at lines 2718–2800. | -| `logicalId` and the topology use full addresses | **PASS** | `logicalIdProp` and the unchanged topology builder take graph IDs; `application-topology.test.ts`. | -| Env keys from the full address, `-` to `_`, in the serializer and local copies | **PASS** | `serializer.ts` `addressKeySegments`; `local-target/src/compute.ts` lines 79–94; test with `my-shop`. F01 concerns the transition only. | -| User-facing addresses are full | **PASS** | `serviceOrigin` error, report tree (`render-deployment.ts` line 82), `dev url`, docs. F07 is a display gap. | -| Local dev rewrites the record; bucket folders and lookups follow | **PASS** | `normalized()` with `formatVersion`; `bucketDataDir`; tests listed above. | -| Design records and docs updated in this PR | **PASS** | ADR-0057, index, principle, glossary, domain docs, guides, skill, examples. F01, F03, F04 are accuracy fixes. | - -### Summary - -| Result | Count | ACs | -| --- | --- | --- | -| PASS | 9 | addresses, resource IDs, renamed values (unit), platform names, `logicalId`, env keys, user-facing addresses, local record, docs | -| NOT VERIFIED | 3 | critical condition, round trip, new stage | -| WEAK | 1 | `prisma dev` data | -| FAIL | 0 | | - ---- - -# Round 3 — fixes for the round-2 findings - -Commits `b6a2b273`, `82afb495`, `0c8e807e` on top of `49ce6e91`. Two deviations from the round-2 suggestions were accepted by the coordinator and are not reopened: the clash check consults the platform rather than failing on the graph alone, and F07 is a note in the upgrade guide rather than a code change. - -## Verdict on the round-2 findings - -| Round-2 finding | Status | Evidence | -| --- | --- | --- | -| F01 env key clash | **Fixed, with two small gaps (R3-F01, R3-F02)** | `target/src/env-key-clash.ts` enumerates every row a service writes, under both keys: each `paramEntries` entry (service-own and dependency-input), the input document, every reserved provider param (accepted RPC keys, streams key, origin), and every generated or origin leaf. That is the same set `descriptors/compute.ts` writes (lines 197, 236, 278, 329–362). Tests cover the module case, another letter case, `-` and `_` in the app name, and a service named like the app with a dependency input (`env-key-clash.test.ts`). `runPreflight` checks the clash before its own fill step, and the alchemy process (where the first resource and the `DATABASE_URL` claim are written) starts after preflight. The scope check matches Composer's writes: production class with no branch, or preview class on the stage's Branch. | -| F02 QA plan | **Fixed** in `49ce6e91` | Plan D6 plants a custom domain, a hand-set variable, a hand-made Connection, a hand-made access key and checks the default connection; QA restarts on a new Project. | -| F03 default connection rotation | **Fixed** | `upgrade-effects.md` now states the condition. I checked the argument: upstream rotates only when neither the stored attributes nor the API read hold a connection string; v0.34.0's upstream Database stores them on create and on every update; only a row rewritten by `lowering/src/state/legacy-resources.ts` lacks them (lines 223–235), and such a row has no `logicalId`, so the first deploy with `logicalIdProp` (first tagged in `v0.29.1-dev.5`) updated it and stored them. A stage last deployed by v0.34.0 cannot hit it. | -| F04 core-model sketch | Fixed | `name: address`. | -| F05 `Load` doc comment | Fixed | `graph.ts`. | -| F06 ADR-0051 citations | Fixed | `shared.ts`, `empty-scope.ts`. | -| F07 emulator labels | Resolved by a guide note | `docs/guides/deploying.md`; the note says `--fresh` deletes local data. | - -**The fix loses nothing.** Deleting the old owner's row with that key removes a `COMPOSER_` row Composer wrote. Its value comes from Composer's own sources (config, the renamed `GeneratedParam` state, service-key state), so the rerun writes it under the old owner's new key. The running Deployment keeps its environment snapshot. The garbage-collection delete of the old generation then gets `NotFound` and continues. I checked `prisma project env delete` in `wip/prisma-cli` (`packages/cli/src/commands/project/env-delete.ts`). `--role production` and `--branch ` delete exactly one row in that scope, and a Composer stage's Branch `gitName` is the stage name, so the printed commands target the right row. A new stage, or a stage that has moved, passes: the old key is absent, or the old owner's new key exists. - -## Round-3 findings - -### R3-F01 — "Deploy failed before changing anything" is not true: the Branch's topology and the Build record are already written - -- **Location:** `packages/1-prisma-cloud/1-extensions/target/src/preflight.ts` line 308 (the error); ADR-0057 lines 123 and 138; `docs/guides/deploying.md` line 497; `upgrade-effects.md` line 44. -- **Issue:** `executeDeploy` attaches the reporters, which replace the Branch's application topology and record the Build, at `cli/src/operations/execute-deploy-destroy.ts` line 465. Preflight starts at line 497. So when the clash check fails, the topology already names the full addresses, while the rows still hold the short `logicalId`s, until a deploy succeeds. No data is at risk. But the message and three documents say nothing changed. The ADR's `serviceOrigin` paragraph already describes this same situation accurately. -- **Suggestion:** Use the `serviceOrigin` wording: "before any resource is created, changed or deleted; the Branch's topology has already been replaced". Alternatively, move the check ahead of `attachReporters`. - -### R3-F02 — A chain of clashes takes one failed deploy per link - -- **Location:** `packages/1-prisma-cloud/1-extensions/target/src/preflight.ts`, `pendingEnvKeyClashes`. -- **Issue:** A clash on key K is skipped when the old owner's new key K' exists. If K' is itself the old key of a third node, it exists before any move, for example `shop.x`, `shop.shop.x` and `shop.shop.shop.x` in the app `shop`. In that case only the K' clash is reported. After the user deletes K', the next deploy reports K. Every run still stops before any resource is written, so this is safe, but the user needs three deploys and two error messages. -- **Suggestion:** Count K' as "already moved" only when K' is not the key of another clash in the list. Then every pending variable is reported at once. Add a test with the three-node chain. - -## Acceptance criteria (round 3) - -Unchanged from round 2, except that the critical condition's open code issues (F01, F03) are closed. It stays **NOT VERIFIED** only because QA (D6) has not run. - ---- - -# Round 4 — R3-F01 and R3-F02 - -Commits `b34ac515`, `648a4872`. **Verdict: SATISFIED.** - -- **R3-F01 fixed.** The error (`preflight.ts`), ADR-0057's clash paragraph and consequence, the upgrade guide and `upgrade-effects.md` now say that no resource has been created, changed or deleted, and that the Branch's topology has been replaced and the build recorded. That matches the order in `execute-deploy-destroy.ts`: reporters attach at line 465, preflight starts at line 497. No other copy of "before changing anything" remains. One leftover detail: the ADR consequence still says "until that one old variable is deleted", but with a chain there can be several. The guide correctly says "delete each". Not blocking. -- **R3-F02 fixed.** `pendingEnvKeyClashes` now counts a row under the old owner's new key as moved only if no pending clash has that key. The walk always ends: each step moves from a clash's key to the old owner's new key, and the new key is the old key plus the app-name segment, so it is strictly longer. No cycle is possible. Row lookups are memoized. I traced the three-node chain by hand. With the old rows K and K' present and K'' absent, both K and K' are reported. Once K'' exists, neither is. -- **The tests assert what they claim.** "reports every link in one run" expects both `COMPOSER_SHOP_X_ORIGIN belongs to "shop.shop.x"` and `COMPOSER_SHOP_SHOP_X_ORIGIN belongs to "shop.shop.shop.x"`. A successful preflight would turn into `[object Map]` and fail both checks. Before this fix, the first check would have failed, so the test tells the two behaviours apart. "passes once every node has moved" fails if preflight throws. - -The critical condition has no open code issues. It is still NOT VERIFIED until QA (D6) runs. diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/system-design-review.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/system-design-review.md deleted file mode 100644 index 6b237c20a..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/reviews/system-design-review.md +++ /dev/null @@ -1,92 +0,0 @@ -# System design review — A1, fully qualified node identity (round 2) - -Range: `origin/main...HEAD` on `a1-full-platform-names` (pushed as `drive/fully-qualified-node-addresses`), 33 commits, 109 files. - -Read for this review: the repo `CLAUDE.md`; `docs/design/01-principles/`; the ADR index; ADR-0057 in full; ADR-0051, and the amendment notes on ADR-0006, ADR-0019, ADR-0048, ADR-0052 and ADR-0053; ADR-0056 (merged from main); the glossary entry "Address and logical ID"; the changed parts of `core-model.md`, `deploy-cli.md`, `config-params.md`, `local-dev.md`, `alchemy-lowering.md` and `pdp-data-model.md`; the project `spec.md`, `design-notes.md` and `handover.md`; the spike findings; the slice `spec.md`, `plan.md` and `upgrade-effects.md`; the round-1 reviews. - -## Verdict - -The design is now coherent. Round 1's main concern, that the address without the app name survived in two helpers and in four platform labels, is gone: one helper (`addressBelowApp`) remains, used only to build the IDs the previous release wrote, and every platform name is the full address. The local record's old-versus-new decision is a version marker, not a guess. The merge with main and the renumber to ADR-0057 are clean: the index keeps main's ADR-0056 and lists ADR-0057 once, and no reference to the identity record as ADR-0056 remains outside the plan's own history. - -One design gap remains. ADR-0057 argues that env keys cannot collide, which is true for one format, but the move between the two formats can make a new key equal an old key of another node. The ADR records this as a consequence, but the remedy it gives contradicts its own consequence about renames (S01). - -## What changes, and the invariants it relies on - -1. Every address starts with the deployed app name; the name matches `[A-Za-z0-9][A-Za-z0-9_-]*` (`core/src/graph.ts`), and below the root, segments are letters and digits. -2. A node's main resource ID is its address; other pieces are `
.[.]` (`target/src/resource-ids.ts`). Roles are nouns. -3. No resource ID is a node address, because modules have no resources and services and resources have no children. -4. Every resource declares the ID the previous release built (`target/src/legacy-resource-ids.ts`), computed by removing the known `.` prefix. -5. Every platform name is the address, checked against that kind's limit, never shortened. -6. Local dev: the record key, the main resource ID and the address are one string; both the emulators hook and the local providers go through `resourceId`. - -Invariant 6 was the round-1 cross-package concern (S05). Both sides now call the same function, so it holds by construction. - -## Subsystem fit and boundaries - -- Core changed only in how Load builds an address and in refusing a bad app name. Resource IDs, names, env keys and the migration live in the Prisma Cloud target and `local-target`. Dependency direction is unchanged. -- The compatibility code is isolated in two files whose headers say when to delete them (`legacy-resource-ids.ts`, `legacy-address-keys.ts`). Good. -- `ProvisionEdge` gains `app` so provisioners can build former IDs. It is the root's ID, already on the graph; passing it is the least change. -- The env key segment function is duplicated in `local-target/src/compute.ts` for the existing layering reason. Acceptable. - -## Naming and typology - -| Name | Assessment | -| --- | --- | -| `resourceId(address, ...role)` | Says what it builds. | -| `addressBelowApp(address, app)` | Says exactly what it returns, and throws for an address outside the app. Replaces round 1's `resourceLabel`/`legacyAddress` pair. | -| `legacyNodeResourceId`, `legacyEnvVarId`, `legacy*Key` | Accurate and removable. | -| `fromLegacyId` | An alias of `renamedFrom`; one search target for removal. Keep. | -| `validatePlatformName(kind, address)` | Names the concept (a platform name) and the kind; replaces `validateName`, which checked one limit for every kind. | -| `EMULATOR_INSTANCES_FORMAT_VERSION` | Accurate. | -| Roles `deployment`, `env`, `generated`, `servicekey`, `streamkey`, `connection`, `warmup`, `migration`, `accesskey` | All nouns; round 1's `conn` and `key` are gone. | - -## ADR-0057 against the code - -Accurate: the address rule and app-name grammar; the resource-ID table, which matches the code and the tests; the env key rule; the platform-name rule, including the 13-character Connection suffix and why the limit is 52; that App, Database and Bucket names update in place and Connections and access keys are replaced; that replacement deletes only the stored object; the `serviceOrigin` and `--name` cost; what a failing `serviceOrigin` has already changed; local dev's version marker. - -Inaccurate or incomplete: - -- Line 136, the env key clash (S01). -- Line 119 and `upgrade-effects.md` do not say that an in-place Database update can rotate the database's default connection when the state holds no connection secrets. That connection is outside the graph. See code-review F03. - -Doc consistency: the principle, glossary, `deploy-cli.md`, `config-params.md`, `pdp-data-model.md`, `local-dev.md`, `alchemy-lowering.md` and the index agree with the ADR. `core-model.md` has one stale sketch comment (code-review F04). - -## Test strategy at the architectural level - -- Literal former IDs per node kind are the right evidence for a compatibility rule, and they exist. -- Limits per kind are tested at, and one over, each boundary. -- Nothing automated can prove the former IDs equal real v0.34.0 state; only QA can. The QA plan must also cover what the spike did not: App, Bucket and access key name changes, planted out-of-graph state, and the default connection (code-review F02). -- No test covers the env key clash. With S01's detection, one unit test over a graph with a module named like the app would. - -## Findings - -**S01 — The format change can make keys collide, and the recorded remedy re-identifies a node** -- Location: ADR-0057 lines 88 and 136; `upgrade-effects.md` line 37. -- Issue: Line 88 says the `-` to `_` replacement cannot make two keys collide. Within one format that holds. Across the two formats it does not: a new key of one node can equal an old key of another while the old row still exists. That happens when a module or service directly below the root has the app's name in any letter case, or when modules spell an app name containing `-` or `_`. Line 136 records only the module case, and its remedy, renaming a node, gives that node a new identity, which by line 127 deletes its rows and, for a database or bucket, its data. -- Suggestion: Say in line 88 that the guarantee is for one format, and that the move between formats is handled separately. In line 136, state the full condition, have the deploy detect it before applying (see code-review F01 for how), and give a remedy that changes no identity: delete the superseded `COMPOSER_` variable, then deploy again. - -**S02 (low) — The glossary says a Connection's name is the address, but on the platform it is the address plus a suffix** -- Location: glossary "Address and logical ID". -- Issue: Upstream appends `-` and a 12-character token to every Connection name. ADR-0057 and `alchemy-lowering.md` say so; the glossary does not, so a reader looking for a Connection named `shop.catalog` in Console will not find one. -- Suggestion: Add one clause: a Connection's platform name is the address followed by upstream's suffix. - -## Out of scope for this pass - -Implementation correctness, failure modes and AC verification are in `code-review.md`. - ---- - -# Round 3 - -Read: the diffs of `b6a2b273`, `82afb495` and `0c8e807e`; ADR-0057 lines 88, 123 and 138; the glossary entry; `upgrade-effects.md`; the upgrade guide. - -- **S01 (the env key clash): fixed.** - - Line 88 now limits the no-collision claim to one format. - - The new paragraph states every form of the clash, and the check matches it. - - The consequence no longer tells the user to rename a node, and says why renaming is wrong. - - The check consults the platform, so a new stage or a stage that has moved never fails. That deviates from the round-2 suggestion; the coordinator accepted it, and it is correct. - - `env-key-clash.ts` is a compatibility file with a deletion note, next to `legacy-resource-ids.ts`. That is the right place. -- **S02 (the Connection name suffix): fixed** in the glossary. -- **New, low:** the clash paragraph (line 123) and consequence (line 138) say the deploy fails "before changing anything". At that point the topology is already replaced, which the `serviceOrigin` paragraph states correctly. Use the same wording in both places (code-review R3-F01). - -Verdict for this pass: the design is coherent; one wording fix remains. diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/spec.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/spec.md deleted file mode 100644 index 17e54de69..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/spec.md +++ /dev/null @@ -1,66 +0,0 @@ -# A1 — Fully qualified node identity, with automatic migration - -## At a glance - -```ts -export default module('shop', ({ provision }) => { - const catalog = provision(postgres({ name: 'catalog', ... })); - provision(api, { id: 'api', deps: { db: catalog }, params: { origin: serviceOrigin('shop.api') } }); -}); -``` - -| | Today | After | -| --- | --- | --- | -| addresses | `shop`, `catalog`, `api` | `shop`, `shop.catalog`, `shop.api` | -| row `logicalId` | `catalog` | `shop.catalog` | -| Alchemy resource IDs | `catalog-db`, `catalog-conn`, `api-svc`, `api-deploy` | `shop.catalog`, `shop.catalog.connection`, `shop.api`, `shop.api.deployment` | -| env keys | `COMPOSER_API_...` | `COMPOSER_SHOP_API_...` (`-` in the app name becomes `_`) | - -The first deploy of an existing app with this change renames every resource in place. No database, bucket, generated secret or RPC key is recreated. - -## Chosen design - -From [design-notes.md](../../design-notes.md), decisions 1 to 5. Grounded in the code at `origin/main` 70fcd82c: - -- **Addresses.** Load (`packages/0-framework/1-core/core/src/load-module.ts`, around lines 528, 774, 813) prefixes every child address with the root's ID. The root's ID is the deployed app name everywhere. Today the CLI pipeline graph uses the authored name while the deploy lowering uses `--name` (`core/src/control/deploy.ts:578`); both must use the deployed name, so the topology's root `logicalId` matches the Project's. -- **Resource IDs.** Every descriptor builds its main resource's ID from the address and each other piece's ID as `
.` (compute: `descriptors/compute.ts`; postgres: `descriptors/shared.ts`, `orm-postgres.ts`, `raw-postgres.ts`; bucket: `descriptors/bucket.ts`, `s3-credentials.ts`; environment variable rows; generated values; service keys and stream keys in `control/extension.ts`). Every resource is piped through `renamedFrom()` from `alchemy/Rename`. Today's ID is computed from the address with the first segment removed, exactly as today's code builds it. -- **Values that must survive.** Generated values (`GeneratedParam`), RPC service keys and stream keys must be renamed, not recreated, so their values do not change. -- **Every platform name is the full address.** The App display name, the local Database name, and the Connection, Bucket and access key names are the full address, on new stages and on the upgrade. The upgrade recreates each Connection and access key with new credentials; this is accepted (design note 5). -- **`logicalId`.** `logicalIdProp` writes the full address; the topology builder (`lowering/src/builds/application-topology.ts`) writes full addresses for every node and parent. -- **Env keys.** `configKey` and its local copies (`target/src/serializer.ts`, `local-target/src/compute.ts`) build keys from the full address with `-` replaced by `_`. -- **User-facing addresses.** `serviceOrigin`, `dev url`, deploy reports, log labels and error messages use the full address. -- **Local dev.** On the first `prisma dev` after upgrading, the per-directory record (`emulator-instances.json`) is rewritten to full-address keys, and anything else keyed by address (bucket data directories, local database lookup names) is moved or mapped, so local data and ports survive. -- **Design records and docs, in this PR.** A new decision record replaces the node-identity decision (the index marks it replaced). Update the principle "A node's address is its identity, everywhere", the glossary entry "Address and logical ID", and the address and resource-ID descriptions in `10-domains/core-model.md`, `10-domains/deploy-cli.md`, `05-prisma-cloud/alchemy-lowering.md` and `05-prisma-cloud/pdp-data-model.md`. Update guides, the agent skill, examples and READMEs that write addresses (for example `serviceOrigin('api')` in `examples/auth/module.ts` and the auth module README). - -## Coherence rationale - -One concept, the form of a node's identity, applied to every place it is written. Splitting it would ship a release where some strings are in the new form and some in the old, and existing apps would migrate twice. The diff is wide but uniform; a reviewer checks one rule against each call site, plus the migration. - -## Scope - -**In:** everything above. - -**Out:** platform commands that accept addresses (B1), `deploy list` (A2), the entry in the config (A3), any protection against an older Composer deploying after migration. - -## Pre-investigated edge cases - -- The Alchemy rename spike ([assets/spike-alchemy-renames.md](../../assets/spike-alchemy-renames.md)) verified Database, Connection and App renames, renames on a new stage, partial-failure reruns and dotted IDs in hosted state. It did **not** cover buckets, bucket keys, environment variable rows, generated values or service keys. QA must. -- After a killed deploy, the dead process holds the deploy lease for about 60 seconds. -- `validateName` caps every name at 65 characters, but the platform's limits differ by kind: Connection 65, Bucket 100, App and Project 256, access key none. Validation uses the real limit per kind and fails with a clear message naming the address; it never shortens. -- Root names may contain `-` and `_`; child provision IDs are letters and digits only. - -## Slice-specific done conditions - -- **Critical (Will, 2026-10-23): upgrading from the last Composer release to this one loses no application data.** No database is dropped, no bucket or object deleted, no custom domain removed, and no platform state that the Composer graph does not represent is touched. QA plants such state before the upgrade (a custom domain on a service, an environment variable set by hand, an extra Connection made by hand on the database) and shows it is unchanged after. Only Composer's own Connections and access keys are recreated, with new credentials (design note 5). - -- A fabricated app with a database holding rows, a bucket holding objects, a generated secret, an RPC edge and a `serviceOrigin`, deployed with the current release, then redeployed from this branch: rows, objects, the secret's value and the RPC key are unchanged; rows carry full-address `logicalId`s; the deploy state has the new resource IDs and none of the old. -- The same app deploys from scratch to a new stage from this branch. -- `prisma dev` on a directory last run with the current release keeps its local database's data. - -## Open questions - -None. - -## References - -- [Project spec](../../spec.md), [design notes](../../design-notes.md), [spike findings](../../assets/spike-alchemy-renames.md) diff --git a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md b/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md deleted file mode 100644 index 1fb4076b5..000000000 --- a/.drive/projects/node-addressing/slices/a1-fully-qualified-identity/upgrade-effects.md +++ /dev/null @@ -1,46 +0,0 @@ -# Upgrade effects: every resource Composer declares - -What the first deploy from this branch does to a stage the last release deployed. Example app `shop`, database `shop.catalog`, service `shop.api`, bucket `shop.files`. - -Every resource declares `renamedFrom()` (`legacy-resource-ids.ts`), so Alchemy moves its state entry to the new ID before diffing. Then the upstream diff decides between update in place and replace. A replace is create-first by default (`node_modules/alchemy/src/Apply.ts`, "By default a replacement is create-first"; no Composer resource sets `deleteFirst`). The old generation is deleted after the new one exists. - -Upstream sources below are under `node_modules/alchemy/src/Prisma/`. Composer's own resources are under `packages/1-prisma-cloud/1-extensions/target/src/` and `packages/1-prisma-cloud/0-lowering/lowering/src/`. - -## Platform deploy - -| Resource | What the upgrade changes | Update or replace | What a replace deletes | -|---|---|---|---| -| App (`shop.api`, was `api-svc`) | ID; `logicalId` `api` → `shop.api`; `displayName` `api` → `shop.api` | Update. `App.ts` diff replaces only when the project changes; a `logicalId` or `displayName` change is `update`. | Nothing. The App keeps its id, so its custom domains (`CustomDomain.ts` binds to `appId`) and endpoint stay. | -| Deployment (`shop.api.deployment`, was `api-deploy`) | ID; its env rows' keys change, so its triggers change | Replace (`Deployment.ts` diff: `triggersChanged` → `replace`), as on any config change today | The old Deployment of that App only (`delete` checks it belongs to `output.appId`). Not the App or its domains. | -| EnvironmentVariable, `COMPOSER_` config, input and generated-value rows | ID; key `COMPOSER_API_…` → `COMPOSER_SHOP_API_…` | Replace (`EnvironmentVariable.ts` diff: key change → `replace`) | The old row only. `delete` fetches the row by the stored `environmentVariableId`, checks project, branch, class and key equal the stored ones (`ensureVariableIdentity`), and skips system-managed rows. A hand-set variable is never in Composer's state, so it cannot be deleted. Values: config is recomputed; generated values come from the `GeneratedParam` state, which is renamed and not regenerated. | -| Database (`shop.catalog`, was `catalog-db`) | ID; `logicalId` `catalog` → `shop.catalog`. A Branch-attached database is given no `name` (`stageDatabase`), so its physical name is generated from the ID and changes | Update. `Database.ts` diff replaces only on project, region, default flag or source change; `logicalId` and name changes are `update`. Spike confirmed data kept. | Nothing is deleted. The default connection is rotated only under one condition, below. | -| Connection (`shop.catalog.connection`, was `catalog-conn`) | ID; `name` `catalog` → `shop.catalog` | **Replace** (`Connection.ts` diff: `nameChanged` → `replace`) | The old Connection only. `delete` loads it by the stored `connectionId`, refuses if its database or name differ from the stored ones, then deletes that one id. The database and any hand-made Connection on it are untouched. The new Connection's credentials reach every consuming service in the same deploy through its env rows. | -| PgWarm (`shop.catalog.warmup`, was `catalog-warm`) | ID; its `url` input is the new Connection's | Update (Composer's `pg-warm-resource.ts`: no diff; `delete` is a no-op) | Nothing. | -| OrmMigration (`shop.catalog.migration`, was `catalog-migrate`) | ID | Update (`orm-migration-resource.ts`: `delete` is a no-op). Migrations already applied are recorded in the database, not the state. | Nothing. | -| Bucket (`shop.files`, was `files-bucket`) | ID; `logicalId` and `name` → `shop.files` | Update (`Bucket.ts` diff: name and `logicalId` changes are `update`). The display name is a label; the provider-side S3 bucket name and objects stay (`Bucket.ts` doc, lines 93–95). | Nothing. | -| BucketAccessKey (`shop.files.accesskey`, was `files-key`) | ID; `name` → `shop.files` | **Replace** (`BucketAccessKey.ts` diff: name change → `replace`) | The old key only: `deleteBucketKey({ bucketId, keyId })` with the stored ids. The bucket, its objects and any hand-made key stay. Consumers get the new key in the same deploy. | -| S3Credentials (`
`, was `-creds`) | ID | Update (`s3-credentials-resource.ts`: returns the stored pair; `delete` is a no-op) | Nothing. | -| GeneratedParam (`shop.api.generated.`) | ID | Update (`generated-param-resource.ts`: returns the stored value; `delete` is a no-op) | Nothing. The value is kept. | -| ServiceKey, RPC (`.servicekey.`) and streams (`.streamkey`) | ID | Update (`lowering/src/compute/ServiceKey.ts`: value kept in state) | Nothing. Keys are kept. | -| Project, Branch | Nothing new from this slice | Not changed | Nothing. | - -## Local dev - -The local-target providers (`packages/1-prisma-cloud/0-lowering/local-target/src/`) key each instance by resource ID through the per-directory record, which is rewritten on the first `prisma dev` (`legacy-address-keys.ts`). Their Database, Connection, Bucket and BucketAccessKey providers have no diff that replaces, and their `delete` is a no-op, so a changed `name` deletes nothing. The bucket data folder keeps its old name. - - -## When a Database update rotates its default connection - -Every upgrade updates every Database, because its `logicalId` and derived name change. At the end of an update, upstream calls `recoverDatabaseConnectionSecrets` (`Database.ts` lines 916–933; it runs whenever `olds` is set, which it is on every update). That function rotates the database's default connection (`createConnectionRotate`, `Internal/DatabaseSecrets.ts` lines 105–165) only when it has no connection string: neither the deploy state's stored attributes (`directConnectionString`, `pooledConnectionString`, `accelerateConnectionString`, used when the stored `databaseId` matches) nor the database read from the API, which never returns them. The default connection is not in the Composer graph. - -A stage last deployed by v0.34.0 cannot hit this. v0.34.0 pins the same upstream version (`alchemy@2.0.0-beta.83`), whose Database stores the connection strings in its attributes on create, and again on every update. The only Database state without them is a row rewritten from a pre-upstream release (`lowering/src/state/legacy-resources.ts` leaves them out). Such a row has no `logicalId` either, so the first deploy with any release that writes `logicalId` (the first was v0.29.1-dev.5) updated it, rotated the default connection then, and stored the strings. That one-time rotation is documented in the "Upgrading to the upstream Prisma resources" section of `docs/guides/deploying.md`. So this upgrade rotates a default connection only for a stage last deployed by a release older than that, which is not an upgrade from the last release. - -## Verdict - -No step deletes application data, and no Connection outside the graph is rotated for a stage last deployed by v0.34.0. No database is dropped. No bucket or object is deleted. No App is replaced, so no custom domain is removed. Only Composer's own Connections, access keys, Deployments and `COMPOSER_` rows are replaced. Each replace deletes one platform object, by the id Composer stored, after checking that its identity still matches. - -## Findings outside the data question - -1. **The move can give one row's new env key to another row's old key.** It happens with a module or a service directly below the root named like the app in any letter case (`shop.x` and `shop.shop.x`), or modules that spell an app name with `-` or `_` (`my-shop.x` and `my-shop.my.shop.x`). Without a check, the replacement create hits `Conflict` in `EnvironmentVariable.ts` partway through the deploy and deletes nothing. Deploy preflight (`target/src/env-key-clash.ts`, called from `runPreflight`) now finds every such key from the graph and, while the old owner's row still holds it on the stage and has not moved to its new key, fails the deploy before any resource is created, changed or deleted; the Branch's application topology has already been replaced and the build recorded. A chain of clashes is reported in one run. The fix it gives loses nothing: delete that one variable with `prisma project env delete`, then deploy again. -2. **Upstream shortens Connection names.** The platform name of a Connection is `physicalInstanceName(name, instanceId)` (`Internal/EnvName.ts`): the name cut to 52 characters, then `-` and a 12-character token. So the Connection's platform name is `shop.catalog-`, not exactly the address, and upstream never sends more than 65 characters. Composer therefore limits a Connection's address to 52 characters, so the address is never cut. -3. **A Branch-attached database has no Composer name.** Upstream refuses an explicit name together with branch attachment at create, so its platform name is generated from the resource ID. Only the local dev Database takes the address as `name`. diff --git a/.drive/projects/node-addressing/slices/a2-deploy-list/plan.md b/.drive/projects/node-addressing/slices/a2-deploy-list/plan.md deleted file mode 100644 index d731f1bf0..000000000 --- a/.drive/projects/node-addressing/slices/a2-deploy-list/plan.md +++ /dev/null @@ -1,25 +0,0 @@ -# A2 — dispatch plan - -Spec: [spec.md](spec.md). Branch `drive/deploy-list`, cut from `origin/main` `87d001de`. Dispatches run in order. The PR opens after review and QA. - -### D1. The command - -- **Outcome:** `deploy list` in composer-cli's family, with `--stage`, `--name`, `--entry`, and human, stdout and JSON output as the spec says. The join and kind mapping are a pure function with unit tests: root, service, both Postgres types, bucket, module, a node with no row, a missing topology. The command test uses a fake Management API client. -- **Gate:** `pnpm typecheck`, `pnpm lint`, `pnpm lint:deps`, composer-cli tests. - -### D2. The end-to-end check and the docs - -- **Outcome:** the deploy-verify-destroy action runs `deploy list --json` after deploy and checks it against the platform rows. The design records and guides from the spec are updated. -- **Gate:** `pnpm lint`; the e2e workflow on the PR. - -### Review - -`/drive-code-review` on the branch (no walkthrough). Fix findings in follow-up dispatches. - -### D3. prisma-cli mounts the command - -- **Outcome:** a prisma/prisma-cli PR adds `"deploy list"` next to `"emulator list"`, once the composer-cli release that has it is out. - -### D4. QA - -- **Outcome:** deploy an example to a named stage on the real platform, run `deploy list` for it and for production, check every node, then delete the test Project. diff --git a/.drive/projects/node-addressing/slices/a2-deploy-list/qa.md b/.drive/projects/node-addressing/slices/a2-deploy-list/qa.md deleted file mode 100644 index a3a1e047e..000000000 --- a/.drive/projects/node-addressing/slices/a2-deploy-list/qa.md +++ /dev/null @@ -1,129 +0,0 @@ -# QA — A2 `deploy list` against the real platform - -Run 2026-10-27 on `drive/deploy-list` at `5464a84d`, after `pnpm build`, under bun, from `examples/storefront-auth`. - -Environment: `~/.config/prisma-compose/deploy.env` (service token, workspace, Alchemy password), plus `PRISMA_REGION=us-east-1` and `AUTH_SIGNING_SECRET=sk_test_ci_storefront_auth`, as CI sets them. - -The installed `prisma` does not mount `deploy list` yet (D3). The human tables below come from a scratch copy of `scripts/e2e-verify-deploy-list.ts` that mounts the same Composer family with the same `createCli` options, passes its arguments through, and prints stdout and stderr as they are. The human table goes to stderr; the tab-separated lines go to stdout. - -App name `a2-qa-99d5e9`, named stage `qa-99d5e9`. - -## Results - -| Check | Result | -| --- | --- | -| Deploy to production | PASS | -| `e2e-verify-deploy-list.ts` on production | PASS | -| Human table on production | PASS | -| Deploy to the named stage | PASS | -| `e2e-verify-deploy-list.ts` on the named stage (`STAGE=qa-99d5e9`) | PASS | -| Human table on the named stage | PASS | -| `deploy list --stage does-not-exist` fails with `DEPLOY.TARGET_NOT_FOUND` | PASS | -| Project deleted and gone | PASS | - -The named stage's listing shows its own rows (`db_paj9…`, `cps_eju3…`, `cps_c0q5…`), not production's, though the Project holds rows for both Branches with the same `logicalId`s. - -## Production - -``` -$ bun ../../node_modules/.bin/prisma deploy --name a2-qa-99d5e9 -... -✔ Deployed a2-qa-99d5e9 to production in 46.7s. -``` - -``` -$ STACK_NAME=a2-qa-99d5e9 bun ../../scripts/e2e-verify-deploy-list.ts -deploy list --name a2-qa-99d5e9 --json exited 0: -{"kind":"result","envelope":{"ok":true,"commandId":"deploy.list","result":[{"address":"a2-qa-99d5e9","kind":"project","platformId":"proj_x46eyjv91k1femz0acf82vdt","url":null},{"address":"a2-qa-99d5e9.auth","kind":"module","platformId":null,"url":null},{"address":"a2-qa-99d5e9.auth.database","kind":"postgres","platformId":"db_wi2h43um3td2e828a689xw08","url":null},{"address":"a2-qa-99d5e9.auth.service","kind":"service","platformId":"cps_v4lvdnj8la6pqcugsglxv1b5","url":"https://v4lvdnj8la6pqcugsglxv1b5.ewr.prisma.build"},{"address":"a2-qa-99d5e9.storefront","kind":"service","platformId":"cps_cnd2dlaije0vyimghp7qfdp6","url":"https://cnd2dlaije0vyimghp7qfdp6.ewr.prisma.build"}],...}} - -Topology on branch br_k0h9v9o7cfz9sz1y2ueuayri: - module a2-qa-99d5e9 - module a2-qa-99d5e9.auth - resource a2-qa-99d5e9.auth.database - service a2-qa-99d5e9.auth.service - service a2-qa-99d5e9.storefront -Rows in project proj_x46eyjv91k1femz0acf82vdt: - app cps_cnd2dlaije0vyimghp7qfdp6 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.storefront - app cps_v4lvdnj8la6pqcugsglxv1b5 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.auth.service - database db_wi2h43um3td2e828a689xw08 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.auth.database -deploy list shows every topology node with its platform ID and URL. -``` - -``` -$ deploy list --name a2-qa-99d5e9 --format human -Address Kind Platform ID URL -a2-qa-99d5e9 project proj_x46eyjv91k1femz0acf82vdt — -a2-qa-99d5e9.auth module — — -a2-qa-99d5e9.auth.database postgres db_wi2h43um3td2e828a689xw08 — -a2-qa-99d5e9.auth.service service cps_v4lvdnj8la6pqcugsglxv1b5 https://v4lvdnj8la6pqcugsglxv1b5.ewr.prisma.build -a2-qa-99d5e9.storefront service cps_cnd2dlaije0vyimghp7qfdp6 https://cnd2dlaije0vyimghp7qfdp6.ewr.prisma.build -``` - -## Named stage - -``` -$ bun ../../node_modules/.bin/prisma deploy --name a2-qa-99d5e9 --stage qa-99d5e9 -... -✔ Deployed a2-qa-99d5e9 to qa-99d5e9 in 45.3s. -``` - -``` -$ STACK_NAME=a2-qa-99d5e9 STAGE=qa-99d5e9 bun ../../scripts/e2e-verify-deploy-list.ts -deploy list --name a2-qa-99d5e9 --stage qa-99d5e9 --json exited 0: -{"kind":"result","envelope":{"ok":true,"commandId":"deploy.list","result":[{"address":"a2-qa-99d5e9","kind":"project","platformId":"proj_x46eyjv91k1femz0acf82vdt","url":null},{"address":"a2-qa-99d5e9.auth","kind":"module","platformId":null,"url":null},{"address":"a2-qa-99d5e9.auth.database","kind":"postgres","platformId":"db_paj9wnn6izpx5ma5p9hv1pyo","url":null},{"address":"a2-qa-99d5e9.auth.service","kind":"service","platformId":"cps_eju3icljkbwes2op4hoivjkd","url":"https://eju3icljkbwes2op4hoivjkd.ewr.prisma.build"},{"address":"a2-qa-99d5e9.storefront","kind":"service","platformId":"cps_c0q52ww83ng7uzn387pjtyvb","url":"https://c0q52ww83ng7uzn387pjtyvb.ewr.prisma.build"}],...}} - -Topology on branch br_yf243sg4fu64rgxcu1inhfvv: - module a2-qa-99d5e9 - module a2-qa-99d5e9.auth - resource a2-qa-99d5e9.auth.database - service a2-qa-99d5e9.auth.service - service a2-qa-99d5e9.storefront -Rows in project proj_x46eyjv91k1femz0acf82vdt: - app cps_cnd2dlaije0vyimghp7qfdp6 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.storefront - app cps_v4lvdnj8la6pqcugsglxv1b5 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.auth.service - app cps_eju3icljkbwes2op4hoivjkd branch=br_yf243sg4fu64rgxcu1inhfvv logicalId=a2-qa-99d5e9.auth.service - app cps_c0q52ww83ng7uzn387pjtyvb branch=br_yf243sg4fu64rgxcu1inhfvv logicalId=a2-qa-99d5e9.storefront - database db_wi2h43um3td2e828a689xw08 branch=br_k0h9v9o7cfz9sz1y2ueuayri logicalId=a2-qa-99d5e9.auth.database - database db_paj9wnn6izpx5ma5p9hv1pyo branch=br_yf243sg4fu64rgxcu1inhfvv logicalId=a2-qa-99d5e9.auth.database -deploy list shows every topology node with its platform ID and URL. -``` - -``` -$ deploy list --name a2-qa-99d5e9 --stage qa-99d5e9 --format human -Address Kind Platform ID URL -a2-qa-99d5e9 project proj_x46eyjv91k1femz0acf82vdt — -a2-qa-99d5e9.auth module — — -a2-qa-99d5e9.auth.database postgres db_paj9wnn6izpx5ma5p9hv1pyo — -a2-qa-99d5e9.auth.service service cps_eju3icljkbwes2op4hoivjkd https://eju3icljkbwes2op4hoivjkd.ewr.prisma.build -a2-qa-99d5e9.storefront service cps_c0q52ww83ng7uzn387pjtyvb https://c0q52ww83ng7uzn387pjtyvb.ewr.prisma.build -``` - -## A stage that does not exist - -``` -$ deploy list --name a2-qa-99d5e9 --stage does-not-exist --json -{"kind":"result","envelope":{"ok":false,"commandId":"deploy.list","error":{"code":"DEPLOY.TARGET_NOT_FOUND","severity":"error","summary":"Nothing deployed for a2-qa-99d5e9/does-not-exist.","nextActions":[{"kind":"user-choice","label":"Deploy it first."}],...}} -exit 2 -``` - -## Teardown - -``` -$ bun ../../scripts/composer-destroy.ts --name a2-qa-99d5e9 --stage qa-99d5e9 -Done: 18 succeeded (27.5s) -Destroyed. - -$ bun ../../scripts/composer-destroy.ts --name a2-qa-99d5e9 --production -Done: 18 succeeded (28.7s) -Removed the Project (proj_x46eyjv91k1femz0acf82vdt) — nothing was left in it. -Destroyed. - -$ curl -H "Authorization: Bearer $PRISMA_SERVICE_TOKEN" https://api.prisma.io/v1/projects/proj_x46eyjv91k1femz0acf82vdt -404 - -$ deploy list --name a2-qa-99d5e9 --json -... "code":"DEPLOY.TARGET_NOT_FOUND","summary":"Nothing deployed for a2-qa-99d5e9." ... -exit 2 -``` - -No Project named `a2-qa-99d5e9` remains in the workspace's project list. diff --git a/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/code-review.md b/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/code-review.md deleted file mode 100644 index 01f18b96b..000000000 --- a/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/code-review.md +++ /dev/null @@ -1,106 +0,0 @@ -# Code review — A2 `prisma deploy list` - -Range: `origin/main...drive/deploy-list` (merge base `87d001de`, head `2e16ca74`). Spec: [../spec.md](../spec.md). - -## Summary - -The command does what the spec says, the join is correct, and the tests are well aimed. The gaps are in the end-to-end check (it does not check `kind`, and it only runs on production), one unhelpful error, and the resource-type table being copied a third time. - -Checked locally: `bun test` for `test/deploy-list.test.ts` and `test/deployed-nodes.test.ts` (21 pass), `scripts/deploy-list-check.test.ts` (8 pass), `pnpm lint:deps` (clean). The Management API SDK's `getV1Services`, `getV1Databases` and `getV1Buckets` all accept `projectId` and `branchId` filters, so the Branch filter is real and rows from another stage with the same `logicalId` cannot leak in. - -## What looks solid - -- The join is a pure function with one test per kind, plus missing-row, empty and order cases. -- The command test asserts the actual request parameters (`branchId` on `/v1/services`, `gitName` on the Branch lookup), not just the output. -- An invalid `--stage` is rejected before any network call, and the test asserts that no request was made. -- The Management API code is lazily imported, so loading the family stays cheap. -- The e2e check reads the platform independently of the command, through `/v1/apps` and its own Branch filter. - -## Findings - -### F01 — The e2e check never compares `kind` (should-fix) - -- **Location:** `scripts/deploy-list-check.ts` lines 63–86. -- **Issue:** `deployListMismatches` checks address order, platform ID and URL, but not `kind`. A regression that printed `module` for every node, or `raw-postgres` instead of `postgres`, would pass the real-platform check. -- **Suggestion:** Compute the expected kind from the topology node (`project` for the root, `service`, `postgres` for both Postgres types, `bucket` for `s3`, `module`, otherwise the raw `type`) and report a mismatch. Add a case to `scripts/deploy-list-check.test.ts`. - -### F02 — A credential with no workspace gives an unhelpful error (should-fix) - -- **Location:** `packages/9-public/composer-cli/src/family/list-deployed.ts` lines 47–49. -- **Issue:** It fails with `DEPLOY.CONTAINER_FAILED` and the message "The active credential names no workspace.", with no `fix`. `prisma deploy` in the same situation says `PRISMA_WORKSPACE_ID` is required (`packages/1-prisma-cloud/1-extensions/target/src/container.ts` line 163). The user learns what is wrong but not what to do, and the two commands disagree. -- **Suggestion:** Use the same message deploy uses, or add a `fix` naming `PRISMA_WORKSPACE_ID`. Add a command test for it. - -### F03 — The target's resource types are restated a third time (should-fix) - -- **Location:** `packages/9-public/composer-cli/src/family/deployed-nodes.ts` lines 11–15. -- **Issue:** `RESOURCE_KINDS` repeats which target types (`postgres`, `raw-postgres`, `s3`) have a database or bucket row. The same table is in `scripts/platform-identity.ts` (lines 23–27) and `packages/1-prisma-cloud/1-extensions/target/src/local-target/emulators.ts` (line 43). If the target adds a database type, `deploy list` shows its raw type with no platform ID, and nothing fails: the e2e check reads its own copy. -- **Suggestion:** Export one map from resource type to row list from the Prisma Cloud domain (for example next to `readDeployedStage`) and use it in `deployed-nodes.ts` and `scripts/platform-identity.ts`. - -### F04 — The e2e check only covers production (should-fix) - -- **Location:** `.github/actions/deploy-verify-destroy/action.yml` lines 68–74. -- **Issue:** The step sets no `STAGE`, and the action only deploys production. The named-stage path (Branch by `gitName`, rows filtered by that Branch) is proven only against the fake client. The spec's done condition asks for "a real deployed stage". -- **Suggestion:** Make D4 QA explicitly run `deploy list --stage ` against a real named-stage deploy and record the output in the slice, since the e2e workflow has no stage deploy to reuse. - -### F05 — Dead branch in `readDeployedStage` (nit) - -- **Location:** `packages/1-prisma-cloud/0-lowering/lowering/src/deployed.ts` lines 46–47. -- **Issue:** `resolveContainer` always returns `branchId` for a named stage and `defaultBranchId` for production, so `branchId === undefined` cannot happen. Returning `undefined` there would read as "nothing deployed", which hides a real bug if it ever did happen. -- **Suggestion:** Fail with an error ("resolved no Branch") as `scripts/platform-state.ts` does, instead of returning `undefined`. - -### F06 — The human-table test checks only two substrings (nit) - -- **Location:** `packages/9-public/composer-cli/test/deploy-list.test.ts` lines 240–248. -- **Issue:** It asserts that stderr contains `shop.catalog.db` and the URL. It would pass if the columns were missing or misordered. -- **Suggestion:** Also assert the header row (`Address`, `Kind`, `Platform ID`, `URL`) and that one row shows `shop.catalog.db`, `postgres` and `db_1` on the same line. - -### F07 — The release claim depends on work not yet done (nit) - -- **Location:** `docs/guides/deploying.md` lines 321–322, `skills/prisma-composer-core-concepts/SKILL.md` line 408. -- **Issue:** Both say `prisma deploy list` "first ships in the `prisma` release after 8.0.0-rc.26". That depends on the D3 prisma-cli pull request, which has not been opened. -- **Suggestion:** Recheck the version when D3 merges and correct both lines if it ships later. - -## Deferred (out of scope) - -- **Read failures use `DEPLOY.CONTAINER_FAILED`.** A topology or row read that fails with a 500 surfaces as a container failure (`list-deployed.ts` lines 27–33). It is the nearest existing ADR-0044 code; a read-specific code would mean amending ADR-0044, which is beyond this slice. -- **Two rows with one `logicalId` on a Branch.** The join takes the first match (`deployed-nodes.ts` lines 29–31 and 40). `e2e-verify-identity.ts` already fails a deploy that leaves duplicates, so this is guarded elsewhere. - -## Already addressed - -After this review, fixed on the branch: - -- F01: `aa414ec7`. The check compares `kind`. -- F02: `90cf60cf`. The error says `PRISMA_WORKSPACE_ID` is required, as deploy does. -- F03: `3ee8e3b9`. One map, `RESOURCE_ROW_KINDS`, in `lowering/src/resource-rows.ts`, used by the command, the scripts and the emulators. -- F04: real named-stage QA in [qa.md](../qa.md). -- F05: `5464a84d`. A missing Branch fails with an error. -- F06: `90cf60cf`. The test checks the header and two full rows. -- F07: the docs no longer name a release version. - -## Acceptance-criteria verification - -The spec has no numbered acceptance criteria. These are its "Chosen design" bullets and done conditions, read as criteria. - -| AC | Verdict | Detail | -| --- | --- | --- | -| AC1: Lives in composer-cli next to `emulator list` | **PASS** | `src/exports/family.ts` adds `'deploy list'`; test "the published family ships it". | -| AC2: Reads the application topology, not Alchemy state | **PASS** | `deployed.ts` lines 50–54 read only the topology endpoint; the fake client throws on any other route, and the command tests pass. | -| AC3: Join by `logicalId` in the Branch's lists, filtered by project and Branch; root is the Project | **PASS** | `deployed.ts` lines 67–77 send `projectId` and `branchId`; `deploy-list.test.ts` lines 160–162 assert the query; `deployed-nodes.test.ts` covers root and the `logicalId` match among decoys. | -| AC4: Kind mapping (`project`, `service`, `postgres` for both, `bucket`, `module`, else raw type; empty ID with no row) | **PASS** | `deployed-nodes.test.ts` has one test per case, including `s3-credentials` and a missing row. The e2e check does not verify kind (F01). | -| AC5: URL is the service's `appEndpointDomain` | **PASS** | Unit test and command test assert `url: 'https://api.prisma.app'` from the row; e2e compares it against `/v1/apps`. | -| AC6: Stage lookup is find-only by `logicalId` and `gitName`, production by default; `--name` and `--entry` work | **PASS** | Command tests assert the `gitName` query and the Branch used for the topology, and the Project chosen under `--name` and `--entry`. | -| AC7: Nothing deployed fails with `DEPLOY.TARGET_NOT_FOUND`, naming app and stage | **PASS** | Table test covers no Project, no Branch (message names `shop/pr-42`), 404 topology and empty topology. | -| AC8: Human table, tab-separated stdout, `--json` array with `null` | **PASS** | stdout test asserts the exact text; JSON test asserts the exact array with `null`s. The human-table test is loose (F06). | -| AC9: prisma-cli mounts `deploy list` | **NOT VERIFIED** | D3, not in this branch. | -| AC10: Design records and guides updated | **PASS** | ADR-0050 and ADR-0053 amendments, the ADR index, `deploy-cli.md`, `deploying.md`, `running-locally.md` and the skill all describe the command consistently with the code. | -| Done 1: Against a real deployed stage, every node has the right IDs and URLs | **NOT VERIFIED** | Production is covered by the e2e step once it runs in CI; a named stage is not covered anywhere yet (F04). D4 QA pending. | -| Done 2: The e2e workflow runs it after deploy and checks `--json` against the platform | **WEAK** | The step exists and checks order, IDs and URLs, but not kind (F01). It has not been seen running on a pull request yet. | - -### Summary - -| Result | Count | ACs | -| --- | --- | --- | -| PASS | 9 | AC1–AC8, AC10 | -| FAIL | 0 | | -| NOT VERIFIED | 2 | AC9, Done 1 | -| WEAK | 1 | Done 2 | diff --git a/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/system-design-review.md b/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/system-design-review.md deleted file mode 100644 index a49b9f727..000000000 --- a/.drive/projects/node-addressing/slices/a2-deploy-list/reviews/system-design-review.md +++ /dev/null @@ -1,52 +0,0 @@ -# System-design review — A2 `prisma deploy list` - -Range: `origin/main...drive/deploy-list` (merge base `87d001de`, head `2e16ca74`, 12 commits). -Spec: [../spec.md](../spec.md). No pull request yet. - -Read before reviewing: `docs/design/01-principles/` (both files), the ADR index, ADR-0050 and ADR-0053 with their new amendments, ADR-0044's `DEPLOY` codes, `docs/design/10-domains/deploy-cli.md`. - -## What the branch adds - -A read-only command, `deploy list`, that prints one row per node of the application topology the last deploy wrote to a stage's Branch, joined by `logicalId` to the Branch's service, database and bucket rows. Three new pieces carry it: - -- `readDeployedStage` in the Prisma Cloud lowering package (`packages/1-prisma-cloud/0-lowering/lowering/src/deployed.ts`). It finds the Project and Branch with the existing find-only `resolveContainer`, reads the topology, and lists the Branch's rows filtered by `projectId` and `branchId`. -- `resolveAppIdentity` and `validateStageName`, exposed on a new `@internal/cli/app-identity` subpath, so a command can learn the app name without Load or assembly. -- The command itself in `@prisma/composer-cli`, next to `emulator list`, with the join as a pure function (`deployed-nodes.ts`) and the Management API work lazily imported (`list-deployed.ts`). - -## Fit with the recorded design - -- **Placement matches ADR-0053.** `emulator list` set the pattern: a command that needs Prisma Cloud code is added to the family by `@prisma/composer-cli`, and the framework CLI does not grow a target hook. `deploy list` follows it exactly. The framework family's doc comment and both ADR amendments say so. No principle is contradicted: the framework core still knows nothing about Prisma Cloud. -- **Node identity.** The join is by full address against `logicalId`, which is what the "address is identity everywhere" principle and ADR-0057 require. Row IDs are shown as data, never used as keys. Good. -- **No deploy-lease contention.** Reading the topology rather than Alchemy state is the right call and is recorded in `deploy-cli.md`. -- **Command shape.** ADR-0050 says `prisma deploy` is a bare verb. `deploy list` makes `deploy` both a command and a group, the same shape `dev` already has with `dev url` and `dev stop`. The ADR-0050 amendment states this. It does not reverse the decision; it extends it as ADR-0053 did. -- **Error codes.** `DEPLOY.TARGET_NOT_FOUND` and `DEPLOY.STAGE_INVALID` are existing ADR-0044 codes used with the meaning `destroy` already gives them. - -## Naming and typology - -- `@internal/cli/app-identity`: named for the concept it exposes (who the app is). It also exports `validateStageName`, which is not app identity. Acceptable as "what a read-only command needs before a lookup", but the subpath's doc comment has to explain the stage check; a reader would not guess it from the name. No action needed. -- `DeployedStage`, `DeployedRow`, `DeployedServiceRow`, `DeployedNode`: consistent "deployed" prefix across the lowering read and the command's output row. Each name says what it is. No qualifier-prefix problem. -- `listDeployed` / `ListDeployed` / `ListDeployedInput`, parallel to `listEmulators` / `ListEmulators`. Symmetric with the precedent. - -### The `kind` column mixes three vocabularies - -The spec settled this, so it is an observation for Will, not a finding. The `kind` value comes from three different sources: - -- `project` describes the platform row (the root's topology kind is `module`). -- `service` and `module` are the topology's structural `kind`. -- `postgres` and `bucket` are renamed resource types (`raw-postgres` becomes `postgres`, `s3` becomes `bucket`), while every other resource passes its raw `type` through (`s3-credentials`). - -So `kind` is an open set whose values do not share one meaning. An agent filtering on `kind == "bucket"` works today; an agent asking "which nodes are resources" cannot answer from this column. If that matters later, adding the topology's `kind` and `type` as separate JSON fields would answer it without changing the human table. - -### The resource-type vocabulary now has three copies - -`deployed-nodes.ts` restates which target resource types become a database or a bucket row. `scripts/platform-identity.ts` has the same table, and the target's `local-target/emulators.ts` has a third (`DATABASE_TYPES`). The types themselves belong to the Prisma Cloud target pack. If the target adds a database type, `deploy list` will silently show the raw type with no platform ID, and the e2e check will agree with it because it reads its own copy. See code-review F03. - -## Test strategy at the architecture level - -- The join is a pure function with unit tests per kind. The command is tested through the engine with a fake Management API, including request parameters (the Branch filter). This is the right split. -- The e2e check reads the platform independently (`/v1/apps` across the whole Project, then filters by Branch itself) rather than reusing `readDeployedStage`. That independence is what makes it a real check of the command, and it is worth keeping. -- `scripts/platform-state.ts` now holds the shared platform read for both e2e checks. It imports lowering source files by relative path, as the old `e2e-verify-identity.ts` did. No change in kind. - -## Out of scope here - -Implementation correctness, failure modes and acceptance criteria are in [code-review.md](code-review.md). Adopter learnability of the guide text would be a devrel review. diff --git a/.drive/projects/node-addressing/slices/a2-deploy-list/spec.md b/.drive/projects/node-addressing/slices/a2-deploy-list/spec.md deleted file mode 100644 index 940673ff5..000000000 --- a/.drive/projects/node-addressing/slices/a2-deploy-list/spec.md +++ /dev/null @@ -1,60 +0,0 @@ -# A2 — `prisma deploy list` lists what is deployed on a stage - -## At a glance - -``` -$ prisma deploy list --stage pr-42 -ADDRESS KIND PLATFORM ID URL -shop project proj_123 -shop.api service svc_456 https://….prisma.app -shop.catalog module -shop.catalog.db postgres db_789 -shop.files bucket bkt_012 - -$ prisma deploy list --json # production, machine-readable -``` - -## Chosen design - -From design note 8, grounded in the code at `origin/main` `87d001de`: - -- **Where it lives.** `packages/9-public/composer-cli`, next to `emulator list`. It reads Prisma Cloud data, and the framework CLI has no extension hook for "read what is deployed". Adding one for a single command is not worth it. -- **What it reads.** The stage's application topology (`GET /v1/projects/{projectId}/branches/{branchId}/application-topology`), which every deploy writes. Not the Alchemy state: reading that needs the deploy lease and would collide with a running deploy. -- **The join.** Each topology node is one row of output, in topology order. Its platform row is found by `logicalId` in the Branch's services, databases and buckets lists (filtered by `projectId` and `branchId`). The root node's row is the Project. -- **Kind.** `project` for the root, `service`, `postgres` for both Postgres types, `bucket`, `module` for other modules. Any other resource shows its topology `type`. A node with no platform row has an empty platform ID. -- **URL.** Services only: the row's `appEndpointDomain`, which is already a full `https://` URL and is the same URL a deploy prints. -- **Finding the stage.** The same find-only lookup deploy uses: the Project by `logicalId` (the app name), the Branch by `gitName`, production when no `--stage` is given. The app name comes from the entry, as `deploy` resolves it (`resolveAppIdentity`), with `--name` and `--entry` working as on `deploy`. -- **Nothing deployed.** No Project, no Branch, or no topology (404 or no nodes) fails with `DEPLOY.TARGET_NOT_FOUND`, naming the app and stage. -- **Output.** Human: a table. `stdout`: one tab-separated line per node. `--json`: an array of `{ address, kind, platformId, url }`, with `null` for missing values. -- **Mounting in `prisma`.** The `prisma` host in prisma/prisma-cli mounts each Composer command by name. A small prisma-cli PR adds `"deploy list"` next to `"emulator list"`. -- **Design records, in this PR.** ADR-0053 (or ADR-0050, whichever lists the family's commands) gets an amendment for `deploy list`. `deploy-cli.md`, `docs/guides/deploying.md` (a short section on listing a stage), and the agent skill show it. - -## Scope - -**In:** everything above. - -**Out:** -- Platform rows with no topology node (for example after a failed topology write). The topology drives the list. -- Showing a node's status or its last deploy. -- Addresses on platform commands (B1). - -## Pre-investigated edge cases - -- The topology is written before the apply, and the write is best-effort. After a failed deploy a node may have no row; it lists with an empty platform ID. -- S3 credentials resources and non-root modules have no row. -- Production has no Branch filter for `isDefault`; reuse the existing paged lookup. - -## Slice-specific done conditions - -- Against a real deployed stage, the command lists every node with the right IDs and URLs. -- The e2e deploy workflow runs it after deploy and checks its `--json` output against the platform rows. Until prisma-cli ships `deploy list`, the check may run the Composer command family directly rather than through `prisma`. - -## Open questions - -None. - -## References - -- [Project spec](../../spec.md), [design notes](../../design-notes.md) decision 8. -- ADR-0023, ADR-0024, ADR-0043, ADR-0044, ADR-0050, ADR-0053, ADR-0057. -- `docs/design/05-prisma-cloud/pdp-data-model.md`, `build-reporting.md`. diff --git a/.drive/projects/node-addressing/slices/a3-entry-in-config/plan.md b/.drive/projects/node-addressing/slices/a3-entry-in-config/plan.md deleted file mode 100644 index 92b2f0f15..000000000 --- a/.drive/projects/node-addressing/slices/a3-entry-in-config/plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# A3 — dispatch plan - -Spec: [spec.md](spec.md). Branch `drive/deploy-entry-in-config`, cut from `origin/main` `561f5336`. Dispatches run in order. The PR opens after review and QA. - -### D1. Design records describe the entry in the config - -- **Outcome:** ADR-0003, ADR-0049 and ADR-0050 carry amendment notes and updated text. ADR-0044 lists `CONFIG.ENTRY_MISSING`. `deploy-cli.md`, `local-dev.md` and the glossary show `prisma deploy` and `prisma dev` without a positional entry. The ADR index entries mention the amendments. -- **Builds on:** the spec. -- **Gate:** `pnpm lint`. - -### D2. The config's entry, the `--entry` flag, and the operations - -- **Outcome:** the section check accepts `entry`. `deploy`/`dev` take `--entry` and no positional. The operations take an optional `entry` that overrides `config.value.entry`. A missing entry fails with `CONFIG.ENTRY_MISSING`. The entry path given to the Alchemy child is absolute. Unit tests cover: the config entry resolved against the config file's directory from another cwd, the override, the missing case, an invalid `entry` value, and a leftover positional. -- **Builds on:** D1. -- **Gate:** `pnpm typecheck`, `pnpm lint`, cli package tests. - -### D3. Every caller in the repo uses the config's entry - -- **Outcome:** each example's and the website's `prisma.config.ts` has `entry`. Package scripts, `scripts/composer-destroy.ts` (now `--entry`), the deploy-verify-destroy action, `deploy-docs.yml`, the guides, the skill, `gotchas.md`, the READMEs and every test that passes a positional entry use the new form. `git grep` finds no `prisma deploy ` or `prisma dev ` outside ADR history text. -- **Builds on:** D2. -- **Gate:** `pnpm typecheck`, `pnpm lint`, `pnpm lint:deps`, cli tests, `scripts/*.test.ts`, `test/integration` after `pnpm build`. - -### Review - -`/drive-code-review` on the branch (no walkthrough). Fix findings in follow-up dispatches. - -### D4. QA - -- **Outcome:** - - From an example directory: `prisma deploy` with the config's entry deploys to the real platform. - - `prisma deploy --entry` from another directory works. - - With no entry, the error is `CONFIG.ENTRY_MISSING`. - - `prisma dev` and `prisma dev --detach`/`dev stop` run from the config's entry. - - Every test Project is deleted afterwards. -- **Builds on:** review fixes. -- **Hands to:** evidence for the PR description. diff --git a/.drive/projects/node-addressing/slices/a3-entry-in-config/qa.md b/.drive/projects/node-addressing/slices/a3-entry-in-config/qa.md deleted file mode 100644 index ec39ec883..000000000 --- a/.drive/projects/node-addressing/slices/a3-entry-in-config/qa.md +++ /dev/null @@ -1,48 +0,0 @@ -# A3 QA: the entry lives in the config - -Run on 2026-10-23 on branch `drive/deploy-entry-in-config` at `1bdd405f`, after `pnpm install` and `pnpm build`. Commands used the workspace's `prisma` (`bun ../../node_modules/.bin/prisma`, as the example's scripts do), with credentials from `~/.config/prisma-compose/deploy.env` and `PRISMA_REGION=us-east-1`. Logs are in `wip/qa-a3/` of this worktree, which git ignores. No secret values are recorded here. - -**Verdict:** every check passed. - -The app is `examples/storage`, used in place. Its `prisma.config.ts` has `entry: './module.ts'`, and its root module is `storage-example`. - -## Step 1. Deploy from the config's entry - -| Check | Result | Evidence | -|---|---|---| -| `prisma deploy --name qa-a3-4ca9r0` from `examples/storage`, with no entry argument | PASS | "Deployed qa-a3-4ca9r0 to production". It created Project `proj_vs7kdo4y09kzwcuztkn3shh1` with `qa-a3-4ca9r0.storage.db` (`db_g3armpvzim0y20jddcdabw7a`), `qa-a3-4ca9r0.storage.service` and `qa-a3-4ca9r0.blobs` (`cps_hdhuig86otxi183urfnrhppf`). | -| The service answers | PASS | `/health` returns the blob store banner. `PUT /blobs/qa-a3.txt` returns `size: 8`, and `GET` returns `hello-a3`. | -| The same deploy from `examples/storage/src/blobs` | PASS | It found `prisma.config.ts` and its `entry` from the subdirectory. It planned 13 updates and 2 replaces (the two Deployments) on the same resource ids, and created nothing new. The blob written before is still served. | - -## Step 2. `--entry` overrides the config - -| Check | Result | Evidence | -|---|---|---| -| `prisma deploy --entry qa-a3-other.ts` deploys that module | PASS | `qa-a3-other.ts`, a temporary file in `examples/storage`, exports the root module `qa-a3-other-grpzno`. The deploy created Project `qa-a3-other-grpzno` (`proj_are8u63veea3aylj69u608bl`) with `qa-a3-other-grpzno.storage.db`, `qa-a3-other-grpzno.storage.service` and `qa-a3-other-grpzno.blobs`. Its service answers. No `storage-example` Project was created, so the config's module was not used. | -| Cleanup | PASS | I deleted that Project with `prisma project delete`, and the API now returns `resource-not-found` for it. I removed the temporary file, and `git status` shows no change under `examples/storage`. | - -## Step 3. Errors - -These ran in `wip/qa-a3/noentry`, a copy of the example whose `node_modules` links to the example's. - -| Check | Result | Evidence | -|---|---|---| -| No `entry` in the config and no `--entry` | PASS | The command exits with code 2, and the output is `✘ [CONFIG.ENTRY_MISSING] No entry module is set, so there is no app to load.` The fix line names both ways to set it: `entry: './module.ts'` in the `composer` section of prisma.config.ts, or `--entry `. It fails in "load config and app" after 2 ms, before any "connect to project and branch" step. The same error appears with the platform credentials unset. No Project was created. | -| `prisma deploy module.ts` (a leftover positional) | PASS | The output is `✘ [CLI.INVALID_ARGUMENTS] Too many arguments, expected 0 but encountered "module.ts"`, from the CLI engine. | -| `entry` set to a missing file | PASS | The output is `✘ [COMPOSE.ENTRY_UNLOADABLE] Failed to import entry module ".../noentry/does-not-exist.ts"`. The fix line says ``Check `entry: "./does-not-exist.ts"` in .../noentry/prisma.config.ts. It is relative to that file's directory.`` | - -I made one mistake along the way. My first attempt to remove `entry` from the copy did not remove it. That run loaded the entry and got as far as contacting the platform, where it stopped with `DEPLOY.CONTAINER_FAILED` because no region was set. It created nothing, and the API lists no `qa-a3-noentry` Project. That run is not evidence for any check above. - -## Step 4. Local dev - -| Check | Result | Evidence | -|---|---|---| -| `prisma dev --detach` from `examples/storage` with no entry | PASS | "dev: ready". It started `storage-example.blobs` on `http://localhost:3084` and `storage-example.storage.service` on `:3083`. The root name `storage-example` comes from the config's `module.ts`. | -| `prisma dev url storage-example.blobs` | PASS | It printed `http://localhost:3084`. A blob was written and read back. The short address `blobs` fails with `DEV.ADDRESS_UNKNOWN` and lists the full addresses. | -| `prisma dev stop` leaves nothing running | PASS | "Stopped storage-example". Ports 3083 and 3084 and the local database port 51963 no longer listen, and the service processes 51703 and 51742 are gone. The shared compute and postgres emulators on 4316 and 4302 started at 15:11, before this QA run, so I left them alone. | - -## Step 5. Teardown - -| Check | Result | Evidence | -|---|---|---| -| Every Project deleted | PASS | I deleted `qa-a3-4ca9r0` (`proj_vs7kdo4y09kzwcuztkn3shh1`) and `qa-a3-other-grpzno` (`proj_are8u63veea3aylj69u608bl`) with `prisma project delete --confirm `. `prisma project list` shows no `qa-a3` Project. `GET /v1/projects/` returns `resource-not-found` for both. | diff --git a/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/code-review.md b/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/code-review.md deleted file mode 100644 index d03611f0b..000000000 --- a/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/code-review.md +++ /dev/null @@ -1,134 +0,0 @@ -# Code review — A3, deploy's entry file lives in the config - -Branch `drive/deploy-entry-in-config`, range `origin/main...HEAD`, 4 commits, 68 files. Reviewed against the slice [spec](../spec.md) and [plan](../plan.md), design note 6, and ADR-0003, ADR-0044, ADR-0049 and ADR-0050 as amended on the branch. Prisma CLI source read in `wip/prisma-cli` where needed. - -Settled and not reopened: the entry is an input to the tool, not an application fact; the config's `entry` resolves against the config file's directory and `--entry` against the cwd; the positional is removed and the engine's own "Too many arguments" error is kept. - -## Summary - -The code is correct. One function, `resolveEntryPath` (`cli/src/pipeline.ts` lines 85–99), decides the entry for every command and operation: -- an override resolves against `cwd`; -- the section's `entry` resolves against the directory of the config file, which `checkConfigSource` has already made absolute; -- with neither, it raises `CONFIG.ENTRY_MISSING`. - -The Alchemy child gets the absolute path from `loadEntry`, so it does not depend on the child's working directory. CI, the scripts and the package scripts all moved. - -What remains is in the docs: -- two copy-paste commands still pass `module.ts` and now fail (F01); -- four decision records still state `prisma deploy ` or show `prisma dev module.ts` as the current form (F02); -- there is no upgrade note for users whose config has no `entry` (F03). - -## What looks solid - -- **One resolution rule, used everywhere.** `runPipeline` (deploy, destroy and dev, including dev's rebuild on change, `execute-dev.ts` lines 82 and 320) and `resolveAppIdentity` (log) both call `resolveEntryPath`. The section's `entry` resolves against `path.dirname` of the file `checkConfigSource` resolved against `cwd`, so a relative `config.file` from a script also works. -- **The Alchemy child.** `generate-stack.ts` and `dev/generate-dev-stack.ts` take `entryPath` from `LoadedEntry.path`, which is absolute. -- **The section check.** `entry` is in the allowed keys. A non-string or empty value gives `CONFIG.FIELD_INVALID` with `field: 'entry'`, and the unknown-field messages list `entry`. Tests: `entry-path.test.ts`. -- **The commands.** `deploy` and `dev` take `--entry` and no positional. The family test runs `deploy module.ts` and `dev module.ts` and asserts exit 2, the engine's "Too many arguments" message, and that no operation was called. -- **The operations.** `entry` is optional on deploy, destroy, dev and log. Two tests cover the new behaviour: an operation test runs `deployWithDeps` from another cwd with a config `entry` and reaches Alchemy, and a missing-entry test asserts `CONFIG.ENTRY_MISSING` and that Alchemy never ran. -- **Callers moved.** - - Every example's `prisma.config.ts`, the website and the local-dev fixture have `entry`. - - The package scripts, the `deploy-verify-destroy` action, `deploy-docs.yml` and `destroy-guard.sh` (now two arguments) no longer pass a positional, and nothing calls `destroy-guard.sh` with three. - - `composer-destroy.ts` takes `--entry` and refuses positionals. - - The integration tests pass `--entry` or rely on the fixture's config. -- **ADR-0044** registers `CONFIG.ENTRY_MISSING`. ADR-0049 and ADR-0050 match the code. - -## Findings - -### F01 — Two documented commands still pass `module.ts`, which now fails - -- **Location:** `docs/guides/deploying.md` line 67; `skills/prisma-composer-core-concepts/SKILL.md` line 362. -- **Issue:** Both show `bun node_modules/prisma/dist/prisma.js deploy module.ts`. That now exits 2 with "Too many arguments". The skill is what agents copy from. -- **Suggestion:** Drop `module.ts` from both. Re-run the plan's `git grep` with a pattern that matches this form, for example `deploy module.ts`, not only `prisma deploy `. - -### F02 — Four decision records still give the positional as the current command - -- **Location:** - - ADR-0024 line 3 and ADR-0041 line 3, the ADR-0050 amendment notes: "deploys run as `prisma deploy …`" and "the command is `prisma dev `"; - - ADR-0053 lines 12 and 18 (`prisma dev module.ts`, `prisma dev module.ts --detach`); - - ADR-0056 line 52 (`prisma dev module.ts`); - - the index entries for ADR-0041 (`docs/design/90-decisions/README.md` line 65) and ADR-0024. -- **Issue:** Agents treat these records as binding. ADR-0053 and ADR-0056 are recent and show the command as it is used now. ADR-0024 and ADR-0041 state the command shape in their own amendment notes. Only ADR-0003, ADR-0049 and ADR-0050 were amended. -- **Suggestion:** Add a one-line amendment note to each of the four, as the branch does for ADR-0003: "Amended 2026-10-23: `prisma dev`/`prisma deploy` take no positional entry; the entry is `entry` in the `composer` section, or `--entry` (ADR-0050)." Leave the bodies unchanged, and update the two index entries. This keeps each record's history and stops it reading as current. - -### F03 — No upgrade note for an existing project - -- **Location:** `docs/guides/deploying.md` and `docs/guides/getting-started.md` (no upgrade section). -- **Issue:** A user who upgrades and runs `prisma deploy module.ts` gets the engine's "Too many arguments, expected 0", which does not say where the entry went. Running `prisma deploy` without adding `entry` gives `CONFIG.ENTRY_MISSING`, which does. Keeping the engine's error is settled, so the guide is the only place that can connect the two. -- **Suggestion:** Add a short "Upgrading" paragraph: the positional entry is gone; add `entry: './module.ts'` to the `composer` section, or pass `--entry`. - -### F04 — The guide says the commands "find it from any directory", but `prisma dev` keeps its record in the directory it runs in - -- **Location:** `docs/guides/deploying.md` line 104. -- **Issue:** The entry is now found from a subdirectory too, because the engine uses the nearest `prisma.config.ts`. But `prisma dev` keeps its record of emulator instances, its bucket data and the state `prisma dev url` reads under `/.prisma-composer/dev` (`execute-dev.ts` line 75; ADR-0052). Running it from a subdirectory starts a second, empty local copy of the app. -- **Suggestion:** Say that `prisma deploy` works from any directory, and that `prisma dev`, `dev url` and `dev stop` should be run from the same directory each time, normally the one holding `prisma.config.ts`. - -### F05 — `CONFIG.ENTRY_MISSING` tells an API caller to pass `--entry` - -- **Location:** `packages/0-framework/3-tooling/cli/src/composer-config.ts` lines 127–134. -- **Issue:** The same finding is raised for the operations, where the override is the `entry` input. The test asserts the CLI wording for `deployWithDeps`. -- **Suggestion:** Say "or pass `--entry ` (the `entry` input when calling `@prisma/composer/control`)". - -### F06 — A wrong `entry` path fails without saying where the path came from - -- **Location:** `packages/0-framework/3-tooling/cli/src/load-entry.ts` lines 38–42. -- **Issue:** `COMPOSE.ENTRY_UNLOADABLE` gives "Failed to import entry module "/abs/…": Cannot find module…" with no fix. Before this slice the user had just typed the path. Now it usually comes from a file they may not be looking at. -- **Suggestion:** Have `resolveEntryPath` return where the path came from, and add a fix naming it. For example: "Check `entry` in /path/prisma.config.ts (relative to that file)", or "Check the `--entry` path (relative to the current directory)". - -### F07 — `composer-destroy.ts` says both entries resolve against the current directory - -- **Location:** `scripts/composer-destroy.ts` lines 8–9. -- **Issue:** The section's `entry` resolves against the config file's directory. That matters when `--config` names a file somewhere else. -- **Suggestion:** "The entry is the section's `entry`, relative to the config file, or `--entry`, relative to the current directory." - -### F08 — ADR-0050's amendment names only `destroy` and `log` as having an optional `entry` - -- **Location:** ADR-0050 line 3. -- **Issue:** All four operations have it, as ADR-0049's amendment says. -- **Suggestion:** "The operations' `entry` input is optional and overrides the config's." - -## Deferred (out of scope) - -- `prisma dev` watching the entry file, and re-reading `entry` when the config changes during a session. The spec excludes both. - -## Acceptance-criteria verification - -| AC | Verdict | Detail | -| --- | --- | --- | -| The section takes an optional non-empty `entry`; `CONFIG.FIELD_INVALID` otherwise | **PASS** | `composer-config.ts` `checkFields`; `entry-path.test.ts` checks `''`, `42` and an array. | -| The config's `entry` resolves against the config file's directory; `--entry` against cwd | **PASS** | `resolveEntryPath`; unit tests, plus `operations.test.ts` running from another cwd and reaching Alchemy. | -| `deploy`/`dev` lose the positional and gain `--entry`, which wins | **PASS** | `commands/deploy.ts`, `dev.ts`; the family tests pass `--entry` through and leave it undefined without the flag. The precedence itself is tested in `resolveEntryPath`. | -| The operations' `entry` is optional and overrides the config | **PASS** | Types and `resolveEntryPath`; operation test for the config path. | -| No entry fails before any work with `CONFIG.ENTRY_MISSING`, naming both sources | **PASS** (F05) | Operation test asserts the code and fix text and that Alchemy did not run. | -| A leftover positional is not silently ignored | **PASS** | Family test asserts exit 2 and "Too many arguments" for `deploy` and `dev`. The engine's error is the accepted outcome; say so in the PR. | -| Every repo caller moved | **WEAK** | Code, CI, scripts and package scripts moved. Two documented commands did not (F01). | -| The Alchemy child gets an absolute entry path | **PASS** | `LoadedEntry.path`; `generate-stack.test.ts`, `stack-file-import.test.ts`. | -| ADR-0003, ADR-0049, ADR-0050 amended; ADR-0044 lists the code; domain docs, guides, skill, gotchas | **WEAK** | Done as listed, but ADR-0024, ADR-0041, ADR-0053 and ADR-0056 still state the positional (F02). | -| A real deploy of one example from its config's entry | **NOT VERIFIED** | QA (D4) not run. | - -### Summary - -| Result | Count | ACs | -| --- | --- | --- | -| PASS | 7 | field check, resolution, commands, operations, missing entry, leftover positional, absolute path | -| WEAK | 2 | repo callers, design records | -| NOT VERIFIED | 1 | real deploy | -| FAIL | 0 | | - ---- - -# Round 2 - -Commits `a7184afe`, `1bdd405f` on top of `45236626`. **Verdict: SATISFIED.** - -| Finding | Status | Evidence | -| --- | --- | --- | -| F01 positional in two commands | Fixed | `deploying.md` and `SKILL.md` show `… prisma.js deploy`. A repo-wide `git grep` for `deploy module.ts`, `dev module.ts`, `deploy ` and `dev ` now finds four kinds of hit: ADR bodies kept as history under their new amendment notes, the upgrade note (deliberate), and the retired-binary lint test's fixtures. | -| F02 records not amended | Fixed | ADR-0024, ADR-0041, ADR-0053 and ADR-0056 carry an amendment note that cites ADR-0050. The ADR-0024 and ADR-0041 index entries mention it. | -| F03 no upgrade note | Fixed | `deploying.md` quotes the engine's error for `prisma deploy module.ts` / `prisma dev module.ts` and gives both fixes. | -| F04 "from any directory" | Fixed | The guide now limits the claim to `prisma deploy` below the config, and says to run `prisma dev`, `dev url` and `dev stop` from one directory. | -| F05 missing-entry fix text | Fixed | The fix names the `entry` input for `@prisma/composer/control`; the test asserts the new text. | -| F06 unloadable entry | Fixed | `resolveEntryPath` returns the path and a fix that names its source: the config file and its `entry` value, or the `--entry` value and the cwd. `loadEntry` adds it to `COMPOSE.ENTRY_UNLOADABLE`. A new operation test asserts both texts. The JSX and default-export errors keep their own fixes, which is right. | -| F07 `composer-destroy.ts` header | Fixed | | -| F08 ADR-0050 operations list | Fixed | It names all four operations. | - -QA (D4) is still outstanding for the real-deploy criterion. diff --git a/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/system-design-review.md b/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/system-design-review.md deleted file mode 100644 index 1e3b06387..000000000 --- a/.drive/projects/node-addressing/slices/a3-entry-in-config/reviews/system-design-review.md +++ /dev/null @@ -1,60 +0,0 @@ -# System design review — A3, deploy's entry file lives in the config - -Range: `origin/main...HEAD` on `drive/deploy-entry-in-config`, 4 commits, 68 files. - -Read for this review: -- the repo `CLAUDE.md` and `docs/design/01-principles/`; -- design note 6, and the project and slice specs and plan; -- the ADR index; -- ADR-0003, ADR-0044, ADR-0049 and ADR-0050 as amended on the branch; -- ADR-0017, ADR-0024, ADR-0041, ADR-0053 and ADR-0056, where they state the command shape; -- the changed parts of `deploy-cli.md`, `local-dev.md`, `core-model.md` and the glossary. - -## Verdict - -The change fits the recorded design. ADR-0003's rule that application facts stay in code still holds. `entry` says where to look, not what the application is, and the amendment says so in the same terms as design note 6. The new field sits in the section ADR-0049 owns. The resolution rule is the CLI engine's rule for path fields, so Composer adds no rule of its own. The positional's removal is one change applied across commands, operations, scripts and CI. - -Two design-level gaps: four older records still state the positional as the current form (S01), and the guide's "from any directory" conflicts with local dev's per-directory record (S02). - -## Subsystem fit - -- The field lives in `PrismaAppConfig` (core) with a one-line doc comment, but core never reads it. Only the CLI's `resolveEntryPath` interprets it. Core stays free of file paths at run time, as ADR-0017 intends. -- One function owns precedence and resolution. Every command and operation goes through it, so the CLI and `@prisma/composer/control` cannot drift apart. -- `CONFIG.ENTRY_MISSING` belongs in the `CONFIG` namespace: the failure is a missing config field with a CLI override. It carries `where: `. - -## Naming - -- `entry` matches the existing word for this file in the commands, the operations and the docs. `--entry` matches it. -- `resolveEntryPath(override, source, cwd)` says what it returns. The parameter name `override` states the precedence. - -## Decision records against the code - -Accurate: -- the ADR-0003 amendment and its note on the rejected alternative; -- ADR-0049's allowed fields, validation and resolution base; -- ADR-0050's command shapes and script example; -- ADR-0044's registry entry. - -Incomplete: S01, and the ADR-0050 amendment's mention of only `destroy` and `log` (code-review F08). - -## Findings - -**S01 — Records that state the command shape were not amended** -- Location: ADR-0024 and ADR-0041 (their ADR-0050 amendment notes), ADR-0053 (lines 12, 18), ADR-0056 (line 52), and the index entries for ADR-0024 and ADR-0041. -- Issue: These are the records an agent reads for local dev and stages. ADR-0053 and ADR-0056 show `prisma dev module.ts` as the way to run the app. ADR-0024 and ADR-0041 state `prisma deploy ` and `prisma dev ` in amendment notes that read as current. -- Suggestion: Add a one-line amendment note to each and update the index entries. Keep the bodies as history, as the branch does for ADR-0003. - -**S02 — "From any directory" holds for deploy, not for local dev** -- Location: `docs/guides/deploying.md` line 104. -- Issue: ADR-0052 makes local dev's record per working directory. With the entry in the config, `prisma dev` now runs from a subdirectory, and gets a second local copy of the app. -- Suggestion: Say that local dev's commands use the directory they run in (code-review F04). - ---- - -# Round 2 - -Commits `a7184afe`, `1bdd405f`. -- **S01 fixed.** ADR-0024, ADR-0041, ADR-0053 and ADR-0056 have amendment notes citing ADR-0050, and the index entries say so. The bodies are kept as history. -- **S02 fixed.** The guide confines "from any directory" to `prisma deploy`, and states local dev's per-directory record. - -No new design findings. Verdict: SATISFIED. diff --git a/.drive/projects/node-addressing/slices/a3-entry-in-config/spec.md b/.drive/projects/node-addressing/slices/a3-entry-in-config/spec.md deleted file mode 100644 index 33870a6a4..000000000 --- a/.drive/projects/node-addressing/slices/a3-entry-in-config/spec.md +++ /dev/null @@ -1,69 +0,0 @@ -# A3 — Deploy's entry file lives in the config - -## At a glance - -```ts -// prisma.config.ts -export default definePrismaConfig({ - composer: composer({ - entry: './module.ts', - extensions: [prismaCloud(), nodeBuild()], - state: prismaState(), - }), -}); -``` - -``` -$ prisma deploy --stage pr-42 # was: prisma deploy module.ts --stage pr-42 -$ prisma dev # was: prisma dev module.ts -$ prisma deploy --entry other.ts # overrides the config for one run -``` - -## Chosen design - -From design note 6, grounded in the code at `origin/main` `561f5336`: - -- **The field.** The `composer` section takes an optional `entry: string`. The engine-free section check (`packages/0-framework/3-tooling/cli/src/composer-config.ts`) adds `entry` to the allowed keys, next to `extensions` and `state`. It checks that `entry` is a non-empty string when present, and reports `CONFIG.FIELD_INVALID` otherwise. -- **Where the path resolves.** The config's `entry` resolves against the directory of the file that declared the section (`ComposerConfigSource.file`). This is how the Prisma 8 CLI engine resolves path fields in `prisma.config.ts` (`resolveSectionPath`: against the declaring file, never the cwd). `--entry` resolves against the command's working directory, as Prisma's own path flags do. -- **The commands.** `prisma deploy` and `prisma dev` lose their positional `` and gain `--entry ` (`cli/src/family/commands/deploy.ts`, `dev.ts`). `--entry` wins over the config. -- **The operations.** `deploy`, `destroy`, `dev` and `log` on `@prisma/composer/control` keep their `entry` input, but it becomes optional. When it is given, it overrides the config and resolves against `cwd`, as today. When it is absent, the operation uses `config.value.entry`, resolved against `config.file`'s directory. -- **No entry anywhere.** If neither the config nor `--entry` (or the operation's `entry`) gives an entry, the command fails before any work with a new code, `CONFIG.ENTRY_MISSING`. The message names both ways to supply one: `entry` in the `composer` section of `prisma.config.ts`, or `--entry `. The code is added to ADR-0044's registry. -- **A leftover positional.** `prisma deploy module.ts` must not silently ignore `module.ts`. If the CLI engine lets a command see unexpected positionals, fail with a message saying the entry moved to `entry` in the config or `--entry`. Otherwise the engine's own unknown-argument error is acceptable. Say which in the PR. -- **The repo's own uses.** The examples, the website, `scripts/composer-destroy.ts`, the deploy-verify-destroy CI action and the docs-deploy workflow put `entry` in each `prisma.config.ts` and stop passing a positional. `composer-destroy.ts` takes `--entry` instead of a positional. -- **Design records, in this PR.** - - ADR-0003: amend. The config now carries one input to the tool, the entry path. It names no application facts; those stay in code. Its rejected alternative, "a config that names the application", stays rejected for application facts (`app`, `target`, `name`). - - ADR-0049: amend the list of allowed section fields. - - ADR-0050: amend the command shapes. - - Update `docs/design/10-domains/deploy-cli.md`, `local-dev.md`, the glossary where it shows the command, the guides, the agent skill, `gotchas.md` and the READMEs. - -## Coherence rationale - -One input, the entry path, moves from the command line into the config, with every place that passes it updated in the same change. Splitting it would leave a release where some docs or CI scripts pass a positional the commands no longer take. - -## Scope - -**In:** everything above. - -**Out:** -- `dev` watching the entry file (today it watches the config file and each build's watch paths). -- Deploying part of an app by pointing the entry at a submodule (a project non-goal). -- `deploy list` (A2). - -## Pre-investigated edge cases - -- `@prisma/composer/control` callers that pass `entry` keep working unchanged; only callers that omit it are new. -- The generated stack file for the Alchemy child process already imports the declaring `prisma.config.ts`. The resolved entry path it is given must be absolute, so it doesn't depend on the child's working directory. - -## Slice-specific done conditions - -- `prisma deploy` and `prisma dev` run from the config's entry. `--entry` overrides it. A missing entry fails with `CONFIG.ENTRY_MISSING`, naming both sources. -- A real deploy of one example to the platform, from its config's entry, succeeds. That deploy is QA's evidence. - -## Open questions - -None. - -## References - -- [Project spec](../../spec.md), [design notes](../../design-notes.md) decision 6. -- ADR-0003, ADR-0044, ADR-0049, ADR-0050. diff --git a/.drive/projects/node-addressing/spec.md b/.drive/projects/node-addressing/spec.md deleted file mode 100644 index cd3a5f84f..000000000 --- a/.drive/projects/node-addressing/spec.md +++ /dev/null @@ -1,84 +0,0 @@ -# Purpose - -Let people and agents name any deployed part of a Composer app with one string, and administer it through the platform commands they already have. Today a node's identity leaves out the app name, removing an app or one of its environments takes a hand-written script, and nothing lists what is deployed. - -# At a glance - -``` -$ prisma deploy list --stage pr-42 -ADDRESS KIND PLATFORM ID URL -shop project proj_123 -shop.api service svc_456 https://… -shop.catalog postgres db_789 - -$ prisma service logs @pr-42:shop.api # any platform command takes an address -$ prisma branch delete pr-42 --confirm pr-42 # removes the stage and everything on it -$ prisma project delete proj_123 --confirm proj_123 # removes the app and every stage -``` - -```ts -// prisma.config.ts -composer: composer({ entry: './module.ts', extensions: [...], state: prismaState() }) -``` - -``` -$ prisma deploy --stage pr-42 # entry from the config; --entry overrides it -``` - -Under the hood, a node's address, its platform `logicalId` and its Alchemy resource IDs all become the same fully qualified form (`shop.catalog`, `shop.catalog.connection`). Existing apps move over on their next deploy without losing data. - -The decisions and their reasons are in [design-notes.md](design-notes.md). - -# Non-goals - -- A Composer teardown command, for the whole app or a subtree. Teardown uses platform commands. The subtree design is kept in `.drive/deferred.md`. -- Protecting a stage that has migrated from a later deploy by an older Composer. -- Deploying part of an app by pointing the entry at a submodule. The entry still names the whole application. -- Changing the Project's `logicalId`. It is already the app name. -- Changing how the programmatic `destroy` operation works. Docs stop recommending it for teardown. - -# Place in the larger world - -The work spans three repositories: - -- **prisma/composer**: addresses, resource IDs, environment variable keys, the migration, the entry in the config, `deploy list`, the decision records and docs. -- **prisma/prisma-cli**: resolving `@stage:address` in platform commands, and `branch delete`. Composer's team does this work. -- **prisma/pdp-control-plane**: rejecting `:` in Branch names (PRO-225) and a cascading Branch delete in the Management API. - -The platform treats `logicalId` as an opaque string and learns containment from explicit parent links, so it needs no change for the new format. Upstream Alchemy (`2.0.0-beta.83`) provides the rename declaration and updates a changed `logicalId` in place. - -# Cross-cutting requirements - -- **One form of node identity.** Every surface that names a node uses the fully qualified address: `logicalId`s, the topology, resource IDs, CLI input and output, errors, `serviceOrigin`, `dev url`. -- **No data loss when upgrading.** Critical. The first deploy of an existing app with the new Composer keeps its databases' rows, buckets' objects, generated secrets and RPC service keys, and leaves every piece of platform state the Composer graph does not represent untouched: custom domains, environment variables set by hand, Connections made by hand. Local `prisma dev` keeps its databases. -- **Every new or changed command** follows the CLI's help standard, gives JSON with `--json`, exits non-zero on failure, and has an end-to-end test against the real Management API, as the `prisma` CLI repository requires. -- **Recorded decisions are updated before the code that depends on them merges.** Agents treat the design docs as binding. - -# Transitional-shape constraints - -- The address format, resource IDs, environment variable keys and the rename migration ship in one Composer release, so existing apps migrate once. -- Address resolution in platform commands may ship before the migration. It matches the `logicalId` exactly, so it works for any stage deployed by a Composer version that writes the format you type. -- `branch delete` in the CLI waits for the cascading delete in the API. - -# Project DoD - -- A test app deployed with the current Composer release, then redeployed with the new one, keeps its database rows, bucket objects, generated secrets and RPC keys. Its rows' `logicalId`s and its deploy state's resource IDs are in the new form. -- A new stage deployed from scratch with the new Composer works. -- The asks app's production deploy is upgraded the same way with its data intact, as the final production test. -- `prisma deploy list --stage ` lists every node, and `prisma service logs @:
` reaches the right service. -- `prisma branch delete --confirm ` removes a stage while its services are running. -- An agent given only the docs can deploy an app, remove it with `prisma project delete`, and confirm with `prisma project list` that it is gone. -- The design records, principles, glossary and docs describe the new design, and none describe the old address format as current. - -# Open questions - -None blocking. The three upstream Alchemy behaviours in design note 5 are verified by the first slice before anything else is built. - -# References - -- [design-notes.md](design-notes.md) -- `docs/design/90-decisions/ADR-0051-a-nodes-address-is-its-logical-id-on-the-platform.md`: the identity decision this replaces. -- `docs/design/90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md`, `ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md`, `ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md`: amended for the entry in the config. -- pdp-control-plane `projects/branch-topology/spec.md`: the platform's topology and `logicalId` design. -- prisma-cli `docs/product/command-principles.md`: the rules for subjects, `list` and `delete`. -- PRO-225: reject `:` in Branch names. From c4df71a5a3c281083a39947306a196ae380897b9 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 28 Oct 2026 10:31:54 +0100 Subject: [PATCH 4/4] docs: say deleting the Project by address removes its deploy state Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/guides/deploying.md | 5 +++-- skills/prisma-composer-core-concepts/SKILL.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index 1d7785563..65f8a5be7 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -340,8 +340,9 @@ prisma postgres delete @storefront-auth.auth.database --confirm @storefront-auth ``` The command finds the row whose `logicalId` is the address, then acts exactly -as it does when given the ID. It never changes the deploy state, so the next -deploy recreates anything deleted this way. This needs a `prisma` CLI release +as it does when given the ID. Deleting a service, database or bucket this way +leaves the deploy state alone, so the next deploy recreates it. Deleting the +Project removes the deploy state with it, as described below. This needs a `prisma` CLI release newer than 8.0.0-rc.28. ## Removing an app or an environment diff --git a/skills/prisma-composer-core-concepts/SKILL.md b/skills/prisma-composer-core-concepts/SKILL.md index 0141a1b3b..4291278cb 100644 --- a/skills/prisma-composer-core-concepts/SKILL.md +++ b/skills/prisma-composer-core-concepts/SKILL.md @@ -415,8 +415,9 @@ a `prisma` CLI release that includes it; 8.0.0-rc.26 does not. **Using an address with platform commands.** A `prisma project|service|postgres|bucket` command takes `@:
` in place of the platform ID (`prisma service logs @pr-42:shop.api`); no environment means production. It -acts on that one row and never changes the deploy state, so the next deploy -recreates anything deleted this way. It needs a `prisma` CLI release newer than +acts on that one row. Deleting a service, database or bucket this way leaves +the deploy state alone, so the next deploy recreates it; deleting the Project +removes its deploy state too. It needs a `prisma` CLI release newer than 8.0.0-rc.28. **Removing an app** with every environment is a platform command: find the Project