diff --git a/.drive/projects/alchemy-provider-adoption/design-notes.md b/.drive/projects/alchemy-provider-adoption/design-notes.md new file mode 100644 index 000000000..8fc3287f6 --- /dev/null +++ b/.drive/projects/alchemy-provider-adoption/design-notes.md @@ -0,0 +1,92 @@ +# Design notes — alchemy-provider-adoption + +## Principles + +- Own zero Management-API wrapper code that upstream also owns. +- Composer's local-dev iteration speed must not depend on upstream review latency (operator decision, 2026-08-03). +- Upstream's opinionated guards are adopted, not fought — each one we checked (named-DB+branch refusal, system-managed env refusal, pooled-first URL) was correct or workaroundable on our side. + +## The model + +Upstream's provider for the postgres and compute families (buckets stay Composer-provided until the upstream release ships them); two provider *layers* on Composer's side: + +- deploy: upstream's live providers (needs the `liveProviderLayer` export or a local rebuild of its wiring — client layer + individual `*Provider()`s). +- dev: Composer's emulator providers bound to upstream's resource classes, substituted at `LowerOptions.providers` (`deploy.ts:203`) exactly as ADR-0041 does today. + +State: hosted Postgres store unchanged; rows migrate off the colliding type-ids. Auth: `Layer.succeed(PrismaEnvironment, {token, baseUrl})`, skipping alchemy's profile store. + +## Alternatives considered + +- **Contribute emulators upstream** (original proposal, in wip notes): rejected for now — couples our dev loop to Sam's dual-mode design and review cadence. +- **Adopt `ProviderLayer.dual`**: solves cross-mode state stamping we don't need (dev and deploy use disjoint state stores). Revisit if that ever changes. +- **Vendor `src/Prisma/` into Composer**: works on beta.59 (provider uses no newer core APIs) but inherits `@prisma/dev` dep + permanent drift. Only a fallback if the beta bump stalls badly. +- **Keep our six resources**: rejected — the spike showed upstream is strictly more hardened on deploy lifecycle and we'd keep paying API drift. + +## Decision: the compute family adopts App + Deployment + EnvironmentVariable, not Compute + +Decided in slice 2, with the descriptor rewiring in front of us. Composer binds upstream's three low-level resources; `Prisma.Compute` is not used at all. + +**What decided it — a dependency cycle Compute cannot express.** Every Compute service gets a `COMPOSER_
_ORIGIN` environment row whose value is that same service's own platform-assigned endpoint domain (ADR-0039; the value function is `selfOriginValue` in `control/extension.ts`). `Prisma.Compute` is one resource that owns the app, its environment rows, and its deployment together, so that row would be an input of the very resource that produces the domain — a self-edge. Alchemy's planner fails such a cycle unless the resource implements `precreate` to signal an attribute early, and no Prisma provider implements `precreate`. Splitting the app out is what makes the wiring legal: `Prisma.App` is created in `provision` and hands out `appEndpointDomain` before any environment row is written, and `Prisma.Deployment` is created afterwards in `deploy`. The same split is what lets one service's row carry another service's origin without ordering the two deployments against each other. + +**Three more reasons, none of them decisive alone.** + +- *Environment ownership.* Compute manages the rows itself, keyed by an `environmentVariableIds` map it stores in its own attributes, and refuses any row in scope that is not in that map. Migrating Composer's existing per-key `EnvironmentVariable` state rows into one Compute resource's map has no honest mapping; keeping them as resources does. +- *ADR-0005.* Compute carries build, framework detection, entrypoint inference, and effect-native bundling. `artifactPath` bypasses all of it, but the bypass is a prop value, not a structural guarantee. `Prisma.Deployment` has no build path at all to fall through to. +- *The local emulators.* Compute is a `Platform` (runtime context, bindings, dev process spawning). The three low-level classes are plain resources, which the emulator providers bind to exactly as they bound Composer's own three. + +**What we give up by not taking Compute:** preview/stable health checks, automatic rollback, and — the one that matters — environment values folded into the fingerprint that decides whether a new deployment is needed. See below. + +## The environment→deployment edge after the swap (PRO-211) + +Upstream's `Prisma.Deployment` has no `environment` prop, so the edge rides `app`: the descriptor builds that prop as an expression over the app id AND every environment row's id, resolving to the app id itself (`compute/deployment-edge.ts`). Alchemy derives its dependency graph from the resource references a prop's value is built from, so every variable write is scheduled before the deployment is created. That is the ordering PRO-211 needs, and the ordering is what `docs/design/05-prisma-cloud/alchemy-lowering.md` records as the edge's job. + +**`app` is the only prop that can carry it**, and this is not a style preference. Upstream's diff reads `{portMapping, skipCodeUpload, artifactPath, artifactContentType}` as one block and returns "no opinion" the moment any of them is unresolved (`Deployment.ts:361-367`). A brand-new variable has no persisted state, so the planner resolves its reference to a bare resource expression (`Plan.ts:369-371`) — meaning a deploy that adds a variable would leave that whole block unresolved, the artifact comparison would never run, the engine would fall back to a plain update, and reconcile would keep the running deployment *while recording the new artifact's fingerprint as deployed*. The code change would be dropped, and every later deploy would agree it had already shipped. `app` sits outside that block and its own check treats an unresolved app as unchanged (`Deployment.ts:376-378`, `concreteIdsChanged`). The first implementation of this slice used `artifactPath` and had exactly that defect; `compute/__tests__/deployment-edge.test.ts` fails if it ever comes back, because it drives the real Output machinery and upstream's real diff rather than eager-collapse stubs. + +The swap initially lost a side effect the old provider had: because Composer's deleted `Deployment` created a brand-new deployment on every reconcile, a changed environment *value* shipped a new deployment as well. With upstream handed a stable artifact path, an unchanged artifact planned an update, its reconcile re-used the existing deployment, and a value-only change reached the platform's variable row but not the running deployment until the next artifact change. + +**That regression is closed Composer-side** (`compute/deploy-fingerprint.ts`): the artifact hard-link directory is named from a hash of the service's environment material, so upstream's resolved-path comparison replaces the deployment exactly when the environment (or artifact) changed and reuses it otherwise. The material is non-secret by construction (ADR-0042 rows carry literals and pointers, never values); pointed platform variables contribute their `updatedAt` metadata, read at preflight and transported across the CLI→Alchemy process boundary on the framework preflight channel (the transport is load-tested end to end — the first implementation lost the timestamps at the process boundary and no in-process test could see it). Secret-bearing rows contribute wiring identity only; the module comment records the accepted narrowing (a value re-issued under a stable resource identity waits for the next fingerprint-moving change) and the flows it affects. `redeployOn` (upstream, in review) is the eventual carrier at the marked seam. + +The mechanisms ruled out and why: value hashes in state (offline-guessing target — the rule survives, refined to "non-secret material only"); `EnvironmentVariable.updatedAt` through a Deployment replacement prop (not in the variable's stables, and it moves on every deploy anyway); a per-run generation path (shipped briefly — restored the old always-redeploy behavior at the cost of all reuse; superseded by the fingerprint). + +## The poison DATABASE_URL rows are gone + +`application.provision` used to overwrite the platform's seeded `DATABASE_URL` and `DATABASE_URL_POOLED` with `"-"` so nothing could rely on the platform default. The platform marks both system-managed, and upstream's `EnvironmentVariable` refuses to manage a system-managed variable, so those writes are removed rather than reshaped (they would fail the deploy). What still holds the line is the ban at the authoring end: `param.ts` and `secret.ts` reject both names, so no Composer-written row can carry one, and `configKey` puts every Composer row in the `COMPOSER_` namespace. + +Existing poison state rows are marked `removalPolicy: "retain"` on read (see `state/legacy-resources.ts`), so the engine drops the state row, calls no API, and reports `retained` — the truthful verb. The deployed smoke run caught the first version of this: it reported `deleted`, which told an operator the platform variable was gone when it was still there. + +Residual, and it differs by stage: + +- A stage Composer never deployed before the swap: `DATABASE_URL` holds the platform's own template value. An app reading it directly gets a working default rather than something that fails loudly — that is the protection we lost. +- A stage Composer HAD deployed: the `"-"` placeholder it wrote is still on the platform, user-managed (`isManagedBySystem: false`), and stays until an operator deletes it. `docs/guides/deploying.md` gives the call. So a migrated stage keeps the old fail-loudly behaviour by accident, indefinitely, unless someone cleans up. + +## What the swap costs us, precisely + +One behaviour got worse and is not mitigated on our side; a second was worse for a while and is now restored (see the PRO-211 section above). + +**App delete retry budget: 5 minutes → about 4 seconds.** Composer's deleted `ComputeService` provider retried the platform's "did not reach a delete-safe state" 409 on an exponential schedule capped at 5 minutes. Upstream's `destroyApp` (`ComputeLifecycle.ts:276-310`) retries any conflict up to 5 times, sleeping 250ms · 2^attempt between consecutive attempts (four waits: 250ms + 500ms + 1s + 2s = 3.75 seconds of waiting in total; the final failed attempt returns without sleeping) — and it does NOT drain the app's deployments first; it deletes the App and relies on the platform's cascade. Alchemy does delete a *tracked* `Prisma.Deployment` before the App that owns it, because the resource graph orders them, but any untracked deployment still winding down can still 409 the App delete past that budget. A destroy of a stage that was serving traffic seconds earlier is the case to watch. + +**Environment-value change redeploys again — by replacing every deployment on every deploy.** The gap and its Composer-side fix, its cost, and the `redeployOn` hand-off are covered above. + +## Upstream asks (slice 3) + +- **A `Prisma.Deployment` prop for "recreate when these inputs change" (`redeployOn`; companion upstream commit in flight).** Until it ships, Composer detects change itself via the deploy fingerprint (`deploy-fingerprint.ts`), which cannot see a value re-issued under a stable resource identity. `Compute` already folds `env` into its fingerprint and stores it `Redacted`; the low-level resource needs the same seam to close that last gap. +- **Raise or make configurable the App delete-retry budget** (or drain the app's deployments before deleting it). +- **Export `PrismaUploadClient` / open the `alchemy/Prisma/Internal/*` subpath.** Its package export is explicitly `null`, so the scoped upload client cannot be composed privately by an outside stack; the only alternative is overriding the ambient `HttpClient`, which is a much blunter instrument. + +## Why no environment-derived fingerprint exists yet (the search, recorded) + +Everything an `EnvironmentVariable` exposes was checked for "moves when the value moves": + +- `updatedAt` moves on EVERY deploy, not on every change: upstream's diff returns an update whenever the desired value is resolved, to heal out-of-band drift (`EnvironmentVariable.ts:290-296`), and reconcile then PATCHes unconditionally (`:378-386`). Folding it into a deployment prop would restore Composer's OLD behaviour of shipping a new deployment on every single deploy — not value-change detection. +- `valueKid` identifies the encryption key, not the value; it carries no change semantics. +- The plaintext is write-only and never read back, so nothing observable distinguishes "same value re-applied" from "new value". + +The durable statement: **the only attribute that moves at all fires on every deploy** — and it is not in the variable's stables, so it cannot even ride a plan-time diff. Any real fix must come from the deployment side, which is where the deploy fingerprint (and eventually `redeployOn`) sits. + +## Open questions + +Tracked in spec.md (state-migration mechanics; first released beta). The Compute-vs-App+Deployment question is settled above. + +## References + +`wip/alchemy-prisma-provider-notes-for-aman.md`; spike session artifacts; upstream PRs #416, #963. diff --git a/.drive/projects/alchemy-provider-adoption/plan.md b/.drive/projects/alchemy-provider-adoption/plan.md new file mode 100644 index 000000000..29ce547f0 --- /dev/null +++ b/.drive/projects/alchemy-provider-adoption/plan.md @@ -0,0 +1,43 @@ +# Project Plan — alchemy-provider-adoption + +## Summary + +Three slices: two stacked (postgres family, then compute family) and one parallel (upstream contributions). The spike that grounded this plan is this project's originating session; call-site inventory is in `spec.md` References. + +**Spec:** `.drive/projects/alchemy-provider-adoption/spec.md` + +## Slices + +### Slice 1 — Postgres family adoption (TML-3154) + +Bump alchemy to the first released beta containing the Prisma provider; wire upstream live providers + `PrismaEnvironment` auth; rename our collection tag; swap `Project`/`Database`/`Connection` to upstream classes; rewire postgres/prisma-next descriptors; create-then-PATCH branch attach; `directConnectionString`; state-row migration (mechanics decided here: aliases vs SQL); rebind postgres emulator provider. + +- **Builds on:** nothing (first slice). +- **Hands to:** slice 2 — alchemy bumped, upstream live-provider wiring + auth layer in place, collection tag renamed, state-migration mechanism proven on the postgres rows. + +### Slice 2 — Compute family adoption (TML-3155) + +Swap `ComputeService`/`Deployment`/`EnvironmentVariable`; decide Compute vs App+Deployment; `artifactPath`-only enforcement (ADR-0005); env parity + `DATABASE_URL` exclusion; state migration on compute rows; rebind compute emulator provider. + +- **Builds on:** slice 1's hand-off. +- **Hands to:** close-out — Composer fully on upstream for the six resources; old implementations deleted. + +### Slice 3 — Upstream contributions (TML-3156) — parallel + +Fork alchemy-run/alchemy (wmadden-electric), then ONE implementation PR (per the operator override below): `liveProviderLayer` export, bucket resources, and the generic `postgresState` backend, implemented directly — no asks filed. `PgWarm` offered in the same conversation. + +- **Builds on:** nothing (written against upstream shapes directly). +- **Hands to:** slice-1 dependency softening (the export); Composer bucket deletion at close-out if the bucket PR merges + releases in time (otherwise buckets stay per transitional constraint). + +## Sequencing + +- Stack: 1 → 2. +- Parallel: 3 alongside both. +- **Operator overrides (2026-08-03):** all Composer-side slices land on THIS branch (no per-slice branches; one Composer PR at the end). Slice 3 is ONE implementation PR to alchemy-run/alchemy — `liveProviderLayer` export, bucket resources, and the postgres state backend implemented directly, no asks filed. Upstream branch: `prisma-provider-composer-needs` in `~/Projects/prisma/alchemy` (push blocked until the wmadden-electric fork exists). + +## Close-out (required) + +- [ ] Verify all acceptance criteria in `.drive/projects/alchemy-provider-adoption/spec.md` +- [ ] Migrate long-lived docs into `docs/` (ADR for the adoption + revised local-dev seam; alchemy-lowering.md rewrite) +- [ ] Strip repo-wide references to `.drive/projects/alchemy-provider-adoption/**` +- [ ] Delete `.drive/projects/alchemy-provider-adoption/` diff --git a/.drive/projects/alchemy-provider-adoption/spec.md b/.drive/projects/alchemy-provider-adoption/spec.md new file mode 100644 index 000000000..cada9c6de --- /dev/null +++ b/.drive/projects/alchemy-provider-adoption/spec.md @@ -0,0 +1,70 @@ +# Purpose + +Stop maintaining Composer's own Alchemy resources for Prisma Cloud. The official `alchemy/Prisma` provider (alchemy-run/alchemy, PR #416) now covers the same Management API surface with a more hardened deployment lifecycle, and it is written by our own colleague. Every line of API-wrapper code we keep is drift risk against the Management API and duplicated effort against upstream. After this project, Composer consumes upstream for everything that is genuinely about Prisma Cloud, contributes the pieces upstream lacks that are generic, and keeps locally only what encodes Composer concepts. + +A second aim: keep Composer's **local dev emulation** iterating on our own timeline. The emulators stay in Composer, driving upstream's live providers and our emulator providers through the same provider-layer substitution seam we use today — explicitly *not* blocked on upstream's dev-mode design (Sam's `ProviderLayer.dual`, #963) settling. + +# At a glance + +Four workstreams: + +1. **Adopt** — replace Composer's six overlapping resources (`Project`, `Database`, `Connection`, `ComputeService`, `Deployment`, `EnvironmentVariable`, ~670 lines in `packages/1-prisma-cloud/0-lowering/lowering/src/`) with upstream's resource classes and live providers. Requires the alchemy bump beta.59 → beta.66+ (the provider ships inside the `alchemy` package; beta.59 has no `Prisma/` directory). +2. **Port** — rewire Composer to upstream's shapes: descriptor call sites to upstream prop/attribute names; state rows migrated off the five colliding type-ids; our provider collection tag renamed (upstream also uses `'Prisma'`, and Effect context merge silently drops one of two same-key collections); auth via `Layer.succeed(PrismaEnvironment, …)` instead of `fromProfile()` (which prompts on TTY / hard-fails non-interactive); `directConnectionString` bound explicitly (upstream's `databaseUrl` resolves pooled-first); platform-seeded `DATABASE_URL` kept out of the resource graph (verified `isManagedBySystem: true`); branch attachment via create-then-PATCH (verified in PDP: create+attach are separate transactions, no idempotency key). +3. **Contribute upstream** — object storage resources (~161 lines; upstream deferred exactly the routes we call) and the generic core of the Postgres state store (~450 lines; only alchemy state backend with distributed locking, which upstream's own `Compute` docstring asks users to find). `PgWarm` offered to upstream; drop ours if they solve cold-start in `Database`/`Connection`. +4. **Keep local** — the dev emulators (~3,200 lines + s3-protocol) and the five Composer-concept resources (`ServiceKey`, `GeneratedParam`, `S3Credentials`, `PnMigration`, state-store policy layer). Emulators plug in behind `LowerOptions.providers` exactly as today, now paired with upstream's live providers. + +Aman has agreed to the direction (call, 2026-08-03). Composition landed without waiting: the local wiring rebuilds the live provider layer from upstream's exported resource classes. The `liveProviderLayer()` export ask (or an equivalent override on `providers()`) remains as a temporary-dependency cleanup — once upstream exports it, the local rebuild is deleted. + +# Non-goals + +- **Contributing the emulators upstream.** Explicitly reversed from the original proposal: they stay in Composer so we iterate without upstream review latency. Revisit only after upstream's dev-mode design (dual) settles. +- **Adopting `ProviderLayer.dual` / upstream dev mode.** Composer keeps its layer-swap seam and split state universes (localState for dev, hosted store for deploy). Dual solves a problem we don't have yet. +- **Adopting upstream's `Prisma.Compute` build/bundle conveniences.** We hand upstream pre-built artifacts via `artifactPath` only (ADR-0005: the framework never bundles/transforms user code). Auto-build, framework detection, and Effect-native bundling are never exercised by Composer. +- **Adopting alchemy's profile/credential store.** Composer keeps env-var credentials (`PRISMA_SERVICE_TOKEN` via `Config.redacted`). +- **Deterministic database names with branch attachment in one create.** Upstream's guard is correct (verified against PDP); we adopt create-then-PATCH rather than asking for the guard to be relaxed. + +# Place in the larger world + +- Upstream: `alchemy-run/alchemy`, provider at `packages/alchemy/src/Prisma` (14.5k lines, merged 2026-07-29). Owner-of-record for merges is Sam Goodwin; Aman authored the Prisma provider. Contributions land there as PRs. +- Composer side: the lowering package (`packages/1-prisma-cloud/0-lowering/lowering`) shrinks to buckets (until upstreamed), state store, container resolution; the extension (`packages/1-prisma-cloud/1-extensions/target`) keeps descriptors + the five Composer-concept resources; `local-target` + `dev-emulators` unchanged in ownership, rewired to upstream resource shapes. +- The forcing-function-apps project consumes this: its object-storage and dev workstreams sit directly on the seams this project moves. + +# Cross-cutting requirements + +- **No regression for deployed stages.** Existing state rows reference the old type-ids and attribute shapes (`{id, name}` vs upstream's `databaseId`). Every stage must deploy cleanly across the migration without recreating live resources; destroy of pre-migration rows must still resolve a provider. +- **ADR-0005 holds everywhere.** Only `artifactPath` (or `Prisma.Deployment`'s equivalent) is ever exercised; no code path may fall through to upstream build/bundle/entrypoint inference. +- **Env parity rules survive the port.** ADR-0019/0029/0032 serialization, the `COMPOSER_*` namespace, and the poison-`DATABASE_URL` exclusion must behave identically on upstream `EnvironmentVariable`. +- **Local dev keeps working at every intermediate commit** — the emulator providers must bind to whichever resource classes are current. +- **Pinned upstream version.** Alchemy stays pinned exact (as today); each bump is a deliberate change with the beta-to-beta breaking-change review this project's spike established (beta.60–65 were all Cloudflare/AWS-scoped). + +# Transitional-shape constraints + +- Adoption is per-resource-family, not big-bang: postgres family (Project/Database/Connection) and compute family (App/Deployment/EnvironmentVariable) may land in separate slices, each leaving main deployable. +- Until the upstream `liveProviderLayer` export lands in a release, Composer may carry a small local reimplementation of upstream's provider wiring (client layer + individual `*Provider()` calls) — accepted drift risk, removed the moment the export ships. +- Bucket resources stay in Composer until the upstream contribution merges and releases; the s3/s3-store descriptors must tolerate either home. + +# Project DoD + +- [x] Composer's six overlapping resource implementations are deleted; lowering/descriptors consume `alchemy/Prisma` classes. +- [x] `alchemy` pinned at a released version ≥ the first beta containing the Prisma provider; CI green. +- [x] A pre-existing deployed stage (created before the migration) deploys and destroys cleanly on the new stack. +- [x] Local dev (`prisma-composer dev`) runs the full example topology on the emulators against upstream resource shapes. +- [x] Deployed smoke suite passes (storefront-auth or equivalent example) on Prisma Cloud. +- [x] Object-storage resources PR and state-store PR opened upstream (merge is not in our gift; opened + review-responsive is the bar). +- [x] The upstream ask (export `liveProviderLayer` or equivalent) is filed and either landed or worked around per the transitional constraint. +- [x] ADR recorded documenting the adoption and the revised local-dev seam (ADR-0048). + +# Open questions + +- ~~Compute vs App+Deployment~~ — resolved: the low-level `App`+`Deployment` pair, with Composer's own env dependency edge (`deployment-edge.ts`). The `COMPOSER_*_ORIGIN` self-edge needs the App before env rows, and composite `Compute` owns a build path ADR-0005 rules out (ADR-0048, design-notes.md). +- ~~State migration mechanics~~ — resolved: rows are rewritten on read in the hosted store (`state/legacy-resources.ts`), with type-id aliases so old rows resolve; no destroy-and-recreate (ADR-0048, design-notes.md). +- ~~Which released beta first contains the provider~~ — resolved: `alchemy@2.0.0-beta.67` is the adopted pin. + +# References + +- Session evaluation + notes for Aman: `wip/alchemy-prisma-provider-notes-for-aman.md` +- Memory: `alchemy-upstream-prisma-provider` (blockers now resolved by decisions above) +- Upstream provider: https://github.com/alchemy-run/alchemy/pull/416 +- Engine dual-mode: https://github.com/alchemy-run/alchemy/pull/963 +- Linear project: https://linear.app/prisma-company/project/alchemy-prisma-provider-adoption-79d1f6cc7bff +- ADR-0005 (no bundling), ADR-0019/0023/0024 (containers), ADR-0029/0032 (env serialization), ADR-0034 (hosted state), ADR-0041 (local dev pipeline) diff --git a/docs/design/03-domain-model/glossary.md b/docs/design/03-domain-model/glossary.md index b2d82f9fd..f792b3925 100644 --- a/docs/design/03-domain-model/glossary.md +++ b/docs/design/03-domain-model/glossary.md @@ -357,7 +357,7 @@ substituted at any Input) and a real deployment. ## Provisioning plane — the compile target (Alchemy / Effect) The exact substrate the authoring nouns lower **down to**, grounded in what our -providers already use (`packages/alchemy`, `alchemy@2.0.0-beta.59`, +providers already use (`packages/alchemy`, `alchemy@2.0.0-beta.67`, `effect@4-beta`). Building the next layer of abstraction means defining each authoring noun as *the compile-target terms it emits*. Two families: Alchemy's IaC definition language, and the Effect primitives Alchemy is itself built on. @@ -375,17 +375,16 @@ is in `layering.md`; this is the term-by-term catalogue. `→` **Topology / implicit root Module**. - **Resource\ {
package(ctx: LowerContext, input: PackageInput):
Effect.Effect