Skip to content

TML-2536: route on-disk contract reads through ContractSerializer + strict deserializer (v2)#533

Merged
wmadden merged 38 commits into
mainfrom
tml-2536-contract-deserializer-seam-v2
May 19, 2026
Merged

TML-2536: route on-disk contract reads through ContractSerializer + strict deserializer (v2)#533
wmadden merged 38 commits into
mainfrom
tml-2536-contract-deserializer-seam-v2

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented May 18, 2026

Re-cut of #520 against current origin/main (the original branch became unrebaseable due to overlapping CLI refactors). Same scope, cleaner history, planned as an orphan slice using drive-domain-model (D0–D7).

Resolves TML-2536.
Closes #520.

Summary

  • Bug: Four CLI commands (migration plan, migration new, migration show, migrate) read contracts off disk via JSON.parse(...) as Contract, bypassing the family's ContractSerializer and skipping the kind-discriminator canonicalization that storage.types requires. A silent legacy-shape normalizer in SqlStorage was masking the divergence in-process, so nothing failed loudly even when the demo's migrations stopped functioning.
  • Fix: Every on-disk contract read now crosses the serializer seam (familyInstance.validateContract) at the read site. The SqlStorage.normaliseTypeEntry fallthrough is removed so the deserializer is strict. Both behaviours are enforced going forward by a structural lint (lint:no-contract-cast) and a demo-level CI regression gate (demo-migration-plan).
  • Breaking change: pre-strict on-disk contracts (storage.types carrying untagged [name, sql-name, codec-ref] triples) no longer parse. Shipped with codemods + instructions for both end-users (skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10) and extension authors (skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10).
  • Manual QA: projects/tml-2536-contract-deserializer-seam/manual-qa.md walks 11 scenarios end-to-end through the demo (including the two upgrade journeys and the re-enactment of the original PN-CLI-4020).

What's in the slice (D0–D6)

D Title Notes
D0 Spec + plan + scout cherry-picks 47763874d + 65e18457e (drive-domain-model brief), plus deterministic prerequisite picks (dd054f651, 7dbbfe609, a918547ae, ed21bdcb9, d0c8bee5a)
D1 CLI seam closure 9c0eb39bcmigration-plan.ts, migration-new.ts, migration-show.ts, migrate.ts all now go through validateContract; db-verify.ts clarifying comment; test mocks updated
D2 End-user upgrade entry 607c551150.9-to-0.10/instructions.md + stamp-storage-types-kind.ts codemod (preserves CLI compact-array on-disk shape)
D3 Extension-author upgrade entry a86ed0051 — mirrors D2 plus source-side rules (toStorageTypeInstance / PostgresEnumType)
D4 Manual QA revision 52b10a906 — adds upgrade-journey scenarios for both audiences
D5 Fixups bundle Lint guard 8d7b3fc4e, demo CI gate dbf1bcae4 + --json 214554ab7, test hygiene bd64120b4 + 82e7cce44, and demo-state bookend 2751bf7cb (absorbs TML-2521 namespaces drift so the gate is green)
D6 Blind-cast rationale comments 6c4800d21 — documents the remaining ten as unknown as Contract allowlist entries

Validation

  • pnpm typecheck — green
  • pnpm lint:no-contract-cast — clean (no as Contract casts outside the rationale-comment allowlist)
  • pnpm lint:deps — clean
  • pnpm check:upgrade-coverage --mode pr — clean
  • pnpm fixtures:check — clean
  • pnpm --filter @prisma-next/cli test — 27/27
  • pnpm --filter prisma-next-demo migration:plan:check — clean (plan is no-op, no file drift)
  • rg "as Contract" packages/1-framework/3-tooling/cli/src/commands/ — zero non-test hits
  • pnpm test:packages — 8719/8733 pass; 7 failures across 3 files are infrastructure-flaky (postgres connection / portal C_17 / cross-file parallelism). client.test.ts passes 27/27 when run in isolation.

Test plan

  • CI green on this PR (especially demo-migration-plan and lint:no-contract-cast).
  • Walk through the upgrade-journey scenarios in projects/tml-2536-contract-deserializer-seam/manual-qa.md (Scenarios 10 + 11) on a fresh demo checkout.
  • Re-enact PN-CLI-4020 by reverting Scenario 0's codemod application and confirming the strict deserializer raises a structured error pointing at the offending file.

Summary by CodeRabbit

  • New Features

    • Contracts are now deserialized via the family serializer at read time across CLI and migration flows.
    • Added a regression lint script to detect raw "as Contract" casts.
    • Codemods and scripts to stamp required type metadata on migration snapshots; migrations now emit explicit start/end snapshots.
  • Bug Fixes

    • Clearer, contextual error messages for contract deserialization/validation failures.
    • More consistent contract handling across CLI commands.
  • Documentation

    • Upgrade guide for 0.9→0.10 and updated migration system docs.
  • Tests

    • New snapshot deserialization coverage for Postgres-shaped contracts.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Routes on-disk contract reads through familyInstance.deserializeContract; enforces explicit kind on SQL storage.type entries; updates CLI, family APIs, serializers, tests, fixtures, lint script, upgrade codemods, and error exports to match the new seam and validation model.

Changes

Contract deserialization & storage-type normalization

Layer / File(s) Summary
Migration snapshots & fixtures
examples/prisma-next-demo/migrations/**, packages/3-targets/3-targets/postgres/test/fixtures/*, packages/3-targets/3-targets/postgres/test/snapshot-read-shapes.test.ts
Regenerated demo migrations and added Postgres snapshot fixtures; start/end contract snapshots and read-shape tests include storage.types kind discriminators and assert serializer deserialization.
SqlStorage strict normalization
packages/2-sql/1-core/contract/src/ir/sql-storage.ts, packages/2-sql/1-core/contract/test/sql-storage.test.ts
SqlStorage requires explicit kind on storage.types; untagged triples no longer auto-convert and produce diagnostics naming offending entries; tests validate acceptance/rejection cases.
CLI & migration tooling: deserialize at read site
packages/1-framework/3-tooling/cli/src/commands/*, packages/1-framework/3-tooling/migration/src/*
Commands now build control stack/familyInstance early and call familyInstance.deserializeContract(JSON.parse(...)) when reading on-disk contracts; aggregate loader inputs accept deserializeContract.
Control API & client wiring
packages/1-framework/3-tooling/cli/src/control-api/*, packages/1-framework/3-tooling/cli/src/control-api/operations/*
Control client operations switched from validateContract → deserializeContract; emit/enrich flows call deserialize prior to downstream processing.
Family API & serializers
packages/2-sql/9-family/*, packages/2-mongo-family/9-family/*, packages/1-framework/1-core/framework-components/*
Renamed family method validateContractdeserializeContract; family implementations now expose deserialize seam; serializers are canonical deserializers for on-disk reads.
Blind-cast pattern & load-time casts
packages/*/*/load-ts-contract.ts, packages/*/*/control-target.ts, packages/3-targets/*/src/*
Replaced bare as Contract with as unknown as Contract and added comments documenting that validation is expected upstream; sqlite target added runtime instanceof SqlStorage guard.
Lint & CI regression guard
scripts/lint-no-contract-cast.mjs, package.json, .github/workflows/ci.yml
Added grep-based lint to fail on bare as Contract in production sources (allow as unknown as Contract), with comment/string stripping and allowlist; wired into CI lint job via pnpm lint:code.
Upgrade docs & codemods
skills/upgrade/*, skills/extension-author/* (stamp-storage-types-kind.ts)
Added user/extension-author upgrade instructions and idempotent codemods to stamp kind into migration snapshots with --check dry-run and strict validation.
ContractValidationError API & exports
packages/1-framework/0-foundation/contract/src/contract-validation-error.ts, src/exports/*
Introduced ContractValidationError and ContractValidationPhase, added re-export, removed legacy validate-contract export surface; updated imports/tests across codebase.
SQL validators refactor
packages/2-sql/1-core/contract/src/validators.ts, tests
Internalized structural validator and switched callers/tests to validateSqlContractFully, updating tests accordingly.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I'm a rabbit who hops through the seam,
I stamp each contract tidy and clean,
No blind cast will pass,
Deserialize at the last,
Hop — now storage kinds are seen!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2536-contract-deserializer-seam-v2

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-author-tools@533

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@533

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@533

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@533

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@533

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-arktype-json@533

@prisma-next/extension-cipherstash

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-cipherstash@533

@prisma-next/middleware-cache

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-cache@533

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@533

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@533

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@533

@prisma-next/extension-postgis

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-postgis@533

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@533

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@533

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@533

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@533

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@533

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@533

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@533

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@533

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@533

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@533

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@533

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@533

@prisma-next/ts-render

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ts-render@533

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@533

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@533

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@533

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@533

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@533

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@533

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@533

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@533

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@533

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@533

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@533

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@533

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@533

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@533

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@533

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@533

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@533

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@533

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-builder@533

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@533

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@533

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@533

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@533

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@533

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@533

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@533

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@533

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@533

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@533

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@533

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@533

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@533

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@533

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@533

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@533

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@533

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@533

commit: 549bfde

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
packages/1-framework/3-tooling/cli/src/commands/migrate.ts (1)

176-177: ⚡ Quick win

Keep the read-site boundary explicit with as unknown.

This now crosses the serializer seam correctly, but the repo pattern for on-disk reads is validateContract(JSON.parse(raw) as unknown). Keeping the parsed payload as unknown here avoids silently reintroducing an untyped boundary at the exact seam this PR is tightening.

Suggested fix
-    contractRaw = familyInstance.validateContract(JSON.parse(contractContent));
+    contractRaw = familyInstance.validateContract(JSON.parse(contractContent) as unknown);

As per coding guidelines packages/**/src/**/*.ts: "All on-disk contract reads must route through the seam: call familyInstance.validateContract(JSON.parse(raw) as unknown) instead of casting JSON.parse(...) as Contract".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/3-tooling/cli/src/commands/migrate.ts` around lines 176
- 177, The JSON parse at the on-disk boundary is missing the explicit unknown
cast; update the call that constructs contractRaw so the parsed payload is
treated as unknown before validation: pass JSON.parse(contractContent) as
unknown into familyInstance.validateContract (i.e., use the unknown cast at the
seam where contractContent is parsed) so the code reads through the serializer
seam rather than reintroducing an untyped boundary.
packages/1-framework/3-tooling/cli/src/commands/migration-new.ts (1)

99-101: ⚡ Quick win

Use the repo’s explicit unknown seam pattern here too.

The command now validates at the read site, which is the right fix. It should still keep the parsed JSON at unknown when crossing that seam so this path matches the workspace rule used elsewhere.

Suggested fix
-    toContract = familyInstance.validateContract(JSON.parse(contractJsonContent));
+    toContract = familyInstance.validateContract(JSON.parse(contractJsonContent) as unknown);

As per coding guidelines packages/**/src/**/*.ts: "All on-disk contract reads must route through the seam: call familyInstance.validateContract(JSON.parse(raw) as unknown) instead of casting JSON.parse(...) as Contract".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts` around
lines 99 - 101, The code currently parses JSON directly into a typed value
before validation; instead pass the parse result through the repo's unknown
seam: call familyInstance.validateContract(JSON.parse(contractJsonContent) as
unknown) to produce toContract. Update the call site referencing
familyInstance.validateContract, the toContract assignment, and the
contractJsonContent parse so the raw read is cast to unknown before validation.
packages/1-framework/3-tooling/cli/src/commands/migration-show.ts (1)

379-381: ⚡ Quick win

Preserve the explicit unknown boundary on the app-contract read.

The seam is now in the right place. One small follow-up: keep JSON.parse(...) wrapped as unknown before passing it to validateContract(...) so this read site follows the repo’s standard deserialization pattern.

Suggested fix
-    appContract = familyInstance.validateContract(JSON.parse(contractJsonContent));
+    appContract = familyInstance.validateContract(JSON.parse(contractJsonContent) as unknown);

As per coding guidelines packages/**/src/**/*.ts: "All on-disk contract reads must route through the seam: call familyInstance.validateContract(JSON.parse(raw) as unknown) instead of casting JSON.parse(...) as Contract".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-show.ts` around
lines 379 - 381, The call to familyInstance.validateContract currently passes
JSON.parse(contractJsonContent) directly; wrap the parsed value with the
explicit unknown seam before validation (i.e., pass
JSON.parse(contractJsonContent) as unknown into familyInstance.validateContract)
so appContract is produced via the repository's standard deserialization pattern
used by validateContract.
packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts (1)

99-100: ⚡ Quick win

Apply the explicit unknown boundary at both contract read sites.

This file now routes both on-disk contracts through the serializer seam, which is good. To fully match the repo contract-read pattern, both parsed payloads should cross that seam as unknown rather than as an implicit untyped value.

Suggested fix
-    return validateContract(JSON.parse(raw));
+    return validateContract(JSON.parse(raw) as unknown);
-    toContract = familyInstance.validateContract(JSON.parse(contractJsonContent));
+    toContract = familyInstance.validateContract(JSON.parse(contractJsonContent) as unknown);

As per coding guidelines packages/**/src/**/*.ts: "All on-disk contract reads must route through the seam: call familyInstance.validateContract(JSON.parse(raw) as unknown) instead of casting JSON.parse(...) as Contract".

Also applies to: 216-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts` around
lines 99 - 100, The parsed JSON is being passed as an untyped value; change both
contract read sites to cross the serializer seam with an explicit unknown
boundary by casting the JSON.parse payload to unknown before validating—e.g.
replace validateContract(JSON.parse(raw)) with validateContract(JSON.parse(raw)
as unknown) (and similarly where
familyInstance.validateContract(JSON.parse(raw)) appears around the later block)
so both reads use the explicit unknown boundary.
.cursor/rules/contract-normalization-responsibilities.mdc (1)

1-120: 💤 Low value

Consider extracting the diagram to keep the rule file under the soft limit.

The file is at 120 lines, exceeding the 100-line soft limit for rulecard files. The 32-line ASCII diagram (lines 15-46) is valuable but could be moved to a durable location under docs/architecture docs/ and linked from this rule, per the guideline that says "move long examples to docs/". This would bring the rule file back under the soft limit while keeping the diagram accessible.

As per coding guidelines: "Keep rulecard files under 100 lines (soft limit) and never exceed 200 lines (hard limit); move long examples to docs/"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.cursor/rules/contract-normalization-responsibilities.mdc around lines 1 -
120, The rule file "Contract normalization responsibilities" exceeds the
100-line soft limit because of the large ASCII diagram; extract that diagram
block (the on-disk JSON → familyInstance.validateContract →
target.contractSerializer.deserializeContract flow) into a new durable doc under
docs/architecture (e.g., "contract-serialization-seam.md"), paste the diagram
there with any needed caption, then replace the diagram in the rule with a short
one-line pointer/link and a brief sentence referencing the moved diagram (and
ensure the rule stays under 100 lines); update any internal references that cite
the diagram to the new docs/architecture location.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`:
- Around line 99-115: The traversal currently collects any CONTRACT_FILES found
anywhere (in walk), which can rewrite non-migration fixtures; limit collection
to files that live under a migrations ancestor by checking the directory
ancestry before pushing to out. Update the walk function (and its call sites if
needed) so that when you encounter a file (the branch using CONTRACT_FILES and
out.push) you first verify the current dir path contains a directory named
"migrations" (e.g., by checking dir path segments or carrying an inMigrations
flag during recursion) and only push join(dir, entry.name) when that check
passes; keep SKIP_DIRS logic and other behavior unchanged.
- Line 80: The import uses join from 'node:path' which violates the TypeScript
guideline; replace the import to use 'pathe' instead and update any references
to the join symbol accordingly (change the import statement that currently
imports join from 'node:path' to import join from 'pathe' so the existing usages
of join in this file—e.g., in functions or constants that build paths—use
pathe's cross-platform implementation).

In `@skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/instructions.md`:
- Line 77: Update the serializer description to reflect the actual codemod
behavior: replace the claim that it uses JSON.stringify(value, null, 2) + '\n'
with a note that the script uses a custom formatter which inlines short
primitive arrays while otherwise pretty-printing (matching the codemod's
serializer implementation), so that readers understand the inlining behavior and
won't expect a strict JSON.stringify output.

In
`@skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`:
- Around line 99-115: The walk function currently adds any CONTRACT_FILES found
anywhere; restrict rewrites to only snapshot files under a migrations/ ancestor
by threading an inMigrations boolean through the recursion: change walk(dir:
string) to walk(dir: string, inMigrations = false), set nextInMigrations =
inMigrations || entry.name === 'migrations' before recursing, and only push
join(dir, entry.name) to out when entry.isFile() &&
CONTRACT_FILES.has(entry.name) && inMigrations is true; update the initial call
to walk(root) accordingly and preserve SKIP_DIRS handling.
- Line 80: The file currently imports join from 'node:path' but project TS
tooling expects 'pathe'; update the import to pull join from 'pathe' and keep
all usages of join (seen around the calls that construct paths on the lines
where join is used) unchanged — replace the import statement that references
'node:path' with an import from 'pathe' so the codemod uses the repo-standard
path library.

---

Nitpick comments:
In @.cursor/rules/contract-normalization-responsibilities.mdc:
- Around line 1-120: The rule file "Contract normalization responsibilities"
exceeds the 100-line soft limit because of the large ASCII diagram; extract that
diagram block (the on-disk JSON → familyInstance.validateContract →
target.contractSerializer.deserializeContract flow) into a new durable doc under
docs/architecture (e.g., "contract-serialization-seam.md"), paste the diagram
there with any needed caption, then replace the diagram in the rule with a short
one-line pointer/link and a brief sentence referencing the moved diagram (and
ensure the rule stays under 100 lines); update any internal references that cite
the diagram to the new docs/architecture location.

In `@packages/1-framework/3-tooling/cli/src/commands/migrate.ts`:
- Around line 176-177: The JSON parse at the on-disk boundary is missing the
explicit unknown cast; update the call that constructs contractRaw so the parsed
payload is treated as unknown before validation: pass
JSON.parse(contractContent) as unknown into familyInstance.validateContract
(i.e., use the unknown cast at the seam where contractContent is parsed) so the
code reads through the serializer seam rather than reintroducing an untyped
boundary.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts`:
- Around line 99-101: The code currently parses JSON directly into a typed value
before validation; instead pass the parse result through the repo's unknown
seam: call familyInstance.validateContract(JSON.parse(contractJsonContent) as
unknown) to produce toContract. Update the call site referencing
familyInstance.validateContract, the toContract assignment, and the
contractJsonContent parse so the raw read is cast to unknown before validation.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts`:
- Around line 99-100: The parsed JSON is being passed as an untyped value;
change both contract read sites to cross the serializer seam with an explicit
unknown boundary by casting the JSON.parse payload to unknown before
validating—e.g. replace validateContract(JSON.parse(raw)) with
validateContract(JSON.parse(raw) as unknown) (and similarly where
familyInstance.validateContract(JSON.parse(raw)) appears around the later block)
so both reads use the explicit unknown boundary.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-show.ts`:
- Around line 379-381: The call to familyInstance.validateContract currently
passes JSON.parse(contractJsonContent) directly; wrap the parsed value with the
explicit unknown seam before validation (i.e., pass
JSON.parse(contractJsonContent) as unknown into familyInstance.validateContract)
so appContract is produced via the repository's standard deserialization pattern
used by validateContract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 8d75736c-2433-42eb-92e8-b8d639bd6a2a

📥 Commits

Reviewing files that changed from the base of the PR and between 2c6f676 and 6c4800d.

⛔ Files ignored due to path filters (3)
  • projects/tml-2536-contract-deserializer-seam/manual-qa.md is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/plans/plan.md is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/spec.md is excluded by !projects/**
📒 Files selected for processing (51)
  • .cursor/rules/as-contract-cast-smell.mdc
  • .cursor/rules/contract-normalization-responsibilities.mdc
  • .cursor/rules/validate-contract-usage.mdc
  • .github/workflows/ci.yml
  • docs/architecture docs/subsystems/7. Migration System.md
  • examples/prisma-next-demo/migrations/app/20260422T0720_initial/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260422T0742_migration/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260422T0742_migration/start-contract.json
  • examples/prisma-next-demo/migrations/app/20260422T0748_migration/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260422T0748_migration/start-contract.json
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/end-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/migration.json
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/migration.ts
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/ops.json
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/start-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260518T1701_namespaces_bookend/start-contract.json
  • examples/prisma-next-demo/package.json
  • examples/prisma-next-demo/scripts/check-migration-plan.ts
  • package.json
  • packages/1-framework/0-foundation/contract/src/hashing.ts
  • packages/1-framework/3-tooling/cli/src/commands/db-verify.ts
  • packages/1-framework/3-tooling/cli/src/commands/migrate.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-show.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts
  • packages/1-framework/3-tooling/cli/src/load-ts-contract.ts
  • packages/1-framework/3-tooling/cli/test/commands/migration-invariants.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/migration-tamper.test.ts
  • packages/1-framework/3-tooling/migration/src/aggregate/strategies/graph-walk.ts
  • packages/1-framework/3-tooling/migration/src/aggregate/strategies/synth.ts
  • packages/2-sql/1-core/contract/src/ir/sql-storage.ts
  • packages/2-sql/1-core/contract/test/sql-storage.test.ts
  • packages/2-sql/4-lanes/query-builder/src/root.ts
  • packages/2-sql/5-runtime/test/sql-context.codec-context.test.ts
  • packages/3-extensions/pgvector/test/migrations/planner.contract-to-schema-ir.test.ts
  • packages/3-mongo-target/1-mongo-target/src/core/control-target.ts
  • packages/3-targets/3-targets/postgres/src/exports/control.ts
  • packages/3-targets/3-targets/postgres/test/fixtures/snapshot-read-shapes/codec-instance.json
  • packages/3-targets/3-targets/postgres/test/fixtures/snapshot-read-shapes/postgres-enum.json
  • packages/3-targets/3-targets/postgres/test/snapshot-read-shapes.test.ts
  • packages/3-targets/3-targets/sqlite/src/core/control-target.ts
  • scripts/lint-no-contract-cast.mjs
  • skills-contrib/drive-code-review/SKILL.md
  • skills-contrib/drive-pr-local-review/SKILL.md
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/instructions.md
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts
  • skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts

Comment thread skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/instructions.md Outdated
wmadden added a commit that referenced this pull request May 19, 2026
Re-apply the drive-qa-plan skill against the shipped state of PR #533
(branch `tml-2536-contract-deserializer-seam-v2`) — the previous draft
was written in D4 against the now-superseded PR #520 and predated D5
(demo CI gate, lint guard, namespaces-bookend migration) and D6
(rationale comments).

What changed:

- Header now points at PR #533 and frames both audiences (CLI users +
  extension authors) up-front; the implementation-history bullet list
  in "What this script is testing" mentions the shipped guardrails by
  their concrete names (`lint:no-contract-cast`, the
  `Demo `migration plan` (no-op)` CI job, `stamp-storage-types-kind.ts`).
- Added an Isolation column to the TOC and an explicit `Isolation` line
  to every scenario (and the exploratory charter), tagging each with the
  strictest tag it actually needs: `workspace` for DB / tree mutators,
  `read-only` for the rule-coherence pass, `external` for the
  `gh pr checks` scenario.
- Added "Preconditions: no prerequisite scenarios" / "Scenario N
  completed" lines so the runner has explicit DAG edges (Scenario 2
  depends on 1; Scenario 8 depends on 1 or 3; everything else is
  independent).
- Stripped every severity pre-classification (🛑 Blocker / ⚠️ High /
  📝 Follow-up) from failure-mode lists in Scenarios 9, 10, 11. The
  script now enumerates failure-mode categories only; severity belongs
  in the runner report.
- Updated Scenario 4 (negative control for the strict deserializer) to
  target `migrations/app/20260518T1701_namespaces_bookend/end-contract.json`
  — the head migration that the planner now reads as the predecessor
  snapshot after D5's bookend addition. The old target
  (`20260422T0720_initial`) no longer exercises the predecessor-read
  path on its own.
- Updated Scenario 3 to apply the *full* checked-in history (initial
  plus bookend) and `migration show` both, so the re-enactment touches
  the literal pre-bug artefact and the new head.
- Updated Scenario 7 to reference PR #533 instead of #520 and to name
  the actual CI job display name (`Demo `migration plan` (no-op)`).
- Updated Scenario 10's oracle and step 8 expectations: the demo
  `migration plan` is now a *clean* no-op end-to-end (D5's namespaces
  bookend closed the residual TML-2521 drift), so the prior "you may
  still see PN-CLI-4020" carve-out is gone — any non-zero / non-no-op
  envelope is now a finding.
- Pre-flight now uses the workspace-canonical CLI invocation
  (`node ../../packages/.../cli.js`) the rest of the script already
  uses, so the demo-rooted scenarios all read the same way.
- "Scenarios deliberately not in this script" now explicitly excludes
  the static-pass half of AC-2 / AC-8 (re-running the lint over a clean
  tree) and AC-11; the sign-off coverage map maps AC-2 to Scenario 5
  (the gate-of-gate negative control) and points the AC-11 row at the
  exclusion table.

No scenarios were deleted under the litmus test — every existing
scenario already fell into one of the six legitimate buckets. Each
scenario now names which bucket it falls under so the runner can
quickly verify the script's honesty.

Scope is script-revision only: no spec / plan / source / CI / skill
changes. The runner skill (`drive-qa-run`) drives the script and owns
the report.
wmadden added a commit that referenced this pull request May 19, 2026
Manual-QA pass against PR #533 surfaced four rough-edges in the
prisma-next-demo. Mirrored locally per the convention established by
`examples/prisma-next-postgis-demo/gotchas.md` (each entry references its
upstream Linear ticket in the [PN] Gotchas project).

Entries:
- TML-2495 (pre-existing): `db init` PN-MIG-5001 declaredButUnmigrated for
  extension spaces unless `migration plan` is run first. Same bug class the
  postgis demo already filed; reproduces here against pgvector.
- TML-2564 (new): `migration status` reports `never-planned` after `db init`
  even when markerHash === headHash. Status taxonomy categorises by how the
  marker was set, not by sync state.
- TML-2565 (new): `migration show` (no argument) silently means
  `migration heads`. Overloaded verb; targetless behaviour reads
  src/prisma/contract.json and enumerates per-space leaf migrations, which
  is unrelated to the targeted behaviour's per-migration inspection.
- TML-2566 (new, F-7 system bug): Loaded predecessor contract snapshots are
  not verified against their persisted storageHash at load time.
  Semantically-inconsistent end-contract.json passes `migration plan`
  silently. Follow-on from TML-2536's deserializer-seam closure: the load
  happens; the post-load hash-recomputation-and-compare step does not.

All three new tickets reference TML-2536's manual-QA report at
`projects/tml-2536-contract-deserializer-seam/manual-qa-reports/2026-05-18-claude-opus-4-7.md`
for repro artefacts.

Source-of-truth for these entries is the Linear tickets, not this file;
this file is the in-repo mirror so a reader bumping into one of these
behaviours has a local pointer.

Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/prisma-next-demo/gotchas.md`:
- Around line 29-33: The fenced code block that starts with "PN-MIG-5001 —
Contract-space layout violation" is missing a language identifier and triggers
markdownlint MD040; update the opening fence from ``` to a language-tagged fence
such as ```text or ```console so the block is treated as plain CLI output
(ensure the block still contains the same lines: "PN-MIG-5001 — Contract-space
layout violation", "[declaredButUnmigrated] pgvector", and the pgvector
message).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: f066f465-8d2f-4098-ae4c-b197d4737680

📥 Commits

Reviewing files that changed from the base of the PR and between 6c4800d and f725405.

⛔ Files ignored due to path filters (7)
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/2026-05-18-claude-opus-4-7.md is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/F-1/migration-show-on-corrupted-end-contract.txt is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/F-7/scenario-9-probes.txt is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/F-8/scenario-9-probes.txt is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/F-9/scenario-10-codemod-format-divergence.txt is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/scenario-9-probes.txt is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa.md is excluded by !projects/**
📒 Files selected for processing (2)
  • examples/prisma-next-demo/gotchas.md
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/instructions.md

Comment thread examples/prisma-next-demo/gotchas.md Outdated
wmadden added a commit that referenced this pull request May 19, 2026
Every CLI on-disk contract read now passes the parsed payload as
`unknown` into `validateContract`, matching the documented seam
pattern (`familyInstance.validateContract(JSON.parse(raw) as unknown)`)
in `.cursor/rules/contract-normalization-responsibilities.mdc`. This
keeps the read boundary at the serializer seam explicit and avoids
silently reintroducing an untyped step inside the validator call.

Touches the five read sites flagged by CodeRabbit on PR #533:

- `migrate.ts` (contractRaw)
- `migration-new.ts` (toContract)
- `migration-show.ts` (appContract)
- `migration-plan.ts` (validateContract(raw) and familyInstance.validateContract(contractJsonContent))
wmadden added a commit that referenced this pull request May 19, 2026
…btree

The codemod claimed (in the colocated instructions) to "walk the
project root [and] descend into every directory named migrations/",
but the actual walk recursed into every non-skipped subtree and
stamped any `start-contract.json` / `end-contract.json` it found.
That could unintentionally rewrite non-migration fixtures (e.g.
inline contract test snapshots) in downstream user projects.

Thread an `inMigrations` flag through `walk(...)` so:

- the recursion still visits every non-skipped subtree (we cannot
  short-circuit because `migrations/` need not sit at any fixed
  depth);
- but a CONTRACT_FILES match is only collected once we have
  crossed (or are below) a directory literally named `migrations`.

The fix is applied identically to both copies of the codemod (user
upgrade skill + extension-author upgrade skill) so they remain
byte-identical, per the existing convention.

Flagged by CodeRabbit on PR #533.
wmadden added a commit that referenced this pull request May 19, 2026
…uctions

The user-facing instructions claimed the codemod re-serialises with
`JSON.stringify(value, null, 2) + '\n'`, but the script
actually uses a custom pretty-printer (`formatJson`) that inlines
short primitive arrays while otherwise matching `JSON.stringify(null,
2)` indentation. Aligning the prose with the shipped behaviour
matches the extension-author copy and prevents misleading diff
expectations.

Flagged by CodeRabbit on PR #533.
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented May 19, 2026

Follow-ups on CodeRabbit's review body (the five nitpicks bundled in the review summary; not on individual review threads):

Addressed:

  • packages/1-framework/3-tooling/cli/src/commands/migrate.ts:176-177 — added explicit as unknown at the seam in 450a38e07.
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts:99-101 — same, in 450a38e07.
  • packages/1-framework/3-tooling/cli/src/commands/migration-show.ts:379-381 — same, in 450a38e07.
  • packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts:99-100, 216-218 — both read sites updated, in 450a38e07.

All five now follow the documented seam pattern (familyInstance.validateContract(JSON.parse(raw) as unknown)) from .cursor/rules/contract-normalization-responsibilities.mdc. pnpm lint:no-contract-cast is clean.

Not addressing in this PR:

  • .cursor/rules/contract-normalization-responsibilities.mdc:1-120 — the nitpick claims the rule file is 120 lines and exceeds the 100-line soft limit, and recommends extracting the diagram to docs/architecture docs/. The file is actually 98 lines (wc -l .cursor/rules/contract-normalization-responsibilities.mdc → 98), comfortably under the soft limit. No extraction is needed; the diagram stays inline.

wmadden added a commit that referenced this pull request May 19, 2026
Every CLI on-disk contract read now passes the parsed payload as
`unknown` into `validateContract`, matching the documented seam
pattern (`familyInstance.validateContract(JSON.parse(raw) as unknown)`)
in `.cursor/rules/contract-normalization-responsibilities.mdc`. This
keeps the read boundary at the serializer seam explicit and avoids
silently reintroducing an untyped step inside the validator call.

Touches the five read sites flagged by CodeRabbit on PR #533:

- `migrate.ts` (contractRaw)
- `migration-new.ts` (toContract)
- `migration-show.ts` (appContract)
- `migration-plan.ts` (validateContract(raw) and familyInstance.validateContract(contractJsonContent))

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…btree

The codemod claimed (in the colocated instructions) to "walk the
project root [and] descend into every directory named migrations/",
but the actual walk recursed into every non-skipped subtree and
stamped any `start-contract.json` / `end-contract.json` it found.
That could unintentionally rewrite non-migration fixtures (e.g.
inline contract test snapshots) in downstream user projects.

Thread an `inMigrations` flag through `walk(...)` so:

- the recursion still visits every non-skipped subtree (we cannot
  short-circuit because `migrations/` need not sit at any fixed
  depth);
- but a CONTRACT_FILES match is only collected once we have
  crossed (or are below) a directory literally named `migrations`.

The fix is applied identically to both copies of the codemod (user
upgrade skill + extension-author upgrade skill) so they remain
byte-identical, per the existing convention.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…uctions

The user-facing instructions claimed the codemod re-serialises with
`JSON.stringify(value, null, 2) + '\n'`, but the script
actually uses a custom pretty-printer (`formatJson`) that inlines
short primitive arrays while otherwise matching `JSON.stringify(null,
2)` indentation. Aligning the prose with the shipped behaviour
matches the extension-author copy and prevents misleading diff
expectations.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2536-contract-deserializer-seam-v2 branch from a7a7049 to bf0e7a7 Compare May 19, 2026 05:28
wmadden added a commit that referenced this pull request May 19, 2026
Re-apply the drive-qa-plan skill against the shipped state of PR #533
(branch `tml-2536-contract-deserializer-seam-v2`) — the previous draft
was written in D4 against the now-superseded PR #520 and predated D5
(demo CI gate, lint guard, namespaces-bookend migration) and D6
(rationale comments).

What changed:

- Header now points at PR #533 and frames both audiences (CLI users +
  extension authors) up-front; the implementation-history bullet list
  in "What this script is testing" mentions the shipped guardrails by
  their concrete names (`lint:no-contract-cast`, the
  `Demo `migration plan` (no-op)` CI job, `stamp-storage-types-kind.ts`).
- Added an Isolation column to the TOC and an explicit `Isolation` line
  to every scenario (and the exploratory charter), tagging each with the
  strictest tag it actually needs: `workspace` for DB / tree mutators,
  `read-only` for the rule-coherence pass, `external` for the
  `gh pr checks` scenario.
- Added "Preconditions: no prerequisite scenarios" / "Scenario N
  completed" lines so the runner has explicit DAG edges (Scenario 2
  depends on 1; Scenario 8 depends on 1 or 3; everything else is
  independent).
- Stripped every severity pre-classification (🛑 Blocker / ⚠️ High /
  📝 Follow-up) from failure-mode lists in Scenarios 9, 10, 11. The
  script now enumerates failure-mode categories only; severity belongs
  in the runner report.
- Updated Scenario 4 (negative control for the strict deserializer) to
  target `migrations/app/20260518T1701_namespaces_bookend/end-contract.json`
  — the head migration that the planner now reads as the predecessor
  snapshot after D5's bookend addition. The old target
  (`20260422T0720_initial`) no longer exercises the predecessor-read
  path on its own.
- Updated Scenario 3 to apply the *full* checked-in history (initial
  plus bookend) and `migration show` both, so the re-enactment touches
  the literal pre-bug artefact and the new head.
- Updated Scenario 7 to reference PR #533 instead of #520 and to name
  the actual CI job display name (`Demo `migration plan` (no-op)`).
- Updated Scenario 10's oracle and step 8 expectations: the demo
  `migration plan` is now a *clean* no-op end-to-end (D5's namespaces
  bookend closed the residual TML-2521 drift), so the prior "you may
  still see PN-CLI-4020" carve-out is gone — any non-zero / non-no-op
  envelope is now a finding.
- Pre-flight now uses the workspace-canonical CLI invocation
  (`node ../../packages/.../cli.js`) the rest of the script already
  uses, so the demo-rooted scenarios all read the same way.
- "Scenarios deliberately not in this script" now explicitly excludes
  the static-pass half of AC-2 / AC-8 (re-running the lint over a clean
  tree) and AC-11; the sign-off coverage map maps AC-2 to Scenario 5
  (the gate-of-gate negative control) and points the AC-11 row at the
  exclusion table.

No scenarios were deleted under the litmus test — every existing
scenario already fell into one of the six legitimate buckets. Each
scenario now names which bucket it falls under so the runner can
quickly verify the script's honesty.

Scope is script-revision only: no spec / plan / source / CI / skill
changes. The runner skill (`drive-qa-run`) drives the script and owns
the report.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Manual-QA pass against PR #533 surfaced four rough-edges in the
prisma-next-demo. Mirrored locally per the convention established by
`examples/prisma-next-postgis-demo/gotchas.md` (each entry references its
upstream Linear ticket in the [PN] Gotchas project).

Entries:
- TML-2495 (pre-existing): `db init` PN-MIG-5001 declaredButUnmigrated for
  extension spaces unless `migration plan` is run first. Same bug class the
  postgis demo already filed; reproduces here against pgvector.
- TML-2564 (new): `migration status` reports `never-planned` after `db init`
  even when markerHash === headHash. Status taxonomy categorises by how the
  marker was set, not by sync state.
- TML-2565 (new): `migration show` (no argument) silently means
  `migration heads`. Overloaded verb; targetless behaviour reads
  src/prisma/contract.json and enumerates per-space leaf migrations, which
  is unrelated to the targeted behaviour's per-migration inspection.
- TML-2566 (new, F-7 system bug): Loaded predecessor contract snapshots are
  not verified against their persisted storageHash at load time.
  Semantically-inconsistent end-contract.json passes `migration plan`
  silently. Follow-on from TML-2536's deserializer-seam closure: the load
  happens; the post-load hash-recomputation-and-compare step does not.

All three new tickets reference TML-2536's manual-QA report at
`projects/tml-2536-contract-deserializer-seam/manual-qa-reports/2026-05-18-claude-opus-4-7.md`
for repro artefacts.

Source-of-truth for these entries is the Linear tickets, not this file;
this file is the in-repo mirror so a reader bumping into one of these
behaviours has a local pointer.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2536-contract-deserializer-seam-v2 branch from bf0e7a7 to 1b0251b Compare May 19, 2026 05:31
wmadden added a commit that referenced this pull request May 19, 2026
Every CLI on-disk contract read now passes the parsed payload as
`unknown` into `validateContract`, matching the documented seam
pattern (`familyInstance.validateContract(JSON.parse(raw) as unknown)`)
in `.cursor/rules/contract-normalization-responsibilities.mdc`. This
keeps the read boundary at the serializer seam explicit and avoids
silently reintroducing an untyped step inside the validator call.

Touches the five read sites flagged by CodeRabbit on PR #533:

- `migrate.ts` (contractRaw)
- `migration-new.ts` (toContract)
- `migration-show.ts` (appContract)
- `migration-plan.ts` (validateContract(raw) and familyInstance.validateContract(contractJsonContent))

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…btree

The codemod claimed (in the colocated instructions) to "walk the
project root [and] descend into every directory named migrations/",
but the actual walk recursed into every non-skipped subtree and
stamped any `start-contract.json` / `end-contract.json` it found.
That could unintentionally rewrite non-migration fixtures (e.g.
inline contract test snapshots) in downstream user projects.

Thread an `inMigrations` flag through `walk(...)` so:

- the recursion still visits every non-skipped subtree (we cannot
  short-circuit because `migrations/` need not sit at any fixed
  depth);
- but a CONTRACT_FILES match is only collected once we have
  crossed (or are below) a directory literally named `migrations`.

The fix is applied identically to both copies of the codemod (user
upgrade skill + extension-author upgrade skill) so they remain
byte-identical, per the existing convention.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…uctions

The user-facing instructions claimed the codemod re-serialises with
`JSON.stringify(value, null, 2) + '\n'`, but the script
actually uses a custom pretty-printer (`formatJson`) that inlines
short primitive arrays while otherwise matching `JSON.stringify(null,
2)` indentation. Aligning the prose with the shipped behaviour
matches the extension-author copy and prevents misleading diff
expectations.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
Comment thread .github/workflows/ci.yml Outdated
Comment on lines +85 to +110
demo-migration-plan:
name: Demo `migration plan` (no-op)
# Regression gate for TML-2536: the bug crashed `prisma-next
# migration plan` against `examples/prisma-next-demo` because the
# demo's predecessor end-contract bypassed the family
# `ContractSerializer` seam. Closing the bug also requires that
# the demo's checked-in migration history continues to plan as a
# no-op end-to-end; this job exercises that path on every PR so
# the demo stops working = failed CI, not undetected rot.
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
- uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
- name: Install dependencies (skip bin linking)
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Build packages (needed for the CLI binary)
run: pnpm build
- name: Link bins
run: pnpm install --frozen-lockfile
- name: Run migration plan against checked-in history
run: pnpm --filter prisma-next-demo migration:plan:check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have its own workflow step instead of an e2e test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — a workflow job per regression test isn't sustainable. Stripped the demo-migration-plan job in b3f8cbe0e. The check itself belongs inside the CLI — migration check --compiled with an opt-in flag that loads migration.ts files and verifies outputs match inputs — filed as TML-2568 in the [PN] May: Migrations project. Once that lands the regression coverage moves into the existing e2e harness, not a bespoke workflow job.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty strange. Why does this need a dedicated script? This is the kind of thing a user would want to do in CI too: are my migrations in order? Can't we use prisma-next migration check for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right diagnosis. Confirmed: migration check runs structural integrity (hash recompute over metadata+ops, refs, reachability, stored-string compare of metadata.to vs end-contract.json.storage.storageHash) but does not invoke the planner against live contract.json — so it can't catch the TML-2536 regression class (planner crashing on deserialization) or generic drift. Filed TML-2568 to extend migration check --compiled with an opt-in flag that loads migration.ts and verifies outputs match inputs (opt-in because it loads user code; database-credential-free so it can gate CI before deploy creds). Deleted the dedicated script + workflow in b3f8cbe0e ahead of the proper fix.

Comment thread examples/prisma-next-demo/gotchas.md Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record this as gotchas in Linear and delete the file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ddcd9c9d6. All four gotchas (TML-2495, TML-2564, TML-2565, TML-2566) are already in Linear; deleted the local mirror. Postgis-demo's gotchas.md (the pattern I was mirroring) stays for now — separate cleanup pass.

@@ -13,6 +13,12 @@ function sha256(content: string): string {
}

function hashContract(section: Record<string, unknown>): string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a function which operates on a portion of a contract?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right diagnosis, deferred to a follow-up.

hashContract constructs a stubbed full Contract envelope (populating roots:{}, models:{}, extensionPacks:{}, capabilities:{}, meta:{}, profileHash:'' as constant noise) and routes it through canonicalizeContract with a naive JSON.parse(JSON.stringify(c)) serializer. So computeStorageHash doesn't actually hash storage — it hashes a stubbed full envelope with storage embedded inside it. Same collision resistance in practice (the stubbed slots contribute zero entropy), but the function does not do what its name says.

The clean fix is per-slice trivial implementations: each compute*Hash takes only its slice + target identifiers and hashes a canonical envelope containing only those fields. No shared hashContract helper, no stubbed whole-Contract construction, no as unknown as Contract cast.

I attempted this refactor in-PR and discovered a serious test-gap: hashing.test.ts verifies structural properties (sha256 format, stability, sensitivity, key-order independence) but does NOT lock specific hash values. The refactor changed the hashed bytes silently — caught only by prisma-next contract emit against the demo (extension headRef.hash mismatch on pgvector). That hash change cascades into every stored hash in tree + every downstream user's checked-in contract.json / end-contract.json.

Filed as TML-2569. Three deliverables: (1) lock hash values in tests, (2) refactor hashing.ts to per-slice trivial impls, (3) handle the breaking-change rollout (internal regen of pgvector + demo + fixtures; user-facing recipe via 0.9-to-0.10 upgrade instructions). Natural next slice after this PR merges.

Comment on lines +120 to +125
* Codec-triple authors that have an untagged shape on hand can call
* `toStorageTypeInstance(...)` (which stamps the `'codec-instance'`
* discriminator) before constructing `SqlStorage`. On-disk reads
* cross `familyInstance.validateContract` first; the structural
* arktype schema rejects untagged entries earlier, so this throw
* only fires for in-memory authoring bugs.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we remove this behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — the loose fallthrough was removed in commit bd64120b4; what remains is the strict guard at the construction boundary.

Pre-TML-2536, normaliseTypeEntry was a recovery function: it would normalise untagged codec triples to a tagged StorageTypeInstance shape silently, masking format drift. That's the behaviour the bug landed on top of — bypassing the deserializer was invisible because normaliseTypeEntry would patch up untagged input on the way through.

Post-TML-2536, the recovery branches are gone. The function is now what its name should imply: it asserts structural validity at the SqlStorage constructor boundary, throwing a diagnostic naming the entry + its kind (or "missing kind discriminator") for anything that isn't already a tagged StorageTypeInstance or a PostgresEnumStorageEntry class instance. On-disk reads can't reach this throw — the family seam's arktype schema rejects untagged entries earlier. The throw only fires for in-memory authoring bugs (test fixtures, future construction sites that construct SqlStorage directly without crossing the serializer).

Removing the function entirely is on the table but I'd rather keep the construction-boundary guard as defense in depth — it's the only line of defense if a future code path constructs SqlStorage from a raw object that bypassed the serializer. If that becomes load-bearing surface area, we'd want it asserting loudly rather than silently passing through. Could rename to assertTypeEntryShape if the normalise name reads cold given the behavior change — worth doing if you agree.

Comment on lines +44 to +52
// Blind cast: the framework SPI signature
// (`control-migration-types.ts § contractToSchema`) types
// `contract` as the generic `Contract | null`. Inside the
// sqlite target descriptor we know any contract reaching
// this method is SQL-family — the family contract resolver
// would have refused to construct a sqlite target binding
// otherwise — so we narrow the generic to
// `Contract<SqlStorage>` for the lowering call.
return contractToSchemaIR(contract as unknown as Contract<SqlStorage> | null, {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could easily be replaced with a type predicate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b84dc23d3. Added a local isSqlContract(contract): contract is Contract<SqlStorage> | null predicate that checks contract.storage instanceof SqlStorage, and replaced the as unknown as Contract<SqlStorage> | null cast with a runtime guard. The narrowing is now real, not asserted; the SPI signature stays as-is. Typecheck clean; lint:no-contract-cast still clean. Kept the predicate local to control-target.ts since it's the only call site — hoist into @prisma-next/family-sql if it proves useful elsewhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted the whole manual-qa-reports/artefacts/ tree in fc81d49a0 (F-1, F-7, F-8, F-9, scenario-9-probes.txt) — the inline narrative in the QA report is the durable record. Updated the two surviving artefact-link references in the report to note the captures have been pruned post-merge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uncertain we want this lint script

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping for now. Three landings on the table for a follow-up: (a) widen the regex to AST-based detection (catches typed annotations like let foo: Contract = JSON.parse(...) which the current regex misses); (b) replace with a biome/eslint rule for better integration into the standard lint flow; (c) drop entirely once architect F-A3 (write-side seam asymmetry) is closed and the convention is complete. Filing as low-priority follow-up; the current guard is small, narrow, and bounded by the allowlist size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid modifying drive skills. Modify drive/code/README.md instead (introduced on #522 )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7c25a522e. Reverted the 6-line "Smells to surface" block. Moved the same content into drive/code/README.md (new file, follows the drive/qa/README.md structure). #522 (which introduces the drive/<workflow>/README.md convention) can land independently — no conflict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid modifying drive skills. Modify drive/code/README.md instead (introduced on #522 )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix as the drive-code-review/SKILL.md sibling: reverted in 7c25a522e, content moved to drive/code/README.md.

wmadden added a commit that referenced this pull request May 19, 2026
…n.ts

Per PR #533 review comments 3263947775 + 3263956074: a per-PR workflow
job for every regression test isn't sustainable, and the check belongs
inside `prisma-next migration check` (see TML-2568) so downstream users
can run it in their own CI without a bespoke script.

- Remove `demo-migration-plan` job from `.github/workflows/ci.yml`.
- Delete `examples/prisma-next-demo/scripts/check-migration-plan.ts`.
- Remove `migration:plan:check` from `examples/prisma-next-demo/package.json` scripts.

Until TML-2568 lands and the e2e harness picks up the regression
coverage, the TML-2536 regression class is guarded only by the test
suite + `lint:no-contract-cast`. Accepted trade-off per review.
wmadden added a commit that referenced this pull request May 19, 2026
Per PR #533 review comment 3263959905: gotchas are tracked in Linear;
no need for a local mirror.

Entries previously listed in this file are filed as:
- TML-2495 — db init fails for extension spaces unless migration plan is run first
- TML-2564 — migration status reports never-planned after db init when markerHash === headHash
- TML-2565 — migration show (no argument) silently means migration heads
- TML-2566 — predecessor end-contract.json is not verified against its persisted storageHash at load time

Postgis-demo's gotchas.md (which this file was mirroring) stays for now;
can be cleaned up in a separate pass.
wmadden added a commit that referenced this pull request May 19, 2026
…act predicate

Per PR #533 review comment 3264043123: the `contract as unknown as
Contract<SqlStorage> | null` cast in
`sqliteControlTargetDescriptor.contractToSchema` encoded an SPI-typing
workaround as a blind cast. Replace with a local `isSqlContract` type
predicate that checks `contract.storage instanceof SqlStorage` and
throws a named diagnostic when the invariant is violated.

- Promote `SqlStorage` import from `type` to value so `instanceof`
  works.
- Add `isSqlContract(contract): contract is Contract<SqlStorage> | null`
  predicate local to `control-target.ts` (small blast radius; hoist if
  it proves useful elsewhere).
- Replace the cast with a runtime guard + throw on violation. The
  type narrowing is now real, not asserted.

Typecheck clean (`pnpm typecheck` 134/134); `lint:no-contract-cast`
still clean.
wmadden added a commit that referenced this pull request May 19, 2026
…eferences

Per PR #533 review comment 3264043861: artefact captures from the QA
run aren't durable evidence; the inline narrative in the QA report
itself is sufficient post-merge.

- Delete `projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/`
  in its entirety (F-1, F-7, F-8, F-9, scenario-9-probes.txt).
- Update the QA report (`2026-05-18-claude-opus-4-7.md`) to drop the
  two surviving artefact link references; replace with a note that raw
  captures have been pruned.
wmadden added a commit that referenced this pull request May 19, 2026
Per PR #533 review comments 3264049516 + 3264049883: project-specific
review knowledge belongs in `drive/<workflow>/README.md` (convention
introduced on #522), not in generic `skills-contrib/drive-*/SKILL.md`
bodies.

- Revert the "Smells to surface during the principal-engineer pass"
  section added to `skills-contrib/drive-code-review/SKILL.md` and
  `skills-contrib/drive-pr-local-review/SKILL.md`.
- Create `drive/code/README.md` (missing category README) carrying
  the `as Contract` smell entry, structured like `drive/qa/README.md`.

#522 (which introduces the `drive/<workflow>/README.md` convention)
can land independently — no conflict.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`:
- Around line 64-67: Update the header comment in stamp-storage-types-kind.ts to
accurately state that files are serialized using formatJson(...) (which applies
inline short primitive arrays) rather than JSON.stringify(value, null, 2) +
'\n'; locate the top-of-file comment near the storage.types description and
replace the incorrect serialization text with a note that the transformation
writes via formatJson(...) so diffs may include formatting differences (inline
short primitive arrays) outside storage.types.

In
`@skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`:
- Around line 64-67: Update the file header comment that currently claims output
is produced by `JSON.stringify(value, null, 2) + '\n'` to instead state that
output is produced by `formatJson(...)` with short primitive-array inlining (the
same serializer used in this transform), so maintenance notes accurately reflect
the actual serialization used for `storage.types` and any snapshots; search for
the header text in this file and replace the JSON.stringify reference with a
description mentioning `formatJson` and short primitive-array inlining (keep
mention of `storage.types` and the CLI formatting behavior intact).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b8b5dfe2-a705-4e68-882c-0d84444468ab

📥 Commits

Reviewing files that changed from the base of the PR and between f725405 and 7c25a52.

⛔ Files ignored due to path filters (2)
  • projects/tml-2536-contract-deserializer-seam/manual-qa-reports/2026-05-18-claude-opus-4-7.md is excluded by !projects/**
  • projects/tml-2536-contract-deserializer-seam/manual-qa.md is excluded by !projects/**
📒 Files selected for processing (10)
  • drive/code/README.md
  • packages/1-framework/3-tooling/cli/src/commands/migrate.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-show.ts
  • packages/3-targets/3-targets/sqlite/src/core/control-target.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/instructions.md
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts
  • skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts
✅ Files skipped from review due to trivial changes (1)
  • drive/code/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/1-framework/3-tooling/cli/src/commands/migrate.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-show.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts

Comment on lines +64 to +67
* The transformation re-serialises each affected file via
* `JSON.stringify(value, null, 2) + '\n'` — the same formatting the
* CLI uses when authoring snapshots originally, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the header comment to match actual serialization behavior.

The comment still says the rewrite uses JSON.stringify(value, null, 2) + '\n', but the script writes with formatJson(...) (including inline short primitive arrays). Keeping this accurate avoids misleading diff expectations.

🛠️ Proposed fix
- * The transformation re-serialises each affected file via
- * `JSON.stringify(value, null, 2) + '\n'` — the same formatting the
- * CLI uses when authoring snapshots originally, so the diff outside
+ * The transformation re-serialises each affected file via a custom
+ * pretty-printer (`formatJson(...) + '\n'`) that matches
+ * `JSON.stringify(..., null, 2)` indentation while inlining short
+ * primitive arrays, so the diff outside
  * `storage.types` is zero on files the CLI generated. Hand-edited
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* The transformation re-serialises each affected file via
* `JSON.stringify(value, null, 2) + '\n'` the same formatting the
* CLI uses when authoring snapshots originally, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
* The transformation re-serialises each affected file via a custom
* pretty-printer (`formatJson(...) + '\n'`) that matches
* `JSON.stringify(..., null, 2)` indentation while inlining short
* primitive arrays, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@skills/extension-author/prisma-next-extension-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`
around lines 64 - 67, Update the header comment in stamp-storage-types-kind.ts
to accurately state that files are serialized using formatJson(...) (which
applies inline short primitive arrays) rather than JSON.stringify(value, null,
2) + '\n'; locate the top-of-file comment near the storage.types description and
replace the incorrect serialization text with a note that the transformation
writes via formatJson(...) so diffs may include formatting differences (inline
short primitive arrays) outside storage.types.

Comment on lines +64 to +67
* The transformation re-serialises each affected file via
* `JSON.stringify(value, null, 2) + '\n'` — the same formatting the
* CLI uses when authoring snapshots originally, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct the serializer description in the file header.

The header states JSON.stringify(value, null, 2) + '\n', but output is produced by formatJson(...) with short primitive-array inlining. Updating this keeps maintenance/debug expectations accurate.

🛠️ Proposed fix
- * The transformation re-serialises each affected file via
- * `JSON.stringify(value, null, 2) + '\n'` — the same formatting the
- * CLI uses when authoring snapshots originally, so the diff outside
+ * The transformation re-serialises each affected file via a custom
+ * pretty-printer (`formatJson(...) + '\n'`) that matches
+ * `JSON.stringify(..., null, 2)` indentation while inlining short
+ * primitive arrays, so the diff outside
  * `storage.types` is zero on files the CLI generated. Hand-edited
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* The transformation re-serialises each affected file via
* `JSON.stringify(value, null, 2) + '\n'` the same formatting the
* CLI uses when authoring snapshots originally, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
* The transformation re-serialises each affected file via a custom
* pretty-printer (`formatJson(...) + '\n'`) that matches
* `JSON.stringify(..., null, 2)` indentation while inlining short
* primitive arrays, so the diff outside
* `storage.types` is zero on files the CLI generated. Hand-edited
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@skills/upgrade/prisma-next-upgrade/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts`
around lines 64 - 67, Update the file header comment that currently claims
output is produced by `JSON.stringify(value, null, 2) + '\n'` to instead state
that output is produced by `formatJson(...)` with short primitive-array inlining
(the same serializer used in this transform), so maintenance notes accurately
reflect the actual serialization used for `storage.types` and any snapshots;
search for the header text in this file and replace the JSON.stringify reference
with a description mentioning `formatJson` and short primitive-array inlining
(keep mention of `storage.types` and the CLI formatting behavior intact).

wmadden added a commit that referenced this pull request May 19, 2026
…ract

Sweep finishes the partial rename in the working tree by:

- Fixing two malformed JSDoc blocks (control-instances.ts, sql + mongo
  family control-instance.ts) whose embedded `/**` glob broke typecheck.
- Updating the remaining `validateContract` references (comments,
  READMEs, lint-script error string, drive/code README) to point at the
  family-instance method `deserializeContract` and dropping the now-
  redundant cursor rule `contract-normalization-responsibilities.mdc`.
- Realigning `.cursor/rules/as-contract-cast-smell.mdc` on the
  `familyInstance.deserializeContract(...)` idiom.
- Renaming the stale call site `validateContract(JSON.parse(...))` in
  `migration-plan.ts`'s `readPredecessorEndContract` to the local
  `deserializeContract` parameter the function already declares.

The freestanding framework function `validateContract<T>` keeps its
name in this commit; Note 1 follow-up collapses it into the family
serializer.
wmadden added 9 commits May 19, 2026 11:16
…eferences

Per PR #533 review comment 3264043861: artefact captures from the QA
run aren't durable evidence; the inline narrative in the QA report
itself is sufficient post-merge.

- Delete `projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/`
  in its entirety (F-1, F-7, F-8, F-9, scenario-9-probes.txt).
- Update the QA report (`2026-05-18-claude-opus-4-7.md`) to drop the
  two surviving artefact link references; replace with a note that raw
  captures have been pruned.

Signed-off-by: Will Madden <madden@prisma.io>
Per PR #533 review comments 3264049516 + 3264049883: project-specific
review knowledge belongs in `drive/<workflow>/README.md` (convention
introduced on #522), not in generic `skills-contrib/drive-*/SKILL.md`
bodies.

- Revert the "Smells to surface during the principal-engineer pass"
  section added to `skills-contrib/drive-code-review/SKILL.md` and
  `skills-contrib/drive-pr-local-review/SKILL.md`.
- Create `drive/code/README.md` (missing category README) carrying
  the `as Contract` smell entry, structured like `drive/qa/README.md`.

#522 (which introduces the `drive/<workflow>/README.md` convention)
can land independently — no conflict.

Signed-off-by: Will Madden <madden@prisma.io>
…ract

Sweep finishes the partial rename in the working tree by:

- Fixing two malformed JSDoc blocks (control-instances.ts, sql + mongo
  family control-instance.ts) whose embedded `/**` glob broke typecheck.
- Updating the remaining `validateContract` references (comments,
  READMEs, lint-script error string, drive/code README) to point at the
  family-instance method `deserializeContract` and dropping the now-
  redundant cursor rule `contract-normalization-responsibilities.mdc`.
- Realigning `.cursor/rules/as-contract-cast-smell.mdc` on the
  `familyInstance.deserializeContract(...)` idiom.
- Renaming the stale call site `validateContract(JSON.parse(...))` in
  `migration-plan.ts`'s `readPredecessorEndContract` to the local
  `deserializeContract` parameter the function already declares.

The freestanding framework function `validateContract<T>` keeps its
name in this commit; Note 1 follow-up collapses it into the family
serializer.

Signed-off-by: Will Madden <madden@prisma.io>
…ly serializer

Removes the freestanding `validateContract<T>` framework function and
its per-family wrappers; the family `ContractSerializerBase` SPI is now
the only public seam for hydrating an on-disk JSON envelope into a
typed `Contract`.

- `@prisma-next/contract`: drop `validate-contract.ts` and its export
  (`./validate-contract`). `ContractValidationError` migrates to its
  own module (`./contract-validation-error`), which is the new import
  path for consumers that need to type-test the error class.
- `@prisma-next/sql-contract`: `validateSqlContract` is no longer
  exported; its body is inlined as `validateSqlContractStructure`
  (module-private) and reached only through `validateSqlContractFully`,
  which the family `SqlContractSerializerBase` already routes through.
  Tests realign on `validateSqlContractFully`.
- `@prisma-next/mongo-contract`: `validate-mongo-contract.ts` and its
  test file are gone. The family `MongoContractSerializerBase` already
  composed `MongoContractSchema` + `validateContractDomain` +
  `validateMongoStorage` directly, so no logic migrates — only the
  freestanding wrapper disappears. The two consumers (mongo-demo cache
  test, mongo-query-builder result-shape test) move to the family
  serializer or the fixture-seam cast that `as-contract-cast-smell.mdc`
  explicitly permits in tests.
- Cursor rule `.cursor/rules/validate-contract-usage.mdc` is deleted —
  the type-parameter discipline it prescribed only applied to the
  freestanding function.
- README and comment touch-ups in `family-sql` / `mongo-contract` /
  `sql-contract` reflect the seam-only surface.

Signed-off-by: Will Madden <madden@prisma.io>
Brings `db verify` into line with every other CLI on-disk read (TML-2536):
the contract is hydrated at the read site via
`familyInstance.deserializeContract`, and the downstream `dbVerify`
client/op now accepts the already-deserialized `Contract` instead of
running the SerializerBase pipeline a second time.

- `commands/db-verify.ts`: construct the family instance up front, drop
  the multi-paragraph "we read as raw JSON because the seam happens
  downstream" rationale comment, and route the JSON through
  `familyInstance.deserializeContract` (with a structured-error wrap
  for `ContractValidationError`).
- `control-api/types.ts`: narrow `DbVerifyOptions.contract` from
  `unknown` to `Contract` and document the new precondition.
- `control-api/client.ts`: `dbVerify` no longer re-deserializes — the
  hydrated value flows straight into `executeDbVerify`.

`client.verify` and `VerifyOptions.contract` keep their `unknown`
surface for now: integration tests still pass raw JSON through that
entry point, and re-deserialization of an already-hydrated Contract is
a no-op for the SQL/Mongo SerializerBase pipelines.

Signed-off-by: Will Madden <madden@prisma.io>
…ance directly, async readFile

- F01: drop the inner createControlStack + family.create pair in migration-new.ts; reuse the outer family instance constructed up-front for the seam-of-record read.
- F02: readPredecessorEndContract now takes the ControlFamilyInstance directly rather than a (json) => deserializeContract closure. The family-instance is the seam; callers depend on it without an extra abstraction layer.
- F09: migration-new.ts now uses await readFile from node:fs/promises to match the other four CLI commands that read the contract file.

Signed-off-by: Will Madden <madden@prisma.io>
The classifier (looksLikeUntaggedCodecTriple) now rejects pg/enum@1 entries whose typeParams.values is not a string[], so a single diagnostic shape — "neither stamped nor untagged codec triple — hand-edit required" — covers both missing-codecId and malformed-enum-values cases.

The defensive runtime check inside stampEntry stays as an invariant marker so a future loosening of the predicate cannot silently produce a malformed StampedPostgresEnum; it is unreachable under the normal flow.

Applied byte-identically to both copies (verified via diff).

Signed-off-by: Will Madden <madden@prisma.io>
…itly

Documents that the lint:no-contract-cast script only enforces the cast-keyword variant of the smell (as Contract / as Contract<...>); structural-assignment variants are not lint-enforced and rely on review + convention. Avoids the rule's prose over-claiming what the grep catches.

Signed-off-by: Will Madden <madden@prisma.io>
lint:no-contract-cast is a code-shape lint, not a dependency-graph lint — chaining it into lint:deps mis-taxonomises the failure (a future as Contract regression would surface as a "Lint dependencies" failure rather than a "Lint code shape" one).

- package.json: drop lint:no-contract-cast from the lint:deps chain; add a new lint:code script that runs it (kept lint:no-contract-cast as a standalone entry for ad-hoc local invocation).
- ci.yml: add a "Lint code shape" step right after "Lint dependencies" so the CI surface mirrors the script taxonomy.

The new lint:code slot is named generically so future workspace-level code-shape lints can chain into it.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2536-contract-deserializer-seam-v2 branch from 549bfde to c486e1c Compare May 19, 2026 09:18
@wmadden wmadden merged commit 334ad82 into main May 19, 2026
12 checks passed
wmadden added a commit that referenced this pull request May 19, 2026
Re-apply the drive-qa-plan skill against the shipped state of PR #533
(branch `tml-2536-contract-deserializer-seam-v2`) — the previous draft
was written in D4 against the now-superseded PR #520 and predated D5
(demo CI gate, lint guard, namespaces-bookend migration) and D6
(rationale comments).

What changed:

- Header now points at PR #533 and frames both audiences (CLI users +
  extension authors) up-front; the implementation-history bullet list
  in "What this script is testing" mentions the shipped guardrails by
  their concrete names (`lint:no-contract-cast`, the
  `Demo `migration plan` (no-op)` CI job, `stamp-storage-types-kind.ts`).
- Added an Isolation column to the TOC and an explicit `Isolation` line
  to every scenario (and the exploratory charter), tagging each with the
  strictest tag it actually needs: `workspace` for DB / tree mutators,
  `read-only` for the rule-coherence pass, `external` for the
  `gh pr checks` scenario.
- Added "Preconditions: no prerequisite scenarios" / "Scenario N
  completed" lines so the runner has explicit DAG edges (Scenario 2
  depends on 1; Scenario 8 depends on 1 or 3; everything else is
  independent).
- Stripped every severity pre-classification (🛑 Blocker / ⚠️ High /
  📝 Follow-up) from failure-mode lists in Scenarios 9, 10, 11. The
  script now enumerates failure-mode categories only; severity belongs
  in the runner report.
- Updated Scenario 4 (negative control for the strict deserializer) to
  target `migrations/app/20260518T1701_namespaces_bookend/end-contract.json`
  — the head migration that the planner now reads as the predecessor
  snapshot after D5's bookend addition. The old target
  (`20260422T0720_initial`) no longer exercises the predecessor-read
  path on its own.
- Updated Scenario 3 to apply the *full* checked-in history (initial
  plus bookend) and `migration show` both, so the re-enactment touches
  the literal pre-bug artefact and the new head.
- Updated Scenario 7 to reference PR #533 instead of #520 and to name
  the actual CI job display name (`Demo `migration plan` (no-op)`).
- Updated Scenario 10's oracle and step 8 expectations: the demo
  `migration plan` is now a *clean* no-op end-to-end (D5's namespaces
  bookend closed the residual TML-2521 drift), so the prior "you may
  still see PN-CLI-4020" carve-out is gone — any non-zero / non-no-op
  envelope is now a finding.
- Pre-flight now uses the workspace-canonical CLI invocation
  (`node ../../packages/.../cli.js`) the rest of the script already
  uses, so the demo-rooted scenarios all read the same way.
- "Scenarios deliberately not in this script" now explicitly excludes
  the static-pass half of AC-2 / AC-8 (re-running the lint over a clean
  tree) and AC-11; the sign-off coverage map maps AC-2 to Scenario 5
  (the gate-of-gate negative control) and points the AC-11 row at the
  exclusion table.

No scenarios were deleted under the litmus test — every existing
scenario already fell into one of the six legitimate buckets. Each
scenario now names which bucket it falls under so the runner can
quickly verify the script's honesty.

Scope is script-revision only: no spec / plan / source / CI / skill
changes. The runner skill (`drive-qa-run`) drives the script and owns
the report.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Manual-QA pass against PR #533 surfaced four rough-edges in the
prisma-next-demo. Mirrored locally per the convention established by
`examples/prisma-next-postgis-demo/gotchas.md` (each entry references its
upstream Linear ticket in the [PN] Gotchas project).

Entries:
- TML-2495 (pre-existing): `db init` PN-MIG-5001 declaredButUnmigrated for
  extension spaces unless `migration plan` is run first. Same bug class the
  postgis demo already filed; reproduces here against pgvector.
- TML-2564 (new): `migration status` reports `never-planned` after `db init`
  even when markerHash === headHash. Status taxonomy categorises by how the
  marker was set, not by sync state.
- TML-2565 (new): `migration show` (no argument) silently means
  `migration heads`. Overloaded verb; targetless behaviour reads
  src/prisma/contract.json and enumerates per-space leaf migrations, which
  is unrelated to the targeted behaviour's per-migration inspection.
- TML-2566 (new, F-7 system bug): Loaded predecessor contract snapshots are
  not verified against their persisted storageHash at load time.
  Semantically-inconsistent end-contract.json passes `migration plan`
  silently. Follow-on from TML-2536's deserializer-seam closure: the load
  happens; the post-load hash-recomputation-and-compare step does not.

All three new tickets reference TML-2536's manual-QA report at
`projects/tml-2536-contract-deserializer-seam/manual-qa-reports/2026-05-18-claude-opus-4-7.md`
for repro artefacts.

Source-of-truth for these entries is the Linear tickets, not this file;
this file is the in-repo mirror so a reader bumping into one of these
behaviours has a local pointer.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Every CLI on-disk contract read now passes the parsed payload as
`unknown` into `validateContract`, matching the documented seam
pattern (`familyInstance.validateContract(JSON.parse(raw) as unknown)`)
in `.cursor/rules/contract-normalization-responsibilities.mdc`. This
keeps the read boundary at the serializer seam explicit and avoids
silently reintroducing an untyped step inside the validator call.

Touches the five read sites flagged by CodeRabbit on PR #533:

- `migrate.ts` (contractRaw)
- `migration-new.ts` (toContract)
- `migration-show.ts` (appContract)
- `migration-plan.ts` (validateContract(raw) and familyInstance.validateContract(contractJsonContent))

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…btree

The codemod claimed (in the colocated instructions) to "walk the
project root [and] descend into every directory named migrations/",
but the actual walk recursed into every non-skipped subtree and
stamped any `start-contract.json` / `end-contract.json` it found.
That could unintentionally rewrite non-migration fixtures (e.g.
inline contract test snapshots) in downstream user projects.

Thread an `inMigrations` flag through `walk(...)` so:

- the recursion still visits every non-skipped subtree (we cannot
  short-circuit because `migrations/` need not sit at any fixed
  depth);
- but a CONTRACT_FILES match is only collected once we have
  crossed (or are below) a directory literally named `migrations`.

The fix is applied identically to both copies of the codemod (user
upgrade skill + extension-author upgrade skill) so they remain
byte-identical, per the existing convention.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden deleted the tml-2536-contract-deserializer-seam-v2 branch May 19, 2026 09:20
wmadden added a commit that referenced this pull request May 19, 2026
…uctions

The user-facing instructions claimed the codemod re-serialises with
`JSON.stringify(value, null, 2) + '\n'`, but the script
actually uses a custom pretty-printer (`formatJson`) that inlines
short primitive arrays while otherwise matching `JSON.stringify(null,
2)` indentation. Aligning the prose with the shipped behaviour
matches the extension-author copy and prevents misleading diff
expectations.

Flagged by CodeRabbit on PR #533.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…n.ts

Per PR #533 review comments 3263947775 + 3263956074: a per-PR workflow
job for every regression test isn't sustainable, and the check belongs
inside `prisma-next migration check` (see TML-2568) so downstream users
can run it in their own CI without a bespoke script.

- Remove `demo-migration-plan` job from `.github/workflows/ci.yml`.
- Delete `examples/prisma-next-demo/scripts/check-migration-plan.ts`.
- Remove `migration:plan:check` from `examples/prisma-next-demo/package.json` scripts.

Until TML-2568 lands and the e2e harness picks up the regression
coverage, the TML-2536 regression class is guarded only by the test
suite + `lint:no-contract-cast`. Accepted trade-off per review.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Per PR #533 review comment 3263959905: gotchas are tracked in Linear;
no need for a local mirror.

Entries previously listed in this file are filed as:
- TML-2495 — db init fails for extension spaces unless migration plan is run first
- TML-2564 — migration status reports never-planned after db init when markerHash === headHash
- TML-2565 — migration show (no argument) silently means migration heads
- TML-2566 — predecessor end-contract.json is not verified against its persisted storageHash at load time

Postgis-demo's gotchas.md (which this file was mirroring) stays for now;
can be cleaned up in a separate pass.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…act predicate

Per PR #533 review comment 3264043123: the `contract as unknown as
Contract<SqlStorage> | null` cast in
`sqliteControlTargetDescriptor.contractToSchema` encoded an SPI-typing
workaround as a blind cast. Replace with a local `isSqlContract` type
predicate that checks `contract.storage instanceof SqlStorage` and
throws a named diagnostic when the invariant is violated.

- Promote `SqlStorage` import from `type` to value so `instanceof`
  works.
- Add `isSqlContract(contract): contract is Contract<SqlStorage> | null`
  predicate local to `control-target.ts` (small blast radius; hoist if
  it proves useful elsewhere).
- Replace the cast with a runtime guard + throw on violation. The
  type narrowing is now real, not asserted.

Typecheck clean (`pnpm typecheck` 134/134); `lint:no-contract-cast`
still clean.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…eferences

Per PR #533 review comment 3264043861: artefact captures from the QA
run aren't durable evidence; the inline narrative in the QA report
itself is sufficient post-merge.

- Delete `projects/tml-2536-contract-deserializer-seam/manual-qa-reports/artefacts/`
  in its entirety (F-1, F-7, F-8, F-9, scenario-9-probes.txt).
- Update the QA report (`2026-05-18-claude-opus-4-7.md`) to drop the
  two surviving artefact link references; replace with a note that raw
  captures have been pruned.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Per PR #533 review comments 3264049516 + 3264049883: project-specific
review knowledge belongs in `drive/<workflow>/README.md` (convention
introduced on #522), not in generic `skills-contrib/drive-*/SKILL.md`
bodies.

- Revert the "Smells to surface during the principal-engineer pass"
  section added to `skills-contrib/drive-code-review/SKILL.md` and
  `skills-contrib/drive-pr-local-review/SKILL.md`.
- Create `drive/code/README.md` (missing category README) carrying
  the `as Contract` smell entry, structured like `drive/qa/README.md`.

#522 (which introduces the `drive/<workflow>/README.md` convention)
can land independently — no conflict.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…ract

Sweep finishes the partial rename in the working tree by:

- Fixing two malformed JSDoc blocks (control-instances.ts, sql + mongo
  family control-instance.ts) whose embedded `/**` glob broke typecheck.
- Updating the remaining `validateContract` references (comments,
  READMEs, lint-script error string, drive/code README) to point at the
  family-instance method `deserializeContract` and dropping the now-
  redundant cursor rule `contract-normalization-responsibilities.mdc`.
- Realigning `.cursor/rules/as-contract-cast-smell.mdc` on the
  `familyInstance.deserializeContract(...)` idiom.
- Renaming the stale call site `validateContract(JSON.parse(...))` in
  `migration-plan.ts`'s `readPredecessorEndContract` to the local
  `deserializeContract` parameter the function already declares.

The freestanding framework function `validateContract<T>` keeps its
name in this commit; Note 1 follow-up collapses it into the family
serializer.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…ly serializer

Removes the freestanding `validateContract<T>` framework function and
its per-family wrappers; the family `ContractSerializerBase` SPI is now
the only public seam for hydrating an on-disk JSON envelope into a
typed `Contract`.

- `@prisma-next/contract`: drop `validate-contract.ts` and its export
  (`./validate-contract`). `ContractValidationError` migrates to its
  own module (`./contract-validation-error`), which is the new import
  path for consumers that need to type-test the error class.
- `@prisma-next/sql-contract`: `validateSqlContract` is no longer
  exported; its body is inlined as `validateSqlContractStructure`
  (module-private) and reached only through `validateSqlContractFully`,
  which the family `SqlContractSerializerBase` already routes through.
  Tests realign on `validateSqlContractFully`.
- `@prisma-next/mongo-contract`: `validate-mongo-contract.ts` and its
  test file are gone. The family `MongoContractSerializerBase` already
  composed `MongoContractSchema` + `validateContractDomain` +
  `validateMongoStorage` directly, so no logic migrates — only the
  freestanding wrapper disappears. The two consumers (mongo-demo cache
  test, mongo-query-builder result-shape test) move to the family
  serializer or the fixture-seam cast that `as-contract-cast-smell.mdc`
  explicitly permits in tests.
- Cursor rule `.cursor/rules/validate-contract-usage.mdc` is deleted —
  the type-parameter discipline it prescribed only applied to the
  freestanding function.
- README and comment touch-ups in `family-sql` / `mongo-contract` /
  `sql-contract` reflect the seam-only surface.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
Brings `db verify` into line with every other CLI on-disk read (TML-2536):
the contract is hydrated at the read site via
`familyInstance.deserializeContract`, and the downstream `dbVerify`
client/op now accepts the already-deserialized `Contract` instead of
running the SerializerBase pipeline a second time.

- `commands/db-verify.ts`: construct the family instance up front, drop
  the multi-paragraph "we read as raw JSON because the seam happens
  downstream" rationale comment, and route the JSON through
  `familyInstance.deserializeContract` (with a structured-error wrap
  for `ContractValidationError`).
- `control-api/types.ts`: narrow `DbVerifyOptions.contract` from
  `unknown` to `Contract` and document the new precondition.
- `control-api/client.ts`: `dbVerify` no longer re-deserializes — the
  hydrated value flows straight into `executeDbVerify`.

`client.verify` and `VerifyOptions.contract` keep their `unknown`
surface for now: integration tests still pass raw JSON through that
entry point, and re-deserialization of an already-hydrated Contract is
a no-op for the SQL/Mongo SerializerBase pipelines.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…ance directly, async readFile

- F01: drop the inner createControlStack + family.create pair in migration-new.ts; reuse the outer family instance constructed up-front for the seam-of-record read.
- F02: readPredecessorEndContract now takes the ControlFamilyInstance directly rather than a (json) => deserializeContract closure. The family-instance is the seam; callers depend on it without an extra abstraction layer.
- F09: migration-new.ts now uses await readFile from node:fs/promises to match the other four CLI commands that read the contract file.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
The classifier (looksLikeUntaggedCodecTriple) now rejects pg/enum@1 entries whose typeParams.values is not a string[], so a single diagnostic shape — "neither stamped nor untagged codec triple — hand-edit required" — covers both missing-codecId and malformed-enum-values cases.

The defensive runtime check inside stampEntry stays as an invariant marker so a future loosening of the predicate cannot silently produce a malformed StampedPostgresEnum; it is unreachable under the normal flow.

Applied byte-identically to both copies (verified via diff).

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 19, 2026
…itly

Documents that the lint:no-contract-cast script only enforces the cast-keyword variant of the smell (as Contract / as Contract<...>); structural-assignment variants are not lint-enforced and rely on review + convention. Avoids the rule's prose over-claiming what the grep catches.

Signed-off-by: Will Madden <madden@prisma.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant