From d0c8c4ee3b247ac0cb6f91213e830bd6601d128d Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 1 Sep 2026 16:15:17 +0000 Subject: [PATCH 1/4] docs(project): add compact orm migration state record Signed-off-by: Steven McClankerton --- projects/compact-orm-migration-state/plan.md | 27 ++++++++ .../reviews/code-review.md | 36 ++++++++++ .../brief-D1-R1.md | 39 +++++++++++ .../compact-migration-resource-state/plan.md | 9 +++ .../compact-migration-resource-state/spec.md | 58 +++++++++++++++++ projects/compact-orm-migration-state/spec.md | 65 +++++++++++++++++++ .../compact-orm-migration-state/trace.jsonl | 15 +++++ 7 files changed, 249 insertions(+) create mode 100644 projects/compact-orm-migration-state/plan.md create mode 100644 projects/compact-orm-migration-state/reviews/code-review.md create mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md create mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md create mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md create mode 100644 projects/compact-orm-migration-state/spec.md create mode 100644 projects/compact-orm-migration-state/trace.jsonl diff --git a/projects/compact-orm-migration-state/plan.md b/projects/compact-orm-migration-state/plan.md new file mode 100644 index 00000000..8cf0d822 --- /dev/null +++ b/projects/compact-orm-migration-state/plan.md @@ -0,0 +1,27 @@ +# Compact OrmMigration state — Plan + +**Spec:** `projects/compact-orm-migration-state/spec.md` +**Linear Project:** N/A — operator explicitly waived Linear tracking for this project. + +## At a glance + +This is a single-slice project. One coherent change moves the deploy-time contract out of persisted `OrmMigration` props, reloads and attests it at reconciliation, proves large contracts no longer enlarge state, and updates the governing design documentation. + +## Composition + +### Stack (deliver in order) + +1. **Slice `compact-migration-resource-state`** — Linear: N/A + - **Outcome:** `PrismaOrm.Migration` remains a tracked Alchemy Resource while newly persisted state is independent of contract size; reconciliation deterministically loads the config-declared contract, verifies its identity, and runs the unchanged replay-only migration path. + - **Builds on:** The settled project spec and existing `OrmMigration`, ORM config-loading, and migration-control surfaces. + - **Hands to:** A deployable, documented Resource shape that supports contracts larger than 100 KB without sending the full contract to Alchemy state. + - **Focus:** Compact Resource props; deterministic contract loading through `prisma.config.ts`; declared-vs-loaded contract attestation; preservation of target-ref, invariant, extension-pack, local/hosted, and replay-only semantics; focused large-contract persistence coverage; ADR-0022 and affected domain-document updates. Generic transient props, Action conversion, API-limit changes, and proactive legacy-state compaction remain out of scope. + +## Dependencies (external) + +- [x] Prisma ORM exposes supported config and contract-loading surfaces — available in the pinned ORM toolchain. +- [x] Alchemy persists Resource props and requires no engine change when the full contract is removed from those props. + +## Sequencing rationale + +No multi-slice sequencing is warranted. The Resource-shape change, provider loading path, attestation, tests, and documentation form one reviewable invariant: the contract remains available to migration execution but never becomes newly persisted Alchemy state. Splitting these would create an intermediate state that either cannot reconcile or can migrate with an unattested contract, violating the project's transitional-shape constraint. diff --git a/projects/compact-orm-migration-state/reviews/code-review.md b/projects/compact-orm-migration-state/reviews/code-review.md new file mode 100644 index 00000000..7acf1f2e --- /dev/null +++ b/projects/compact-orm-migration-state/reviews/code-review.md @@ -0,0 +1,36 @@ +# Code review — `compact-orm-migration-state` + +> Initial scaffold. The reviewer maintains this document across rounds. The orchestrator and implementer read it but do not edit it. + +## Summary + +- **Current verdict:** pending first round +- **Dispatches SATISFIED:** none +- **Acceptance scoreboard totals:** 0 PASS / 0 FAIL / 3 NOT VERIFIED +- **Open findings:** 0 +- **Open escalations:** 0 + +## Acceptance criteria scoreboard + +| AC ID | Description | Dispatch | Status | Evidence | +| ----- | ----------- | -------- | ------ | -------- | +| AC-1 | State for a contract larger than 100 KB contains no full contract | D1 | NOT VERIFIED — round 1 pending | — | +| AC-2 | Contract loading and attestation fail before migration on mismatch/unreadable artifact | D1 | NOT VERIFIED — round 1 pending | — | +| AC-3 | Existing migration semantics remain covered and design documentation is aligned | D1 | NOT VERIFIED — round 1 pending | — | + +## Subagent IDs + +- **Implementer:** `8007f277-6169-47f` — first spawned in D1 R1. +- **Reviewer:** pending — first spawn in D1 R1. + +## Orchestrator notes + +- Linear tracking was explicitly waived by the operator for this project. + +## Findings log + +_(no findings yet)_ + +## Round notes + +_(round 1 will land here)_ diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md new file mode 100644 index 00000000..e41801fa --- /dev/null +++ b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md @@ -0,0 +1,39 @@ +# Brief: Compact and attest OrmMigration state + +## Task + +Keep `PrismaOrm.Migration` as a tracked Alchemy Resource while removing the full emitted contract from persisted Resource props. Make reconciliation deterministically load the contract output identified by `prisma.config.ts`, attest it against the contract declared by the database resource before migration begins, then pass it into the unchanged replay-only migration path. Prove newly persisted migration state remains compact for a contract larger than 100 KB and align ADR-0022 plus directly affected domain documentation. + +## Scope + +**In:** `packages/1-prisma-cloud/1-extensions/target` migration-resource, ORM-config, descriptor, and directly related test surfaces; focused state-shape/persistence proof; ADR-0022 and directly affected domain docs; compatibility with ordinary convergence from prior rows; committing the already-authored `projects/compact-orm-migration-state/` artifacts without modifying their intent. + +**Out:** Generic transient Resource props; converting the migration to an Action; Management API changes; proactive legacy-state compaction; migration graph, marker, target-ref, invariant, extension-pack, retry, or local/hosted semantic changes; unrelated cleanup. + +## Completed when + +- [ ] `OrmMigration` persisted props contain compact contract attestation but no `contractJson` or equivalent full-contract value, with current-contract attestation distinct from `targetHash` for named refs. +- [ ] Reconciliation loads the config-declared emitted contract via Prisma ORM-supported APIs and rejects missing/unreadable or identity-mismatched artifacts before opening or mutating the database. +- [ ] Focused tests include a contract larger than 100 KB and prove the newly persisted migration state is independent of that contract's size while existing migration behavior remains covered. +- [ ] ADR-0022 and directly affected domain documentation distinguish deploy-time contract loading from compact persisted migration state. +- [ ] Canonical targeted tests, target-package typecheck/build checks, and lint for changed files pass; report the exact commands selected from repository conventions. + +## Standing instruction + +Stay focused on the goal; control scope. Trivial-and-related fixes that obviously serve the goal go in the same dispatch with a one-line note in your wrap-up message. Anything that pulls you off the goal—even if useful—halts and surfaces. + +## References + +- Slice spec: `projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md` +- Slice plan: `projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md` +- Project spec: `projects/compact-orm-migration-state/spec.md` +- Project plan: `projects/compact-orm-migration-state/plan.md` +- Code review log: `projects/compact-orm-migration-state/reviews/code-review.md` (read-only) +- Governing principles: `docs/design/01-principles/` +- Governing decisions: ADR-0022, ADR-0041, ADR-0045 + +## Operational metadata + +- **Model tier:** mid — cross-cutting Resource-state and ORM-loader change with tests and design docs. +- **Time-box:** 90 minutes. Overrun means halt and surface. +- **Halt conditions:** Prisma ORM has no supported way to load the config-declared emitted contract; satisfying the task requires filename guessing or an in-memory/global side channel; config and declared contract cannot be attested without changing public authoring semantics; an out-of-scope surface must change; validation cannot be made green in scope. diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md new file mode 100644 index 00000000..990c7f0b --- /dev/null +++ b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md @@ -0,0 +1,9 @@ +# Compact migration resource state — Dispatch plan + +## Dispatch 1: Compact and attest OrmMigration state + +- **Outcome:** Newly converged `PrismaOrm.Migration` resources persist no full contract, while reconciliation deterministically reloads and attests the config-declared contract before invoking the unchanged migration engine; focused tests prove the behavior with a contract larger than 100 KB and documentation records the boundary. +- **Builds on:** The slice spec's chosen design and the existing `OrmMigration`, ORM config-resolution, Postgres descriptor, and replay-only migration surfaces. +- **Hands to:** A reviewed, deployable Composer change whose Alchemy state size is independent of ORM contract size, with mismatch failures occurring before migration and the design documented durably. +- **Focus:** Remove `contractJson` from persisted migration props; add compact current-contract attestation distinct from target ref; use Prisma ORM-supported config/contract loading; preserve existing migration semantics and local/hosted parity; add focused lowering, provider, and persistence coverage; update ADR-0022 and directly affected domain docs. Do not introduce generic transient props, convert to Action, modify the Management API, or proactively compact legacy rows. +- **Validation gate:** Run the directly affected target-extension test files, including the focused large-contract persistence and mismatch tests; run the target package's typecheck/build gate; run repository lint for changed files. The implementer must identify and report the repository's canonical concrete commands during reconnaissance, then run the complete selected gate once at dispatch completion. diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md new file mode 100644 index 00000000..d38f05d5 --- /dev/null +++ b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md @@ -0,0 +1,58 @@ +# Slice: Compact migration resource state + +_(Parent project: `projects/compact-orm-migration-state/`. This slice makes ORM contract size independent of newly persisted Alchemy state.)_ + +## At a glance + +Change `PrismaOrm.Migration` so its persisted props carry compact contract identity rather than the complete emitted contract. Reconciliation reloads the config-declared contract, attests it against the database resource's declaration, and then enters the unchanged replay-only migration path. + +## Chosen design + +`OrmMigrationProps` no longer contains `contractJson`. It carries a compact attestation of the current declared contract alongside the existing target-ref identity, invariants, extension-pack identities, paths, and resolved database URL. + +The Postgres descriptor continues to read the declared `dataContract` while lowering. It derives the compact attestation and target ref there, then creates `OrmMigration` without embedding the contract. + +During reconciliation, the provider uses Prisma ORM's supported config and contract-loading surfaces to load the contract output identified by `prisma.config.ts`. Before opening or mutating the database, it compares the loaded contract's storage identity with the persisted declaration attestation. A mismatch or unreadable artifact fails the deploy explicitly. A successful check supplies the loaded full contract to the existing `applyOrmMigration` function; migration planning and execution remain unchanged. + +The current contract attestation is distinct from `targetHash`: a named target ref may intentionally select an older migration destination while the config still identifies the current emitted contract artifact. + +## Coherence rationale + +The Resource shape, deterministic reload path, attestation, large-contract proof, and governing documentation are one invariant and one rollback unit: the migration receives the right full contract without persisting it. Splitting them would temporarily leave reconciliation unable to run or able to use an unattested artifact. + +## Scope + +**In:** `OrmMigration` props and provider reconciliation; ORM config resolution required to load the emitted contract; Postgres descriptor wiring; focused lowering/provider/persistence tests including a contract larger than 100 KB; compatibility with prior persisted props during ordinary convergence; ADR-0022 and directly affected domain documentation. + +**Out:** Generic transient Resource props; Alchemy Action conversion; Management API limits; proactive legacy-state compaction; changes to migration graph planning, marker semantics, target refs, invariants, extension-pack execution, retries, or local/hosted provider selection. + +## Pre-investigated edge cases + +| Edge case | Disposition | Notes | +| --------- | ----------- | ----- | +| Emitted contract exceeds 100 KB | Cover with focused persistence proof | The full value must not appear under any equivalent persisted prop. | +| Named target ref points behind the current contract head | Keep current-contract attestation separate from `targetHash` | Selecting an older authored destination remains valid. | +| Config-loaded contract differs from the declared `dataContract` | Fail before migration or database mutation | Never silently choose either source. | +| Existing state contains legacy `contractJson` props | Tolerate through ordinary convergence | No proactive rewrite or cleanup is required. | + +## Slice-specific done conditions + +- [ ] A focused persistence test with a contract larger than 100 KB proves newly persisted `PrismaOrm.Migration` state contains no full-contract value. +- [ ] Contract mismatch and unreadable-contract failures occur before migration execution. +- [ ] ADR-0022 and directly affected domain documentation describe the compact-state/config-loaded contract boundary. + +## Open Questions + +None. + +## References + +- Parent project: `projects/compact-orm-migration-state/spec.md` +- Linear issue: N/A — operator waived Linear tracking. +- `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md` +- `docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md` +- `docs/design/90-decisions/ADR-0045-deploy-state-lives-behind-the-platform-state-api.md` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts` diff --git a/projects/compact-orm-migration-state/spec.md b/projects/compact-orm-migration-state/spec.md new file mode 100644 index 00000000..675d7946 --- /dev/null +++ b/projects/compact-orm-migration-state/spec.md @@ -0,0 +1,65 @@ +# Compact OrmMigration state + +## Purpose + +Allow Composer to deploy Prisma ORM contracts larger than the Management API's 100 KB Alchemy-state write limit. The size of an application's contract must not determine whether Composer can persist deployment state. + +## At a glance + +Today `PrismaOrm.Migration` carries the full emitted `contractJson` as an Alchemy Resource prop. Alchemy persists every prop, so a sufficiently large contract makes the state write fail even though the migration result itself is only a compact schema identity. + +After this project, the migration remains a tracked Alchemy Resource, but its persisted state contains only compact identity and location data. At reconcile time, Composer deterministically loads the deploy-time contract artifact identified by `prisma.config.ts`, verifies that it matches the contract declared by the database resource, and hands the loaded contract to Prisma ORM's replay-only migration engine. Contract size therefore affects local migration processing, not hosted-state payload size. + +## Non-goals + +- Raising or changing the Management API's 100 KB state-write limit. +- Introducing a generic Alchemy facility for non-persisted or transient Resource props. +- Converting `OrmMigration` from a Resource to an Alchemy Action. +- Proactively compacting existing `PrismaOrm.Migration` state rows; old rows may remain in their existing shape until ordinary convergence rewrites them. +- Changing migration planning, target-ref, invariant, extension-pack, replay-only, retry, or database-marker semantics. +- Recovering state rows that a platform-side response limit prevents the client from reading. + +## Place in the larger world + +- `OrmMigration` is a Composer-owned Alchemy Resource in the Prisma Cloud target extension. It remains the tracked, dependency-aware deploy step established by ADR-0022 and shared unchanged by hosted deploy and local development under ADR-0041. +- Alchemy's stock state client persists Resource input props through the Management API route established by ADR-0045. Alchemy has no per-prop opt-out, so the contract must cease to be a Resource prop rather than merely be ignored by provider diffing. +- Prisma ORM remains the contract and migration authority. Composer uses Prisma ORM's stock config and contract-loading surfaces; it does not infer filenames or reconstruct contracts. +- The database resource's declared `dataContract` remains the runtime and wiring contract. The contract output identified by `prisma.config.ts` becomes the deploy-time artifact consumed by migration reconciliation, with an explicit identity check connecting the two. + +## Cross-cutting requirements + +- No full contract value may appear in the persisted inputs of a newly converged `PrismaOrm.Migration` resource; persisted state size must be independent of contract size. +- Migration reconciliation must load the contract through the path and output semantics declared by `prisma.config.ts`, using Prisma ORM's supported loaders rather than filename or directory guessing. +- The Resource must persist a compact attestation for the declared contract and fail before migration if the loaded deploy-time artifact does not match it. A named migration target may point to an older ref, so the current contract attestation and target-ref hash remain distinct concepts. +- The existing target identity remains complete: target hash, sorted invariants, and sorted extension-pack head hashes must continue to trigger the same migration decisions as before. +- Hosted deployment and local development must continue to use the same `OrmMigration` Resource and provider behavior. +- Deploy remains replay-only: the loaded contract supplies Prisma ORM's destination-contract context but never authorizes Composer to synthesize schema operations. +- The change must preserve the architectural rule that the framework does not bundle, transform, discover, or guess application artifacts. + +## Transitional-shape constraints + +- Existing persisted rows require no proactive migration. During rollout, Composer must tolerate prior rows containing `contractJson`; normal convergence may replace them with the compact shape. +- Every intermediate state must either use the existing contract prop safely or use the new attested config-loading path; no intermediate state may migrate using an unverified contract artifact. + +## Project Definition of Done + +- [ ] Team-DoD floor items are inherited from the repository's canonical team-DoD when present. +- [ ] A focused persistence test uses an emitted contract larger than 100 KB and proves newly persisted `PrismaOrm.Migration` state contains no `contractJson` or equivalent full-contract value. +- [ ] `OrmMigration` reconciliation loads the contract identified by `prisma.config.ts` and successfully supplies it to the unchanged Prisma ORM migration path. +- [ ] A mismatch between the database resource's declared contract attestation and the config-loaded contract fails before any migration operation runs. +- [ ] Existing target-ref, invariant, extension-pack, hosted-deploy, and local-development behavior remains covered and passing. +- [ ] ADR-0022 and affected domain documentation distinguish the deliberately carried deploy-time contract from the compact state persisted for the migration Resource. + +## Open Questions + +None. + +## References + +- `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md` +- `docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md` +- `docs/design/90-decisions/ADR-0045-deploy-state-lives-behind-the-platform-state-api.md` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts` +- `packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts` diff --git a/projects/compact-orm-migration-state/trace.jsonl b/projects/compact-orm-migration-state/trace.jsonl new file mode 100644 index 00000000..46e8db3a --- /dev/null +++ b/projects/compact-orm-migration-state/trace.jsonl @@ -0,0 +1,15 @@ +{"event_id":"84340b75-bd19-44e5-8e2d-07c1af54e767","schema_version":"1","ts":"2026-09-01T15:28:05.432Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/compact-orm-migration-state/spec.md","spec_kind":"project","byte_length":5889,"edge_cases_count":null,"open_questions_count":0,"dod_items_count":6} +{"event_id":"810fdf6b-0966-4f41-bf0e-ca3584092023","schema_version":"1","ts":"2026-09-01T15:36:51.072Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/compact-orm-migration-state/plan.md","plan_kind":"project","byte_length":2307,"dispatch_count":null,"slice_count":1,"dispatch_size_distribution":null,"open_items_count":0} +{"event_id":"20ecf06c-4040-4862-b1ab-e06109bfd17c","schema_version":"1","ts":"2026-09-01T15:37:26.634Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-amended","plan_path":"projects/compact-orm-migration-state/plan.md","plan_kind":"project","byte_length":2315,"bytes_delta":8,"dispatch_count":null,"slice_count":1,"dispatch_size_distribution":null,"open_items_count":0,"reason":"operator-correction","dispatches_added":null,"dispatches_removed":null,"dispatches_resized":null} +{"event_id":"fce29fcf-ae8a-401e-a93d-cc589cb16725","schema_version":"1","ts":"2026-09-01T15:51:07.794Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"health-check-fired","cadence":"opening-rollup","drift_signal_count":0,"max_drift_severity":"none","recommended_next":"Author compact-migration-resource-state slice spec and dispatch plan, then run the slice."} +{"event_id":"fa3945c7-bd02-463a-b8a8-aea4660ac94b","schema_version":"1","ts":"2026-09-01T15:51:09.678Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md","spec_kind":"slice","byte_length":4529,"edge_cases_count":4,"open_questions_count":0,"dod_items_count":3} +{"event_id":"272b0bbd-6b3e-4c48-aa6f-ba9e406e9b95","schema_version":"1","ts":"2026-09-01T15:54:33.718Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md","plan_kind":"slice","byte_length":1691,"dispatch_count":1,"slice_count":null,"dispatch_size_distribution":{"S":0,"M":1,"L":0,"XL":0},"open_items_count":0} +{"event_id":"f8229593-bba0-442d-b2fe-4fbbc934be9c","schema_version":"1","ts":"2026-09-01T15:54:35.416Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"slice-started","slice_slug":"compact-migration-resource-state","slice_index":1,"linear_ref":null} +{"event_id":"2cb1ea16-4caf-4484-a9fc-0be277d8a559","schema_version":"1","ts":"2026-09-01T15:56:49.017Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","dispatch_name":"D1 compact and attest OrmMigration state","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} +{"event_id":"ed0c5ad9-7677-4da1-838c-975210ff25c4","schema_version":"1","ts":"2026-09-01T15:56:50.910Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","round_number":1} +{"event_id":"fb762e09-a000-40d8-a5dc-d94548b5278a","schema_version":"1","ts":"2026-09-01T15:56:53.162Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","brief_byte_length":3537,"brief_content_hash":"dd78634a2b27e56c4aefcbabd1ddcceac64e03d8fb83d957c93db5ec4f712fc4","brief_disposition":"initial"} +{"event_id":"8b045f8e-8d6a-425b-8ad5-cfc0cc367134","schema_version":"1","ts":"2026-09-01T15:58:31.475Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","verdict":"stop-condition","findings_filed":0,"wall_clock_ms":96653} +{"event_id":"7cf4ea84-1189-4404-99a1-adbaf918122d","schema_version":"1","ts":"2026-09-01T15:58:33.003Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","result":"failed","wall_clock_ms":98546} +{"event_id":"e9e57139-22ab-4ae7-b7bb-c93fb8bd9239","schema_version":"1","ts":"2026-09-01T15:58:34.611Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","dispatch_name":"D1 compact and attest OrmMigration state","subagent_type":"general-purpose","model":"openai-codex/gpt-5.4","parent_dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7"} +{"event_id":"d03b35b1-7bda-463e-9d22-d5723a8fc594","schema_version":"1","ts":"2026-09-01T15:58:36.175Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","round_number":1} +{"event_id":"b3dddee8-b2de-43c4-a719-a213177f60ff","schema_version":"1","ts":"2026-09-01T15:58:37.946Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","brief_byte_length":3537,"brief_content_hash":"dd78634a2b27e56c4aefcbabd1ddcceac64e03d8fb83d957c93db5ec4f712fc4","brief_disposition":"initial"} From 78f40b4661746f15d41be1035da730f31ce1a0d4 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 1 Sep 2026 16:47:13 +0000 Subject: [PATCH 2/4] fix(prisma-cloud): compact orm migration state Signed-off-by: Steven McClankerton --- docs/design/10-domains/local-dev.md | 2 +- ...2-data-deps-carry-a-prisma-orm-contract.md | 43 ++-- ...nding-carries-the-url-and-a-lazy-client.md | 8 +- docs/guides/building-an-app.md | 9 +- .../src/__tests__/control-lowering.test.ts | 49 ++++- .../target/src/__tests__/orm-config.test.ts | 24 +++ .../__tests__/orm-migration-resource.test.ts | 195 ++++++++++++++---- .../src/__tests__/orm-target-ref.test.ts | 12 ++ .../target/src/descriptors/orm-postgres.ts | 5 +- .../1-extensions/target/src/orm-config.ts | 29 ++- .../1-extensions/target/src/orm-migrate.ts | 11 +- .../target/src/orm-migration-resource.ts | 49 +++-- skills/prisma-composer-core-concepts/SKILL.md | 10 +- 13 files changed, 353 insertions(+), 93 deletions(-) diff --git a/docs/design/10-domains/local-dev.md b/docs/design/10-domains/local-dev.md index 4e9f3648..5d0737ee 100644 --- a/docs/design/10-domains/local-dev.md +++ b/docs/design/10-domains/local-dev.md @@ -185,7 +185,7 @@ SQLite test server remains a testing utility, not part of the dev loop. ### Postgres -The emulator is the ORM CLI's local Postgres (`prisma dev`), **one named, detached instance per `Database` resource** — instance names are derived from the app and database ids, so instances are isolated, discoverable (`prisma dev ls`), and survive across dev sessions for warm starts. Migrations are not special-cased: `OrmMigration` runs exactly as it does in a deploy, against the local URL — replay-only (ADR-0022 as revised), so it applies committed migrations and never synthesizes schema. Dev-loop schema iteration therefore happens through the ORM's own `prisma db update`, run directly against the emulator database: `db update` moves the database and its marker to the current contract, and the pipeline's migration step no-ops because the marker matches the target. A dev run against a database that was neither updated nor covered by a planned migration hits the same structured refusal a deploy would, naming both exits (`prisma db update` to iterate, `contract emit` + `migration plan` to author the path). `PgWarm` is near-instant locally and is kept (not stubbed) so the provider set stays uniform. +The emulator is the ORM CLI's local Postgres (`prisma dev`), **one named, detached instance per `Database` resource** — instance names are derived from the app and database ids, so instances are isolated, discoverable (`prisma dev ls`), and survive across dev sessions for warm starts. Migrations are not special-cased: `OrmMigration` runs exactly as it does in a deploy, against the local URL — replay-only (ADR-0022 as revised), so it applies committed migrations and never synthesizes schema. At reconcile time it reloads the emitted `contract.json` identified by `prisma.config.ts`, attests its `storageHash` against the compact contract identity persisted in deploy state, and only then opens the database. Dev-loop schema iteration therefore happens through the ORM's own `prisma db update`, run directly against the emulator database: `db update` moves the database and its marker to the current contract, and the pipeline's migration step no-ops because the marker matches the target. A dev run against a database that was neither updated nor covered by a planned migration hits the same structured refusal a deploy would, naming both exits (`prisma db update` to iterate, `contract emit` + `migration plan` to author the path). `PgWarm` is near-instant locally and is kept (not stubbed) so the provider set stays uniform. ### Buckets: a disk-backed S3 emulator diff --git a/docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md b/docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md index dcbee856..c4c10796 100644 --- a/docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md +++ b/docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md @@ -49,9 +49,10 @@ The two ends of the resource pull in opposite directions, so they enter by different doors. The **contract** is *consumed*: it types and wires the resource and gives the deploy the schema version to migrate to. The **`prisma.config.ts`** is *located*, by path only — deploy-only metadata -the migration step reads to find the migrations directory. The app build never -imports it, because importing it would pull Prisma ORM's CLI, migration engine, -and source providers into the user's bundle. One contract per database. +the migration step reads to find the emitted `contract.json` and the +migrations directory. The app build never imports it, because importing it +would pull Prisma ORM's CLI, migration engine, and source providers into the +user's bundle. One contract per database. At deploy, the lowering gains a migration step per `postgres` resource. Its target is a **ref** — `{ hash, invariants }` — and the live database carries a @@ -80,9 +81,11 @@ otherwise → migrate (replay The ref comes from the resource's optional `targetRef` (naming a `migrations/app/refs/.json` file), or defaults to the head: the emitted contract's hash with zero invariants. The tracked migration resource is keyed on -the ref's identity (hash plus sorted invariants), so a data-only change still -produces a distinct deploy step. Synthesized diff-and-apply (`dbUpdate`) is never -run against a deployed database — only `migrate` is. +the ref's identity (hash plus sorted invariants), and its persisted props carry +only compact contract identity plus the config/migrations paths — not the full +emitted contract — so a data-only change still produces a distinct deploy step +without pushing `contract.json` into Alchemy state. Synthesized diff-and-apply +(`dbUpdate`) is never run against a deployed database — only `migrate` is. Bare `postgres()` is unchanged: the untyped escape hatch, the `any` of data deps, the same role `http()` plays for communication. @@ -111,19 +114,21 @@ own subpath entry, never re-exported from the index — so a service that opts o never loads `@prisma/orm-postgres` or `pg` at runtime. **Consume the contract; locate the config.** The runtime and the type system -only need to *consume* the contract: `contract.json` (the data the framework -hands the runtime at *hydrate* — the boot-time step that builds each -dependency's client) and `contract.d.ts` (types), both lightweight and -importable into the app build with no deploy machinery attached. The deploy migration step needs to -*locate* the config — the `prisma.config.ts` from which Prisma ORM resolves -the migrations directory — but it needs only the **path**, a string, read at -deploy time. Passing the config as a path rather than an import is what keeps -Prisma ORM's CLI and migration engine out of the user's bundle while still -giving the deploy lowering what it needs. A single contract is Prisma ORM's -mainline single-space model, so the user authors one contract that serves every -consuming module. Each consumer sees the full contract type; per-consumer -least-privilege slices are the deferred multi-contract extension (see -Alternatives). +need to *consume* the contract: `contract.json` (the data the framework hands +the runtime at *hydrate* — the boot-time step that builds each dependency's +client) and `contract.d.ts` (types), both lightweight and importable into the +app build with no deploy machinery attached. The deploy migration step needs to +*locate* the config — the `prisma.config.ts` from which Prisma ORM resolves the +emitted `contract.json` output and the migrations directory — but it needs only +the **path**, a string, read at deploy time. Passing the config as a path +rather than an import is what keeps Prisma ORM's CLI and migration engine out +of the user's bundle while still giving the deploy lowering what it needs. The +migration resource therefore persists only a compact attestation of the current +declared contract and reloads the full emitted contract at reconcile time. A +single contract is Prisma ORM's mainline single-space model, so the user +authors one contract that serves every consuming module. Each consumer sees the +full contract type; per-consumer least-privilege slices are the deferred +multi-contract extension (see Alternatives). **Schema checking is a build/deploy-time job, not a runtime one.** The authoritative check is the deploy. `migrate` walks the authored graph from the diff --git a/docs/design/90-decisions/ADR-0040-the-orm-binding-carries-the-url-and-a-lazy-client.md b/docs/design/90-decisions/ADR-0040-the-orm-binding-carries-the-url-and-a-lazy-client.md index ab1cc50d..743be39d 100644 --- a/docs/design/90-decisions/ADR-0040-the-orm-binding-carries-the-url-and-a-lazy-client.md +++ b/docs/design/90-decisions/ADR-0040-the-orm-binding-carries-the-url-and-a-lazy-client.md @@ -89,9 +89,11 @@ or invalidity. run against a contract the runtime's validator would reject — the storage-hash check at wiring remains the compatibility check that matters (ADR-0022). -3. `OrmMigration` and the deploy lowering are untouched: provisioning - `postgres({ name, contract, config })` still migrates at deploy. An app - owning its client gets framework-run migrations with no operator step. +3. `postgres({ name, contract, config })` still migrates at deploy. The + migration resource now persists only compact contract identity and reloads + the emitted contract artifact from `prisma.config.ts` at reconcile time, so + an app owning its client still gets framework-run migrations with no + operator step. ## Alternatives considered diff --git a/docs/guides/building-an-app.md b/docs/guides/building-an-app.md index a18d73ec..408edbfd 100644 --- a/docs/guides/building-an-app.md +++ b/docs/guides/building-an-app.md @@ -182,7 +182,8 @@ deps: { db: postgres(catalogData) } An options object is the resource end — the module that owns the database provisions it, naming the `prisma.config.ts` path (relative to the -module file) so the deploy can find `migrations/`: +module file) so the deploy can reload the emitted `contract.json` and find +`migrations/`: ```ts const db = provision( @@ -191,7 +192,11 @@ const db = provision( ``` Because both ends share the contract value, the deploy refuses to wire a -service against a database whose schema doesn't match. +service against a database whose schema doesn't match. The migration resource +persists only compact contract identity in deploy state; the full emitted +contract is reloaded from `prisma.config.ts` at reconcile time. If that +artifact is missing, unreadable, or no longer matches the declared contract, +the deploy fails before touching the database. [`examples/orm-demo`](../../examples/orm-demo/) is the minimal working version; [`examples/store/modules/catalog`](../../examples/store/modules/catalog/) is diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts index 624d125f..100d2e1f 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts @@ -610,7 +610,7 @@ describe("prismaCloud().nodes['postgres'] — the resource descriptor", () => { 'prisma.config.ts', ); - test('default stage: the Database attaches the default Branch; the migration runs on its warmed url', async () => { + test('default stage: the Database attaches the default Branch; the migration persists compact contract attestation on its warmed url', async () => { await withEnv({}, async () => { const target = prismaCloud({ workspaceId: 'ws_1' }); const node = postgres({ @@ -640,11 +640,56 @@ describe("prismaCloud().nodes['postgres'] — the resource descriptor", () => { ], ]); const [migrateId, migrateProps] = recorded.pnMigrate[before.migrate] ?? ['', {}]; + const persisted = migrateProps as Record; expect(migrateId).toBe('pndata-migrate'); - expect((migrateProps as { url: unknown }).url).toBe('postgres://pndata-conn'); + expect(persisted['url']).toBe('postgres://pndata-conn'); + expect(persisted['currentContractHash']).toBe(widgetContractJson.storage.storageHash); + expect(persisted['targetHash']).toBe(widgetContractJson.storage.storageHash); + expect(persisted['migrationsDir']).toBe(path.join(path.dirname(widgetConfig), 'migrations')); + expect(persisted['configPath']).toBe(widgetConfig); + expect(persisted['packHeadRefHashes']).toEqual([]); + expect('contractJson' in persisted).toBe(false); expect(result.entities).toEqual([{ kind: 'postgres-database', id: 'pndata-db#cloud-id' }]); }); }); + + test('a contract larger than 100 KB does not enlarge newly persisted migration props', async () => { + await withEnv({}, async () => { + const oversizedProof = 'x'.repeat(110_001); + expect(oversizedProof.length).toBeGreaterThan(100_000); + const target = prismaCloud({ workspaceId: 'ws_1' }); + const lower = async (contractJson: unknown) => { + const node = postgres({ + name: 'oversized', + contract: dataContract(contractJson), + config: widgetConfig, + }); + const ctx = { + id: 'oversized', + node, + graph: { edges: [], nodes: [] }, + application: { + projectId: 'shop-project#cloud-id', + branchId: undefined, + defaultBranchId: 'br_default', + branchless: false, + }, + } as unknown as LowerContext; + const before = recorded.pnMigrate.length; + await runAsync(resourceDescriptorOf(target, 'postgres')(ctx)); + return recorded.pnMigrate[before]?.[1]; + }; + + const compact = await lower(widgetContractJson); + const oversized = await lower({ ...widgetContractJson, oversizedProof }); + const compactJson = JSON.stringify(compact); + const oversizedJson = JSON.stringify(oversized); + + expect(oversizedJson).toBe(compactJson); + expect(oversizedJson).not.toContain(oversizedProof); + expect('contractJson' in ((oversized ?? {}) as Record)).toBe(false); + }); + }); }); describe("prismaCloud().nodes['credentials'] — the resource descriptor", () => { diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-config.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-config.test.ts index 94e33e3e..8321b0a0 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-config.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-config.test.ts @@ -9,6 +9,7 @@ import { describe, expect, test } from 'bun:test'; import * as path from 'node:path'; import { + loadContractJson, type PnExtensionPack, packHeadRefHashes, resolveMigrationsDir, @@ -19,6 +20,9 @@ import { GADGET_PACK_ID, gadgetPack, } from './fixtures/packed-contract/pack.ts'; +import widgetContractJson from './fixtures/widget-contract/emitted/contract.json' with { + type: 'json', +}; const widgetConfig = path.join( import.meta.dir, @@ -50,6 +54,26 @@ describe('resolveOrmConfig', () => { expect(project.extensionPacks).toEqual([]); }); + test('resolves the emitted contract artifact path from the config output', async () => { + const project = await resolveOrmConfig(widgetConfig); + expect(project.contractArtifactPath).toBe( + path.join(path.dirname(widgetConfig), '..', 'emitted', 'contract.json'), + ); + expect(path.isAbsolute(project.contractArtifactPath)).toBe(true); + }); + + test('without explicit output, the emitted contract path defaults next to the contract source', async () => { + const project = await resolveOrmConfig(packedConfig); + expect(project.contractArtifactPath).toBe( + path.join(path.dirname(packedConfig), 'contract.json'), + ); + }); + + test('loadContractJson reads the emitted contract the config identifies', async () => { + const project = await resolveOrmConfig(widgetConfig); + expect(await loadContractJson(project.contractArtifactPath)).toEqual(widgetContractJson); + }); + test('surfaces declared extension packs with their contract-space heads', async () => { const project = await resolveOrmConfig(packedConfig); expect(project.extensionPacks.map((p) => p.id)).toEqual([GADGET_PACK_ID]); diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts index af9e2dc6..55d2a369 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts @@ -133,6 +133,123 @@ describe("the real providers' tags resolve by type (direct context, no cross-mod }); }); +const widgetConfig = path.join( + import.meta.dir, + 'fixtures', + 'widget-contract', + 'source', + 'prisma.config.ts', +); +const gadgetConfig = path.join( + import.meta.dir, + 'fixtures', + 'gadget-contract', + 'source', + 'prisma.config.ts', +); +const widgetHash = targetStorageHash(widgetContractJson); +const gadgetHash = targetStorageHash(gadgetContractJson); + +const reconcile = (input: { + readonly url: string; + readonly migrationsDir: string; + readonly configPath: string; + readonly currentContractHash: string; + readonly targetHash: string; + readonly invariants?: readonly string[]; + readonly packHeadRefHashes?: readonly string[]; + readonly refName?: string; +}) => + ormMigrationProviderService.reconcile({ + id: 'db', + fqn: 'db', + instanceId: 'db', + news: { + url: input.url, + migrationsDir: input.migrationsDir, + configPath: input.configPath, + currentContractHash: input.currentContractHash, + targetHash: input.targetHash, + invariants: input.invariants ?? [], + packHeadRefHashes: input.packHeadRefHashes ?? [], + ...(input.refName !== undefined ? { refName: input.refName } : {}), + }, + olds: undefined, + output: undefined, + // The plan session / bindings are unused by this provider's reconcile. + session: undefined as never, + bindings: undefined as never, + }); + +const matchFailure = (effect: Effect.Effect) => + Effect.runPromise( + effect.pipe( + Effect.match({ + onSuccess: () => ({ failed: false as const, error: undefined }), + onFailure: (error: unknown) => ({ failed: true as const, error }), + }), + ), + ); + +describe('OrmMigration contract loading and attestation', () => { + const impossibleUrl = 'postgres://127.0.0.1:1/never-opened'; + + test('a mismatched config-loaded contract fails before any database access', async () => { + const outcome = await matchFailure( + reconcile({ + url: impossibleUrl, + migrationsDir: path.join(path.dirname(widgetConfig), 'migrations'), + configPath: widgetConfig, + currentContractHash: gadgetHash, + targetHash: widgetHash, + }), + ); + + expect(outcome.failed).toBe(true); + expect(outcome.error).toBeInstanceOf(OrmMigrationError); + expect((outcome.error as OrmMigrationError).code).toBe('CONTRACT_IDENTITY_MISMATCH'); + }); + + test('a missing emitted contract artifact fails before any database access', async () => { + const dir = fs.mkdtempSync(path.join(import.meta.dir, 'tmp-missing-contract-')); + try { + const configPath = path.join(dir, 'prisma.config.ts'); + const contractPath = path.relative( + dir, + path.join(import.meta.dir, 'fixtures', 'widget-contract', 'source', 'contract.ts'), + ); + fs.writeFileSync( + configPath, + `import { definePrismaConfig } from '@prisma/cli-engine';\n` + + "import { defineConfig } from '@prisma/orm-postgres/config';\n\n" + + 'export default definePrismaConfig({\n' + + ' orm: defineConfig({\n' + + ` contract: ${JSON.stringify(contractPath)},\n` + + " output: './missing',\n" + + " db: { connection: 'postgres://localhost:5432/placeholder' },\n" + + ' }),\n' + + '});\n', + ); + + const outcome = await matchFailure( + reconcile({ + url: impossibleUrl, + migrationsDir: path.join(dir, 'migrations'), + configPath, + currentContractHash: widgetHash, + targetHash: widgetHash, + }), + ); + + expect(outcome.failed).toBe(true); + expect(outcome.error).toBeInstanceOf(OrmMigrationError); + expect((outcome.error as OrmMigrationError).code).toBe('CONTRACT_ARTIFACT_UNREADABLE'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +}); + const pg: TestPostgres | undefined = startTestPostgres(); if (pg === undefined) { @@ -148,32 +265,6 @@ describe.skipIf(pg === undefined)('OrmMigration reconcile routes through applyOr let testDb: TestDatabase; let url: string; - // Drive the reconcile through the exported provider service directly — no - // Effect layer to build, so the routing assertion can't be flaked by - // environment-specific layer internals. - const reconcile = (contractJson: unknown) => - ormMigrationProviderService.reconcile({ - id: 'db', - fqn: 'db', - instanceId: 'db', - news: { - url, - contractJson, - migrationsDir, - targetHash: targetStorageHash(contractJson), - invariants: [], - // No packs declared: reconcile must not touch configPath (the path - // deliberately points nowhere). - packHeadRefHashes: [], - configPath: path.join(migrationsDir, 'no-such-prisma.config.ts'), - }, - olds: undefined, - output: undefined, - // The plan session / bindings are unused by this provider's reconcile. - session: undefined as never, - bindings: undefined as never, - }); - beforeAll(async () => { migrationsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-pn-res-')); // Replay-only: the deploy pipeline applies only authored migrations, so @@ -188,28 +279,52 @@ describe.skipIf(pg === undefined)('OrmMigration reconcile routes through applyOr if (migrationsDir !== undefined) fs.rmSync(migrationsDir, { recursive: true, force: true }); }); - test('reconcile applies the contract then no-ops on the resolved props', async () => { - const targetHash = targetStorageHash(widgetContractJson); - const first = await Effect.runPromise(reconcile(widgetContractJson)); - expect(first.storageHash).toBe(targetHash); - const second = await Effect.runPromise(reconcile(widgetContractJson)); - expect(second.storageHash).toBe(targetHash); + test('reconcile loads the config-declared contract, applies it, then no-ops on the resolved props', async () => { + const first = await Effect.runPromise( + reconcile({ + url, + migrationsDir, + configPath: widgetConfig, + currentContractHash: widgetHash, + targetHash: widgetHash, + }), + ); + expect(first.storageHash).toBe(widgetHash); + const second = await Effect.runPromise( + reconcile({ + url, + migrationsDir, + configPath: widgetConfig, + currentContractHash: widgetHash, + targetHash: widgetHash, + }), + ); + expect(second.storageHash).toBe(widgetHash); }); test('reconcile re-throws a no-path failure: the Effect REJECTS with OrmMigrationError', async () => { // Ensure the DB is signed at widgetHash (idempotent if already there). - await Effect.runPromise(reconcile(widgetContractJson)); + await Effect.runPromise( + reconcile({ + url, + migrationsDir, + configPath: widgetConfig, + currentContractHash: widgetHash, + targetHash: widgetHash, + }), + ); // Target a DIFFERENT contract (gadget) with no authored migration path. The // provider's `catch: (e) => e` must route the thrown OrmMigrationError into // the Effect's error channel — so the reconcile FAILS, not succeeds. - const outcome = await Effect.runPromise( - reconcile(gadgetContractJson).pipe( - Effect.match({ - onSuccess: () => ({ failed: false as const, error: undefined }), - onFailure: (error: unknown) => ({ failed: true as const, error }), - }), - ), + const outcome = await matchFailure( + reconcile({ + url, + migrationsDir, + configPath: gadgetConfig, + currentContractHash: gadgetHash, + targetHash: gadgetHash, + }), ); expect(outcome.failed).toBe(true); diff --git a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts index 0719c758..ad7358f5 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts @@ -113,6 +113,18 @@ describe('resolveTargetRef', () => { }); }); + test('a named targetRef can point behind the current contract head', async () => { + await withTempDir(async (dir) => { + const refsDir = spaceRefsDirectory(spaceMigrationDirectory(dir, APP_SPACE_ID)); + fs.mkdirSync(refsDir, { recursive: true }); + await writeRef(refsDir, 'baseline', { hash: B, invariants: [] }); + expect(await resolveTargetRef(dir, contractJson, 'baseline')).toEqual({ + hash: B, + invariants: [], + }); + }); + }); + test('a missing named targetRef fails loudly with TARGET_REF_NOT_FOUND', async () => { await withTempDir(async (dir) => { let thrown: unknown; diff --git a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts index f3d96917..615d0fe9 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts @@ -4,7 +4,7 @@ import type { NodeDescriptor } from '@internal/core/config'; import type { Lowering } from '@internal/core/deploy'; import * as Effect from 'effect/Effect'; import { packHeadRefHashes, resolveOrmConfig } from '../orm-config.ts'; -import { resolveTargetRef } from '../orm-migrate.ts'; +import { resolveTargetRef, targetStorageHash } from '../orm-migrate.ts'; import { OrmMigration } from '../orm-migration-resource.ts'; import { isPostgresResourceNode } from '../orm-postgres.ts'; import { PgWarm } from '../pg-warm-resource.ts'; @@ -28,6 +28,7 @@ export function postgresDescriptor(o: () => ResolvedCloudOptions): NodeDescripto throw new Error(`postgres lowering received a non-postgres node (${id}).`); } const contractJson = node.provides.__cmp.contractJson; + const currentContractHash = targetStorageHash(contractJson); const { migrationsDir, extensionPacks } = yield* Effect.promise(() => resolveOrmConfig(node.config), ); @@ -51,8 +52,8 @@ export function postgresDescriptor(o: () => ResolvedCloudOptions): NodeDescripto // invariant) still triggers reconcile. yield* OrmMigration(`${id}-migrate`, { url: warm.url, - contractJson, migrationsDir, + currentContractHash, targetHash: ref.hash, invariants: [...ref.invariants].sort(), packHeadRefHashes: packHeadRefHashes(extensionPacks), diff --git a/packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts b/packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts index b52b8d1c..8f9ebfb5 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts @@ -1,8 +1,9 @@ /** * Resolves a `postgres` resource's `prisma.config.ts` path to the - * project facts the deploy needs (ADR-0022, slice 2): the on-disk migrations - * directory the control client's `migrate` reads, and the declared - * extension packs. Deploy-time only: loads PN's config (via c12) and applies + * project facts the deploy needs (ADR-0022, slice 2): the emitted + * `contract.json` artifact path, the on-disk migrations directory the control + * client's `migrate` reads, and the declared extension packs. Deploy-time + * only: loads PN's config (via c12) and applies * PN's own convention — `migrations.dir`, or the default `migrations/`, * relative to the config file's directory (mirrors the CLI's * `resolveMigrationPaths`). Imported by `control.ts` + tests, never by @@ -37,6 +38,8 @@ export type PnExtensionPack = NonNullable< export interface ResolvedOrmConfig { /** The absolute migrations directory PN reads authored migration packages from. */ readonly migrationsDir: string; + /** The absolute emitted contract artifact path the config identifies. */ + readonly contractArtifactPath: string; /** The config's declared extension packs (`[]` when it declares none). */ readonly extensionPacks: readonly PnExtensionPack[]; } @@ -48,10 +51,20 @@ export async function resolveOrmConfig(configPath: string): Promise return (await resolveOrmConfig(configPath)).migrationsDir; } +/** Loads the emitted `contract.json` at the resolved artifact path. */ +export async function loadContractJson(contractArtifactPath: string): Promise { + // Freshen the specifier so repeated dev-loop reconciles re-read the file + // after `prisma contract emit` updates it in place. + const loaded = await import(`${contractArtifactPath}?t=${Date.now()}`, { + with: { type: 'json' }, + }); + return loaded.default; +} + /** * The pack-head identity entries the `OrmMigration` resource folds into its * diff key: `":"` — each pack's contract-space head ref, diff --git a/packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts b/packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts index 32d99927..b6576890 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts @@ -67,14 +67,19 @@ export interface OrmMigrationOutcome { * migration path from the marker's state (or from empty, for a fresh * database) to the target ref. `RUNNER_FAILED` — a migration errored while * applying. `CONTRACT_INVALID` — the contract carries no - * `storage.storageHash`, so no target can be resolved. `TARGET_REF_NOT_FOUND` - * — the resource named a `targetRef` with no readable - * `migrations/app/refs/.json`. + * `storage.storageHash`, so no target can be resolved. + * `CONTRACT_ARTIFACT_UNREADABLE` — the emitted `contract.json` identified by + * `prisma.config.ts` could not be loaded. `CONTRACT_IDENTITY_MISMATCH` — the + * config-loaded emitted contract's storage hash does not match the database + * resource's declared current contract. `TARGET_REF_NOT_FOUND` — the resource + * named a `targetRef` with no readable `migrations/app/refs/.json`. */ export type OrmMigrationFailureCode = | 'MIGRATION_PATH_NOT_FOUND' | 'RUNNER_FAILED' | 'CONTRACT_INVALID' + | 'CONTRACT_ARTIFACT_UNREADABLE' + | 'CONTRACT_IDENTITY_MISMATCH' | 'TARGET_REF_NOT_FOUND'; /** A deploy-failing migration error — surfaced, never swallowed. */ diff --git a/packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts b/packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts index 806a9f0a..77b40156 100644 --- a/packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts +++ b/packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts @@ -21,16 +21,21 @@ import { Resource } from 'alchemy'; import * as Provider from 'alchemy/Provider'; import * as Effect from 'effect/Effect'; -import { resolveOrmConfig } from './orm-config.ts'; -import { applyOrmMigration } from './orm-migrate.ts'; +import { loadContractJson, resolveOrmConfig } from './orm-config.ts'; +import { applyOrmMigration, OrmMigrationError, targetStorageHash } from './orm-migrate.ts'; export interface OrmMigrationProps { /** The live DB connection string (an Alchemy Output at wiring time, resolved at apply). */ readonly url: string; - /** The deserialized contract (`node.provides.__cmp.contractJson`) — what migrate applies. */ - readonly contractJson: unknown; /** On-disk migrations root, resolved from the resource's `prisma.config.ts`. */ readonly migrationsDir: string; + /** + * The current declared contract's `storage.storageHash`, persisted as compact + * attestation and checked against the config-loaded emitted contract before + * any database access. Distinct from `targetHash`: a named ref can point + * behind the current contract head. + */ + readonly currentContractHash: string; /** The target ref's hash — half the diff/identity key. */ readonly targetHash: string; /** @@ -53,8 +58,7 @@ export interface OrmMigrationProps { readonly packHeadRefHashes: readonly string[]; /** * The resource's `prisma.config.ts` path — where reconcile reloads the - * declared extension-pack descriptors from when `packHeadRefHashes` is - * non-empty. + * emitted contract artifact and any declared extension-pack descriptors. */ readonly configPath: string; } @@ -89,17 +93,32 @@ export const ormMigrationProviderService: Provider.ProviderService reconcile: ({ news }) => Effect.tryPromise({ try: async () => { - // Descriptors are reloaded here, not carried in props: props persist - // in Alchemy state and a descriptor is live code. Loaded only when - // the key says packs are declared, so a pack-free project never pays - // (or depends on) the config load at apply time. - const extensionPacks = - news.packHeadRefHashes.length > 0 - ? (await resolveOrmConfig(news.configPath)).extensionPacks - : []; + // The config is reloaded here, not carried in props: props persist in + // Alchemy state and the emitted contract is a deploy-time artifact. + const { contractArtifactPath, extensionPacks } = await resolveOrmConfig(news.configPath); + let contractJson: unknown; + try { + contractJson = await loadContractJson(contractArtifactPath); + } catch (error) { + const summary = error instanceof Error ? error.message : String(error); + throw new OrmMigrationError( + 'CONTRACT_ARTIFACT_UNREADABLE', + `could not load the emitted contract identified by "${news.configPath}" at ` + + `"${contractArtifactPath}": ${summary}`, + ); + } + const loadedContractHash = targetStorageHash(contractJson); + if (loadedContractHash !== news.currentContractHash) { + throw new OrmMigrationError( + 'CONTRACT_IDENTITY_MISMATCH', + `the emitted contract identified by "${news.configPath}" has storage hash ` + + `"${loadedContractHash}", but the database resource declared ` + + `"${news.currentContractHash}"`, + ); + } return applyOrmMigration({ url: news.url, - contractJson: news.contractJson, + contractJson, migrationsDir: news.migrationsDir, ref: { hash: news.targetHash, invariants: news.invariants }, extensionPacks, diff --git a/skills/prisma-composer-core-concepts/SKILL.md b/skills/prisma-composer-core-concepts/SKILL.md index 22d2254d..f46c3cc5 100644 --- a/skills/prisma-composer-core-concepts/SKILL.md +++ b/skills/prisma-composer-core-concepts/SKILL.md @@ -239,7 +239,7 @@ Two kinds of Postgres dependency: `prisma contract emit`) is referenced by both the dependency end (`deps: { db: postgres(catalogData) }`) and the resource end, which also names the `prisma.config.ts` path so the deploy's migration step can - find `migrations/`. + reload the emitted `contract.json` and find `migrations/`. **Deploys are replay-only**: they apply the migrations committed under `migrations/` and never create schema themselves. Every schema change, @@ -256,8 +256,12 @@ If no authored path reaches the target contract, deploy (and `dev` against a stale local database) refuses with `MIGRATION_PATH_NOT_FOUND`; its message lists the two ways out: author the missing migration, or, when iterating against a local -database only, `prisma db update`. Never skip step 3 before a deploy. See -`examples/store/modules/catalog` for the complete pattern. +database only, `prisma db update`. The tracked migration resource persists only +compact contract identity in deploy state; if the emitted contract artifact +named by `prisma.config.ts` is missing, unreadable, or no longer matches the +declared `dataContract(...)` value, deploy fails before touching the database. +Never skip step 3 before a deploy. See `examples/store/modules/catalog` for the +complete pattern. ## Deploy model: converge, don't script From eebd0c4137199e8ffd8bf51ed51233bbb70b78f4 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 1 Sep 2026 17:01:00 +0000 Subject: [PATCH 3/4] docs(project): record compact orm migration state closeout Signed-off-by: Steven McClankerton --- .../pr-description.md | 16 ++++++++++++ .../reviews/code-review.md | 26 +++++++++++++------ .../compact-orm-migration-state/trace.jsonl | 2 ++ 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 projects/compact-orm-migration-state/pr-description.md diff --git a/projects/compact-orm-migration-state/pr-description.md b/projects/compact-orm-migration-state/pr-description.md new file mode 100644 index 00000000..58416951 --- /dev/null +++ b/projects/compact-orm-migration-state/pr-description.md @@ -0,0 +1,16 @@ +# Compact ORM migration state + +## PR body + +Allows Prisma ORM contracts larger than the Management API's 100 KB Alchemy-state write limit by keeping the full contract out of persisted migration Resource props. + +## Changes + +- **Migration state:** Replace persisted `contractJson` with compact current-contract identity, kept distinct from migration target refs. +- **Contract reconciliation:** Load the emitted contract through the artifact path normalized by `prisma.config.ts`, attest it before database access, then enter the existing replay-only migration path. +- **Coverage:** Prove contracts larger than 100 KB do not enlarge persisted migration props, and verify unreadable or mismatched artifacts fail before database work. +- **Documentation:** Align ADR-0022, local-development guidance, the app-building guide, and core-concepts material with the config-loaded-contract/compact-state boundary. + +## Why + +Alchemy persists every Resource input prop, so carrying the complete contract made deployability depend on contract size. Keeping only compact identity in state removes that coupling while preserving `OrmMigration` as a dependency-aware Resource and retaining target-ref, invariant, extension-pack, marker, retry, and replay-only semantics. diff --git a/projects/compact-orm-migration-state/reviews/code-review.md b/projects/compact-orm-migration-state/reviews/code-review.md index 7acf1f2e..465c39a2 100644 --- a/projects/compact-orm-migration-state/reviews/code-review.md +++ b/projects/compact-orm-migration-state/reviews/code-review.md @@ -4,9 +4,9 @@ ## Summary -- **Current verdict:** pending first round -- **Dispatches SATISFIED:** none -- **Acceptance scoreboard totals:** 0 PASS / 0 FAIL / 3 NOT VERIFIED +- **Current verdict:** SATISFIED +- **Dispatches SATISFIED:** D1 +- **Acceptance scoreboard totals:** 3 PASS / 0 FAIL / 0 NOT VERIFIED - **Open findings:** 0 - **Open escalations:** 0 @@ -14,14 +14,14 @@ | AC ID | Description | Dispatch | Status | Evidence | | ----- | ----------- | -------- | ------ | -------- | -| AC-1 | State for a contract larger than 100 KB contains no full contract | D1 | NOT VERIFIED — round 1 pending | — | -| AC-2 | Contract loading and attestation fail before migration on mismatch/unreadable artifact | D1 | NOT VERIFIED — round 1 pending | — | -| AC-3 | Existing migration semantics remain covered and design documentation is aligned | D1 | NOT VERIFIED — round 1 pending | — | +| AC-1 | State for a contract larger than 100 KB contains no full contract | D1 | PASS | commit `78f40b46`; large-contract persistence proof in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts` | +| AC-2 | Contract loading and attestation fail before migration on mismatch/unreadable artifact | D1 | PASS | commit `78f40b46`; pre-DB mismatch/unreadable coverage in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts` | +| AC-3 | Existing migration semantics remain covered and design documentation is aligned | D1 | PASS | commit `78f40b46`; target-ref/head coverage in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts`; docs updated in ADR-0022/local-dev/building guide | ## Subagent IDs - **Implementer:** `8007f277-6169-47f` — first spawned in D1 R1. -- **Reviewer:** pending — first spawn in D1 R1. +- **Reviewer:** `c1e42697-b937-434` — first spawned in D1 R1. ## Orchestrator notes @@ -33,4 +33,14 @@ _(no findings yet)_ ## Round notes -_(round 1 will land here)_ +### D1 R1 — SATISFIED + +**Scope:** Dispatch D1. Commits `d0c8c4ee..78f40b46`. + +**Tasks:** Compact persisted migration state, pre-DB contract attestation, and governing docs clean. + +**AC delta:** AC-1 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts`). AC-2 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts`). AC-3 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts`; docs `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md`). + +**Findings:** none. + +**For orchestrator:** none. diff --git a/projects/compact-orm-migration-state/trace.jsonl b/projects/compact-orm-migration-state/trace.jsonl index 46e8db3a..0f8d8315 100644 --- a/projects/compact-orm-migration-state/trace.jsonl +++ b/projects/compact-orm-migration-state/trace.jsonl @@ -13,3 +13,5 @@ {"event_id":"e9e57139-22ab-4ae7-b7bb-c93fb8bd9239","schema_version":"1","ts":"2026-09-01T15:58:34.611Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","dispatch_name":"D1 compact and attest OrmMigration state","subagent_type":"general-purpose","model":"openai-codex/gpt-5.4","parent_dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7"} {"event_id":"d03b35b1-7bda-463e-9d22-d5723a8fc594","schema_version":"1","ts":"2026-09-01T15:58:36.175Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","round_number":1} {"event_id":"b3dddee8-b2de-43c4-a719-a213177f60ff","schema_version":"1","ts":"2026-09-01T15:58:37.946Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","brief_byte_length":3537,"brief_content_hash":"dd78634a2b27e56c4aefcbabd1ddcceac64e03d8fb83d957c93db5ec4f712fc4","brief_disposition":"initial"} +{"event_id":"d52deb37-18e0-4a13-ab14-70bb37441b80","schema_version":"1","ts":"2026-09-01T16:58:51.003Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","verdict":"satisfied","findings_filed":0,"wall_clock_ms":3609474} +{"event_id":"ff888e4d-6930-4795-be3c-dd854ed4b55d","schema_version":"1","ts":"2026-09-01T16:58:53.197Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","result":"completed","wall_clock_ms":3611038} From c46d35e464b47b4c8f78663cd08cf7b0d1e69445 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Wed, 2 Sep 2026 08:17:46 +0000 Subject: [PATCH 4/4] chore(project): remove transient delivery artifacts --- projects/compact-orm-migration-state/plan.md | 27 -------- .../pr-description.md | 16 ----- .../reviews/code-review.md | 46 ------------- .../brief-D1-R1.md | 39 ----------- .../compact-migration-resource-state/plan.md | 9 --- .../compact-migration-resource-state/spec.md | 58 ----------------- projects/compact-orm-migration-state/spec.md | 65 ------------------- .../compact-orm-migration-state/trace.jsonl | 17 ----- 8 files changed, 277 deletions(-) delete mode 100644 projects/compact-orm-migration-state/plan.md delete mode 100644 projects/compact-orm-migration-state/pr-description.md delete mode 100644 projects/compact-orm-migration-state/reviews/code-review.md delete mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md delete mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md delete mode 100644 projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md delete mode 100644 projects/compact-orm-migration-state/spec.md delete mode 100644 projects/compact-orm-migration-state/trace.jsonl diff --git a/projects/compact-orm-migration-state/plan.md b/projects/compact-orm-migration-state/plan.md deleted file mode 100644 index 8cf0d822..00000000 --- a/projects/compact-orm-migration-state/plan.md +++ /dev/null @@ -1,27 +0,0 @@ -# Compact OrmMigration state — Plan - -**Spec:** `projects/compact-orm-migration-state/spec.md` -**Linear Project:** N/A — operator explicitly waived Linear tracking for this project. - -## At a glance - -This is a single-slice project. One coherent change moves the deploy-time contract out of persisted `OrmMigration` props, reloads and attests it at reconciliation, proves large contracts no longer enlarge state, and updates the governing design documentation. - -## Composition - -### Stack (deliver in order) - -1. **Slice `compact-migration-resource-state`** — Linear: N/A - - **Outcome:** `PrismaOrm.Migration` remains a tracked Alchemy Resource while newly persisted state is independent of contract size; reconciliation deterministically loads the config-declared contract, verifies its identity, and runs the unchanged replay-only migration path. - - **Builds on:** The settled project spec and existing `OrmMigration`, ORM config-loading, and migration-control surfaces. - - **Hands to:** A deployable, documented Resource shape that supports contracts larger than 100 KB without sending the full contract to Alchemy state. - - **Focus:** Compact Resource props; deterministic contract loading through `prisma.config.ts`; declared-vs-loaded contract attestation; preservation of target-ref, invariant, extension-pack, local/hosted, and replay-only semantics; focused large-contract persistence coverage; ADR-0022 and affected domain-document updates. Generic transient props, Action conversion, API-limit changes, and proactive legacy-state compaction remain out of scope. - -## Dependencies (external) - -- [x] Prisma ORM exposes supported config and contract-loading surfaces — available in the pinned ORM toolchain. -- [x] Alchemy persists Resource props and requires no engine change when the full contract is removed from those props. - -## Sequencing rationale - -No multi-slice sequencing is warranted. The Resource-shape change, provider loading path, attestation, tests, and documentation form one reviewable invariant: the contract remains available to migration execution but never becomes newly persisted Alchemy state. Splitting these would create an intermediate state that either cannot reconcile or can migrate with an unattested contract, violating the project's transitional-shape constraint. diff --git a/projects/compact-orm-migration-state/pr-description.md b/projects/compact-orm-migration-state/pr-description.md deleted file mode 100644 index 58416951..00000000 --- a/projects/compact-orm-migration-state/pr-description.md +++ /dev/null @@ -1,16 +0,0 @@ -# Compact ORM migration state - -## PR body - -Allows Prisma ORM contracts larger than the Management API's 100 KB Alchemy-state write limit by keeping the full contract out of persisted migration Resource props. - -## Changes - -- **Migration state:** Replace persisted `contractJson` with compact current-contract identity, kept distinct from migration target refs. -- **Contract reconciliation:** Load the emitted contract through the artifact path normalized by `prisma.config.ts`, attest it before database access, then enter the existing replay-only migration path. -- **Coverage:** Prove contracts larger than 100 KB do not enlarge persisted migration props, and verify unreadable or mismatched artifacts fail before database work. -- **Documentation:** Align ADR-0022, local-development guidance, the app-building guide, and core-concepts material with the config-loaded-contract/compact-state boundary. - -## Why - -Alchemy persists every Resource input prop, so carrying the complete contract made deployability depend on contract size. Keeping only compact identity in state removes that coupling while preserving `OrmMigration` as a dependency-aware Resource and retaining target-ref, invariant, extension-pack, marker, retry, and replay-only semantics. diff --git a/projects/compact-orm-migration-state/reviews/code-review.md b/projects/compact-orm-migration-state/reviews/code-review.md deleted file mode 100644 index 465c39a2..00000000 --- a/projects/compact-orm-migration-state/reviews/code-review.md +++ /dev/null @@ -1,46 +0,0 @@ -# Code review — `compact-orm-migration-state` - -> Initial scaffold. The reviewer maintains this document across rounds. The orchestrator and implementer read it but do not edit it. - -## Summary - -- **Current verdict:** SATISFIED -- **Dispatches SATISFIED:** D1 -- **Acceptance scoreboard totals:** 3 PASS / 0 FAIL / 0 NOT VERIFIED -- **Open findings:** 0 -- **Open escalations:** 0 - -## Acceptance criteria scoreboard - -| AC ID | Description | Dispatch | Status | Evidence | -| ----- | ----------- | -------- | ------ | -------- | -| AC-1 | State for a contract larger than 100 KB contains no full contract | D1 | PASS | commit `78f40b46`; large-contract persistence proof in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts` | -| AC-2 | Contract loading and attestation fail before migration on mismatch/unreadable artifact | D1 | PASS | commit `78f40b46`; pre-DB mismatch/unreadable coverage in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts` | -| AC-3 | Existing migration semantics remain covered and design documentation is aligned | D1 | PASS | commit `78f40b46`; target-ref/head coverage in `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts`; docs updated in ADR-0022/local-dev/building guide | - -## Subagent IDs - -- **Implementer:** `8007f277-6169-47f` — first spawned in D1 R1. -- **Reviewer:** `c1e42697-b937-434` — first spawned in D1 R1. - -## Orchestrator notes - -- Linear tracking was explicitly waived by the operator for this project. - -## Findings log - -_(no findings yet)_ - -## Round notes - -### D1 R1 — SATISFIED - -**Scope:** Dispatch D1. Commits `d0c8c4ee..78f40b46`. - -**Tasks:** Compact persisted migration state, pre-DB contract attestation, and governing docs clean. - -**AC delta:** AC-1 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts`). AC-2 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-migration-resource.test.ts`). AC-3 NOT VERIFIED → PASS (commit `78f40b46`, test `packages/1-prisma-cloud/1-extensions/target/src/__tests__/orm-target-ref.test.ts`; docs `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md`). - -**Findings:** none. - -**For orchestrator:** none. diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md deleted file mode 100644 index e41801fa..00000000 --- a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/brief-D1-R1.md +++ /dev/null @@ -1,39 +0,0 @@ -# Brief: Compact and attest OrmMigration state - -## Task - -Keep `PrismaOrm.Migration` as a tracked Alchemy Resource while removing the full emitted contract from persisted Resource props. Make reconciliation deterministically load the contract output identified by `prisma.config.ts`, attest it against the contract declared by the database resource before migration begins, then pass it into the unchanged replay-only migration path. Prove newly persisted migration state remains compact for a contract larger than 100 KB and align ADR-0022 plus directly affected domain documentation. - -## Scope - -**In:** `packages/1-prisma-cloud/1-extensions/target` migration-resource, ORM-config, descriptor, and directly related test surfaces; focused state-shape/persistence proof; ADR-0022 and directly affected domain docs; compatibility with ordinary convergence from prior rows; committing the already-authored `projects/compact-orm-migration-state/` artifacts without modifying their intent. - -**Out:** Generic transient Resource props; converting the migration to an Action; Management API changes; proactive legacy-state compaction; migration graph, marker, target-ref, invariant, extension-pack, retry, or local/hosted semantic changes; unrelated cleanup. - -## Completed when - -- [ ] `OrmMigration` persisted props contain compact contract attestation but no `contractJson` or equivalent full-contract value, with current-contract attestation distinct from `targetHash` for named refs. -- [ ] Reconciliation loads the config-declared emitted contract via Prisma ORM-supported APIs and rejects missing/unreadable or identity-mismatched artifacts before opening or mutating the database. -- [ ] Focused tests include a contract larger than 100 KB and prove the newly persisted migration state is independent of that contract's size while existing migration behavior remains covered. -- [ ] ADR-0022 and directly affected domain documentation distinguish deploy-time contract loading from compact persisted migration state. -- [ ] Canonical targeted tests, target-package typecheck/build checks, and lint for changed files pass; report the exact commands selected from repository conventions. - -## Standing instruction - -Stay focused on the goal; control scope. Trivial-and-related fixes that obviously serve the goal go in the same dispatch with a one-line note in your wrap-up message. Anything that pulls you off the goal—even if useful—halts and surfaces. - -## References - -- Slice spec: `projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md` -- Slice plan: `projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md` -- Project spec: `projects/compact-orm-migration-state/spec.md` -- Project plan: `projects/compact-orm-migration-state/plan.md` -- Code review log: `projects/compact-orm-migration-state/reviews/code-review.md` (read-only) -- Governing principles: `docs/design/01-principles/` -- Governing decisions: ADR-0022, ADR-0041, ADR-0045 - -## Operational metadata - -- **Model tier:** mid — cross-cutting Resource-state and ORM-loader change with tests and design docs. -- **Time-box:** 90 minutes. Overrun means halt and surface. -- **Halt conditions:** Prisma ORM has no supported way to load the config-declared emitted contract; satisfying the task requires filename guessing or an in-memory/global side channel; config and declared contract cannot be attested without changing public authoring semantics; an out-of-scope surface must change; validation cannot be made green in scope. diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md deleted file mode 100644 index 990c7f0b..00000000 --- a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md +++ /dev/null @@ -1,9 +0,0 @@ -# Compact migration resource state — Dispatch plan - -## Dispatch 1: Compact and attest OrmMigration state - -- **Outcome:** Newly converged `PrismaOrm.Migration` resources persist no full contract, while reconciliation deterministically reloads and attests the config-declared contract before invoking the unchanged migration engine; focused tests prove the behavior with a contract larger than 100 KB and documentation records the boundary. -- **Builds on:** The slice spec's chosen design and the existing `OrmMigration`, ORM config-resolution, Postgres descriptor, and replay-only migration surfaces. -- **Hands to:** A reviewed, deployable Composer change whose Alchemy state size is independent of ORM contract size, with mismatch failures occurring before migration and the design documented durably. -- **Focus:** Remove `contractJson` from persisted migration props; add compact current-contract attestation distinct from target ref; use Prisma ORM-supported config/contract loading; preserve existing migration semantics and local/hosted parity; add focused lowering, provider, and persistence coverage; update ADR-0022 and directly affected domain docs. Do not introduce generic transient props, convert to Action, modify the Management API, or proactively compact legacy rows. -- **Validation gate:** Run the directly affected target-extension test files, including the focused large-contract persistence and mismatch tests; run the target package's typecheck/build gate; run repository lint for changed files. The implementer must identify and report the repository's canonical concrete commands during reconnaissance, then run the complete selected gate once at dispatch completion. diff --git a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md b/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md deleted file mode 100644 index d38f05d5..00000000 --- a/projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md +++ /dev/null @@ -1,58 +0,0 @@ -# Slice: Compact migration resource state - -_(Parent project: `projects/compact-orm-migration-state/`. This slice makes ORM contract size independent of newly persisted Alchemy state.)_ - -## At a glance - -Change `PrismaOrm.Migration` so its persisted props carry compact contract identity rather than the complete emitted contract. Reconciliation reloads the config-declared contract, attests it against the database resource's declaration, and then enters the unchanged replay-only migration path. - -## Chosen design - -`OrmMigrationProps` no longer contains `contractJson`. It carries a compact attestation of the current declared contract alongside the existing target-ref identity, invariants, extension-pack identities, paths, and resolved database URL. - -The Postgres descriptor continues to read the declared `dataContract` while lowering. It derives the compact attestation and target ref there, then creates `OrmMigration` without embedding the contract. - -During reconciliation, the provider uses Prisma ORM's supported config and contract-loading surfaces to load the contract output identified by `prisma.config.ts`. Before opening or mutating the database, it compares the loaded contract's storage identity with the persisted declaration attestation. A mismatch or unreadable artifact fails the deploy explicitly. A successful check supplies the loaded full contract to the existing `applyOrmMigration` function; migration planning and execution remain unchanged. - -The current contract attestation is distinct from `targetHash`: a named target ref may intentionally select an older migration destination while the config still identifies the current emitted contract artifact. - -## Coherence rationale - -The Resource shape, deterministic reload path, attestation, large-contract proof, and governing documentation are one invariant and one rollback unit: the migration receives the right full contract without persisting it. Splitting them would temporarily leave reconciliation unable to run or able to use an unattested artifact. - -## Scope - -**In:** `OrmMigration` props and provider reconciliation; ORM config resolution required to load the emitted contract; Postgres descriptor wiring; focused lowering/provider/persistence tests including a contract larger than 100 KB; compatibility with prior persisted props during ordinary convergence; ADR-0022 and directly affected domain documentation. - -**Out:** Generic transient Resource props; Alchemy Action conversion; Management API limits; proactive legacy-state compaction; changes to migration graph planning, marker semantics, target refs, invariants, extension-pack execution, retries, or local/hosted provider selection. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| Emitted contract exceeds 100 KB | Cover with focused persistence proof | The full value must not appear under any equivalent persisted prop. | -| Named target ref points behind the current contract head | Keep current-contract attestation separate from `targetHash` | Selecting an older authored destination remains valid. | -| Config-loaded contract differs from the declared `dataContract` | Fail before migration or database mutation | Never silently choose either source. | -| Existing state contains legacy `contractJson` props | Tolerate through ordinary convergence | No proactive rewrite or cleanup is required. | - -## Slice-specific done conditions - -- [ ] A focused persistence test with a contract larger than 100 KB proves newly persisted `PrismaOrm.Migration` state contains no full-contract value. -- [ ] Contract mismatch and unreadable-contract failures occur before migration execution. -- [ ] ADR-0022 and directly affected domain documentation describe the compact-state/config-loaded contract boundary. - -## Open Questions - -None. - -## References - -- Parent project: `projects/compact-orm-migration-state/spec.md` -- Linear issue: N/A — operator waived Linear tracking. -- `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md` -- `docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md` -- `docs/design/90-decisions/ADR-0045-deploy-state-lives-behind-the-platform-state-api.md` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts` diff --git a/projects/compact-orm-migration-state/spec.md b/projects/compact-orm-migration-state/spec.md deleted file mode 100644 index 675d7946..00000000 --- a/projects/compact-orm-migration-state/spec.md +++ /dev/null @@ -1,65 +0,0 @@ -# Compact OrmMigration state - -## Purpose - -Allow Composer to deploy Prisma ORM contracts larger than the Management API's 100 KB Alchemy-state write limit. The size of an application's contract must not determine whether Composer can persist deployment state. - -## At a glance - -Today `PrismaOrm.Migration` carries the full emitted `contractJson` as an Alchemy Resource prop. Alchemy persists every prop, so a sufficiently large contract makes the state write fail even though the migration result itself is only a compact schema identity. - -After this project, the migration remains a tracked Alchemy Resource, but its persisted state contains only compact identity and location data. At reconcile time, Composer deterministically loads the deploy-time contract artifact identified by `prisma.config.ts`, verifies that it matches the contract declared by the database resource, and hands the loaded contract to Prisma ORM's replay-only migration engine. Contract size therefore affects local migration processing, not hosted-state payload size. - -## Non-goals - -- Raising or changing the Management API's 100 KB state-write limit. -- Introducing a generic Alchemy facility for non-persisted or transient Resource props. -- Converting `OrmMigration` from a Resource to an Alchemy Action. -- Proactively compacting existing `PrismaOrm.Migration` state rows; old rows may remain in their existing shape until ordinary convergence rewrites them. -- Changing migration planning, target-ref, invariant, extension-pack, replay-only, retry, or database-marker semantics. -- Recovering state rows that a platform-side response limit prevents the client from reading. - -## Place in the larger world - -- `OrmMigration` is a Composer-owned Alchemy Resource in the Prisma Cloud target extension. It remains the tracked, dependency-aware deploy step established by ADR-0022 and shared unchanged by hosted deploy and local development under ADR-0041. -- Alchemy's stock state client persists Resource input props through the Management API route established by ADR-0045. Alchemy has no per-prop opt-out, so the contract must cease to be a Resource prop rather than merely be ignored by provider diffing. -- Prisma ORM remains the contract and migration authority. Composer uses Prisma ORM's stock config and contract-loading surfaces; it does not infer filenames or reconstruct contracts. -- The database resource's declared `dataContract` remains the runtime and wiring contract. The contract output identified by `prisma.config.ts` becomes the deploy-time artifact consumed by migration reconciliation, with an explicit identity check connecting the two. - -## Cross-cutting requirements - -- No full contract value may appear in the persisted inputs of a newly converged `PrismaOrm.Migration` resource; persisted state size must be independent of contract size. -- Migration reconciliation must load the contract through the path and output semantics declared by `prisma.config.ts`, using Prisma ORM's supported loaders rather than filename or directory guessing. -- The Resource must persist a compact attestation for the declared contract and fail before migration if the loaded deploy-time artifact does not match it. A named migration target may point to an older ref, so the current contract attestation and target-ref hash remain distinct concepts. -- The existing target identity remains complete: target hash, sorted invariants, and sorted extension-pack head hashes must continue to trigger the same migration decisions as before. -- Hosted deployment and local development must continue to use the same `OrmMigration` Resource and provider behavior. -- Deploy remains replay-only: the loaded contract supplies Prisma ORM's destination-contract context but never authorizes Composer to synthesize schema operations. -- The change must preserve the architectural rule that the framework does not bundle, transform, discover, or guess application artifacts. - -## Transitional-shape constraints - -- Existing persisted rows require no proactive migration. During rollout, Composer must tolerate prior rows containing `contractJson`; normal convergence may replace them with the compact shape. -- Every intermediate state must either use the existing contract prop safely or use the new attested config-loading path; no intermediate state may migrate using an unverified contract artifact. - -## Project Definition of Done - -- [ ] Team-DoD floor items are inherited from the repository's canonical team-DoD when present. -- [ ] A focused persistence test uses an emitted contract larger than 100 KB and proves newly persisted `PrismaOrm.Migration` state contains no `contractJson` or equivalent full-contract value. -- [ ] `OrmMigration` reconciliation loads the contract identified by `prisma.config.ts` and successfully supplies it to the unchanged Prisma ORM migration path. -- [ ] A mismatch between the database resource's declared contract attestation and the config-loaded contract fails before any migration operation runs. -- [ ] Existing target-ref, invariant, extension-pack, hosted-deploy, and local-development behavior remains covered and passing. -- [ ] ADR-0022 and affected domain documentation distinguish the deliberately carried deploy-time contract from the compact state persisted for the migration Resource. - -## Open Questions - -None. - -## References - -- `docs/design/90-decisions/ADR-0022-data-deps-carry-a-prisma-orm-contract.md` -- `docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md` -- `docs/design/90-decisions/ADR-0045-deploy-state-lives-behind-the-platform-state-api.md` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-migration-resource.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/descriptors/orm-postgres.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-config.ts` -- `packages/1-prisma-cloud/1-extensions/target/src/orm-migrate.ts` diff --git a/projects/compact-orm-migration-state/trace.jsonl b/projects/compact-orm-migration-state/trace.jsonl deleted file mode 100644 index 0f8d8315..00000000 --- a/projects/compact-orm-migration-state/trace.jsonl +++ /dev/null @@ -1,17 +0,0 @@ -{"event_id":"84340b75-bd19-44e5-8e2d-07c1af54e767","schema_version":"1","ts":"2026-09-01T15:28:05.432Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/compact-orm-migration-state/spec.md","spec_kind":"project","byte_length":5889,"edge_cases_count":null,"open_questions_count":0,"dod_items_count":6} -{"event_id":"810fdf6b-0966-4f41-bf0e-ca3584092023","schema_version":"1","ts":"2026-09-01T15:36:51.072Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/compact-orm-migration-state/plan.md","plan_kind":"project","byte_length":2307,"dispatch_count":null,"slice_count":1,"dispatch_size_distribution":null,"open_items_count":0} -{"event_id":"20ecf06c-4040-4862-b1ab-e06109bfd17c","schema_version":"1","ts":"2026-09-01T15:37:26.634Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-amended","plan_path":"projects/compact-orm-migration-state/plan.md","plan_kind":"project","byte_length":2315,"bytes_delta":8,"dispatch_count":null,"slice_count":1,"dispatch_size_distribution":null,"open_items_count":0,"reason":"operator-correction","dispatches_added":null,"dispatches_removed":null,"dispatches_resized":null} -{"event_id":"fce29fcf-ae8a-401e-a93d-cc589cb16725","schema_version":"1","ts":"2026-09-01T15:51:07.794Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"health-check-fired","cadence":"opening-rollup","drift_signal_count":0,"max_drift_severity":"none","recommended_next":"Author compact-migration-resource-state slice spec and dispatch plan, then run the slice."} -{"event_id":"fa3945c7-bd02-463a-b8a8-aea4660ac94b","schema_version":"1","ts":"2026-09-01T15:51:09.678Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"spec-authored","spec_path":"projects/compact-orm-migration-state/slices/compact-migration-resource-state/spec.md","spec_kind":"slice","byte_length":4529,"edge_cases_count":4,"open_questions_count":0,"dod_items_count":3} -{"event_id":"272b0bbd-6b3e-4c48-aa6f-ba9e406e9b95","schema_version":"1","ts":"2026-09-01T15:54:33.718Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"plan-authored","plan_path":"projects/compact-orm-migration-state/slices/compact-migration-resource-state/plan.md","plan_kind":"slice","byte_length":1691,"dispatch_count":1,"slice_count":null,"dispatch_size_distribution":{"S":0,"M":1,"L":0,"XL":0},"open_items_count":0} -{"event_id":"f8229593-bba0-442d-b2fe-4fbbc934be9c","schema_version":"1","ts":"2026-09-01T15:54:35.416Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"slice-started","slice_slug":"compact-migration-resource-state","slice_index":1,"linear_ref":null} -{"event_id":"2cb1ea16-4caf-4484-a9fc-0be277d8a559","schema_version":"1","ts":"2026-09-01T15:56:49.017Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","dispatch_name":"D1 compact and attest OrmMigration state","subagent_type":"general-purpose","model":"sonnet","parent_dispatch_id":null} -{"event_id":"ed0c5ad9-7677-4da1-838c-975210ff25c4","schema_version":"1","ts":"2026-09-01T15:56:50.910Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","round_number":1} -{"event_id":"fb762e09-a000-40d8-a5dc-d94548b5278a","schema_version":"1","ts":"2026-09-01T15:56:53.162Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","brief_byte_length":3537,"brief_content_hash":"dd78634a2b27e56c4aefcbabd1ddcceac64e03d8fb83d957c93db5ec4f712fc4","brief_disposition":"initial"} -{"event_id":"8b045f8e-8d6a-425b-8ad5-cfc0cc367134","schema_version":"1","ts":"2026-09-01T15:58:31.475Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","round_id":"ae991534-0400-4adb-8ff0-de843e8894a9","verdict":"stop-condition","findings_filed":0,"wall_clock_ms":96653} -{"event_id":"7cf4ea84-1189-4404-99a1-adbaf918122d","schema_version":"1","ts":"2026-09-01T15:58:33.003Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7","result":"failed","wall_clock_ms":98546} -{"event_id":"e9e57139-22ab-4ae7-b7bb-c93fb8bd9239","schema_version":"1","ts":"2026-09-01T15:58:34.611Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","dispatch_name":"D1 compact and attest OrmMigration state","subagent_type":"general-purpose","model":"openai-codex/gpt-5.4","parent_dispatch_id":"4e9a31ba-4219-4de3-8e5a-695cac0268c7"} -{"event_id":"d03b35b1-7bda-463e-9d22-d5723a8fc594","schema_version":"1","ts":"2026-09-01T15:58:36.175Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-start","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","round_number":1} -{"event_id":"b3dddee8-b2de-43c4-a719-a213177f60ff","schema_version":"1","ts":"2026-09-01T15:58:37.946Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"brief-issued","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","brief_byte_length":3537,"brief_content_hash":"dd78634a2b27e56c4aefcbabd1ddcceac64e03d8fb83d957c93db5ec4f712fc4","brief_disposition":"initial"} -{"event_id":"d52deb37-18e0-4a13-ab14-70bb37441b80","schema_version":"1","ts":"2026-09-01T16:58:51.003Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"round-end","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","round_id":"e1a8467f-dbba-4eaf-980c-a9ab3477cdd7","verdict":"satisfied","findings_filed":0,"wall_clock_ms":3609474} -{"event_id":"ff888e4d-6930-4795-be3c-dd854ed4b55d","schema_version":"1","ts":"2026-09-01T16:58:53.197Z","project_run_id":"compact-orm-migration-state","orchestrator_agent_id":null,"event_type":"dispatch-end","dispatch_id":"3fec004e-c3c3-47f8-afc2-8f5c35f8d509","result":"completed","wall_clock_ms":3611038}