Skip to content

TML-2882: transitional enum2 block makes the new enum PSL-authorable#805

Merged
wmadden-electric merged 27 commits into
mainfrom
tml-2882-slice-transitional-psl-enum2-block-author-the-new-enum
Jun 11, 2026
Merged

TML-2882: transitional enum2 block makes the new enum PSL-authorable#805
wmadden-electric merged 27 commits into
mainfrom
tml-2882-slice-transitional-psl-enum2-block-author-the-new-enum

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Refs TML-2882.
Builds on the merged enum substrate (TML-2850, #750), check enforcement (TML-2851, #755), and typed read surface (TML-2852, #769).
Hands to TML-2855 (member defaults) and TML-2853 (cutover, now reduced to rename + migrate + delete).

At a glance

The demo's PSL schema now authors the new domain-concept enum directly — while native enum keeps working untouched:

enum2 Priority {
  @@type("pg/text@1")
  Low    = "low"
  High   = "high"
  Urgent = "urgent"
}

model Post {
  priority Priority
}

Emit it and the real app uses it end-to-end through the emitted contract — post.priority reads as 'low' | 'high' | 'urgent', db.enums.public.Priority.values returns the ordered tuple, ORDER BY priority sorts by declaration order, and the migration adds the CHECK:

export async function getPostsByPriority(limit = 10) {
  const plan = db.sql.public.post
    .select('id', 'title', 'priority')
    .orderBy('priority')          // declaration order: low, high, urgent
    .limit(limit)
    .build();
  return db.runtime().execute(plan);
}

Before this PR, the new enum mechanism was reachable only through the TS enumType DSL — unreachable from PSL, and therefore from the demo's real emitted-contract app.

Decision

Add a parallel transitional PSL keyword (enum2) that lowers to the new enum mechanism — domain enum entity + storage valueSet entity + field/column valueSet restriction refs + check constraint — reusing the lowering the TS enumType path already has. Native enum is byte-for-byte untouched.

The point is to dissolve the constraint that forced an atomic cutover: enum is one keyword and can't mean both native and the new shape at once, so the new mechanism was unreachable through PSL until the very end of the project. The second keyword makes it PSL-authorable additively, takes it live through the product path (PSL → emit → app) now, and shrinks the cutover (TML-2853) to a rename + migrate + delete with the risky lowering already proven in live use.

The keyword is deliberately disposable: it is retired at the cutover, when enum itself repoints at this lowering.

Reviewer notes

  • The central guarantee is parity: for the same schema, the PSL enum2 path and the TS enumType path emit identical contract JSON, including storageHash. The parity test (interpreter.enum2.test.ts) asserts strict deep equality on every emitted slice plus hash equality — it started life as toMatchObject and was hardened to toEqual after subset matching let a divergence escape (next bullet).
  • Two correction arcs are visible in the history, kept honest rather than rewritten. (1) ead068f57 patched the SQL-family schema verifier and codec-ref resolver to tolerate a stray typeRef the PSL path was leaking onto emitted columns; review caught that the leak was the bug, so 3ec205e9e fixed it at the source and reverted both patches byte-identically. (2) The first round built a dedicated parseEnum2Block grammar production and a per-target entityTypes.enum2 presence stub; review (correctly) rejected both, and aad4e15b399fc662dc rework enum2 onto the generic extension-block grammar and delete the stub mechanism entirely.
  • A latent interpreter bug is fixed in passing: the final contract reconstruction in interpretPslDocumentToSqlContract rebuilt domain namespace slices copying only models/valueObjects, silently dropping enum entities. Nothing PSL-authored produced domain enums until now, so it was unobservable before this PR.
  • The PSL printer silently drops enum2 blocks. Verified no in-scope flow round-trips authored PSL through the printer (it runs only in the schema-inference direction), so printing support is deferred to the cutover, where the keyword is renamed anyway.
  • Known emitter gap (pre-existing, now tracked as TML-2885): the emitted contract.d.ts narrows enum column/field types to the value union, but does not type the domain enum block itself — so on the emitted path, db.enums.<ns>.<Name>.members values are JsonValue-typed at the type level (runtime values are correct). The demo's literal-union type proofs therefore live on the column types (SELECT/INSERT), which the emitter does narrow. Project spec R6 amended to require emit parity; TML-2885 is now a cutover prerequisite.
  • pnpm lint shows two pre-existing no-bare-cast info notices in files this branch never touches; no new casts are added.

How it fits together

  1. Grammar — no enum2-specific parsing (psl-parser/src/parser.ts): enum2 Priority { … } rides the existing generic extension-block grammar (<keyword> <Name> { key = value … }); members are ordinary parameter lines captured raw. The parser gains three generic extension-block capabilities (each independently tested, usable by any contributed block): @@attr(...) block-attribute lines, duplicate-parameter diagnostics, and bare-identifier parameters behind a descriptor opt-in flag.
  2. Interpreter (contract-psl/src/interpreter.ts): processEnum2Declarations consumes the enum2-keyword extension blocks — validates @@type presence (required, never inferred), validates each member's raw value with JSON.parse + codec.decodeJson (bare members default to their name for string-accepting codecs), and enforces well-formedness. Per enum it builds an EnumTypeHandle via the existing enumType() and a ColumnDescriptor in the existing by-name map — so field resolution (priority Priority) is unchanged and doesn't care which block declared the name. Support requires no per-target registration: a target that lacks the codec fails with the unknown-codec diagnostic; the block descriptor lives at the SQL-family level, marked interpreterLowered (the factory invariant now admits blocks lowered by the interpreter rather than an entity-type factory).
  3. Lowering is 100% reuse: the handles flow into the existing buildSqlContractFromDefinition as enums, and the domain enum, storage value-set, field/column valueSet refs, and the table check all fall out of the already-merged build-contract code. Zero new lowering.
  4. Demo goes live (examples/prisma-next-demo): enum2 Priority in the PSL schema, re-emitted contract.json/contract.d.ts, migration 20260610T0000_add_priority_enum (addColumn → setNotNull → addCheckConstraint), and enum-priority / enum-priority-filter subcommands consuming the enum through the emitted contract — including filtering by a live member value via the emitted accessor; seeds and integration fixtures source values from the emitted accessor too, never raw literals.

Behavior changes & evidence

  • The generic extension-block grammar gains @@attr(...) block attributes, duplicate-parameter diagnostics, and opt-in bare-identifier parameters — and enum2 blocks parse through it with zero block-specific grammar. Implementation: parser.ts, psl-extension-block.ts. Evidence: parser-enum2.test.ts.
  • The interpreter lowers enum2 to the new two-plane shape, identically to the TS path — plus ten validation diagnostics (missing @@type, unknown codec, non-JSON / codec-rejected values, bare member under non-string codec, duplicate names/values, native-name collision, namespaced enum2, unsupported target). Implementation: interpreter.ts. Evidence: interpreter.enum2.test.ts — parity incl. storageHash equality, all diagnostics, native+enum2 mixed document, non-string codec.
  • The demo proves the feature through the emitted contract — typed value-union reads, db.enums.public.Priority, declaration-order ORDER BY, and the check-adding migration. Implementation: contract.prisma, get-posts-by-priority.ts, main.ts. Evidence: demo-dx.types.test.ts (non-vacuous: paired .not.toEqualTypeOf<string>() + @ts-expect-error on an out-of-union insert), integration suites.
  • Native enums are untouched — the demo's user_type emits byte-identically; no native fixture changes; the mixed-document test asserts the native slice is unchanged.

Testing performed

  • pnpm test:packages — 799 files, 10,328 tests green on final HEAD
  • pnpm typecheck — 138 tasks green
  • pnpm fixtures:check — zero diff outside the demo's deliberately re-emitted artifacts
  • pnpm lint:deps — clean
  • Demo: type tests + integration suites; enum-priority subcommand run end-to-end (declaration-order sort observed: low, high, urgent). Occasional PGlite portal flakes reproduced and ruled pre-existing.

Skill update

n/a — the enum2 spelling is deliberately transitional and is retired at the cutover (TML-2853), when enum itself gains this meaning. Documenting a keyword scheduled for deletion in user-facing skills would teach syntax users should never adopt long-term; the skill update lands with the cutover rename.

Follow-ups

  • TML-2885 — emit the typed domain enum block so db.enums is literal-typed through the emitted contract (R6 emit parity; surfaced by this PR's review; cutover prerequisite).
  • TML-2855 — enum member defaults (@default(Low) on a real PSL enum field).
  • TML-2853 — cutover: rename enum2enum, migrate remaining native enums, delete the native machinery, add printer support for the final keyword.
  • Numeric-codec SQL rendering (CHECK / ORDER BY for non-text value-sets) stays guarded — tracked separately.

Alternatives considered

  • No new keyword — discriminate within enum on @@type presence. The new syntax is already distinguishable from native, so enum could mean the new shape when @@type is present. Rejected: it re-overloads one keyword with two meanings depending on an attribute — exactly the dual-meaning ambiguity this project removes. An explicit second keyword is unambiguous, and the cutover rename is trivial.
  • Just do the atomic cutover now. Rejected: the cutover can't land until full parity with native (member defaults still outstanding), and it would be a single non-additive big-bang with no prior live validation. The transitional keyword exercises the new mechanism in production use before the irreversible flip.
  • A dedicated parseEnum2Block grammar production instead of the generic extension-block path. Built in the first round (on the premise that the missing @@-attribute support justified a bespoke parse), rejected in review: the right fix for the gap was generic block-attribute support in the extension-block grammar, landed in the rework.
  • A per-target entityTypes.enum2 presence registration. Built in the first round mirroring native enum's Postgres entry, rejected in review: native enums are genuinely Postgres-specific, the domain enum is target-agnostic. Support now falls out of codec resolution; the factory invariant admits interpreter-lowered blocks.
  • @map(...) for member values. Rejected: @map everywhere else in PSL means a physical storage mapping; the member value is a first-class domain value, so it's assigned with =.
  • Keep the SQL-family tolerance patches instead of fixing the typeRef leak. Rejected: they masked a parity violation (PSL emit ≠ TS emit, divergent storageHash). Fixed at the source; the patches reverted as dead code.

Checklist

  • All commits are signed off (git commit -s) per the DCO. The DCO status check will block merge if any commit is missing a Signed-off-by: trailer.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated (or n/a if the change is doc-only / refactor with no behavioural delta).
  • The PR title is in TML-NNNN: <sentence-case title> form (Linear ticket prefix + concise title naming the concrete deliverable). See .claude/skills/create-pr/SKILL.md for the full convention.
  • The Skill update section above is filled in (or stated n/a — internal only).

Summary by CodeRabbit

  • New Features

    • Added Priority enum (Low, High, Urgent) for posts; demo supports priority-aware listing and filtering.
    • Polymorphic Task types (Bug / Feature) exposed in demo.
  • Migrations

    • DB migration adds non-null post.priority and enforces allowed values.
  • CLI / Demo

    • New demo commands to list posts by priority and filter by priority member; seed data updated with priorities.
  • Authoring

    • PSL authoring/parser extended to support enum2-style extension blocks.
  • Tests

    • Expanded tests covering enums, queries, seeding, and contract typings.

wmadden and others added 7 commits June 10, 2026 17:45
…lock

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Parses `enum2 <Name> { @@type("<codec-id>") Name = <literal> … }` into a
distinct PslEnum2 AST node kind alongside the existing native enum parse.

- PslEnum2 / PslEnum2Value types added to framework-components psl-ast,
  including flatPslEnum2s helper and enum2s field on PslNamespace
- PSL_INVALID_ENUM2_MEMBER diagnostic code added to PslDiagnosticCode
- parseEnum2Block dedicated parse in psl-parser: bare members, = value
  members (raw captured text + span), @@type block attribute, @Map
  rejected with diagnostic
- Missing @@type parses through cleanly (required-ness is D2 interpreter
  validation, not grammar)
- Native enum parsing byte-identical untouched
- Exports updated: PslEnum2, PslEnum2Value, flatPslEnum2s from parser index
- parser-enum2.test.ts: 13 cases covering all dispatch requirements

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Move the = value match before the @Map( check so codec-legal string
values containing that substring (e.g. Foo = "@Map(x)") are captured
as raw text rather than rejected with PSL_INVALID_ENUM2_MEMBER.

The @Map rejection now only fires for lines without an assignment,
which is its intended purpose (catching native-enum Member @Map("…")
syntax).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
- Add processEnum2Declarations to interpreter.ts: validates @@type attribute,
  resolves codec via codecLookup, validates member values through decodeJson,
  calls enumType() directly to build EnumTypeHandle
- Thread enum2Handles into buildModelNodeFromPsl and buildSqlContractFromDefinition
- Preserve namespaceSlice.enum in patchedContract reconstruction (was silently dropped)
- Register enum2 presence-signal factory in postgresAuthoringEntityTypes
- Thread codecLookup from provider context into interpretPslDocumentToSqlContract
- Add 13 tests: parity, 9 diagnostics, mixed doc, non-string codec

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…esolution

`resolveContractColumnTypeMetadata` in verify-sql-schema threw when a
column's `typeRef` pointed to a value-set name rather than a
`storage.types` entry. Added an early return that uses the column's
own `codecId`/`nativeType`/`typeParams` when `valueSet` is present.

`codecRefForStorageColumn` in codec-ref-for-column returned `undefined`
for the same case, causing ParamRef-without-codec errors at SQL render
time. Added a fallback that returns `{ codecId: columnDef.codecId }`
when the column has a `valueSet` reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… (D3)

Authors `enum2 Priority { @@type("pg/text@1") Low High Urgent }` in the
demo schema and re-emits the contract. The emitted contract.d.ts narrows
Post.priority to `'low' | 'high' | 'urgent'` — not string.

Adds migration 20260610T0000_add_priority_enum with addColumn,
setNotNull, and addCheckConstraint ops for the priority column.

Adds the `enum-priority` subcommand to main.ts that exercises the
enum accessor and queries posts by priority through the emitted contract.

Adds four type tests in demo-dx.types.test.ts:
- FieldOutputTypes['Post']['priority'] equals the value union
- SELECT priority yields the union in ResultType
- getPostsByPriority rows have priority typed as the union
- INSERT rejects values outside the union via @ts-expect-error

Updates integration test fixtures in repositories and sql-dsl tests to
supply `priority: 'low'` on all post inserts (required after adding the
NOT NULL column).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… revert band-aids (D3-R2)

The PSL enum2 path set typeRef: decl.name on the ColumnDescriptor so the
handle-lookup site in buildModelNodeFromPsl could find the EnumTypeHandle.
buildStorageColumn copies descriptor.typeRef onto the emitted column, so the
internal key leaked into the contract as a dangling typeRef with no matching
storage.types entry, producing a different column shape than the TS path.

Fix: remove typeRef from the enum2 descriptor. Use resolvedField.field.typeName
(the PSL field's unqualified type name, already equal to the enum2 name) at
the lookup site instead. The emitted column is now {codecId, nativeType,
nullable, valueSet} — byte-identical to the TS enumType path.

Harden the D2 parity test: replace toMatchObject (subset) with toEqual
(strict) for all five asserted shapes and add a storageHash equality check.
The test was red before the fix, green after.

Revert both ead068f SQL-family band-aids (verify-sql-schema.ts early-return
on valueSet; codec-ref-for-column.ts valueSet fallback) — they are dead code
now that no enum2 column carries a typeRef. Both files are byte-identical to
their pre-ead068f57 state.

Re-emitted demo contract.json/contract.d.ts and regenerated migration
end-contract.* + migration.json/migration.ts for the add_priority_enum
migration via the existing regen script. Migration ops unchanged:
addColumn → setNotNull → addCheckConstraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 10, 2026 18:05
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Adds enum2 extension-block parsing and a codec-aware interpreter pass; renames value-set discriminators/refs to camelCase; and introduces a demo Priority enum migration plus queries, CLI, seed, and tests.

Changes

Extension Block Infrastructure and enum2 Language

Layer / File(s) Summary
PSL extension-block AST and diagnostics
packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
Adds block-attribute and bare-parameter AST types and diagnostics; PslExtensionBlock now includes blockAttributes and span.
Extension-block re-exports
packages/1-framework/1-core/framework-components/src/control/psl-ast.ts, packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
Re-exports new extension-block AST types and namespace helpers for parser/emitter use.
Parser: extension-block attributes and bare params
packages/1-framework/2-authoring/psl-parser/src/parser.ts, packages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.ts
parseExtensionBlock collects @@ attributes, optionally accepts bare key lines when allowed, records blockAttributes, and exposes parser tests for enum2 forms and diagnostics.
Validator & descriptor flags
packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts, packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
Introduces variadicParameters descriptor flag and AuthoringDiagnosticSink; validator skips unknown-parameter diagnostics when variadic is enabled; authoring collectors/refactors added.
Interpreter: enum2 processing
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts, packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
Adds processEnum2Declarations pass that requires @@type(codecId), resolves codec/native type via codecLookup, decodes bare/JSON members, enforces unique decoded values, creates EnumTypeHandles, and integrates them into model lowering; includes interpreter tests covering parity and diagnostics.
SQL family wiring
packages/2-sql/9-family/src/core/authoring-entity-types.ts, packages/2-sql/9-family/src/core/control-descriptor.ts, packages/2-sql/9-family/src/exports/pack.ts, packages/2-sql/2-authoring/contract-psl/src/provider.ts
Adds enum2 PSL descriptor (variadic parameters + interpreterLowered), wires descriptors and entityTypes into family pack and provider plumbing, and ensures codecLookup is passed to the interpreter.

ValueSetRef and StorageValueSet Naming Normalization

Layer / File(s) Summary
Core contract types and validators
packages/1-framework/0-foundation/contract/src/value-set-ref.ts, packages/2-sql/1-core/contract/src/ir/storage-value-set.ts, packages/2-sql/1-core/contract/src/validators.ts
Renames nameentityName, entityKind: 'value-set''valueSet', and updates StorageValueSet.kind; validators split storage/domain ref schemas and validate by plane.
Builders and emitters
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts, packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts, packages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.ts
Builders/emitters use entityName for enum/value-set lookups and construct valueSet refs with the new field names and discriminator.
Runtime/renderer and targets
packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts, packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts
Sql-renderer resolves enum ORDER BY values via valueSet.entityName; Postgres schema creation uses kind: 'valueSet'.
Tests & fixtures
packages/2-sql/1-core/contract/test/*, packages/1-framework/3-tooling/emitter/test/*, packages/2-sql/2-authoring/contract-ts/test/*, packages/2-sql/9-family/test/*
Updated many tests and fixtures to expect entityKind: 'valueSet' and entityName; added plane-consistency validation tests and updated roundtrip expectations.

Demo Priority Enum End-to-End

Layer / File(s) Summary
Prisma schema and migration artifacts
examples/prisma-next-demo/src/prisma/contract.prisma, examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/*
Adds Priority enum (Low/High/Urgent) mapped to pg/text@1, adds Post.priority, and includes migration ops to add the column, make it NOT NULL, and add post_priority_check.
Generated contracts and DTS/JSON
examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/*, examples/prisma-next-demo/src/prisma/contract.d.ts, examples/prisma-next-demo/src/prisma/contract.json
Generated start/end contract DTS/JSON updated with Priority value-set, post.priority storage mapping, checks, and extension pack metadata (pgvector present).
Queries, CLI, seed, tests
examples/prisma-next-demo/src/queries/get-posts-by-priority.ts, examples/prisma-next-demo/src/main.ts, examples/prisma-next-demo/scripts/seed.ts, examples/prisma-next-demo/test/*
Adds runtime helpers to fetch ordered or filtered posts by priority, CLI commands enum-priority and enum-priority-filter, seed helpers to insert enum-backed priority values, and tests asserting the emitted Post.priority type is the `'low'

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#769: The main PR’s demo changes (adding the Priority enum-backed post.priority field, wiring db.enums.Priority, and using enum-declaration-order semantics for getPostsByPriority/ordering + CHECK-enforced inserts) directly depend on the retrieved PR’s “enums become first-class” implementation (typed enum surfaces and declaration-order ORDER BY rendering).
  • prisma/prisma-next#753: Both PRs make code-level changes to the extension-block PSL pipeline—especially the psl-parser’s handling/validation of declarative extension blocks—so the main PR’s extension-block support changes are directly related to the retrieved PR’s declarative extension-block read-side work.

Suggested reviewers

  • aqrln

🐰 I nibble tokens, peel the crate,
enum2 blooms and valueSets update.
Parsers hum, codecs decode,
Priority hops down the seeded road.
Tests and CLI dance — hooray, celebrate!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2882-slice-transitional-psl-enum2-block-author-the-new-enum

@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@805

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 31b59d6

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 153.84 KB (+0.57% 🔺)
postgres / emit 121.27 KB (+0.02% 🔺)
mongo / no-emit 76.8 KB (+0.17% 🔺)
mongo / emit 70.96 KB (0%)
cf-worker / no-emit 182.79 KB (+0.49% 🔺)
cf-worker / emit 147.02 KB (+0.02% 🔺)

@wmadden wmadden left a comment

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.

There's a surprising amount of custom logic in the parser for a target-contributed block type. I think this is a bit of a mess. Lots of parser logic which should be in the interpreter, and only exposed in Postgres despite being database-independent (check constraints).

Comment thread examples/prisma-next-demo/scripts/seed.ts Outdated
Comment thread examples/prisma-next-demo/test/repositories.integration.test.ts Outdated
Comment thread examples/prisma-next-demo/test/sql-dsl.integration.test.ts Outdated
Comment thread examples/prisma-next-demo/src/main.ts
Comment thread packages/1-framework/1-core/framework-components/src/control/psl-ast.ts Outdated
Comment thread packages/1-framework/2-authoring/psl-parser/src/parser.ts Outdated
Comment thread packages/3-targets/3-targets/postgres/src/core/authoring.ts Outdated
Comment thread packages/3-targets/3-targets/postgres/src/core/authoring.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (1)

1516-1527: 💤 Low value

Consider using ifDefined for consistency.

Line 1525 uses a conditional spread ...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}) to conditionally include the enumTypeHandle property. Per the codebase learning on ifDefined usage, prefer ...ifDefined('enumTypeHandle', enumHandle) for consistency with other conditional spreads in this repo.

♻️ Proposed refactor
       fields: resolvedFields.map((resolvedField) => {
         const enumHandle = input.enum2Handles?.get(resolvedField.field.typeName);
         return {
           fieldName: resolvedField.field.name,
           columnName: resolvedField.columnName,
           descriptor: resolvedField.descriptor,
           nullable: resolvedField.field.optional,
           ...ifDefined('default', resolvedField.defaultValue),
           ...ifDefined('executionDefaults', resolvedField.executionDefaults),
-          ...(enumHandle !== undefined ? { enumTypeHandle: enumHandle } : {}),
+          ...ifDefined('enumTypeHandle', enumHandle),
         };
       }),
🤖 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/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 1516
- 1527, The conditional spread uses a ternary to add enumTypeHandle which is
inconsistent with the project's ifDefined helper; inside the fields mapping for
resolvedFields (the arrow mapping that references enumHandle and resolvedField),
replace the ternary spread ...(enumHandle !== undefined ? { enumTypeHandle:
enumHandle } : {}) with the canonical spread ...ifDefined('enumTypeHandle',
enumHandle) so the property is only included when enumHandle is defined and to
match other uses of ifDefined in the same block.

Source: Learnings

🤖 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/src/queries/get-posts-by-priority.ts`:
- Around line 19-21: The current query orders the text column 'priority'
lexicographically, producing wrong order; replace the lexicographic
.orderBy('priority') with an explicit rank-based ordering (e.g., a
CASE/priority_rank mapping for enum values low=1, high=2, urgent=3) and order by
that computed rank before the existing .orderBy('id'); update the query in
get-posts-by-priority (the chain using .select('id','title','priority') and
.orderBy('priority')) to compute or select the rank (or use an orderByRaw/CASE)
and then .orderBy(rank) then .orderBy('id') so ties use id as before.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Line 453: When nativeType is obtained from
input.codecLookup?.targetTypesFor(codecId)?.[0] but codec is undefined (from
codec = input.codecLookup?.get(codecId)), emit a diagnostic instead of allowing
enum2 members to be validated without a codec; update the interpreter.ts flow
around the const codec = input.codecLookup?.get(codecId) line to detect if
nativeType is truthy and codec is falsy and call the existing
diagnostic/reporting utility with a clear message referencing codecId (and the
enum2 member/context) so inconsistent CodecLookup implementations
(targetTypesFor() vs get()) are surfaced and validation short-circuited.

---

Nitpick comments:
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 1516-1527: The conditional spread uses a ternary to add
enumTypeHandle which is inconsistent with the project's ifDefined helper; inside
the fields mapping for resolvedFields (the arrow mapping that references
enumHandle and resolvedField), replace the ternary spread ...(enumHandle !==
undefined ? { enumTypeHandle: enumHandle } : {}) with the canonical spread
...ifDefined('enumTypeHandle', enumHandle) so the property is only included when
enumHandle is defined and to match other uses of ifDefined in the same block.
🪄 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: ea6ea943-bfdf-48e9-b633-e71314a15829

📥 Commits

Reviewing files that changed from the base of the PR and between dc0f923 and 3ec205e.

⛔ Files ignored due to path filters (2)
  • projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/plan.md is excluded by !projects/**
  • projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.md is excluded by !projects/**
📒 Files selected for processing (26)
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/contract.prisma
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.json
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/ops.json
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/start-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/start-contract.json
  • examples/prisma-next-demo/scripts/seed.ts
  • examples/prisma-next-demo/src/main.ts
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • examples/prisma-next-demo/src/prisma/contract.prisma
  • examples/prisma-next-demo/src/queries/get-posts-by-priority.ts
  • examples/prisma-next-demo/test/demo-dx.types.test.ts
  • examples/prisma-next-demo/test/repositories.integration.test.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
  • packages/3-targets/3-targets/postgres/src/core/authoring.ts

Comment thread examples/prisma-next-demo/src/queries/get-posts-by-priority.ts
Comment thread packages/2-sql/2-authoring/contract-psl/src/interpreter.ts Outdated
wmadden and others added 5 commits June 10, 2026 21:21
Delete all enum2-specific parser logic (parseEnum2Block, PslEnum2, PslEnum2Value,
PslNamespace.enum2s, flatPslEnum2s, PSL_INVALID_ENUM2_MEMBER). The generic
parseExtensionBlock now handles enum2 when the descriptor is registered.

Generic extension-block grammar gains:
- @@attr(…) block-attribute lines (PslExtensionBlockAttribute, stored in blockAttributes)
- Bare-identifier entries (PslExtensionBlockParamBare, kind:'bare'), guarded by
  allowAdditionalParameters on the descriptor
- PSL_EXTENSION_DUPLICATE_PARAMETER diagnostic for repeated keys
- PSL_INVALID_EXTENSION_BLOCK_ATTRIBUTE diagnostic for malformed @@ lines

Register enum2 as a SQL-family-level pslBlockDescriptor (not per-target) in
packages/2-sql/9-family, with a stub entityType entry to satisfy
assertPslBlocksHaveFactories. allowAdditionalParameters:true lets members flow
through without unknown-parameter diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
processEnum2Declarations now accepts PslExtensionBlock[] instead of PslEnum2[].
- @@type attribute is found in decl.blockAttributes (not decl.attributes)
- Members are iterated via Object.entries(decl.parameters); bare entries use the
  member name as the codec input, value entries JSON.parse the raw string
- enum2EntityDescriptor presence check removed: support falls out of codec resolution
  (missing @@type or unknown codec id gives a diagnostic, not a gated absence)
- Duplicate member names are now PSL_EXTENSION_DUPLICATE_PARAMETER from the parser;
  duplicate values remain PSL_ENUM2_DUPLICATE_MEMBER_VALUE from the interpreter
- provider.ts passes pslBlockDescriptors from authoringContributions to parsePslDocument
- Delete postgres stub entityTypes.enum2 (moved to SQL-family level in previous commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ber query

- Export Priority from prisma/contract.ts so tests and seed can reference it
- Replace 'low' as const / 'high' as const / 'urgent' as const literals in
  seed.ts, repositories.integration.test.ts, and sql-dsl.integration.test.ts
  with Priority.members.{Low,High,Urgent} from the TS-authored EnumTypeHandle
- Add getPostsByPriorityMember() to get-posts-by-priority.ts: filters posts by
  a named member using Priority.members[memberName] and a typed WHERE clause
- Wire enum-priority-filter [member] [limit] command into main.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…PSL blocks

Add `interpreterLowered?: boolean` to `AuthoringPslBlockDescriptor`. When set,
`assertPslBlocksHaveFactories` skips the factory-matching check for that block.

Delete the dead `sqlFamilyAuthoringEntityTypes` stub (factory: () => null) from
the SQL family. The `enum2` block is handled by `processEnum2Declarations` in
contract-psl; it has no factory and doesn't need one. Mark it
`interpreterLowered: true` so the assertion is satisfied without the stub.

Add tests for both the relaxed (pass) and un-relaxed (reject) paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…iles

The four demo files (seed.ts, repositories test, sql-dsl test,
get-posts-by-priority.ts) were importing Priority from prisma/contract.ts
(the TS authoring definition) and using its .members values directly.

Replace with a priorityValue(name) helper that reads from
getPriorityEnumFromEmit().members[name] at runtime. EnumAccessor.members
returns Readonly<Record<string, JsonValue>>, which is wider than the
required 'low' | 'high' | 'urgent' union — the blindCast is intentional
and the reason string explains the widening.

Remove the unused `export const Priority` from prisma/contract.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Rework for the CHANGES_REQUESTED review is pushed (5 commits, aad4e15b3..99fc662dc):

  • No custom grammar. parseEnum2Block / PslEnum2 AST / flatPslEnum2s deleted; enum2 rides the generic extension-block grammar. Parser additions are generic capabilities only: @@attr(...) block attributes, duplicate-parameter diagnostics, opt-in bare-identifier parameters.
  • No per-target stub. entityTypes.enum2 is gone at every layer; the factory invariant now admits interpreterLowered block descriptors; the enum2 descriptor lives at the SQL-family level; support falls out of codec resolution.
  • Demo uses the enum itself — seeds/fixtures/queries source values from the emitted accessor, and a new enum-priority-filter command shows the filter-by-member query a real user writes.

Parity guarantee unchanged: PSL emit == TS emit incl. storageHash (strict-equality test). Emitted contract byte-identical through the rework. One disclosure in the body's reviewer notes: the emitted contract.d.ts doesn't type the domain enum block (members are JsonValue-typed at type level on the emitted path; column types carry the literal union) — pre-existing emitter gap, candidate follow-up. PR body updated to match the reworked design.

wmadden and others added 3 commits June 10, 2026 22:15
…885)

The PR #805 review surfaced that the emitter omits the domain enum block
from contract.d.ts, so emitted-path db.enums members type-widen to
JsonValue. R6 now requires literal typing through the emitted contract;
TML-2885 tracks the work and becomes a cutover prerequisite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…member defaults

Carrier (spec §2): the entity coordinate is carried, never derived — plane +
entityKind ('enum' | 'valueSet', equal to the entries slot key) + namespaceId +
entityName + optional spaceId. One kind vocabulary, no translation (ADR 224).

Directional invariant corrected project-wide: domain may reference storage;
storage may never reference domain (storage plannable in isolation). ADR 221
§115's parenthetical is transposed — erratum deferred to the ADR batch. The
enumMember ColumnDefault carrier violates the invariant; TML-2855 respecced to
resolved-literal-in-storage + domain-side member intent (spec §9, alternatives,
design-notes, plan).

D5 proposal settled with planner deltas, incl. StorageValueSet node tag rename
'value-set' -> 'valueSet' folded into D5 scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
- Rename `name` → `entityName` and `entityKind: 'value-set'` → `'valueSet'`
  in `ValueSetRef` to achieve field-name identity with `EntityCoordinate`
  (one-vocabulary rule, ADR 224)
- Rename `StorageValueSet.kind` literal `'value-set'` → `'valueSet'` and
  update `StorageValueSetInput` accordingly
- Rewrite `ValueSetRef` doc comment: one-vocabulary rule, intra-plane
  constraint, correct directional invariant (domain→storage allowed;
  storage→domain forbidden)
- Add `ValueSetRefSchema` plane-consistency assertion: plane must match
  entityKind ('enum'→'domain', 'valueSet'→'storage')
- Mechanical follow-through across all construction and read sites:
  build-contract.ts, validators.ts, contract-to-schema-ir.ts,
  sql-renderer.ts, postgres-schema.ts, generate-contract-dts.ts
- Update all test files (~40 occurrences) and add new tests:
  (a) kind equals entries slot key; (b) plane-consistency passing + violating
- Re-emit demo contract.json/contract.d.ts + migration end-contract.*

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

D5 pushed (b82883b33 specs, 235fe2ff7 code): the ValueSetRef carrier and StorageValueSet node tag are aligned with the entity-coordinate model before this PR persists the first instances — settled with the substrate-slice planner (proposal + settlement deltas).

  • Ref carrier is now the four-tuple coordinate: { plane, namespaceId, entityKind: 'enum' | 'valueSet', entityName, spaceId? } — one kind vocabulary (ref kind = entries slot key = node tag), no translation anywhere (ADR 224).
  • StorageValueSet.kind tag: 'value-set''valueSet' — zero hyphenated survivors repo-wide; the wire is born clean.
  • New validator check: a ref's plane must equal the carrying plane; plus an assertion that ref entityKind equals the slot key it resolves in.
  • Directional invariant corrected project-wide in the specs: domain may reference storage; storage may never reference domain (ADR 221 §115's parenthetical is transposed — erratum queued in the ADR batch). Consequence: the TML-2851-era enumMember ColumnDefault carrier violates it and TML-2855 is respecced to resolved-literal-in-storage + domain-side member intent (zero persisted instances, churn-free).
  • Demo + migration artifacts re-emitted; wire delta is exactly the renames + hash recomputes; native user_type byte-identical; the strict parity test (deep-equal + storageHash, PSL vs TS) re-proves consistency.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/2-sql/1-core/contract/test/validators.test.ts (1)

1304-1326: ⚡ Quick win

Add a regression for storage checks pointing to domain enums.

Line 1304-Line 1326 currently covers only entityKind: 'valueSet' plane matching/mismatching. Please add a case asserting a storage check ref with { plane: 'domain', entityKind: 'enum' } is rejected, so the storage→domain invariant stays locked.

Suggested test case
   it('rejects a storage ref where plane contradicts entityKind', () => {
     const result = storageSchema(
       makeStorageWithCheckRef({
         plane: 'domain',
         entityKind: 'valueSet',
         namespaceId: 'public',
         entityName: 'Role',
       }),
     );
     expect(result).toBeInstanceOf(type.errors);
   });
+
+  it('rejects a storage ref pointing to a domain enum', () => {
+    const result = storageSchema(
+      makeStorageWithCheckRef({
+        plane: 'domain',
+        entityKind: 'enum',
+        namespaceId: 'public',
+        entityName: 'Role',
+      }),
+    );
+    expect(result).toBeInstanceOf(type.errors);
+  });
🤖 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/2-sql/1-core/contract/test/validators.test.ts` around lines 1304 -
1326, Add a regression test that ensures storage checks cannot point to domain
enums: create a new it-block alongside the existing tests that calls
storageSchema(makeStorageWithCheckRef({ plane: 'domain', entityKind: 'enum',
namespaceId: 'public', entityName: 'Role' })) and assert the result is an
instance of type.errors; follow the pattern used in the 'rejects a storage ref
where plane contradicts entityKind' test to keep style consistent and ensure the
storage→domain invariant is enforced for enums.
🤖 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 `@packages/1-framework/2-authoring/psl-parser/src/parser.ts`:
- Around line 1662-1685: The parser currently accepts bare identifiers into
parameters when descriptor.allowAdditionalParameters is true, which lets callers
bypass declared parameter validation; before assigning a bare entry (the block
that handles bareMatch and sets parameters[key] = { kind: 'bare', ... }), check
whether key is declared in descriptor.parameters and if so push a diagnostic
(e.g. reuse PSL_INVALID_EXTENSION_BLOCK_MEMBER or a new code) indicating
declared parameters must be provided as "key = value" and skip adding the bare
entry; ensure you still allow bare entries only for truly additional parameters
and do not overwrite or mask descriptor.parameters validation.

In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 80-94: ValueSetRefSchema currently only enforces internal
consistency (plane matches entityKind) which allows domain enums to be passed
where storage-only refs are required; create a specialized schema (e.g.
StorageValueSetRefSchema) that narrows ValueSetRefSchema to plane === 'storage'
and entityKind === 'valueSet', and replace occurrences that represent
storage-carriers (the usages around the current checks at the sites noted — the
refs validated on lines ~105 and ~221) to validate against
StorageValueSetRefSchema instead of ValueSetRefSchema so storage columns/checks
cannot reference domain enums.

---

Nitpick comments:
In `@packages/2-sql/1-core/contract/test/validators.test.ts`:
- Around line 1304-1326: Add a regression test that ensures storage checks
cannot point to domain enums: create a new it-block alongside the existing tests
that calls storageSchema(makeStorageWithCheckRef({ plane: 'domain', entityKind:
'enum', namespaceId: 'public', entityName: 'Role' })) and assert the result is
an instance of type.errors; follow the pattern used in the 'rejects a storage
ref where plane contradicts entityKind' test to keep style consistent and ensure
the storage→domain invariant is enforced for enums.
🪄 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: e9299003-6fa1-45a5-82ce-7529e4946e4b

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec205e and 235fe2f.

⛔ Files ignored due to path filters (5)
  • projects/enums-as-domain-concept/design-notes.md is excluded by !projects/**
  • projects/enums-as-domain-concept/plan.md is excluded by !projects/**
  • projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/d5-carrier-alignment-proposal.md is excluded by !projects/**
  • projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.md is excluded by !projects/**
  • projects/enums-as-domain-concept/spec.md is excluded by !projects/**
📒 Files selected for processing (45)
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.json
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.json
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.ts
  • examples/prisma-next-demo/scripts/seed.ts
  • examples/prisma-next-demo/src/main.ts
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • examples/prisma-next-demo/src/queries/get-posts-by-priority.ts
  • examples/prisma-next-demo/test/repositories.integration.test.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
  • packages/1-framework/0-foundation/contract/src/value-set-ref.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/1-core/framework-components/test/psl-ast.test.ts
  • packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/2-sql/1-core/contract/src/ir/storage-value-set.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/check-constraint.test.ts
  • packages/2-sql/1-core/contract/test/storage-value-set.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/check-constraint.authoring.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/enum-type.authoring.test.ts
  • packages/2-sql/9-family/src/core/authoring-entity-types.ts
  • packages/2-sql/9-family/src/core/control-descriptor.ts
  • packages/2-sql/9-family/src/core/migrations/contract-to-schema-ir.ts
  • packages/2-sql/9-family/src/exports/pack.ts
  • packages/2-sql/9-family/test/schema-verify.check-constraints.test.ts
  • packages/2-sql/9-family/test/value-set-roundtrip.test.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts
  • packages/3-targets/3-targets/postgres/test/migrations/planner.check-constraints.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
✅ Files skipped from review due to trivial changes (5)
  • packages/1-framework/1-core/framework-components/test/psl-ast.test.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/migration.json
  • examples/prisma-next-demo/test/repositories.integration.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
  • examples/prisma-next-demo/scripts/seed.ts

Comment thread packages/1-framework/2-authoring/psl-parser/src/parser.ts
Comment thread packages/2-sql/1-core/contract/src/validators.ts Outdated
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Heads-up from the stacked TML-2855 work (#808): this branch's fe9ce0f53 (canonicalize empty typeParams) left the demo's copied pgvector space stale — the demo's pgvector contract.json still carried "typeParams":{} + the old storageHash while the pgvector package's own contract had moved. Latent on this branch (surfaces only when the migration tooling runs); #808 re-emits it byte-identical to the package contract. Zero "typeParams":{} survivors tree-wide after that. If #805 gets another emission-touching round, nothing to do — just don't be surprised by the pgvector hunk living in #808.

aad4e15 made blockAttributes required on PslExtensionBlock; this
hand-built fixture predates it. Surfaced as a typecheck failure once
fresh dist propagated (earlier full-typecheck greens were stale-dist).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Re D5 — one correction before merge: the ref-plane check is at the wrong layer

D5 verification came back clean on the branch itself: the carrier shape, the entityKind: 'valueSet' string, the StorageValueSet.kind rename, the persisted demo wire (column/check refs plane: 'storage', domain field ref plane: 'domain', node tag 'valueSet'), and the spec amendments all match the settlement. The only quoted 'value-set' left in the tree is the negative test asserting rejection — correct. One finding to fix pre-merge:

The problem. The new check in validators.ts lives inside ValueSetRefSchema as a narrow:

const expectedPlane = ref.entityKind === 'enum' ? 'domain' : 'storage';

Two things wrong with it:

  1. It's a kind→plane lookup table — the exact pattern the D5 settlement rejected when it decided plane must be carried on the coordinate rather than derived. Nothing guarantees kind names stay unique across planes under pack-contributed kinds; this table would silently mis-fire the day one isn't.
  2. It doesn't enforce the directional invariant it was meant to. A ref is just a coordinate — structurally valid pointing at either plane. The rules live at the call sites, and the same shared schema validates all three (storage column, check constraint, domain field), so it has no site context. Concretely: a storage column carrying { plane: 'domain', entityKind: 'enum', … } passes this check — an internally consistent ref in the forbidden direction (storage → domain). The invariant remains unenforced.

The fix (small, two spots):

  • Delete the narrow. ValueSetRefSchema validates structure only: plane, namespaceId, entityKind, entityName, spaceId?. No plane↔kind coupling.
  • Add the rule at the storage carrier sites only: the valueSet ref on StorageColumnSchema and on CheckConstraintSchema requires plane: 'storage'. That single requirement is the directional invariant — storage may never reference domain — enforced where the carrying structure knows its own plane.
  • The domain field site gets no invariant-derived restriction (the invariant is asymmetric: domain may reference either plane). If you want to pin the domain field's ref to domain enums for now — today's only producer — that's a deliberate design pin of that site; state it as such rather than deriving it from the kind.

Out of scope for #805, noted for completeness: whether a ref actually resolves (contract[plane].namespaces[ns].entries[kind][name] exists) is a separate, kind-agnostic integrity check — same class as FK reference checking — and doesn't belong in the ref schema.

Cosmetic, your call: the new carrier doc comment says "entries slot key" — the settled vocabulary is entity kind / entries key; "slot" is retired. And the branch is currently CONFLICTING with main, so it needs a rebase regardless.

…tional-psl-enum2-block-author-the-new-enum

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

# Conflicts:
#	examples/prisma-next-demo/test/repositories.integration.test.ts
#	examples/prisma-next-demo/test/sql-dsl.integration.test.ts

@coderabbitai coderabbitai Bot left a comment

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.

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/test/sql-dsl.integration.test.ts`:
- Line 15: The import path uses a partial file extension; update the type import
for Contract to remove the `.d` suffix so TypeScript doesn't include file
extensions — change the import statement referencing '../src/prisma/contract.d'
to import from '../src/prisma/contract' (keeping the `import type { Contract }`
form) so the declaration file `contract.d.ts` is referenced without an explicit
extension.
🪄 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: fe4ff605-da9e-4f9a-b60d-207e0b85e0fb

📥 Commits

Reviewing files that changed from the base of the PR and between 53680da and 70e5712.

📒 Files selected for processing (4)
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/test/repositories.integration.test.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/prisma-next-demo/test/repositories.integration.test.ts

Comment thread examples/prisma-next-demo/test/sql-dsl.integration.test.ts Outdated
Comment thread examples/prisma-next-demo/scripts/seed.ts
Comment thread examples/prisma-next-demo/src/prisma/contract.d.ts
Comment thread examples/prisma-next-demo/src/queries/get-posts-by-priority.ts
Comment thread packages/1-framework/2-authoring/psl-parser/src/parser.ts
Comment thread packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
Comment thread packages/2-sql/1-core/contract/src/validators.ts Outdated
wmadden and others added 6 commits June 11, 2026 08:16
…ces interpreterLowered; §5 typing split to TML-2886

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…al drop)

Migration JSON files dropped the empty `"params": []` from execute steps
in line with TML-2867's canonicalization. These were left uncommitted from
the prior merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ore canonical migration ops

Two fixes:

1. contract.d.ts and the migration end-contract.d.ts were emitted with the
TML-2885 emitter (from two branches up the stack) and contained a
domain.namespaces.public.enum block that this branch's emitter does not
produce. Re-emit with pnpm emit:psl rebuilds them cleanly.

2. The migration ops.json files were in a state without "params": []
(matching a future branch's canonical form) but this branch's migration
system still emits empty params arrays. regen-example-migrations.mjs
restores them to the correct state and updates migration hashes accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
The ref is a coordinate: both planes are representable. Whether a given
plane/entityKind pairing is valid depends on where the ref is carried, not
on the ref itself.

- StorageColumnSchema and CheckConstraintSchema use StorageValueSetRefSchema
  (plane:'storage', entityKind:'valueSet') — the only valid shape at these sites.
- ModelFieldSchema uses DomainEnumRefSchema (plane:'domain', entityKind:'enum').
- ValueSetRefSchema loses the cross-field narrow() — it remains only as the
  general coordinate type (currently unused internally; callers use the
  call-site-narrowed schemas directly).

Tests: rework the existing plane-consistency tests to the call-site form,
add CodeRabbit's regression (a storage check ref with {plane:'domain',
entityKind:'enum'} rejects), add a domain field ref acceptance test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…re-declared-key bypass; consolidate parseExtensionBlockAttribute

- Renames `allowAdditionalParameters` → `variadicParameters` across
  descriptor interface, validator, parser, and all call sites.
- Updates the doc comment to describe the general block-body model,
  including the invariant that a declared key used bare is a diagnostic.
- Bare occurrence of a key declared in `descriptor.parameters` now
  emits PSL_INVALID_EXTENSION_BLOCK_MEMBER regardless of variadicParameters.
- Replaces the ad-hoc `parseExtensionBlockAttribute` implementation with
  a consolidated version built on `extractAttributeTokensWithSpans` +
  `parseAttributeToken`.
- Adds parser test for bare-declared-key diagnostic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…; Task 5 smalls

- Deletes `interpreterLowered` from `AuthoringPslBlockDescriptor` and
  `assertPslBlocksHaveFactories`; every PSL block descriptor now requires
  a matching `entityTypes` factory with the same discriminator.
- Adds optional `codecLookup`, `sourceId`, and `diagnostics` to
  `AuthoringEntityContext` so factories can validate values without
  needing a separate channel.
- Registers a real `enum2` entity type factory in the SQL family pack
  (`sqlFamilyEnum2EntityDescriptor`); `processEnum2Declarations` in the
  PSL interpreter shrinks to factory dispatch + handle collection.
- Renames internal "leaf" vocabulary in `framework-authoring.ts`:
  `AuthoringLeafEntry` → `DescriptorEntry`, `collectAuthoringLeafPaths`
  → `collectDescriptorPaths`, `collectAuthoringLeafDiscriminators` →
  `collectDescriptorEntries`, `PslBlockLeafEntry` → (removed, merged
  into `DescriptorEntry`), `collectPslBlockLeafEntries` →
  `collectPslBlockDescriptorEntries`.
- Adds test for `PSL_ENUM2_MISSING_FACTORY` when no factory is registered.
- Uses `ifDefined('enumTypeHandle', enumHandle)` at the model field
  build site.
- Fixes `contract.d` → `contract` import in sql-dsl integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

Fix round for the 2026-06-11 review is pushed (80206cfb7..4903cd3e9). Headlines:

  • interpreterLowered is dead — enum2 now registers a real entityTypes factory ((block, ctx) => EnumTypeHandle) that owns member validation + handle construction; assertPslBlocksHaveFactories is back to full strength. Two general facilities enable it: AuthoringEntityContext carries the codec lookup, and factories get a span-accurate diagnostics sink.
  • Grammar generalized per the settled modelvariadicParameters (renamed), bare identifiers stay as keys-without-values, declared parameters must be key = value (bypass fixed), @@ attribute parsing consolidated onto the shared tokenizer, invented "leaf" vocabulary gone.
  • Plane validation moved to the carrying sites — the ref schema is a neutral coordinate again; storage column/check and domain field sites own direction (+ the regression test).
  • Branch hygiene — the four TML-2867 fixture re-renders committed; the post-merge contract.d.ts had been re-emitted with the stack-tip (TML-2885: emit typed domain enum block — db.enums literal-typed through emit #809) emitter and carried a domain enum block this branch doesn't produce — re-emitted with this branch's own emitter, contract.json byte-identical.
  • CodeRabbit: codec-undefined diagnostic added, ifDefined swap, contract.d import fix. The ORDER-BY-lexicographic finding is incorrectorderBy('priority') on a value-set column renders array_position(ARRAY[…]::text[], col) declaration-order (TML-2852 R8, integration-tested against PGlite).

Spec §5 typing-mechanism divergence from the review discussion is tracked as TML-2886 (post-stack). All threads have replies; gates green (framework-components 397 / psl-parser 244 / contract-psl 242 / family-sql 369 / sql-contract 188; fixtures:check byte-identical; lint:deps clean).

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/2-sql/1-core/contract/src/validators.ts (1)

80-94: 💤 Low value

Consider adding strict property checking for consistency.

Both StorageValueSetRefSchema and DomainEnumRefSchema lack '+': 'reject', while similar reference schemas in this file (ForeignKeyReferenceSchema at line 188, ForeignKeySourceSchema at line 196) include it. Adding strict checking would prevent silently accepting malformed refs with extra properties.

♻️ Suggested change
 const StorageValueSetRefSchema = type({
+  '+': 'reject',
   plane: "'storage'",
   namespaceId: 'string',
   entityKind: "'valueSet'",
   entityName: 'string',
   'spaceId?': 'string',
 });

 const DomainEnumRefSchema = type({
+  '+': 'reject',
   plane: "'domain'",
   namespaceId: 'string',
   entityKind: "'enum'",
   entityName: 'string',
   'spaceId?': 'string',
 });
🤖 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/2-sql/1-core/contract/src/validators.ts` around lines 80 - 94, Add
strict property checking to the two reference schemas by updating
StorageValueSetRefSchema and DomainEnumRefSchema to include the same strictness
marker used elsewhere (e.g., '+': 'reject'); locate the definitions for
StorageValueSetRefSchema and DomainEnumRefSchema and add the '+': 'reject'
property to each schema object so extra properties are rejected consistently
with ForeignKeyReferenceSchema and ForeignKeySourceSchema.
🤖 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 `@packages/2-sql/2-authoring/contract-psl/test/fixtures.ts`:
- Around line 180-188: The empty-enum branch currently reuses the diagnostic
code 'PSL_ENUM2_MISSING_TYPE' which collides with other checks; change the
diagnostic.code in the members.length === 0 branch to a distinct value (e.g.,
'PSL_ENUM2_EMPTY_MEMBERS' or 'PSL_ENUM2_MISSING_MEMBER') and update any
tests/expectations that assert on this diagnostic; locate the branch that pushes
into diagnostics (uses variables diagnostics, block, sourceId, span) and only
change the code string so other fields (message, span, sourceId) remain
unchanged.

In `@packages/2-sql/9-family/src/core/authoring-entity-types.ts`:
- Line 85: Replace the bare cast on the value passed to codec.decodeJson: locate
the call to codec.decodeJson(memberName as unknown as JsonValue) and either
remove the cast entirely if memberName (a string) is already assignable to
JsonValue, or replace the bare cast with a safe production cast using
blindCast<JsonValue, "MemberNameAsJson"> from `@prisma-next/utils/casts` (import
it if missing) so the call becomes codec.decodeJson(blindCast<JsonValue,
"MemberNameAsJson">(memberName)); ensure you modify the same usage in
authoring-entity-types (the memberName variable and codec.decodeJson
invocation).

---

Nitpick comments:
In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 80-94: Add strict property checking to the two reference schemas
by updating StorageValueSetRefSchema and DomainEnumRefSchema to include the same
strictness marker used elsewhere (e.g., '+': 'reject'); locate the definitions
for StorageValueSetRefSchema and DomainEnumRefSchema and add the '+': 'reject'
property to each schema object so extra properties are rejected consistently
with ForeignKeyReferenceSchema and ForeignKeySourceSchema.
🪄 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: 6fa420a9-9c94-48a2-a2b6-1ac4d0580560

📥 Commits

Reviewing files that changed from the base of the PR and between 70e5712 and 4903cd3.

⛔ Files ignored due to path filters (1)
  • projects/enums-as-domain-concept/slices/transitional-psl-enum-keyword/spec.md is excluded by !projects/**
📒 Files selected for processing (17)
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser-enum2.test.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts
  • packages/2-sql/9-family/src/core/authoring-entity-types.ts
  • packages/2-sql/9-family/src/core/control-descriptor.ts
  • packages/2-sql/9-family/src/exports/pack.ts
💤 Files with no reviewable changes (2)
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/migrations/app/20260610T0000_add_priority_enum/end-contract.d.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts
  • packages/2-sql/9-family/src/core/control-descriptor.ts
  • packages/2-sql/9-family/src/exports/pack.ts
  • examples/prisma-next-demo/test/sql-dsl.integration.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts

Comment thread packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
Comment thread packages/2-sql/9-family/src/core/authoring-entity-types.ts Outdated
wmadden and others added 4 commits June 11, 2026 10:06
The self-edge guard copies the whole migrations tree; the suite default
(timeouts.default, 100ms) no longer covers it as migrations accumulate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
processEnum2Declarations input admits undefined contributions (matching
the interpreter's optional input); the entity-context diagnostics sink
wraps the ContractSourceDiagnostic array; codecLookup via ifDefined
(exactOptionalPropertyTypes); optional-chain lint fix in test fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ate CLI member arg at runtime

string is directly assignable to JsonValue, so the enum2 factory's bare-member
decode needs no cast; the demo's enum-priority-filter validates the requested
member against the emitted accessor's names instead of asserting a union.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric added this pull request to the merge queue Jun 11, 2026
Merged via the queue into main with commit 1c13f26 Jun 11, 2026
21 checks passed
@wmadden-electric wmadden-electric deleted the tml-2882-slice-transitional-psl-enum2-block-author-the-new-enum branch June 11, 2026 09:14
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 11, 2026
…LT 'low' (prisma#808)

## Linked issue

Refs
[TML-2855](https://linear.app/prisma-company/issue/TML-2855/slice-enum-member-defaults-defaultadmin-renders-default-value).
**Was stacked on prisma#805 (TML-2882)** — now merged; this PR targets `main`
directly (rebased, four commits).
With TML-2885, the last parity prerequisite before the cutover
(TML-2853).

## At a glance

An enum field declares its default by naming a **member**, on both
authoring surfaces, and it lands as an ordinary literal:

```prisma
enum2 Priority {
  @@type("pg/text@1")
  Low    = "low"
  High   = "high"
  Urgent = "urgent"
}

model Post {
  priority Priority @default(Low)   // names a member — not the raw string "low"
}
```

```ts
field.namedType(Priority).default(Priority.members.Low)   // members only — .default('lwo') is a compile error
```

```sql
priority text NOT NULL DEFAULT 'low'
```

Before this PR, `.default()` on an enum-typed field accepted any
literal, and PSL `@default` had no member awareness.

## Decision

Member-to-literal resolution happens **at lowering, per authoring
surface** — there is **no new `ColumnDefault` variant** and no machinery
below the lowering. The storage column carries the resolved literal (`{
"kind": "literal", "value": "low" }`), so the storage plane stays
plannable in isolation per the directional invariant settled on prisma#805
(domain may reference storage; storage may never reference domain). The
`enumMember` variant the original ticket described predates that
correction and was never shipped; this PR supersedes it (see the
ticket's 2026-06-10 respec comment and project spec §9).

## Reviewer notes

- **The negative type-tests are the heart of the TS surface:**
`EnumScalarFieldBuilder.default()` narrows to the handle's member value
union (`Handle['values'][number]`); the package typecheck passes with
the `@ts-expect-error` directives present, so each rejected literal
genuinely errors. `defaultSql` is a type error (and runtime throw) on
enum builders.
- **A latent stale artifact from the base branch is re-emitted here:**
commit `fe9ce0f53` on the base branch canonicalized empty `typeParams`
out of emitted contracts, but the demo's copied pgvector space was never
re-emitted and still carried the old shape + hash. Running the migration
tooling surfaced it; this PR re-emits it (byte-identical to the pgvector
package's own contract). Tree-wide there are zero `"typeParams":{}`
survivors now.
- **Known follow-up (not exercised by any current usage):** the
member-only `.default()` narrowing is lost if `.nullable()`/`.id()` is
chained before it (base builder methods return the base type). Recorded
in the review ledger; closes when nullable-with-default enum fields are
needed.
- The spec's "At a glance" shows `"default": "low"` as shorthand; the
persisted shape is the existing object form
(`{"kind":"literal","value":"low"}`), identical to every other literal
default on the wire (e.g. `task.status`).

## How it fits together

1. **TS DSL**
([contract-dsl.ts](packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts)):
the `EnumTypeHandle` overload of `namedTypeField` returns an
`EnumScalarFieldBuilder` whose `.default()` accepts only the member
value union and lowers to the existing literal default.
2. **PSL**
([psl-field-resolution.ts](packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts)):
when a field's type resolves to an `enum2`, `@default(<identifier>)`
resolves the member name to its value (literal default); non-member
identifiers, quoted raw values, and function defaults are span-accurate
diagnostics. Non-enum and native-enum `@default` lowering is untouched.
3. **Nothing below the lowering changes** — validator, planner,
`buildColumnDefaultSql`, verification all see an ordinary literal
default.
4. **Demo** ([examples/prisma-next-demo](examples/prisma-next-demo)):
`@default(Low)` in the PSL schema (mirrored in the TS no-emit contract),
re-emitted artifacts, migration `20260610T2216_set_priority_default` (a
single `setDefault` op), and an integration test inserting a post
**without** `priority` and reading back `'low'` — including the
type-level proof that the insert payload's `priority` became optional.

## Behavior changes & evidence

- **TS `.default()` is members-only on enum fields** —
[contract-dsl.ts](packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts);
evidence:
[enum-type.member-defaults.test.ts](packages/2-sql/2-authoring/contract-ts/test/enum-type.member-defaults.test.ts)
(string + int codecs, positive/negative type tests, lowering shape).
- **PSL `@default(member)` resolves on enum2 fields, with three
rejection diagnostics** —
[psl-field-resolution.ts](packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts);
evidence:
[interpreter.enum2.test.ts](packages/2-sql/2-authoring/contract-psl/test/interpreter.enum2.test.ts),
including the strict PSL/TS parity test (deep-equal + `storageHash`)
extended with a defaulted field.
- **The demo proves the default end-to-end** — schema, migration, and
the omit-`priority` insert reading back `'low'`; evidence:
[enum-surface.integration.test.ts](examples/prisma-next-demo/test/enum-surface.integration.test.ts).

## Testing performed

- `pnpm test:packages` — 800 files / 10,352 tests green on final HEAD
- Full `pnpm typecheck` green for all packages this diff can reach
(recurring stale-dist environment artifacts in unrelated packages
adjudicated in review, same shape as on the base branch)
- `pnpm fixtures:check` — clean outside the demo's deliberate changes
- `pnpm lint:deps` — clean

## Skill update

n/a — the `@default(member)` surface rides the transitional `enum2`
keyword, which is retired at the cutover; user-facing docs land with the
cutover rename (same policy as prisma#805).

## Follow-ups

- Builder-chaining: preserve member-only `.default()` narrowing through
`.nullable()`/`.id()` chains when nullable-with-default enum fields
land.
- [TML-2885](https://linear.app/prisma-company/issue/TML-2885) —
emit-typed `db.enums` (the other cutover prerequisite).

## Alternatives considered

- **An `enumMember` `ColumnDefault` variant recording the member in
storage** (the original ticket design). Rejected by the 2026-06-10
directional-invariant settlement: it is a storage → domain reference,
and storage must be plannable in isolation. The resolved literal carries
everything DDL needs; the authored source names the member, so intent is
recoverable by re-emit.
- **Default by raw value (`@default("low")`)**. Rejected (per the
ticket): severs the domain link, can't be membership-checked at
authoring time; it is an explicit diagnostic now.
- **Recording member intent on the domain field in this slice.**
Deferred — additive later if introspection/diffing wants it; no
observable behavior needs it today.

## Checklist

- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO
status check will block merge if any commit is missing a
`Signed-off-by:` trailer.
- [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is
scoped to one logical concern.
- [x] Tests are updated (or `n/a` if the change is doc-only / refactor
with no behavioural delta).
- [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear
ticket prefix + concise title naming the concrete deliverable). See
`.claude/skills/create-pr/SKILL.md` for the full convention.
- [x] The **Skill update** section above is filled in (or stated `n/a —
internal only`).



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enum field defaults: You can now specify default values for enum-typed
fields using `@default(EnumMember)` syntax. This allows enum fields to
be optional on insert operations, with the database automatically
applying the specified default value when omitted.

* **Chores**
* Added upgrade guidance and migration path for enum field defaults
support in v0.14.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Will Madden <madden@prisma.io>
Co-authored-by: Will Madden <madden@prisma.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 11, 2026
…gh emit (prisma#809)

## Linked issue

Refs [TML-2885](https://linear.app/prisma-company/issue/TML-2885).
**Was stacked on prisma#808prisma#805** — both now merged; this PR targets `main`
directly (rebased, four commits).
With TML-2855, completes the cutover's (TML-2853) parity prerequisites.

## At a glance

The emitted `contract.d.ts` now types the domain `enum` block, so
`db.enums` is literal-typed **through the emitted contract** — closing
the R6 emit-parity gap found in prisma#805's review:

```ts
// before (emitted path): runtime correct, types collapsed
db.enums.public.Priority.values        // JsonValue[]
db.enums.public.Priority.members.High  // JsonValue

// after
db.enums.public.Priority.values        // readonly ['low', 'high', 'urgent']
db.enums.public.Priority.members.High  // 'high'
```

```ts
// emitted contract.d.ts — the public namespace gains:
readonly enum: {
  readonly Priority: {
    readonly codecId: 'pg/text@1';
    readonly members: readonly [
      { readonly name: 'Low'; readonly value: 'low' },
      { readonly name: 'High'; readonly value: 'high' },
      { readonly name: 'Urgent'; readonly value: 'urgent' }
    ];
  };
};
```

## Decision

One change site: the emitter renders the per-namespace domain `enum`
block with literal member tuples (order preserved — it's semantic). The
**existing** accessor type chain (`NamespaceEnumAccessors` /
`NamespacedEnums` in `enum-accessor.ts`) already extracts literal
`values`/`members` from exactly this shape, so it is untouched — as are
the runtime and the TML-2852 field narrowing. `contract.json` and every
hash are byte-identical: this is a types-only emission change.

The acceptance evidence is deletion: the demo's
`getPriorityEnumFromEmit()` workaround and its `blindCast`s — whose
reason strings documented exactly this gap — are gone; every consumer
reads `db.enums.public.Priority` directly, cast-free (net −4 `blindCast`
sites).

## Reviewer notes

- **Non-vacuity is compile-level:** without the emitted enum block,
`db.enums.public.Priority` does not exist at the type level, so the demo
type tests fail to compile (verified by deleting the block and watching
six files fail; restored cleanly). Stronger than a runtime assert.
- **One genuinely new `blindCast`** in
[generate-contract-dts.ts](packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts)
(`ns.enum`), byte-identical in idiom to the two pre-existing sibling
casts (`ns.models`, `ns.valueObjects`) it sits beside.
- **A real stack regression was caught and fixed during this slice:**
TML-2882's `aad4e15b3` made `blockAttributes` required on
`PslExtensionBlock`; a cli test fixture predated it. Earlier "full
typecheck green" runs were stale-dist false greens. Fixed on the base
branch (`53680da2e` on prisma#805) and the stack rebased.
- The JSON→TS literal rendering reuses the TML-2852 D4 helpers
(`serializeValue`/`serializeObjectKey`) — no duplication.

## Behavior changes & evidence

- **The emitter renders the domain enum block** —
[generate-contract-dts.ts](packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts);
evidence:
[emitter.integration.test.ts](packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts)
(string codec, int-codec bare-number literals, quoted non-identifier
member keys, enum-less namespaces emit no block; unbound-namespace
coverage folded in).
- **`db.enums` is literal-typed through emit, cast-free in the demo** —
[get-posts-by-priority.ts](examples/prisma-next-demo/src/queries/get-posts-by-priority.ts)
(workaround deleted),
[demo-dx.types.test.ts](examples/prisma-next-demo/test/demo-dx.types.test.ts)
(literal `members.High` / `values` through the emitted artifacts).
- **Types-only:** no `.json` file changes; four `.d.ts` files regenerate
with only the additive enum block.

## Testing performed

- `pnpm test:packages` — green on final HEAD (one adapter-postgres
DB-infra timeout, diff-unreachable)
- `pnpm typecheck` — green incl. `@prisma-next/cli` against fresh dist
(see reviewer note on the fixed stack regression)
- `pnpm fixtures:check` — clean (full re-emit against fresh dist, no
drift)
- `pnpm lint:deps` — clean

## Skill update

n/a — types-only emission change; the user-facing `db.enums` surface was
documented with TML-2852, and the transitional-keyword docs policy
(skill lands at cutover) carries the rest.

## Follow-ups

- TML-2853 (cutover) — parity prerequisites now complete once this stack
merges.

## Alternatives considered

- **Narrowing at the accessor instead of emitting the block**
(parameterize `EnumAccessor` per call site). Rejected: the accessor
chain was already built to extract from the typed contract shape;
emitting the shape fixes every consumer at once and keeps "the `.d.ts`
is the contract" honest.
- **Emitting only the value tuples (not full member pairs).** Rejected:
`members.<Name>` needs the name→value literal pairs; the full tuple is
what `EnumEntryMembers` consumes, and it matches the runtime JSON
one-for-one.

## Checklist

- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO
status check will block merge if any commit is missing a
`Signed-off-by:` trailer.
- [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is
scoped to one logical concern.
- [x] Tests are updated (or `n/a` if the change is doc-only / refactor
with no behavioural delta).
- [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear
ticket prefix + concise title naming the concrete deliverable). See
`.claude/skills/create-pr/SKILL.md` for the full convention.
- [x] The **Skill update** section above is filled in (or stated `n/a —
internal only`).

---------

Signed-off-by: Will Madden <madden@prisma.io>
Co-authored-by: Will Madden <madden@prisma.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric pushed a commit that referenced this pull request Jun 12, 2026
…ers (ADR 224)

The enum2 test added by PR #805 used closed-shape `.entries.table?.` and
`.entries.valueSet?.` dot access, which fails TS4111 after D1 widened
`SqlNamespace.entries` to an index-signature record. Migrate to
`namespaceTables()` / `namespaceValueSets()` helpers, matching the
canonical style established in D1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 15, 2026
…(ADR 224/225) (prisma#812)

**Decision:** a storage namespace stores its entities in one open
dictionary — keyed by entity kind, then by entity name — and that path
is the *only* way the framework addresses, validates, and hydrates them.
This PR makes the runtime types and the validation/hydration machinery
match that model (ADR 224/225). Nothing persisted changes: the wire
format was already this shape, byte for byte.

Here is a namespace exactly as it sits in a committed `contract.json`
today (unchanged by this PR):

```jsonc
// storage.namespaces.public
{
  "id": "public",
  "entries": {
    "table":    { "user": { /* StorageTable */ }, "post": { /* … */ } },
    "type":     { "user_type": { "kind": "postgres-enum", "values": ["admin", "user"] } },
    "valueSet": { "Priority": { "kind": "valueSet", "values": ["low", "high"] } }
  }
}
```

Every entity has a coordinate — `(plane, namespaceId, entityKind,
entityName)` — and one expression resolves any coordinate, for any
entity kind, including kinds a target or extension pack contributes that
the framework has never heard of:

```ts
entityAt(storage, { namespaceId, entityKind, entityName })
// ≡ storage.namespaces[namespaceId].entries[entityKind][entityName]
```

## The problem

The persisted JSON was already open, but the runtime types were not.
Each namespace class declared a closed object with one named field per
kind it knew about:

```ts
// before — PostgresSchema
readonly entries: {
  readonly table: Readonly<Record<string, StorageTable>>;
  readonly type: Readonly<Record<string, PostgresEnumType>>;
  readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
};
```

A closed type forces every consumer to know every kind. The validator
hardcoded `table?`/`type?`/`valueSet?` schema fields; the postgres
serializer hydrated the `type` map through a lookup keyed by the node
tag `'postgres-enum'`; generic code that received a coordinate had to
switch on the kind to pick a property. Adding a new entity kind (the RLS
branch needs policies and roles) meant editing framework and family code
— or smuggling the kind past the closed types, which is what that branch
currently does.

## The change

**The entity kind is the `entries` key** — `table`, `type`, `valueSet`,
`collection` — exactly the strings already persisted. Everything
dispatches on that one string.

**Open types that keep typed property access.** `entries` is typed as
the open dictionary *intersected* with its known kinds as optional keys:

```ts
// after — the SQL family shape (Postgres adds `type?`, Mongo has `collection?`)
readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> & {
  readonly table?: Readonly<Record<string, StorageTable>>;
  readonly valueSet?: Readonly<Record<string, StorageValueSet>>;
};
```

Unknown kinds remain valid (walkers iterate `Object.entries` as before),
but `ns.entries.table` is ordinary typed property access — no helper
layer, no per-call-site casts. Class instances additionally expose
non-enumerable per-kind getters (`schema.type`, `db.collection`), so
`JSON.stringify` emits only `id` + `entries`. Repo-wide there are
exactly two read styles: generic walkers use `entries[kind][name]`;
typed code uses property access or the getters. ~50 production and ~190
test call sites moved.

**Construction is permissive-carry; the JSON boundary fails closed.**
This split is the heart of the open model, so be explicit about where
each rule applies:

- **In-memory construction (builders/constructors) is permissive.** A
builder constructs the kinds it owns a factory for (`table`,
`collection`, …) into IR instances and **freezes-and-carries any other
kind's map untouched**. It does *not* enforce a closed kind list. This
is required by the open model: a pack-contributed kind (an RLS policy, a
Postgres role) must pass through `buildSqlNamespace` /
`buildMongoNamespace` without the family layer knowing its name — if
construction threw on unknown kinds, pack kinds would be un-authorable,
which is the closed-list behavior this PR exists to remove. The carry
tests that assert a synthetic unknown kind survives construction
(frozen, present in `JSON.stringify`, yielded by `elementCoordinates`)
are pinning this promise on purpose.
- **The JSON boundary (validation + hydration) fails closed.** When a
contract is *deserialized*, the kind→schema registry validates every
kind and the hydration dispatch constructs every kind; an unregistered
kind is rejected with an error naming the kind and the namespace id.
This is where corruption and typos are caught — at the point a contract
enters from disk, not at every in-memory construction. The accepted
trade-off: a typo'd kind in hand-authored TypeScript surfaces at
emit-time boundary validation rather than at the `build*` call.

Construction also accepts *input literals only* — the `Instance | Input`
unions and their `v instanceof X ? v : new X(v)` normalization are gone
(hydration constructs from validated JSON; authoring passes literals;
the one caller that fed constructed instances was migrated).

**Validation dispatches through one kind→schema registry.** This *is*
the fail-closed boundary above. Validation walks
`Object.entries(entries)` and validates each inner map against the
schema registered for that kind. The SQL core registers `table` and
`valueSet` into the same registry the postgres pack registers `type`
into — one tier, no privileged built-in fallback — and the postgres pack
owns `PostgresEnumTypeSchema` outright, so the family layer contains
zero target-kind knowledge. A kind nobody registered fails validation
with an error naming the kind and namespace id. Hydration is also
kind-dispatched and fail-closed (the `hydrateEntriesKind` hook, which a
target overrides per kind), and namespace **construction** carries
unknown kinds and builds the kinds it owns — but both of those still use
per-kind code (a `hydrateEntriesKind` hook and an `if (kind === …)`
construction switch) rather than the validation registry. **Unifying
construction and hydration onto one shared kind→factory registry is the
explicit follow-up
[TML-2890](https://linear.app/prisma-company/issue/TML-2890); this PR
delivers the registry on the validation path only.**

**One canonical lookup.** `entityAt(storage, coordinate)` lives beside
`elementCoordinates` in the framework; the hand-rolled two-step lookups
(emitter FK validation and siblings) are gone.

**Docs follow the code**, and one ADR bug is fixed on the way: ADR 221
§115 stated the cross-plane reference invariant backwards. The correct
direction — a domain entity may reference a storage entity, never the
reverse, because the migration planner/runner must consume the storage
plane in isolation — already misled one project design; the
parenthetical now matches reality.

## One breaking change for downstream code

The family-generic `SqlContractSerializer` no longer knows the postgres
`type` kind (per ADR 225, the family carries no target knowledge), so it
rejects postgres contracts with a clear error naming the kind. The fix
is one line, and contract deserialization is now properly generic — no
cast:

```ts
// before
const contract = new SqlContractSerializer(…).deserializeContract(json) as Contract;
// after
const contract = new PostgresContractSerializer(…).deserializeContract<Contract>(json);
```

SQLite and table-only contracts are unaffected. Upgrade instructions are
recorded via the repo's upgrade-instructions process; the examples in
this PR show the migration. The `as Contract` casts are swept from the
demo, retail-store, and mongo-demo apps (three sites in
`multi-extension-monorepo` are a recorded follow-up).

## What deliberately does not change

- **Every persisted byte.** `storageHash` is content-addressed over the
entries subtree, so the persisted shape — keys, key order, presence
semantics — is preserved exactly. `pnpm fixtures:check` passes with zero
committed-artifact diffs (and earned its keep: it caught the one
hash-drift regression introduced mid-review and forced the correct fix).
- **Node-body `kind` tags**, including the two that don't match their
entries key (`'postgres-enum'` under `type`, `'mongo-collection'` under
`collection`). They sit inside hash-covered node bodies, and nothing
dispatches on them anymore — see Alternatives.
- **`elementCoordinates`** — the framework's coordinate walker already
read `entries` structurally; it is the consumer this refactor had to
keep working unchanged, and it did.

## Verification

All acceptance criteria carry committed tests: exact-shape serialization
per concretion (getters absent from JSON), deep-freeze, unknown-kind
rejection at construction/validation/hydration, round-trip unchanged,
and a coordinate-resolution test asserting every `elementCoordinates`
tuple resolves through `entries[entityKind][entityName]` for
representative postgres, sqlite, and mongo contracts. Local gates:
build, typecheck (138/138), lint (79/79), `lint:deps`, `fixtures:check`
(clean tree, zero contract diffs); cast ratchet net negative vs
merge-base. Integration suites run on this PR's CI.

## Known follow-ups (tracked, deliberately not in this PR)

Review surfaced three refinements that are real but are each a distinct,
higher-blast-radius change; bundling them into this already-large branch
is what produced the one regression this PR hit, so they ship as focused
PRs:

- **[TML-2890](https://linear.app/prisma-company/issue/TML-2890) —
Uniform kind dispatch in construction & hydration.** One shared
kind→factory registry replacing the per-kind `if (kind === …)` switches
in the 8 construction/hydration files, so dispatch is uniform
everywhere, not just in validation. (RLS adds its `policy` construction
as a registration rather than a switch branch once this lands — it is
not blocked on it.)
- **[TML-2891](https://linear.app/prisma-company/issue/TML-2891) —
Eliminate the SQL family placeholder concretion.** Delete
`SqlBoundNamespace`/`buildSqlNamespace`/`SqlNamespaceTablesInput`; SQL
namespaces become always-target concretions; supabase emits
`postgres-schema`. This is a wire-format change (committed contracts
regenerate) and must land in isolation; the reverted item-G `instanceof`
guard resolves here.
- **[TML-2892](https://linear.app/prisma-company/issue/TML-2892) —
Migration-author accessor.** A `Contract`/`Storage` accessor for
collections/tables by name, so migrations stop reaching through
`namespaces.__unbound__.entries.collection`.

## Out of scope (deliberate)

RLS (its branch rebases onto this and deletes its workarounds); the
migration planner; `ValueSetRef` (settled in prisma#805); the domain plane's
flat `models`/`valueObjects` shape and the namespace-kind serializer
strings (recorded follow-ups); the ADR 225/126 rewrite batch.

## Alternatives considered

- **A helper-function layer instead of intersection typing.** An earlier
round of this PR shipped
`namespaceTables()`/`namespaceValueSets()`/`namespaceCollections()` to
give typed reads over a fully-widened `entries`. Rejected in review and
deleted: the helpers were a parallel per-kind API duplicating the
getters, and they existed only because the type had been over-widened —
the intersection keeps the dictionary open while making plain property
access type-safe.
- **Keep the `Instance | Input` construction unions.** Also shipped
early, also deleted: the `instanceof`-normalization they force is
unreadable and hides which paths actually carry what. One shape per path
needs no normalization.
- **Rename the legacy node tags to match their entries key**
(`'postgres-enum'` → `'type'`, `'mongo-collection'` → `'collection'`) so
the kind is literally one string everywhere. Rejected: the tags are
persisted inside hash-covered node bodies, so the rename would churn
every committed contract and `storageHash` — and the postgres-enum
machinery is deleted by the enums cutover (TML-2853) regardless. New
kinds (e.g. `valueSet`, post-prisma#805) use one string across coordinate,
entries key, node tag, and registration from day one.
- **Rename the entries keys to match the node tags.** Rejected: the keys
are also persisted — strictly worse churn for the same goal.
- **Keep the closed types and teach each consumer the kind list.**
Rejected: that moves the kind→property mapping into every consumer and
keeps pack-contributed kinds unreachable — the exact model ADR 224
rejects.
- **Translate between kind and key at the serializer boundary, or carry
a mapping field on contributions** (the `entrySlotName` approach the RLS
branch had to invent). Rejected: both are the same translation table in
disguise; ADR 224 explicitly rejects boundary reshaping, and the open
dictionary makes the mapping unnecessary.

**Linear:** TML-2887

---------

Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Co-authored-by: Will Madden <madden@prisma.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants