From dba64541aa0c90866d8d88a85ddfdf996fe9187e Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:35:12 +0200 Subject: [PATCH] =?UTF-8?q?chore(drive):=20close=20out=20hex-composition?= =?UTF-8?q?=20(system-composition)=20=E2=80=94=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DoD met and merged: the System boundary (ADR-0016), the control plane loading through prisma-app.config.ts (ADR-0017), and a reusable System + testing seam (mockService/bootstrapService), proven by unit + integration tests and a live deploy (PR #39). The durable record lives in the ADRs, docs/design/10-domains (system-composition, deploy-cli, testing), skills/testing-prisma-apps, and the code; the transient scratch (plan, spec, slice specs) is removed. Two off-path follow-ups recorded in the completion note (a ControlClient dedup; folding @prisma/alchemy into @prisma/app-cloud). forcing-function-apps consumes the durable ADR outputs; its cron reverse-edge question is answered by ADR-0020. Signed-off-by: willbot Signed-off-by: Will Madden --- .../projects/hex-composition/design-notes.md | 160 +++++------------- .drive/projects/hex-composition/plan.md | 96 ----------- .../slices/extension-config/spec.md | 155 ----------------- .../slices/reusable-system-testing/spec.md | 109 ------------ .drive/projects/hex-composition/spec.md | 142 ---------------- 5 files changed, 47 insertions(+), 615 deletions(-) delete mode 100644 .drive/projects/hex-composition/plan.md delete mode 100644 .drive/projects/hex-composition/slices/extension-config/spec.md delete mode 100644 .drive/projects/hex-composition/slices/reusable-system-testing/spec.md delete mode 100644 .drive/projects/hex-composition/spec.md diff --git a/.drive/projects/hex-composition/design-notes.md b/.drive/projects/hex-composition/design-notes.md index 21e8c752e..da801c9b3 100644 --- a/.drive/projects/hex-composition/design-notes.md +++ b/.drive/projects/hex-composition/design-notes.md @@ -1,113 +1,47 @@ -# System Composition — Design Notes - -The implementation-level record for this project. The design itself lives in -the ADRs (0016 system boundary, 0017 control-plane config) and -`docs/design/10-domains/system-composition.md`; this file tracks where the work -stands, the calls made building it, and what is still open. Kept current — when -a slice closes or a decision lands, it is written here in the same change. - -## Status (2026-07-10) - -- **H1 — system boundary, forwarding, nesting: done.** On the branch, twice - Opus-reviewed. `system(name, { deps?, expose? }, body)`; recursive Load - flatten with hierarchical addresses; four boundary-validation rules. -- **Deploy-module loading: mid-redesign.** The node-owned-loads approach (nodes - carry `targetModule`/`assembler` specifiers and load them) failed empirically - and is being replaced by the extension-config design (below). This is the - active slice. -- **H3 — reusable auth system + same-contract fake, proven live in CI: - pending.** The last slice; builds on the extension-config work. -- Branch `claude/system-composition`. **PR #28 is closed** pending the - extension-config redesign; it reopens when the deploy resolves for real - (the no-env probe fails at missing `PRISMA_WORKSPACE_ID`, not at "Cannot - resolve") and CI's live e2e is green. - -## The model this builds on (durable, post-merge) - -Everything the project once waited on has merged to main; the results that -matter: - -- **Unified dependency model** (#21): one `dependency()` slot factory (no - separate connection/resource ends); `resource()` takes `provides: Contract`; - `provision(id, resource)` returns that contract flattened onto the ref; - `Edge.kind` is `'input' | 'dependency'`. A resource-backed input forwards - across a system boundary like any other — no special case. -- **Always-system root** (#22): the deploy root must be a system; a bare - service is not independently deployable (Load says "wrap it in a system"). - Bundles are keyed by the full dot-joined address. -- **Naming** (#24): the composition unit is a **System** (`system()`); an - **extension** is a control-plane package (was "pack"); the framework is - Prisma App (`@prisma/app*`), the CLI `prisma-app`; the node brand is - `Symbol.for('prisma:node')`. -- **Bindings** (#26): a dependency resolves to a binding — a typed config for - a resource, a client for a protocol-owned kind — constructed app-side; the - declaration carries no driver choice. - -## Current slice: control plane loads through `prisma-app.config.ts` - -Full spec at `slices/extension-config/spec.md` (with the pinned, no-discretion -implementation decisions); recorded as ADR-0017. - -**Why.** Node-owned loads did `import(this.targetModule)` from core's own -module; core depends on no extension, so a real workspace deploy failed to -resolve `@prisma/app-cloud/target` (the same specifier resolves fine from the -app root). Two earlier shapes on this seam also failed: framework-constructed -paths via `createRequire` (hand-rolled resolution, dead on Yarn PnP / Deno), -and a loader thunk with a literal import (breaks the bundler firewall). - -**The design.** A `prisma-app.config.ts` at the app root — loaded by the CLI -with c12, discovered by walking up from the deploy entry, never imported by app -code — statically imports each extension's control descriptor and declares the -deploy's one state store. Deploy tooling looks up control-plane behavior by the -data every node already carries: `extensions[node.extension].nodes[node.type]`. -The firewall becomes a file boundary (app code never reaches the config), not a -bundler trick; resolution is ambient (ordinary static imports from the app -root). The **target concept is deleted** — no `fromEnv()` contract, no -one-target rule; nodes on different platforms coexist, providers compose, and -the one `state:` store is platform-agnostic. Nodes revert to pure frozen data. - -## Implementation calls (settled; flag on divergence) - -- **`InputRef`** — a body's `ctx.inputs` entries are branded wiring values - carrying (system identity, input key). At Load, when the enclosing scope - wires the input, the ref resolves to the real producer; forwarding edges are - recorded so cycle detection sees through boundaries. The brand satisfies the - same `Wiring` assignability a producer's ref-port does (a contract-typed - input carries that contract's `Req`). Per-key object identity (a symbol-keyed - shallow copy) keeps forwarding attribution precise when one producer wires - two inputs. -- **Body executes during Load only**, once per provision, pure by convention; - nothing composition adds runs at runtime. -- **Addresses**: parent address + `.` + provision id. The config-key - derivation joins with `_` and forbids `_`/`.` inside a provision id, so keys - cannot collide at depth; tests cover depth ≥ 2. -- **Nodes are plain frozen data** — identity is the `Symbol.for('prisma:node')` - brand via `isNode()`, never `instanceof`, so a node built by another - installed copy of core still validates. (The extension-config slice removes - the load-method class hierarchy that briefly lived here.) -- **Bundle keys**: the full dot-joined address (provision ids are unique only - within a scope). -- **Workspace-package system** (H3): built via its own `build` script at repo - build time (turbo), standing in for publish-time building; `@prisma/app*` - declared as peer deps as they would be for npm. - -## Open decisions - -- **Auth-system v1 `db` posture (H3).** Boundary input vs internally owned. - The old blocker dissolved — the unified model forwards resource-backed inputs - across boundaries — so either works with no groundwork. Recommendation: - boundary input, since exercising composition is H3's whole point. Decide at - H3 dispatch. - -## Risks (live) - -- **Type-level cost** of `SystemContext`/`Wiring` inference at depth — the - contract types are heavy; keep a three-level nesting case in the type-level - tests. -- **Address depth vs config-key length** — env-var names are finite; deep - nesting yields long keys. Tests document the practical bound rather than - discovering it in production. -- **Harness stalls on large reads** — the extension-config implementer stalled - three times on big reference reads; mitigation is bounded reads + inlined - references + phase-by-phase commits. If it recurs despite that, the cause is - harness flakiness, not the work. +# System Composition — closed (complete) + +The system-composition project (workspace name `hex-composition`) shipped and is +closed. This file is the completion record. The design and implementation are +durable in the ADRs, the domain docs, and the code; this workspace's scratch +(plan, spec, slice specs) has been removed. + +## What shipped + +- **A System has a service's typed boundary** — `system(name, { deps?, expose? }, + body)`, nesting, forwarding as data flow across the boundary. + **ADR-0016**; `docs/design/10-domains/system-composition.md`. +- **The control plane loads through `prisma-app.config.ts`** — the config + statically imports extension descriptors; registries are keyed by + `(extension, node type)`; nodes are pure data; one explicit state store per + deploy. **ADR-0017**; `docs/design/10-domains/deploy-cli.md`. (This replaced a + failed node-owned-loads approach that the live e2e caught.) +- **A reusable System, proven, plus the testing seam** — auth becomes a System + that owns its own database and exposes only its contract; `mockService` + (`@prisma/app/testing`, unit) and `bootstrapService` (`@prisma/app-cloud/testing`, + integration), with no test code in the production node. Proven by a unit test, + an integration test, and a live "Deploy, verify, destroy" e2e. + `docs/design/10-domains/testing.md`; `skills/testing-prisma-apps`; PR #39. + +Built on the merged model: unified dependency slots (#21), always-system root +(#22), the `@prisma/app*` / System naming (#24 = ADR-0014), and +dependencies-resolve-to-bindings (#26 = ADR-0015). + +## Deferred follow-ups (off the critical path, not started) + +- **A `ControlClient`.** Replace the three duplicated `(extension, type)` + lookups (in `deploy.ts`, `validate-coverage.ts`, `assemble-services.ts`) with + one in-memory client that *performs* control-plane operations — not a state + map. Open design; raised in the PR #39 review. +- **Fold `@prisma/alchemy` into `@prisma/app-cloud`.** One consumer, a + misleading generic name; the firewall that matters is a file boundary, so the + package boundary earns nothing. Needs a slice note plus a mechanical PR. + +## Downstream note + +`forcing-function-apps` consumes this project's output (ADR-0016, the unified +model, the H3 swap-the-backing pattern) — those references point at the durable +ADRs and remain valid. Its one open coordination item, the **cron reverse-edge** +(a resource that invokes its consumer on a schedule), is answered by **ADR-0020**: +scheduled work is a *driver*, not a new composition primitive — a scheduler +service depends on the endpoint it triggers. No further composition capability is +owed to that project. diff --git a/.drive/projects/hex-composition/plan.md b/.drive/projects/hex-composition/plan.md deleted file mode 100644 index 77283b5d0..000000000 --- a/.drive/projects/hex-composition/plan.md +++ /dev/null @@ -1,96 +0,0 @@ -# System Composition — Plan - -## Summary - -Three slices: the core boundary/forwarding/nesting reshape, the deploy-pipeline -follow-through (hierarchical bundle keys + adapter resolution anchor), and the -reusable Auth system proven live with its fake. Design contract: ADR-0016 + -`docs/design/10-domains/system-composition.md`. - -**Spec:** `.drive/projects/system-composition/spec.md` · -**Design notes:** `.drive/projects/system-composition/design-notes.md` -**Tracker:** GitHub PRs (repo convention). - -**Rebase baseline (H1 integrated pre-baseline; H2 dispatches post-rebase):** -this branch rebases onto PR #21 (resource decoupling) then PR #22 (always-system -root) once both land. Assume always-system root and bundles-keyed-by-address as -given; `prisma-app-hello` and the e2e hello/redeploy-noop job are gone (repoint -fixtures at `examples/storefront-auth`). Full facts in design-notes.md § -Coordination facts. - -## Sequence - -``` -[H1 core boundary/forwarding/nesting] → [H2 pipeline follow-through] → [H3 auth system + fake, live] -``` - -(H2's adapter-anchor commit is independent of H1 and may be cherry-picked -early if useful; the hierarchical-keys commit depends on H1's addresses.) - -## Legend - -`[ ]` not started · `[~]` in progress · `[x]` done (proof met) - ---- - -## Build slices - -### [ ] Slice H1 — core: boundary, forwarding, nesting - -**Outcome:** `system(name, { deps?, expose? }, body)` with `SystemContext` -(`inputs` + `provision`) and outputs-as-return, per system-composition.md § The -authoring surface. `provision()` gains the system overload returning -`ProvisionedRef`. Load flattens recursively with dot-joined hierarchical -addresses and enforces the four boundary-validation rules (dangling input, -missing/unsatisfied expose, root-with-deps, forwarding cycles), each with a -fix-naming error and a test. Existing system call sites (storefront-auth's -`system.ts`, core/cli/integration tests and fixtures) migrate to the new shape. -Compile-time expose/wiring checks proven with type-level tests (the R6 -`test-d` pattern). -**Proof:** unit + type-level tests for every rule; all gates green; the -existing e2e path still deploys (flat systems are the empty-boundary case). -**Builds on:** main (post-CLI). Coordinates with the resource-decoupling -session (shared files — whoever lands second rebases). -**Hands to:** H2 — hierarchical addresses exist; H3 — the authoring surface. - -### [ ] Slice H2 — pipeline follow-through - -**Outcome:** assembled-bundle correlation keys follow full hierarchical -addresses through `@prisma/app-assemble`, the generated stack file, and -`lower()`'s bundle lookup. Adapter resolution moves to the service's -`build.module` anchor (`${build.pack}/assemble` seeded at the authoring file, -per amended ADR-0004) — an installed system's adapter choice stays internal. -**Proof:** integration test: a fixture package whose service uses an adapter -the consuming app doesn't declare resolves and assembles; a nested-system -fixture deploys through the generated stack file (fake alchemy seam) with -correctly keyed bundles. -**Builds on:** H1 (addresses); the anchor change itself has no H1 dependency. -**Hands to:** H3 — installed systems assemble. - -### [ ] Slice H3 — the reusable Auth system, live, with its fake - -**Outcome:** a workspace package (e.g. `examples/auth-system`) exporting an Auth -system per ADR-0016's grounding example — built runnables shipped in-package, -`@prisma/app*` as peer dependencies, expose = the auth contract; `db` as a -boundary input if resource slots have landed, internally owned otherwise. -`examples/storefront-auth` provisions it (nested system, forwarding both ways) -and the storefront consumes only the contract port. A fake same-contract -service proves substitution in an alternate topology without touching the -storefront. CI e2e flips to the composed topology. Docs synced to what -shipped; project close-out. -**Proof:** CI e2e green — live deploy of the nested topology, round trip -renders, destroy clean; the fake proven by typecheck + an integration test -through the real CLI's Load path. -**Builds on:** H1 + H2. -**Hands to:** project close-out; the shared-resource/data-contract project -inherits a composition-ready model. - ---- - -## Close-out (required) - -- [ ] Verify all Project-DoD items in `spec.md`. -- [ ] Migrate long-lived docs into `docs/` (design docs already live there; - verify nothing lives only in this workspace). -- [ ] Strip repo references to `.drive/projects/system-composition/**`. -- [ ] Final retro; delete `.drive/projects/system-composition/`. diff --git a/.drive/projects/hex-composition/slices/extension-config/spec.md b/.drive/projects/hex-composition/slices/extension-config/spec.md deleted file mode 100644 index 5effff547..000000000 --- a/.drive/projects/hex-composition/slices/extension-config/spec.md +++ /dev/null @@ -1,155 +0,0 @@ -# Slice spec: control plane loads through `prisma-app.config.ts` extension registries - -Replaces the node-owned deploy-module loading on `claude/system-composition`. -Design contract: ADR-0017 (control-plane loads through the app config) + -ADR-0003 (as amended) + `docs/design/10-domains/deploy-cli.md`. Deviations -amend the docs first. - -## Why (empirical forcing function) - -Node-owned loads (`import(this.targetModule)` from core) resolve from CORE's -install location; core depends on no extension package, so a real workspace -deploy fails: `Cannot resolve the target module "@prisma/app-cloud/target"`. -The same specifier resolves fine from the app root — where the app's -dependencies actually live. The live e2e (deploy, verify, destroy) failed on -exactly this. The fix is structural, not another anchor. - -## Requirements (all three must hold) - -1. **Firewall by file boundary.** Control-plane code (provisioning, alchemy, - bundlers) is imported ONLY by `prisma-app.config.ts` and the extensions' - `/control` entries. App code (service modules, the system entry) imports - authoring factories only and never the config. -2. **Per-node control-plane lookup keyed by `(extension ID, node ID)`.** - A node already carries both: `extension` (renamed from `pack`, e.g. - `"@prisma/app-cloud"`) and `type` (e.g. `"compute"`). Lookup: - `config.extensions[node.extension].nodes[node.type]`. -3. **Ambient module resolution only.** The config uses ordinary static - imports, resolved from the app root by the package manager (pnpm, hoisted, - Yarn PnP, Deno). No specifier construction, no `createRequire`, no path - anchoring, no `import(variable)` opacity tricks. The only convention is - finding `prisma-app.config.ts` (walk up from the deploy entry; c12 — the - same mechanism as prisma-next's config-loader). - -## At a glance - -```ts -// examples/storefront-auth/prisma-app.config.ts — CLI-only, never imported by app code -import { defineConfig } from '@prisma/app/config'; -import { prismaCloud, prismaState } from '@prisma/app-cloud/control'; -import { nodeBuild } from '@prisma/app-node/control'; - -export default defineConfig({ - extensions: [prismaCloud(), nodeBuild()], - state: () => prismaState(), // ONE state store per deploy -}); -``` - -## Decisions encoded (operator rulings) - -- Terminology: **extension** — not "pack", not "target". -- **The target concept dies.** No `Target` interface as the one lowering - source, no `fromEnv()` contract, no target inference/selection, no - one-target-per-application rule. Mixed platforms in one app is intended; - alchemy orchestrates. Providers compose (union of the used extensions' - layers); state is the ONE explicit `state:` in config. -- Registries keyed by extension ID then node ID; a build descriptor re-keys - the same way (`extension` + `type`; `kind`/`assembler` die). -- Nodes revert to **plain frozen data** — no classes-for-methods, no - `loadTarget()/loadAssembler()/assemble()`, no specifier fields. Identity - stays the `Symbol.for('prisma:node')` brand via `isNode()`. -- Extension factories (e.g. `prismaCloud()`) read and validate their own env - at construction, failing with the exact variable name — preserving today's - fail-fast UX. -- Firewall tests: the variable-import assertions are obsolete; replace with a - structural guard (nothing reachable from an authoring entry imports a - `/control` entry) and KEEP app-cloud's real-build token check - (`invariants.test.ts`) as the end-proof. - -## Pipeline (CLI) - -parse args → load `prisma-app.config.ts` (c12; walk up from the resolved -entry; missing config is a CliError naming the filename and required export) -→ import entry → `Load` graph → validate every node's and build descriptor's -`(extension, type)` has a registry entry (error names the missing extension -and the config fix) → assemble via registries → generate stack file (the -generated `.prisma-app/alchemy.run.ts` imports the user's config by relative -path and drives `lower()` with its registries + state) → drive alchemy. - -## Proof - -- All repo gates green (typecheck, test, lint, build, casts delta ≤ 0). -- **The decisive probe:** in `examples/storefront-auth`, running the deploy - with NO env fails at the missing `PRISMA_WORKSPACE_ID` error — not at - "Cannot resolve". (The live e2e then proves the full deploy in CI.) -- Grep-clean: no `loadTarget|loadAssembler|targetModule|assembler:` remnants; - no `createRequire` under packages/. -- `test/integration` reworked to config-based fixtures; drop the pnpm - `injected` scaffolding if the ordinary layout now resolves (it should — - resolution moved to the app root). - -## Out of scope - -- Lowering logic itself (provision/serialize/package/deploy bodies) — re-keyed - and re-homed, not rewritten. -- H3 (reusable system package + fake, live in CI) — next slice, builds on this. -- Docs/ADRs — orchestrator-owned, amended alongside (ADR-0017 replaced, - ADR-0003 amended, deploy-cli.md + system-composition.md updated). - -## Pinned decisions (no implementer discretion) - -1. **Descriptor shape — exact:** - ```ts - interface ExtensionDescriptor { - readonly id: string; // the extension's package name - readonly nodes: Record; // ONE registry per extension, keyed by node ID - readonly application?: ApplicationControl; // once-per-lowering hook (today's app-level Project provision, verbatim semantics) - readonly providers?: () => Layer; // today's Target.providers, verbatim - } - type NodeControl = - | ({ kind: 'resource' } & Lowering) // today's resources[type] shape - | ({ kind: 'service' } & ServiceLowering) // today's services[type] shape - | ({ kind: 'build'; assemble(input: AssembleInput): Promise }); - ``` - The `kind` discriminant is REQUIRED and checked at every lookup site against - what the site needs (a resource node looked up against a `service` control is - an error naming (extension, type, expected kind)). Reuse today's - `Lowering`/`ServiceLowering`/`AssembleInput`/`Bundle` types — move, don't - redesign. -2. **App-level Project**: NOT relocated into node controls. It is the - descriptor's `application` hook — same shape, same once-per-lowering timing, - same threading into that extension's service controls as today's target - `application`. -3. **State**: `PrismaAppConfig.state` is REQUIRED, a thunk `() => `. `prismaState(opts?: { workspaceId?: string })` — omitted opts - read `PRISMA_WORKSPACE_ID`, error names the variable — is implemented in - `@prisma/alchemy` but RE-EXPORTED from `@prisma/app-cloud/control`; the - config imports it from the extension, never from `@prisma/alchemy` directly - (that package is the extension's private implementation). Do not redesign - the layer itself. -4. **Config type + loading**: `PrismaAppConfig { extensions: ExtensionDescriptor[]; state: ... }`; - `defineConfig` is a typed identity function. Loading mirrors prisma-next's - `config-loader/src/load.ts` EXACTLY: manual walk-up from the entry file's - dir for the literal filename `prisma-app.config.ts`, then c12 with that - explicit path; `rcFile`/`globalRc`/`packageJson` lookups disabled. CLI - validates the loaded shape field-by-field with CliErrors naming the field. - No arktype, no new validation deps. -5. **Providers composition**: merge ALL listed extensions' `providers()` layers - (config array order, `Layer.mergeAll`); extensions without `providers` - skipped. No "used-extensions-only" filtering. -6. **Signatures**: `lower(root, config, opts)` / `lowering(root, config, opts)` - (config replaces the old target parameter; internal two-level map lookup, - no key-string concatenation). CLI test seam: `RunDeps.config?: PrismaAppConfig` - (provided → skip c12). `assembleServices(graph, config, run?)`. - Generated stack file: keep current entry-import mechanics; replace the - fromEnv import with a RELATIVE import of the user's config - (`path.relative(dirname(generatedFile), configPath)`, posix separators). -7. **Package exports**: add `./control` to app-cloud/app-node/app-nextjs and - `./config` to `@prisma/app`; DELETE `./target` and `./assemble` exports (no - aliases, no deprecation shims). -8. **Renames**: node field `pack` → `extension` (and `requirePack` → - `requireExtension`, all error strings); build descriptor `kind` → `type`, - `assembler` deleted. Integration: config at - `test/integration/prisma-app.config.ts`; test file → - `cli.extension-config.test.ts`; fixture dir `node-owned-loads/` → - `extension-config/`. diff --git a/.drive/projects/hex-composition/slices/reusable-system-testing/spec.md b/.drive/projects/hex-composition/slices/reusable-system-testing/spec.md deleted file mode 100644 index 9a7885c36..000000000 --- a/.drive/projects/hex-composition/slices/reusable-system-testing/spec.md +++ /dev/null @@ -1,109 +0,0 @@ -# Slice spec: reusable auth System + the testing seam, proven live - -The last slice of the system-composition project (H3). It turns the storefront's -auth from an inline service into a **reusable System that owns its database**, -and ships the **testing utilities** that make any app built on the framework -testable at two altitudes — proven by a unit test, an integration test, and the -existing live deploy. - -Design contract: [`docs/design/10-domains/testing.md`](../../../../../docs/design/10-domains/testing.md) -(the testing model) + [`docs/design/10-domains/system-composition.md`](../../../../../docs/design/10-domains/system-composition.md) -(the System boundary). Deviations amend the docs first. - -## Why - -The composition machinery (H1 boundary + ADR-0017 control plane) is built and -merged, but nothing yet proves the value it exists for: that a System can be -**published, reused, and faked**. Today `examples/storefront-auth/systems/auth` -is a bare service whose Postgres is provisioned by the root and wired into its -`db` input. That is not a reusable unit — a consumer would have to know to -provision auth's storage. H3 makes auth a self-contained System and proves an -app composing it can be tested without a cloud. - -## Deliverables - -### 1. The reusable auth System (owns its db) - -`auth` becomes a **System**, not a service: its body provisions its own Postgres -and its own compute service, wires the db in, and exposes the RPC contract as -the System's output. Its boundary has **no `db` input** — it exposes only -`{ rpc: authContract }`. The package declares `@prisma/*` as **peer -dependencies** (as a published reusable System would) and builds via its own -turbo `build` (standing in for publish-time build). The root `system.ts` no -longer provisions the database; it provisions the auth System and storefront, -wiring auth's exposed `rpc` into storefront's `auth` dependency. - -### 2. `@prisma/app/testing` → `mockService` (unit seam) - -Core, target-agnostic. `mockService(service, overrides)` returns a service node -whose `load()` yields `overrides` merged with the service's param defaults, -**typed against the service's `deps`** (a double not assignable to the dep's -hydrated type is a compile error). New export `@prisma/app/testing` -(`packages/app/src/testing.ts`; add `./testing` to the manifest + its tsdown -entry). It performs no module mocking itself — that stays in the test. - -### 3. `bootstrapService` (integration seam) - -The in-process counterpart of the deploy bootstrap. `bootstrapService(service, -config, boot?)` lives in **`@prisma/app-cloud/testing`** (target-specific — -writing the environment is the serializer's job) and returns a handle -`{ url, fetch }`. It writes the chosen config with the target's own `stash` + -`configOf`, boots the real entry, and hands back a driveable server. **No test -code on the production node** — the `compute()` runtime ships only `run`/`load`. -**`server.ts` is not modified.** No `close()`: the entry owns its `Bun.serve` -handle, so teardown rides on bun-test's per-file process isolation (a single -boot per test file, cleaned up when the file's process ends). This is the -accepted trade for leaving the entry untouched. - -### 4. The fake auth (ships from the auth package) - -A `/fake` export on the auth package: an in-memory `verify` (`serve(fakeAuth, { -rpc: { verify: async ({ token }) => ({ ok: token.length > 0 }) } })`), no -Postgres, sharing the real `authContract` so its handler map is typed against -the same contract. Used by both proof tests. - -## Proof - -- **Unit test** — renders storefront's `page.tsx` with `load()` mocked via - `mockService` to a fake `auth`; asserts the rendered output. No server, no env, - no cloud. (vitest — the storefront's runner.) -- **Integration test** — runs the fake auth on a loopback port, boots storefront - via `bootstrapService` with `auth.url` pointed at it, drives the page over - HTTP, asserts the round trip. No cloud. -- **Live e2e** — the existing "Deploy, verify, destroy" job, unchanged in shape, - now deploys the composed **auth System + storefront** to real Prisma Cloud and - verifies the round trip. This is the reusable-System-deployed-for-real proof. -- All repo gates green (typecheck, test, lint, build, casts delta ≤ 0). - -## Out of scope - -- Running a whole composed graph locally (multi-service `dev` orchestration) — - a separate capability the testing doc lists as a non-goal. -- A runner-agnostic module-mock abstraction — `mockService` ships the typed - payload; the `vi.mock`/`mock.module` wiring stays in the tests. -- The post-merge cleanups (LoadedControl-style lookup dedup; folding - `@prisma/alchemy` into `@prisma/app-cloud`) — tracked separately. - -## Decisions (resolved) - -- **Teardown:** option (a) — no `close()`; bun-test's per-file process isolation - cleans up. `server.ts` stays untouched. (A cleaner "target owns the listen" - refactor is explicitly out of scope for H3.) -- **Both paths ship, not either/or.** The unit test (`mockService`) AND the - integration test (`bootstrapService`) are both deliverables. The integration - test boots the **full Next storefront** in-process against a loopback fake auth - — the real round trip. If Next-in-process boot proves genuinely intractable, - fall back to driving a minimal RPC consumer through `bootstrapService` and flag - it in the final report — do not drop the integration path. - -## Notes for implementation - -- `mockService`'s override type is the service's hydrated deps - (`Client` for rpc, the resource binding for resources) plus optional param - overrides — derive it from the node's `Deps`/params, do not hand-roll. -- `bootstrapService` (`@prisma/app-cloud/testing`) reuses `stash` - (serializer.ts) + `configOf` — it must not add a second serialize path, and - must add nothing to the production `compute()` node; writer/reader parity with - deploy is the whole point. -- Keep the auth System's `authContract` and the fake in one package so the - contract cannot drift. diff --git a/.drive/projects/hex-composition/spec.md b/.drive/projects/hex-composition/spec.md deleted file mode 100644 index 01b4a8c0d..000000000 --- a/.drive/projects/hex-composition/spec.md +++ /dev/null @@ -1,142 +0,0 @@ -# System Composition — Project Spec - -## Purpose - -Make systems reusable components. Concretely: after this project, an Auth system -exists as a workspace package, the storefront-auth example installs it and -consumes it through one typed contract port, and a same-contract fake drops -into the same slot without the storefront changing a line. - -## At a glance — the code this project makes possible - -The reusable component (new package `examples/auth-system`): - -```ts -// examples/auth-system/src/system.ts — the package's main export -import { system } from "@prisma/app"; -import { authContract } from "./contract"; -import authService from "./service"; // the existing auth service, moved here - -export default system("auth", { expose: { verify: authContract } }, ({ provision }) => { - const db = provision("db", postgres(/* … */)); // the system provisions the resource… - const api = provision("api", authService, { db }); // …and passes it to the service - return { verify: api.verify }; // child's exposed port becomes the system's output -}); -// (resource-wiring syntax illustrative — follows the resource-decoupling design; -// packaging a service together with its resource is exactly what a system is for) -``` - -The app consuming it (`examples/storefront-auth/system.ts` rewritten): - -```ts -import authSystem from "@prisma-examples/auth-system"; // installed, workspace:* -import storefrontService from "./systems/storefront/src/service"; - -export default system("storefront-auth", {}, ({ provision }) => { - const auth = provision("auth", authSystem); // a system, provisioned like a service - provision("storefront", storefrontService, { auth: auth.verify }); // wired by contract port - return {}; -}); -``` - -The fake (new `examples/storefront-auth/fake/` + an alternate topology file): - -```ts -// system.fake.ts — same slot, same contract, no database; storefront untouched -const auth = provision("auth", fakeAuthService); -provision("storefront", storefrontService, { auth: auth.verify }); -``` - -`prisma-app deploy system.ts` deploys the composed topology; `prisma-app deploy -system.fake.ts` deploys (or Load-checks) the faked one. Design contract: -[ADR-0016](../../../docs/design/90-decisions/ADR-0016-a-system-has-the-same-boundary-as-a-service.md) -+ [system-composition.md](../../../docs/design/10-domains/system-composition.md) -(exact signatures, Load rules, addresses). - -## What gets built, by file - -1. **Core** (`packages/app/src/node.ts`, `graph.ts`): - - `system(name, { deps?, expose? }, body)` replacing `system(name, body)`; - `SystemContext` (`inputs` + `provision`), `SystemOutputs`, `InputRef`; - `SystemNode` carrying the boundary types. - - `provision()` overload accepting `SystemNode` → `ProvisionedRef`. - - Load: recursive flatten; hierarchical dot-joined addresses; four - validation errors (exact texts in system-composition.md § Load), e.g.: - `System "auth" declares input "db" but never forwards it into a provision.` - - Type-level tests (R6 `test-d` pattern) incl. a 3-level nesting case. -2. **Pipeline** (`packages/app-assemble`, `packages/app-cli`, - `packages/app/src/deploy.ts`): - - Bundle correlation keys become full addresses (`auth.api`, not `api`) - through assembly → generated stack file → `lower()` lookup. - - `${build.pack}/assemble` resolves from `build.module` instead of the - deploy entry (ADR-0004 as amended) — an installed system's adapter never - becomes the app's dependency. -3. **The example proof**: - - New workspace package `examples/auth-system` (`@prisma-examples/auth-system`): - the existing auth service + contract move in; own `build` script - producing `dist/server.js`; `@prisma/app*` + `@prisma/app-cloud` as - peer dependencies (exactly as a published system would declare them). - - `examples/storefront-auth` rewired per the code above; its - `systems/auth/` directory dissolves into the package. - - `fake/` service exposing `authContract` from in-memory state + the - `system.fake.ts` topology. - - `.github/workflows/e2e-deploy.yml` keeps deploying `system.ts` — now a - nested topology — unchanged in shape. - -## Non-goals - -- Target-neutral systems; shared resources (tree→DAG); system-level params — - named as extension points in the domain doc, not built here. -- Publishing to the real npm registry (the workspace package exercises the - same resolution and peer-dep mechanics). -- Changing the resource-provisioning model (see Dependencies). - -## Dependencies & coordination - -System-composition rebases onto two already-in-flight branches, in order: -- **PR #21 — resource decoupling.** Services declare resource-input slots; - systems `provision()` resources. **H3 hard-depends on it** — the auth system - provisions its db at system level and passes it to the service (resources are - never service-internal). H1/H2 need only ConnectionEnd inputs. -- **PR #22 — always-system root.** The deploy root must be a system; bare services - are not independently deployable (Load errors with "wrap it in a system"). The - service-root pipeline path, `examples/prisma-app-hello`, and the e2e hello job - are already removed on #22's line. Baseline to build against: always-system - root, bundles-keyed-by-address only. The ADR-0003 amendment for this is - operator-owned on the #22 line — not ours to edit. - -Full rebase facts and the prisma-app-hello/redeploy-noop fallout are in -design-notes.md § Coordination facts. - -## Cross-cutting requirements - -- Every validation rule is a tested, fix-naming error (the CLI project's - error-surface standard). -- Compile-time checks primary, Load `satisfies()` backstop; `lint:casts` - delta ≤ 0; plane-separation and runtime-portability invariants hold - (composition adds nothing to runtime bundles). -- Doc-first covenant: deviations from ADR-0016/system-composition.md amend the - doc, never silently diverge. - -## Project DoD - -- [ ] The three code blocks under "At a glance" compile and run verbatim - (module specifiers aside) in the repo. -- [ ] CI e2e deploys the composed topology live: nested auth system, storefront - round trip renders, destroy clean. -- [ ] The fake topology passes typecheck + an integration test driving the - real CLI through Load; `git diff` between real and fake topologies - touches no storefront file. -- [ ] Integration test proves an installed package's service can use an - adapter the consuming app does not declare (build.module-anchored - resolution). -- [ ] All four Load validation errors exercised by tests asserting message - content; 3-level nesting type-test green. -- [ ] Gates green; docs match shipped reality. - -## Open questions - -None. All four held points are resolved: the breaking `system()` reshape and -the validation-rule set are confirmed; the auth system provisions its db at system -level (never service-internal); H3 queues behind the resource-decoupling -landing and adopts its wiring syntax.