Skip to content

TML-2584: target packs contribute entity descriptors with hydration and validation#552

Merged
wmadden merged 46 commits into
mainfrom
tml-2584-s1a-substrate
May 21, 2026
Merged

TML-2584: target packs contribute entity descriptors with hydration and validation#552
wmadden merged 46 commits into
mainfrom
tml-2584-s1a-substrate

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented May 20, 2026

Today, every new storage-entity kind a target wants to ship — Postgres enums, future Postgres policies, SQLite emulated enums, MongoDB indexes — needs a bespoke serializer override that knows where in the contract those entries live, how to rehydrate them into IR class instances, and how to validate the on-the-wire shape. The override is hand-written, family-specific, and duplicates work the framework already does for built-in entities.

This PR lets a target pack declare those entries as descriptors instead. Postgres is the first consumer:

-// Before: PostgresContractSerializer hand-rolls enum hydration in an override
-class PostgresContractSerializer extends SqlContractSerializer {
-  serializeContract(envelope) {
-    const hydrated = super.serializeContract(envelope);
-    for (const namespace of Object.values(hydrated.storage.namespaces)) {
-      for (const [name, raw] of Object.entries(namespace.types ?? {})) {
-        if (raw?.kind === 'postgres-enum') {
-          namespace.types[name] = new PostgresEnumType(raw); // bespoke
-        }
-      }
-    }
-    return hydrated;
-  }
-}
+// After: Postgres declares its enum as a descriptor; the family base hydrates it
+export const postgresAuthoringEntityTypes = {
+  enum: {
+    kind: 'entity',
+    discriminator: 'postgres-enum',
+    validatorSchema: PostgresEnumTypeSchema,
+    output: { factory: (input) => new PostgresEnumType(input) },
+  },
+} as const satisfies AuthoringEntityTypeNamespace;

The descriptor's discriminator field (already required for every pack contribution) is the single key the family-base serializer and the family's arktype validator both look up. The existing output.factory doubles as the hydration constructor — given the raw envelope it returns the IR instance, so no separate hydrate field is needed. Pack-contributed validatorSchema fragments compose additively with the family's built-in validator — both must accept the entry — so contributions extend the framework's validation surface rather than replacing it.

Decision

This PR ships three coupled pieces that together let pack contributions reach the same end-to-end completeness as built-in entities:

  1. One new optional field on AuthoringEntityTypeDescriptorvalidatorSchema? — wired through the family arktype validators. Hydration reuses the existing output.factory field (no parallel hydrate? field); the family-base hydration registry stores the factory partial-applied with a per-target AuthoringEntityContext. Keyed on the descriptor's existing discriminator field.
  2. A framework-level namespace walk. A free function elementCoordinates(storage) in @prisma-next/framework-components/ir yields { plane, namespaceId, entityKind, entityName } tuples over any Storage-shaped value, walking each namespace's own-enumerable properties structurally — no family-name lookup table, no up-front knowledge of slot vocabularies.
  3. Namespace.kind narrowed to required. Codifies the structural invariant the walk depends on (every namespace carries its family discriminator). Runtime non-enumerable Object.defineProperty(this, 'kind', { value, enumerable: false }) is unchanged, so emitted JSON envelopes don't gain a kind field — only the type-side narrows. The Mongo TS builder and PSL interpreter both construct new MongoStorage(...) instances now, so Mongo kind is materialized at runtime alongside SQL.

Contract also gains an optional domain? plane field as scaffolding for the upcoming domain-plane work; it is empty in this PR.

Mechanism

The descriptor surface

AuthoringEntityTypeDescriptor<Input, Output> gains one field:

  • validatorSchema?: Type<unknown> — an arktype schema fragment for the entry's on-the-wire shape. The family validator looks up the fragment by the entry's kind and composes it with the family's built-in fallback validator (both must pass) when the kind matches the family-owned kind; otherwise it validates against the pack fragment alone. Composition is keyed on discriminator, so two packs contributing different entity kinds never collide.

Hydration reuses the existing output.factory(input, ctx) field — the family-base serializer wraps it as (raw) => factory(raw, ctx) and stores the wrapped function in entityTypeRegistry. The per-target ctx ({ family: 'sql', target: 'postgres' } for Postgres) is a module-level constant in the target pack's serializer.

Authors don't need to know about the registry: declaring the descriptor in a pack's AuthoringContributions.entityTypes is enough — the registry boot wires the rest.

packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts
packages/2-sql/1-core/contract/src/validators.ts
packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts

The namespace-entry hydration path uses an arktype NamespaceRawSchema ({ id: 'string', 'kind?': 'string' }) to narrow the raw envelope, replacing the chain of hand-rolled raw as Record<string, unknown> + obj['id'] as string casts that previously prefixed every slot iteration.

The namespace walk

elementCoordinates(storage) iterates each namespace's own-enumerable properties structurally — it doesn't need to know what slots a family or target has contributed. Whatever entity-bearing maps a namespace exposes (tables, collections, enum, policy, future pack contributions) get walked uniformly. The function is free, not a method on the Storage interface, because emitted Contract<SqlStorage> literals are structurally checked against SqlStorage at every consumption site, and emitted literals carry no method members; adding a method to the interface would break every emitted-fixture consumer.

The yielded coordinate carries a plane axis ('storage' for this walk; 'domain' reserved for the upcoming sibling walk over the domain plane).

packages/1-framework/1-core/framework-components/src/ir/storage.ts

Postgres delegates instead of overriding

PostgresContractSerializer's hand-rolled enum-branch override is gone; the family-base hydration loop dispatches to entityTypeRegistry.get(entry.kind) for every namespace entry, including Postgres enums. The registered factory wraps postgresEnumDescriptor.output.factory(input, POSTGRES_AUTHORING_CTX) so hydration produces the same PostgresEnumType instance the prior override did — byte-identical fixtures, zero drift.

The family-base guard that previously rejected any types-slot population when postgres-enum was unregistered now scopes the rejection to entries whose kind === 'postgres-enum'; entries with a different contributed kind and a matching factory pass through to their own hydration path.

packages/3-targets/3-targets/postgres/src/core/authoring.ts
packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts

Mongo authoring lift

The Mongo TS builder and PSL interpreter previously constructed plain object literals for namespaces, bypassing MongoStorage's class-side kind materialization. Both now construct new MongoStorage(...) instances; MongoStorage.kind and MongoNamespacePayload.kind are installed non-enumerably (mirroring SqlNode), so runtime walks see kind: 'mongo-storage' / kind: 'mongo-namespace' while on-disk envelopes stay byte-identical. The mongoNamespaceKindForDts emitter helper that bridged the gap is deleted.

packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts
packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-storage.ts

What changed (behaviour + evidence)

  • Packs declare entity-type validators alongside the factory. AuthoringEntityTypeDescriptor gains validatorSchema?; the existing output.factory doubles as the hydration constructor. Existing pack contributions compile unchanged (the new field is optional). Evidence: Postgres enum descriptor consumes validatorSchema; postgres serializer round-trip tests pass byte-identically.
  • Pack validator fragments compose additively with family fallback. namespaceSlotEntrySchema(fallbackKind, fragments) runs the family fallback AND the contributed fragment when the entry's kind matches fallbackKind; runs the fragment alone for non-fallback kinds. Evidence: SQL family validator tests pass with the widened composition; postgres pack registration continues to validate enum entries byte-identically.
  • Namespace-entry hydration uses an arktype schema. hydrateSqlNamespaceEntry narrows the raw envelope via NamespaceRawSchema rather than chains of raw as Record<string, unknown> casts. Evidence: family-sql test suite (296 tests) green; postgres / mongo / sqlite fixture round-trips all pass.
  • elementCoordinates(storage) walks any Storage-shaped value. Evidence: synthetic Storage literal in packages/1-framework/1-core/framework-components/test/element-coordinates.test.ts pins the structural contract; packages/2-sql/1-core/contract/test/element-coordinates.test.ts, packages/2-mongo-family/1-foundation/mongo-contract/test/element-coordinates.test.ts, and packages/3-targets/3-targets/postgres/test/element-coordinates.test.ts verify each family/target concretion satisfies the structural promise (including Postgres-promoted namespaces with kind === 'schema').
  • Namespace.kind is required. Codifies the invariant every existing code path already maintains. Type cascade through ~6 source files + ~6 .test-d.ts / handcrafted .d.ts fixture files. On-disk JSON envelopes unchanged.
  • Mongo authoring materializes kind at runtime. Both TS builder and PSL interpreter construct MongoStorage class instances; MongoStorage.kind / MongoNamespacePayload.kind are non-enumerable so emitted envelopes stay unchanged. The emitter helper mongoNamespaceKindForDts is deleted.
  • Contract.domain? field exists. Empty in this PR; populated when the sibling domain-plane walk lands.

What stays out of scope

  • No on-disk shape change. pnpm fixtures:check passes with zero drift.
  • Framework-baseline slot names stay plural. tables, types, collections keep their existing names; new pack contributions use the essence + singular convention (Postgres enums register as enum, not enums or postgresEnums). Rename of the existing plurals is deferred — see follow-ups.
  • The legacy storage.<ns>.types slot still carries Postgres enum entries. Migration to storage.<ns>.enum lands in the next slice; the existing hardcoded SQL 'types?' validator entry coexists additively with the descriptor-driven path until then.
  • Domain plane is allocated but empty. Populated in a follow-up slice along with the sibling elementCoordinates(domain) walk.

Compatibility & risk

  • validateSqlContractFully(json) gains an additive optional second parameter: options?: { contractSchema?: Type<unknown> }. Existing call sites compile unchanged.
  • Namespace.kind type-side narrowing only. External consumers reading namespace.kind as string | undefined now read it as string. The runtime non-enumerable defineProperty pattern is unchanged, so emitted JSON envelopes don't carry a kind field. (SQLite authoring builders still construct plain object literals that don't materialize kind; the SQLite arktype validator keeps its existing 'kind?': 'string' accordingly — see TML-2648 for the SQLite analogue of the Mongo lift landed here.)
  • No emitter changes; no migration required for existing contract.json files.

Testing

pnpm typecheck, pnpm lint:deps, pnpm fixtures:check all pass. Per-package: framework-components, sql-contract, mongo-contract, mongo-contract-psl, mongo-contract-ts, mongo-emitter, target-postgres, family-sql, family-mongo all green. pnpm test:packages has pre-existing flakes in adapter-postgres integration / cipherstash / cli-telemetry / cli redirects that reproduce on origin/main and are unrelated.

Two grep gates pass clean: rg 'looksLikeFlat|normalizeStorageForHydration|stampNamespaceOnTable|normalizeStorageEnvelopeShape|isFlatTablesInput|isFlatTypesInput' packages/ (no relocated-dual-shape leftovers) and rg 'storageSlotKey|reservedStorageSlotKeys|namespaceSlotHydrationRegistry|SLOT_KEYS_BY_NAMESPACE_KIND' packages/ (no remnants of the retired surface — see alternatives). Two additional D7 grep gates: rg '\.hydrate\b|hydrate\?:|hydrate: \(raw' packages/ (zero — the redundant hydrate? field is gone) and rg 'raw as Record<string, unknown>|obj\[.id.\] as string' packages/2-sql/9-family/ (zero — the hand-rolled narrowing in hydrateSqlNamespaceEntry is gone).

Follow-ups

This PR is the first of four slices under TML-2584:

  • Next slice: migrate the legacy storage.<ns>.types slot to storage.<ns>.enum per the essence-singular convention; regenerate the Postgres fixtures that change in the rename; drop the hardcoded SQL 'types?' validator entry.
  • Subsequent slices: populate the domain plane (models, valueObjects); add the sibling elementCoordinates(domain) walk; delete the now-subsumed surfaces (findSqlTable, extractStorageElementNames, UnboundTables<C>, etc.).

Three deferred cleanups have their own tickets, paired to this slice:

  • TML-2634 — rename the framework-baseline plural slots (tablestable, typestype, collectionscollection) to match the essence-singular convention. ~150 consumer call sites + ~50–80 fixture regens; deferred from this slice because of blast radius.
  • TML-2636 — lift namespace concretion entity maps under .entries so the structural-walk invariant becomes invariant-by-construction. Stack on TML-2634.
  • TML-2648 — convert the SQLite authoring builders to construct IR class instances so kind is materialized at runtime; then tighten the SQLite arktype validator to require kind. (Mongo half of this work shipped in this PR.)

Alternatives considered

  • Keep hydrate? as a separate descriptor field. An earlier shape required the pack to declare both output.factory(input, ctx) (the authoring-time constructor) and hydrate(raw) (the serializer-time rehydrator). The two were always one-line wrappers around the same constructor call, with hydrate(raw) = (raw) => new T(raw as TInput) adding only the cast back from unknown. Replaced by storing (raw) => output.factory(raw, ctx) directly in the registry; the registration call site does the one cast once.
  • elementCoordinates() as a required method on the Storage interface. Adding the method would change SqlStorage's structural type; emitted Contract<SqlStorage> literals carry no method members; every emitted-fixture consumer's structural check would fail (~56 typecheck diagnostics across 29 emitted contract.d.ts files). The free-function shape keeps the interface's surface unchanged. Rationale in projects/contract-ir-planes/adrs/0001-contract-planes.md Decision 3.
  • storageSlotKey? field on the descriptor + parallel slot-keyed registry + FamilyDescriptor.reservedStorageSlotKeys + collision validator. An earlier iteration of this PR shipped all four pieces. They were retired in a follow-up cleanup once the redundancy with discriminator became clear: storageSlotKey carried no information discriminator didn't already carry for every entry the registry hydrated; the parallel registry doubled the test surface for zero invariant the single registry didn't already enforce; reservedStorageSlotKeys was a framework-layer table redundantly encoding what contract hydration's structural validation already provides; the collision validator caught nothing the family base's hardcoded built-in slot list didn't already enforce structurally. The shipping shape is the cleaned-up substrate — single entityTypeRegistry, keyed on discriminator.
  • Family-name-keyed lookup table inside elementCoordinates(storage). An earlier shape hardcoded 'sql-namespace' / 'mongo-namespace' into the framework layer (layering violation) and silently failed for Postgres-promoted namespaces (PostgresSchema, runtime kind === 'schema'). The structural property walk needs no up-front knowledge of slot names — contract hydration has already enforced the structural shape; the walk just enumerates entity-bearing properties.
  • Pack-contributed validator fragments replacing the family fallback instead of composing with it. Initial implementation was substitutive; review surfaced that this lets pack contributions silently bypass family-owned invariants. Switched to additive composition (both must pass) keyed on discriminator, with the contributor opting out by not registering a fragment.
  • Pack-contributed slot key as plural collection (enums) or target-prefixed (postgresEnums). Settled at essence + singular (enum). The slot key reads as the entity kind; discriminator carries the contributor identity. Singular form is future-compatible with the rename of the existing plural slots (TML-2634).
  • Hand-rolled narrowing in hydrateSqlNamespaceEntry. The previous shape ran raw as Record<string, unknown> then obj['id'] as string at the top of every namespace hydration, with the id fallback to the namespace's outer key buried in the cast chain. Replaced with an arktype NamespaceRawSchema ({ id: 'string', 'kind?': 'string' }) and an explicit typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId fallback ahead of the parse — the schema validates what it owns (the envelope shape); the call site owns the fallback policy. Undeclared keys (tables, types, etc.) pass through via arktype's default ignore behavior.

Linked issue

Refs TML-2584.

Skill update

n/a — internal substrate. No CLI flags, no prisma-next.config.ts fields, no public TypeScript API breaking changes; the AuthoringEntityTypeDescriptor field additions are all optional and target pack authors, not end-user contract authors.

Checklist

  • All commits are signed off (git commit -s) per the DCO
  • I read CONTRIBUTING.md and the change is scoped to one logical concern
  • Tests are updated (descriptor mechanism, structural walk per concretion, additive validator composition, per-entry serializer guard, arktype namespace-entry narrowing, Mongo class-instance hydration)
  • The PR title is in TML-NNNN: <sentence-case title> form
  • The Skill update section above is filled in

@wmadden wmadden requested a review from a team as a code owner May 20, 2026 18:51
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds required namespace kind discriminators, introduces plane-aware element-coordinate enumeration, converts Mongo/SQL contract schemas to fragment-aware factory functions, threads validator fragments into serializers, updates emitters and type fixtures, and adds tests and build/test aliasing.

Changes

Unified Namespace Discriminators and Fragments-Aware Schemas

Layer / File(s) Summary
Contract IR foundation: domain field, namespace kind, element coordinates
packages/1-framework/0-foundation/contract/src/canonicalization.ts, packages/1-framework/0-foundation/contract/src/contract-types.ts, packages/1-framework/1-core/framework-components/src/ir/namespace.ts, packages/1-framework/1-core/framework-components/src/ir/storage.ts
Contract adds optional domain field with canonical ordering. Namespace and NamespaceBase now require readonly kind: string. EntityCoordinate adds plane: 'domain' | 'storage'. elementCoordinates(storage) structurally walks namespaces and yields storage-plane coordinates.
Framework authoring descriptor extensions
packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
AuthoringEntityTypeDescriptor gains optional hydrate(raw: unknown) -> Output and validatorSchema?: Type<unknown> for per-discriminator hydration and validation fragment wiring.
Framework dependencies and build/test config
packages/1-framework/1-core/framework-components/package.json, packages/1-framework/1-core/framework-components/tsconfig.json, packages/1-framework/1-core/framework-components/vitest.config.ts, packages/1-framework/1-core/framework-components/turbo.json
Adds arktype dependency. TS and Vitest aliases point at built dist artifacts to avoid Turbo build cycles. New turbo.json defines typecheck/test task dependencies on contract builds.
Framework IR exports
packages/1-framework/1-core/framework-components/src/exports/ir.ts
Exports updated to include EntityCoordinate type and elementCoordinates function alongside Storage.
MongoDB schema factories and contract schema
packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts, packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts
Refactors static Mongo schemas into fragment-aware factories: collectionEntrySchema(fragments?), createMongoNamespaceEnvelopeSchema(fragments?), createMongoContractSchema(fragments?). MongoContractSchema set to no-fragment factory result and new factories are re-exported.
MongoDB namespace IR, builder, and emitter
packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-storage.ts, packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-unbound-namespace.ts, packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts, packages/2-mongo-family/3-tooling/emitter/src/index.ts
Tightens MongoNamespacePayload/MongoUnboundNamespace kind to required. Refactors contract builder to precompute storage with storageHash. Adds mongoNamespaceKindForDts helper in emitter to derive kind for DTS emission when literal missing.
Mongo contract serializer fragments integration
packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts
MongoContractSerializerBase optionally builds and caches a fragments-aware arktype schema from provided validatorFragments and uses it during parsing when present.
SQL schema factories and validators
packages/2-sql/1-core/contract/src/validators.ts
Introduces SQL schema factory functions: createNamespaceEntrySchema(fragments?), createSqlStorageSchema(fragments?), createSqlContractSchema(fragments?) and re-exports PostgresEnumTypeSchema. Generalizes validateSqlContractStructure to accept an explicit schema and adds ValidateSqlContractFullyOptions for precomputed-schema use and metadata stripping.
SQL namespace IR kind tightening
packages/2-sql/1-core/contract/src/ir/sql-storage.ts, packages/2-sql/1-core/contract/src/ir/sql-unbound-namespace.ts, packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
SqlNamespacePayload, SqlUnboundNamespace, and BuiltStorage now require non-optional readonly kind: string in namespace entries.
SQL emitter kind guarantee
packages/2-sql/3-tooling/emitter/src/index.ts
Emitter now guarantees namespace kind literal emission using SQL_NAMESPACE_KIND_FALLBACK and always appends the kind part in generated .d.ts types.
SQL contract serializer with hydration and fragments
packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts
Constructor optionally compiles fragments-aware schema from validatorFragments. hydrateSqlNamespaceEntry normalizes id, hydrates tables into StorageTable, hydrates object-valued namespace properties via entityTypeRegistry factories keyed by kind, and validates postgres-enum types post-hydration.
PostgreSQL authoring and serializer integration
packages/3-targets/3-targets/postgres/src/core/authoring.ts, packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts, packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts
Adds hydrate and validatorSchema to Postgres enum contribution, collects authoring entity-type contributions into hydration/fragment registries, and updates serializer to use derived registries; PostgresSchema.kind set via Object.defineProperty for non-enumerability.
Type fixtures and DTS emission
packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts, packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.ts, packages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.ts, packages/2-sql/5-runtime/test/context.types.test-d.ts, packages/3-extensions/sql-orm-client/test/create-input.test-d.ts, packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts, test/integration/test/cli.emit-cli-process.e2e.test.ts, test/integration/test/extension-pgvector-scenario-a.e2e.integration.test.ts
All type-level fixtures and generated .d.ts tests updated to include explicit namespace kind literals ('sql-namespace' or 'mongo-namespace') on namespaces.__unbound__ objects; minor formatting adjustments applied.
Framework control stack and element coordinate tests
packages/1-framework/1-core/framework-components/test/control-stack.test.ts, packages/1-framework/1-core/framework-components/test/element-coordinates.test.ts, packages/3-targets/3-targets/postgres/test/element-coordinates.test.ts
Adds test verifying merging of entityTypes contributions and new elementCoordinates tests asserting storage-plane coordinate enumeration for SQL, Mongo, and Postgres-promoted schema namespaces.
Upgrade instructions and retro findings
skills/extension-author/prisma-next-extension-upgrade/upgrades/0.10-to-0.11/instructions.md, drive/retro/findings.md
Adds 0.10 → 0.11 upgrade instructions describing required Namespace.kind changes in handcrafted contract literals and expands retro findings describing storage interface breaking, cascade coverage gaps, mongo builder fallback/workaround, inherited descriptor vocabulary propagation, unbriefed blocker workaround, tier routing friction, and sizing lessons.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#534: Storage IR updates align with per-namespace layout and cross-namespace FK shape migration by adding required Namespace.kind discriminator and refactoring elementCoordinates to enumerate storage planes.

Suggested reviewers

  • aqrln

Poem

🐇 A rabbit hops through namespaces today,

Kind by kind each storage slot will play,
Fragments hum through factories new,
Plane by plane coordinates come true,
A tiny hop, a tidy API bouquet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title directly reflects the main objective: introducing descriptor-driven entity IR substrate with hydration and validation support, enabling target packs to contribute entity descriptors.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2584-s1a-substrate

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/extension-cipherstash

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: c2cf4ab

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts (1)

144-166: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Non-types namespace slots are still discarded during hydration.

hydrateSqlNamespaceEntry() only asks the registry for 'types' and only re-emits { types }. Any descriptor that registers a different storageSlotKey will validate structurally, then disappear when deserializeContract() rebuilds SqlStorage.

Also applies to: 180-200

🤖 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/9-family/src/core/ir/sql-contract-serializer-base.ts` around
lines 144 - 166, The current hydration only asks for the 'types' slot and
re-emits { types }, causing any other registered storage slots to be discarded
during deserializeContract; update the namespace hydration (the code using
hydrateNamespaceSlot in sql-contract-serializer-base.ts — e.g., the
hydrateSqlNamespaceEntry logic around the hydrateNamespaceSlot('types', obj)
call) to iterate over all registered storage slots (or over keys present in obj)
and call hydrateNamespaceSlot for each slot key, then merge any returned
hydrated slot entries into the returned namespace object via spread so
non-'types' slots are preserved (also apply same fix to the similar block around
lines 180-200).
🧹 Nitpick comments (3)
packages/1-framework/1-core/framework-components/src/control/control-stack.ts (1)

383-387: ⚡ Quick win

Use ifDefined instead of inline conditional spread.

Lines 383–387 use an inline spread/ternary pattern for conditionally adding reservedStorageSlotKeys. Replace with ifDefined() per coding guidelines:

+import { ifDefined } from '`@prisma-next/utils/defined`';
 ...
-    authoringContributions: assembleAuthoringContributions(allDescriptors, {
-      ...(family.reservedStorageSlotKeys !== undefined
-        ? { reservedStorageSlotKeys: family.reservedStorageSlotKeys }
-        : {}),
-    }),
+    authoringContributions: assembleAuthoringContributions(allDescriptors, {
+      ...ifDefined('reservedStorageSlotKeys', family.reservedStorageSlotKeys),
+    }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/1-framework/1-core/framework-components/src/control/control-stack.ts`
around lines 383 - 387, Replace the inline conditional spread used when calling
assembleAuthoringContributions (the authoringContributions property) with the
standardized ifDefined utility: instead of spreading
...(family.reservedStorageSlotKeys !== undefined ? { reservedStorageSlotKeys:
family.reservedStorageSlotKeys } : {}), call
ifDefined(family.reservedStorageSlotKeys, v => ({ reservedStorageSlotKeys: v }))
(or the project's equivalent signature) so assembleAuthoringContributions
receives the same optional field via ifDefined; update the import/usages if
needed to reference ifDefined in control-stack.ts.
packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts (1)

1524-1535: 💤 Low value

Documented cast bridges type/runtime mismatch.

The as unknown as MongoStorageShape<string> cast is explained by the comment and marked for follow-up, but consider whether a type predicate or factory function could narrow the unsafe boundary. If class-instance construction is the intended fix, ensure it's tracked in a follow-up issue.

🤖 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-mongo-family/2-authoring/contract-ts/src/contract-builder.ts`
around lines 1524 - 1535, The current unsafe double-cast "as unknown as
MongoStorageShape<string>" on the local variable storage (constructed from
storageBody and computeStorageHash using definition.target.targetId and
definition.family.familyId) hides a runtime/type mismatch; replace the ad-hoc
cast with a narrow, explicit validation or factory: implement a type predicate
(e.g., isMongoStorageShape(obj): obj is MongoStorageShape<string>) or a factory
function (e.g., createMongoStorageShape(storageBody, computeStorageHash(...)))
that constructs a class-instance or validates required fields (including
handling the plain-literal `kind` vs class-instance behavior) and use that to
produce storage so the unsafe cast is removed; if class-based construction is
intended, ensure the factory returns a proper class instance and add a follow-up
task referencing this change for tracking.
packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts (1)

53-64: ⚡ Quick win

Fail fast when slot hydrators and validator fragments drift.

If a slot is registered in namespaceSlotHydrationRegistry but missing from validatorFragments, the '+': 'reject' schema path rejects that slot before hydrateMongoNamespaceSlot() ever runs. A constructor-time key-set check would turn this into a clear configuration error instead of a later parse failure.

♻️ Proposed guard
   constructor(
     namespaceSlotHydrationRegistry?: ReadonlyMap<string, MongoNamespaceSlotHydrationFactory>,
     validatorFragments?: ReadonlyMap<string, Type<unknown>>,
   ) {
     this.namespaceSlotHydrationRegistry = namespaceSlotHydrationRegistry ?? new Map();
+    if (namespaceSlotHydrationRegistry !== undefined) {
+      const fragmentKeys = new Set(validatorFragments?.keys() ?? []);
+      for (const slotKey of namespaceSlotHydrationRegistry.keys()) {
+        if (!fragmentKeys.has(slotKey)) {
+          throw new Error(
+            `Missing validator fragment for Mongo namespace slot ${JSON.stringify(slotKey)}`,
+          );
+        }
+      }
+    }
     // Mirrors the SQL base: only build a fragments-aware schema when
     // pack contributions exist; otherwise the cached module-level
     // default in `contract-schema.ts` covers the validation path.
     this.contractSchema =
       validatorFragments !== undefined && validatorFragments.size > 0
🤖 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-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts`
around lines 53 - 64, The constructor currently allows mismatched keys between
namespaceSlotHydrationRegistry and validatorFragments which causes the schema to
reject registered slots at parse time; in the constructor of
MongoContractSerializerBase, add a guard that computes the key sets of
namespaceSlotHydrationRegistry and validatorFragments (when validatorFragments
is provided) and throws a clear configuration error if any key in
namespaceSlotHydrationRegistry is missing from validatorFragments, referencing
namespaceSlotHydrationRegistry, validatorFragments, createMongoContractSchema,
contractSchema, and hydrateMongoNamespaceSlot so callers know the mismatch;
perform this check before creating contractSchema so problems fail fast at
construction.
🤖 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 `@drive/retro/findings.md`:
- Line 57: The H2 heading "2026-05-20 · drive-build-workflow · gap" appears more
than once (e.g., the instances at lines showing that heading around the diff and
again at 65); edit each duplicate H2 in drive/retro/findings.md to make anchors
unique by adding a short qualifier (for example append a qualifier like "—
action items", "— root cause", or a date/time suffix) so each "2026-05-20 ·
drive-build-workflow · gap" becomes distinct; ensure the qualifier is brief and
meaningful to preserve readability and update any internal links if present.

In `@packages/1-framework/1-core/framework-components/src/ir/storage.ts`:
- Line 51: Replace the unsafe double-cast used to read a dynamic slot key by
calling Reflect.get(ns, slotKey) instead of (ns as unknown as
Readonly<Record<string, unknown>>)[slotKey]; update the assignment to use
Reflect.get(ns, slotKey) for the slot variable so the result remains unknown and
the existing type-narrowing logic (the check on the next line) continues to
provide runtime safety; ensure you reference the same symbols (ns, slotKey,
slot) when making the change.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts`:
- Around line 355-377: The top-level schema returned by the type(...) call in
contract-schema.ts currently rejects a top-level "domain" member, causing valid
contracts to fail before validateContractDomain() runs; add an optional
"domain?" entry to that top-level type (the object literal passed to return
type(...) / returned as Type<unknown>) with a permissive shape (e.g.,
Record<string, unknown> or unknown) so the schema accepts the shared
Contract.domain plane and lets validateContractDomain() perform the actual
validation.

In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 191-198: The Arktype schemas are out of sync with the new contract
shape: update createNamespaceEntrySchema() to require the namespace 'kind'
(remove the optional marker on 'kind') so namespace entries cannot omit it, and
update createSqlContractSchema() to accept the new optional 'domain' field (add
'domain?' with the appropriate string or specific type) instead of rejecting it;
adjust any related slot/entry merging (the spread of slotEntries) to preserve
these keys, and rely on these updated Arktype validators
(createNamespaceEntrySchema, createSqlContractSchema) so
validateSqlContractFully() uses schema validation rather than manual checks.

In `@packages/2-sql/2-authoring/contract-ts/src/contract-types.ts`:
- Around line 559-570: The namespace type declares the discriminator `kind` as
`string`, but runtime/generated contracts always use the literal
'sql-namespace'; update both branches inside the `namespaces` property (the
`__unbound__` branch and the mapped branch over
`Exclude<DefinitionNamespaces<Definition>, '__unbound__'>`) to use `readonly
kind: 'sql-namespace'` instead of `string` so the TypeScript type matches the
actual discriminator used by `Definition`/namespace implementations (refer to
`namespaces`, `__unbound__`, and `DefinitionNamespaces<Definition>` to locate
the declarations).

---

Outside diff comments:
In `@packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts`:
- Around line 144-166: The current hydration only asks for the 'types' slot and
re-emits { types }, causing any other registered storage slots to be discarded
during deserializeContract; update the namespace hydration (the code using
hydrateNamespaceSlot in sql-contract-serializer-base.ts — e.g., the
hydrateSqlNamespaceEntry logic around the hydrateNamespaceSlot('types', obj)
call) to iterate over all registered storage slots (or over keys present in obj)
and call hydrateNamespaceSlot for each slot key, then merge any returned
hydrated slot entries into the returned namespace object via spread so
non-'types' slots are preserved (also apply same fix to the similar block around
lines 180-200).

---

Nitpick comments:
In
`@packages/1-framework/1-core/framework-components/src/control/control-stack.ts`:
- Around line 383-387: Replace the inline conditional spread used when calling
assembleAuthoringContributions (the authoringContributions property) with the
standardized ifDefined utility: instead of spreading
...(family.reservedStorageSlotKeys !== undefined ? { reservedStorageSlotKeys:
family.reservedStorageSlotKeys } : {}), call
ifDefined(family.reservedStorageSlotKeys, v => ({ reservedStorageSlotKeys: v }))
(or the project's equivalent signature) so assembleAuthoringContributions
receives the same optional field via ifDefined; update the import/usages if
needed to reference ifDefined in control-stack.ts.

In `@packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts`:
- Around line 1524-1535: The current unsafe double-cast "as unknown as
MongoStorageShape<string>" on the local variable storage (constructed from
storageBody and computeStorageHash using definition.target.targetId and
definition.family.familyId) hides a runtime/type mismatch; replace the ad-hoc
cast with a narrow, explicit validation or factory: implement a type predicate
(e.g., isMongoStorageShape(obj): obj is MongoStorageShape<string>) or a factory
function (e.g., createMongoStorageShape(storageBody, computeStorageHash(...)))
that constructs a class-instance or validates required fields (including
handling the plain-literal `kind` vs class-instance behavior) and use that to
produce storage so the unsafe cast is removed; if class-based construction is
intended, ensure the factory returns a proper class instance and add a follow-up
task referencing this change for tracking.

In
`@packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts`:
- Around line 53-64: The constructor currently allows mismatched keys between
namespaceSlotHydrationRegistry and validatorFragments which causes the schema to
reject registered slots at parse time; in the constructor of
MongoContractSerializerBase, add a guard that computes the key sets of
namespaceSlotHydrationRegistry and validatorFragments (when validatorFragments
is provided) and throws a clear configuration error if any key in
namespaceSlotHydrationRegistry is missing from validatorFragments, referencing
namespaceSlotHydrationRegistry, validatorFragments, createMongoContractSchema,
contractSchema, and hydrateMongoNamespaceSlot so callers know the mismatch;
perform this check before creating contractSchema so problems fail fast at
construction.
🪄 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: d52333f2-6f07-4fab-b604-7ad8316cabab

📥 Commits

Reviewing files that changed from the base of the PR and between d312ff7 and 7cacb36.

⛔ Files ignored due to path filters (8)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/contract-ir-planes/adrs/0001-contract-planes.md is excluded by !projects/**
  • projects/contract-ir-planes/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/01-framework-primitives.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/02-descriptor-mechanism.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/spec.md is excluded by !projects/**
  • projects/contract-ir-planes/spec.md is excluded by !projects/**
📒 Files selected for processing (39)
  • drive/retro/findings.md
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/1-core/framework-components/package.json
  • packages/1-framework/1-core/framework-components/src/control/control-stack.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/exports/control.ts
  • packages/1-framework/1-core/framework-components/src/exports/ir.ts
  • packages/1-framework/1-core/framework-components/src/ir/namespace.ts
  • packages/1-framework/1-core/framework-components/src/ir/storage.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-components.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-storage.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-unbound-namespace.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/9-family/src/core/control-descriptor.ts
  • packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts
  • packages/2-mongo-family/9-family/src/exports/ir.ts
  • packages/2-sql/1-core/contract/src/ir/sql-storage.ts
  • packages/2-sql/1-core/contract/src/ir/sql-unbound-namespace.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
  • packages/2-sql/5-runtime/test/context.types.test-d.ts
  • packages/2-sql/9-family/src/core/control-descriptor.ts
  • packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts
  • packages/2-sql/9-family/src/exports/ir.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/3-targets/3-targets/postgres/src/core/authoring.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/extension-pgvector-scenario-a.e2e.integration.test.ts

Comment thread drive/retro/findings.md Outdated
Comment thread packages/1-framework/1-core/framework-components/src/ir/storage.ts Outdated
Comment thread packages/2-sql/1-core/contract/src/validators.ts
Comment thread packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
Comment thread packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts Outdated
@wmadden wmadden force-pushed the tml-2584-s1a-substrate branch from 7cacb36 to 5278a6e Compare May 20, 2026 20:30
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/1-framework/0-foundation/contract/src/canonicalization.ts (1)

17-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

domain is ordered but never emitted into the canonical object.

Line 26 adds domain to top-level ordering, but canonicalizeContractToObject never copies serialized['domain'] into normalized, so domain is dropped before ordering runs.

🛠️ Proposed fix
   const normalized: Record<string, unknown> = {
     ...ifDefined('schemaVersion', options.schemaVersion),
     targetFamily: serialized['targetFamily'],
     target: serialized['target'],
     profileHash: serialized['profileHash'],
     roots: serialized['roots'],
     models: serialized['models'],
     ...ifDefined('valueObjects', serialized['valueObjects']),
+    ...ifDefined('domain', serialized['domain']),
     storage: serialized['storage'],
     ...ifDefined('execution', serialized['execution']),
     extensionPacks: serialized['extensionPacks'],
     capabilities: serialized['capabilities'],
     meta: serialized['meta'],
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/0-foundation/contract/src/canonicalization.ts` around
lines 17 - 32, The TOP_LEVEL_ORDER includes 'domain' but
canonicalizeContractToObject never copies serialized['domain'] into the
normalized object so the domain section is dropped; update
canonicalizeContractToObject to read serialized['domain'] (like it does for
'models', 'valueObjects', etc.) and assign it to normalized['domain'] (or build
normalized.domain) before ordering/serialization so the 'domain' key is emitted
in the final canonical object (refer to the TOP_LEVEL_ORDER constant and the
canonicalizeContractToObject function to locate where other top-level sections
are copied and mirror that logic for 'domain').
🧹 Nitpick comments (1)
packages/1-framework/1-core/framework-components/src/control/control-stack.ts (1)

383-387: 💤 Low value

Use ifDefined() for conditional object spread.

The inline ternary spread pattern can be simplified using the ifDefined utility.

Suggested refactor
+import { ifDefined } from '`@prisma-next/utils/defined`';
 ...
     authoringContributions: assembleAuthoringContributions(allDescriptors, {
-      ...(family.reservedStorageSlotKeys !== undefined
-        ? { reservedStorageSlotKeys: family.reservedStorageSlotKeys }
-        : {}),
+      ...ifDefined(family.reservedStorageSlotKeys, (keys) => ({ reservedStorageSlotKeys: keys })),
     }),

Based on learnings: "prefer ifDefined from prisma-next/utils/defined for conditional object spreads".

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

In
`@packages/1-framework/1-core/framework-components/src/control/control-stack.ts`
around lines 383 - 387, Replace the inline ternary object spread used when
calling assembleAuthoringContributions with the ifDefined helper to make the
conditional spread clearer and consistent: import ifDefined from
"prisma-next/utils/defined" (or the project's defined utility), and change the
spread of { reservedStorageSlotKeys: family.reservedStorageSlotKeys } controlled
by family.reservedStorageSlotKeys into ifDefined(family.reservedStorageSlotKeys,
() => ({ reservedStorageSlotKeys: family.reservedStorageSlotKeys })), keeping
the call site authoringContributions:
assembleAuthoringContributions(allDescriptors, { ...ifDefined(...) }) so the
code uses the utility instead of the inline ternary.
🤖 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/0-foundation/contract/src/contract-types.ts`:
- Around line 49-54: The documented keying for the readonly domain field
(domain[plane][namespaceId][entityKind][entityName]) has four nested map levels
but the type in contract-types.ts only defines three; update the domain type
declaration (the readonly domain? property) to four-level nesting (i.e.,
Record<string, Record<string, Record<string, Record<string, unknown>>>>) so it
matches the docstring and the expected plane→namespaceId→entityKind→entityName
shape.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts`:
- Around line 337-343: The namespace schema currently marks 'kind' optional
('kind?': 'string') but it should be required; update the object passed to
type(...) in contract-schema.ts to use 'kind': 'string' instead of 'kind?':
'string' (i.e., remove the question mark) so the returned Type enforces a
required kind field while keeping the rest of the shape (id, collections,
slotEntries) unchanged.

---

Outside diff comments:
In `@packages/1-framework/0-foundation/contract/src/canonicalization.ts`:
- Around line 17-32: The TOP_LEVEL_ORDER includes 'domain' but
canonicalizeContractToObject never copies serialized['domain'] into the
normalized object so the domain section is dropped; update
canonicalizeContractToObject to read serialized['domain'] (like it does for
'models', 'valueObjects', etc.) and assign it to normalized['domain'] (or build
normalized.domain) before ordering/serialization so the 'domain' key is emitted
in the final canonical object (refer to the TOP_LEVEL_ORDER constant and the
canonicalizeContractToObject function to locate where other top-level sections
are copied and mirror that logic for 'domain').

---

Nitpick comments:
In
`@packages/1-framework/1-core/framework-components/src/control/control-stack.ts`:
- Around line 383-387: Replace the inline ternary object spread used when
calling assembleAuthoringContributions with the ifDefined helper to make the
conditional spread clearer and consistent: import ifDefined from
"prisma-next/utils/defined" (or the project's defined utility), and change the
spread of { reservedStorageSlotKeys: family.reservedStorageSlotKeys } controlled
by family.reservedStorageSlotKeys into ifDefined(family.reservedStorageSlotKeys,
() => ({ reservedStorageSlotKeys: family.reservedStorageSlotKeys })), keeping
the call site authoringContributions:
assembleAuthoringContributions(allDescriptors, { ...ifDefined(...) }) so the
code uses the utility instead of the inline ternary.
🪄 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: 39c33209-3b30-430a-9f12-dfecdf4fcded

📥 Commits

Reviewing files that changed from the base of the PR and between 7cacb36 and 5278a6e.

⛔ Files ignored due to path filters (8)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/contract-ir-planes/adrs/0001-contract-planes.md is excluded by !projects/**
  • projects/contract-ir-planes/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/01-framework-primitives.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/02-descriptor-mechanism.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/spec.md is excluded by !projects/**
  • projects/contract-ir-planes/spec.md is excluded by !projects/**
📒 Files selected for processing (41)
  • drive/retro/findings.md
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/1-core/framework-components/package.json
  • packages/1-framework/1-core/framework-components/src/control/control-stack.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/exports/control.ts
  • packages/1-framework/1-core/framework-components/src/exports/ir.ts
  • packages/1-framework/1-core/framework-components/src/ir/namespace.ts
  • packages/1-framework/1-core/framework-components/src/ir/storage.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-components.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-storage.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-unbound-namespace.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/9-family/src/core/control-descriptor.ts
  • packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts
  • packages/2-mongo-family/9-family/src/exports/ir.ts
  • packages/2-sql/1-core/contract/src/ir/sql-storage.ts
  • packages/2-sql/1-core/contract/src/ir/sql-unbound-namespace.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/5-runtime/test/context.types.test-d.ts
  • packages/2-sql/9-family/src/core/control-descriptor.ts
  • packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts
  • packages/2-sql/9-family/src/exports/ir.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/3-targets/3-targets/postgres/src/core/authoring.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.10-to-0.11/instructions.md
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/extension-pgvector-scenario-a.e2e.integration.test.ts
✅ Files skipped from review due to trivial changes (8)
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.10-to-0.11/instructions.md
  • packages/2-sql/5-runtime/test/context.types.test-d.ts
  • packages/2-sql/1-core/contract/src/ir/sql-unbound-namespace.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-storage.ts
  • packages/2-sql/9-family/src/exports/ir.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/ir/mongo-unbound-namespace.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts

Comment thread packages/1-framework/0-foundation/contract/src/contract-types.ts
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented May 20, 2026

@coderabbitai disposition of the body-level items in your review:

Outside-diff: packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts:144-166 — non-types namespace slots discarded during hydration. Deferring. The S1.A substrate ships the registration surface for non-types pack slots (storageSlotKey + validatorSchema + namespaceSlotHydrationRegistry); fully pluralised hydration that iterates all registered slot keys is a follow-up. No pack today registers a non-types SQL slot, so the discard path is unreachable in shipped code. The completeness work pairs naturally with the same fail-fast guard suggested under the Mongo serializer-base nitpick below.

Nitpick: packages/1-framework/1-core/framework-components/src/control/control-stack.ts:383-387 — use ifDefined. Done — commit 3b00fc3e4. Same semantics, drops the inline conditional spread for the standard helper.

Nitpick: packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts:1524-1535 — type predicate / factory for the documented as unknown as MongoStorageShape<string> cast. Deferring. The cast is documented with an explicit follow-up reference and CodeRabbit itself tagged it Low value. The structural fix is class-instance construction (new MongoStorage(…) / new MongoNamespacePayload(…)) — same construction-path migration named in the SQL kind-required thread above. The cast retires when that migration lands; introducing a type predicate now would just be parallel scaffolding to throw away.

Nitpick: packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts:53-64 — fail-fast on slot-hydrator / validator-fragment drift. Deferring. The guard is genuinely useful, but its surface area is the registry/fragment composition path that's only exercised when a pack registers a non-types slot. Landing it now wires defensive plumbing for a code path that has no live caller; landing it alongside the hydration-loop completeness work above keeps both changes coherent against the first pack that exercises them.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Tip

For best results, initiate chat on the files or code changes.

@wmadden: Understood on all four items — dispositions logged.

  • control-stack.tsifDefined: Confirmed, nicely done. ✅
  • sql-contract-serializer-base.ts — discarded non-types slots / mongo-contract-serializer-base.ts — fail-fast guard: The deferral rationale is sound — both belong to the same coherent follow-up: pluralised hydration iteration over all registered slot keys plus the drift guard. Since no live pack exercises that path today, landing them together when the first non-tables/collections slot appears is the right call.
  • contract-builder.tsas unknown as MongoStorageShape<string> cast: Agreed; threading in a type predicate now would just be scaffolding to discard once the class-construction migration lands.

Would you like me to open a follow-up GitHub issue covering the two co-deferred items (pluralised hydration loop in both serializer bases + the Mongo fail-fast drift guard)? That would give the S1.B–S1.D work a concrete tracked handle for the hydration-completeness slice.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts (1)

357-362: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require kind on Mongo namespace envelopes.

This still accepts namespace entries without kind, so structurally invalid contracts can pass schema validation and only fail later in hydration/storage validation. The tightened contract surface makes Namespace.kind required, so the Mongo envelope should reject missing kinds here.

Proposed fix
 export function createMongoNamespaceEnvelopeSchema(
   fragments?: ReadonlyMap<string, Type<unknown>>,
 ): Type<unknown> {
   return type({
     '+': 'reject',
     id: 'string',
-    'kind?': 'string',
+    kind: 'string',
     'collections?': type({ '[string]': collectionEntrySchema(fragments) }),
   }) as Type<unknown>;
 }
🤖 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-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts`
around lines 357 - 362, The namespace envelope schema currently allows missing
Namespace.kind because it uses an optional key 'kind?' in the returned type;
update the envelope in contract-schema.ts to make 'kind' required (change
'kind?' to 'kind') so the schema rejects entries without a kind and prevents
invalid contracts from passing validation—locate the return in the function that
constructs the namespace envelope (the type({ '+': 'reject', id: 'string',
'kind?': 'string', 'collections?': type({ '[string]':
collectionEntrySchema(fragments) }) }) expression) and remove the optional
marker on kind.
🤖 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/1-core/contract/src/validators.ts`:
- Around line 176-193: Fragment matches (fragments.get(kind)) currently
short-circuit the built-in validator, allowing a fragment to bypass
PostgresEnumTypeSchema; change the flow so that when a fragment is found you
still enforce the built-in enum validation for postgres-enum entries: after
computing parsed = fragment(entry), if kind === 'postgres-enum' run the
PostgresEnumTypeSchema (or the existing fallback validator that implements the
hardcoded types check) against entry and reject if it yields type.errors;
otherwise keep the existing success path for non-enum fragments. Ensure you
reference the same symbols: fragments.get(kind), fragment(entry), fallback, and
PostgresEnumTypeSchema (or the validatorSchema for 'postgres-enum') so the enum
invariant is additive rather than substituted.

In `@packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts`:
- Around line 162-169: The guard around processing types currently trips
whenever the types map is non-empty and 'postgres-enum' is not registered, even
if none of the entries are postgres enums; update the condition in
sql-contract-serializer-base.ts so you first scan/filter typesRaw entries for
those whose value.kind === 'postgres-enum' (or equivalent discriminator) and
only then check this.entityTypeRegistry.get('postgres-enum') === undefined and
bail; in short, replace the broad non-empty check on typesRaw with a targeted
check that at least one entry is actually a 'postgres-enum' before consulting
entityTypeRegistry, using the existing typesRaw and
this.entityTypeRegistry.get('postgres-enum') symbols to locate the logic.

---

Duplicate comments:
In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts`:
- Around line 357-362: The namespace envelope schema currently allows missing
Namespace.kind because it uses an optional key 'kind?' in the returned type;
update the envelope in contract-schema.ts to make 'kind' required (change
'kind?' to 'kind') so the schema rejects entries without a kind and prevents
invalid contracts from passing validation—locate the return in the function that
constructs the namespace envelope (the type({ '+': 'reject', id: 'string',
'kind?': 'string', 'collections?': type({ '[string]':
collectionEntrySchema(fragments) }) }) expression) and remove the optional
marker on kind.
🪄 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: e9ee75f2-2953-462d-acfc-609c55b7dd86

📥 Commits

Reviewing files that changed from the base of the PR and between 5502761 and 9c84c0e.

⛔ Files ignored due to path filters (7)
  • projects/contract-ir-planes/adrs/0001-contract-planes.md is excluded by !projects/**
  • projects/contract-ir-planes/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/03-vocabulary-cleanup.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/dispatches/04-coord-plane-axis.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/plan.md is excluded by !projects/**
  • projects/contract-ir-planes/slices/substrate/spec.md is excluded by !projects/**
  • projects/contract-ir-planes/spec.md is excluded by !projects/**
📒 Files selected for processing (16)
  • drive/retro/findings.md
  • packages/1-framework/1-core/framework-components/src/ir/namespace.ts
  • packages/1-framework/1-core/framework-components/src/ir/storage.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/1-core/framework-components/test/element-coordinates.test.ts
  • packages/1-framework/1-core/framework-components/tsconfig.json
  • packages/1-framework/1-core/framework-components/vitest.config.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts
  • packages/3-targets/3-targets/postgres/src/core/authoring.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-contract-serializer.ts
  • packages/3-targets/3-targets/postgres/src/core/postgres-schema.ts
  • packages/3-targets/3-targets/postgres/test/element-coordinates.test.ts
💤 Files with no reviewable changes (1)
  • packages/3-targets/3-targets/postgres/src/core/authoring.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/3-targets/3-targets/postgres/test/element-coordinates.test.ts

Comment thread packages/2-sql/1-core/contract/src/validators.ts
Comment thread packages/2-sql/9-family/src/core/ir/sql-contract-serializer-base.ts Outdated
Comment thread packages/1-framework/1-core/framework-components/tsconfig.json Outdated
Comment thread packages/1-framework/1-core/framework-components/turbo.json Outdated
Comment thread packages/1-framework/1-core/framework-components/vitest.config.ts
wmadden added a commit that referenced this pull request May 21, 2026
…thetic Storage literal

The D4-R2 test reached into SqlStorage and MongoStorage to exercise
elementCoordinates against real namespace concretions. That import
direction (framework → family) is a layering violation; we previously
papered over it with dist-path aliases and a turbo.json build-order
declaration. Both rejected on review.

Rewrites the framework-side test to feed elementCoordinates a synthetic
Storage literal — which is actually a stronger test, because it pins
the structural contract rather than a particular family concretion.
Family conformance moves to the family's own test package (mirroring the
Postgres test that already lives in @prisma-next/target-postgres). Deletes
the dist-path aliases and turbo.json that existed only to mask the cycle.

Resolves three layering-violation comments on PR #552.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 21, 2026
…e family-owned kind

When a pack contributes a `validatorSchema` fragment for the family-owned
`postgres-enum` kind, the fragment must compose with the built-in
`PostgresEnumTypeSchema` rather than replace it. Previously the
fragment-vs-fallback dispatch was substitutive: a registered fragment
for `postgres-enum` bypassed the family schema entirely, letting
family-owned enum invariants drift with pack contributions.

Pass the fallback kind to `namespaceSlotEntrySchema` and run both the
fallback AND the fragment when the entry kind matches the fallback
kind. For non-fallback kinds, the fragment alone validates (the
family fallback is single-kind-specific and would reject any other
shape). For entries with no matching fragment, fall through to the
fallback unchanged.

Addresses CodeRabbit PR #552 comment 3279123062.

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

The serializer's legacy 'types' slot guard threw whenever the map was
non-empty and the postgres-enum factory was unregistered — even if every
entry carried some other contributed discriminator with a matching
hydration factory. That blocked the new discriminator-based extension
path behind an unrelated Postgres-specific check.

Replace the bulk non-empty check with a targeted scan: only throw when
at least one entry has kind === 'postgres-enum' AND the postgres-enum
factory isn't registered. Entries with other kinds flow through the
existing per-property structural dispatch in hydrateSqlNamespaceEntry.

Addresses CodeRabbit PR #552 comment 3279123067.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden changed the title TML-2584: descriptor-driven entity IR substrate (S1.A) TML-2584: target packs contribute entity descriptors with hydration and validation May 21, 2026
Copy link
Copy Markdown
Contributor Author

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

Manual review complete

@wmadden wmadden force-pushed the tml-2584-s1a-substrate branch from d58ce7e to 8bda0aa Compare May 21, 2026 12:54
wmadden added a commit that referenced this pull request May 21, 2026
…thetic Storage literal

The D4-R2 test reached into SqlStorage and MongoStorage to exercise
elementCoordinates against real namespace concretions. That import
direction (framework → family) is a layering violation; we previously
papered over it with dist-path aliases and a turbo.json build-order
declaration. Both rejected on review.

Rewrites the framework-side test to feed elementCoordinates a synthetic
Storage literal — which is actually a stronger test, because it pins
the structural contract rather than a particular family concretion.
Family conformance moves to the family's own test package (mirroring the
Postgres test that already lives in @prisma-next/target-postgres). Deletes
the dist-path aliases and turbo.json that existed only to mask the cycle.

Resolves three layering-violation comments on PR #552.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 21, 2026
…e family-owned kind

When a pack contributes a `validatorSchema` fragment for the family-owned
`postgres-enum` kind, the fragment must compose with the built-in
`PostgresEnumTypeSchema` rather than replace it. Previously the
fragment-vs-fallback dispatch was substitutive: a registered fragment
for `postgres-enum` bypassed the family schema entirely, letting
family-owned enum invariants drift with pack contributions.

Pass the fallback kind to `namespaceSlotEntrySchema` and run both the
fallback AND the fragment when the entry kind matches the fallback
kind. For non-fallback kinds, the fragment alone validates (the
family fallback is single-kind-specific and would reject any other
shape). For entries with no matching fragment, fall through to the
fallback unchanged.

Addresses CodeRabbit PR #552 comment 3279123062.

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

The serializer's legacy 'types' slot guard threw whenever the map was
non-empty and the postgres-enum factory was unregistered — even if every
entry carried some other contributed discriminator with a matching
hydration factory. That blocked the new discriminator-based extension
path behind an unrelated Postgres-specific check.

Replace the bulk non-empty check with a targeted scan: only throw when
at least one entry has kind === 'postgres-enum' AND the postgres-enum
factory isn't registered. Entries with other kinds flow through the
existing per-property structural dispatch in hydrateSqlNamespaceEntry.

Addresses CodeRabbit PR #552 comment 3279123067.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2584-s1a-substrate branch from 1e6ec81 to 5baa952 Compare May 21, 2026 13:19
wmadden added 7 commits May 21, 2026 15:23
Slice spec for S1.A (Substrate): two-plane IR primitives + entity-coordinate +
pack-contributed entity-kind descriptor mechanism. First slice of the
contract-ir-planes sub-project (TML-2584); first slice of the
target-extensible-ir-namespaces umbrella overall.

What this spec captures:

- Scope: type-additive changes only — Contract gains domain plane (unpopulated),
  Namespace narrows to required { id, kind }, Storage gains
  elementCoordinates() generator, AuthoringContributions.entityTypes extended
  with storageSlotKey + hydrate + validatorSchema. Postgres pack registers
  postgresEnums slot via the new descriptor. No on-disk contract changes; no
  enum-entry migration (S1.B); no cross-ref encoding migration (S1.C); no
  deletion of subsumed surfaces (S1.D).
- Approach: two sequential dispatches inside the slice — D1 framework
  primitives, D2 descriptor mechanism + Postgres registration.
- 15 pre-named edge cases with dispositions (handle / explicitly out / defer).
  Load-bearing ones: Namespace.kind promotion exposing optional-treating
  consumers (F2 territory), instanceof NamespaceBase brand-check sites (4
  files audited), SQL validator hardcoded enum schema staying alongside
  composition surface (F1 territory — risk pre-named so the dispatch loop
  catches a relocation if it happens).
- 5 implementer-degree-of-freedom open questions with working positions
  (EntityCoordinate file location, registry shape, validatorSchema
  composition order, elementCoordinates yield over the types slot, hydrate
  callback signature).
- Slice-DoD with N/A manual-QA + rationale (no user-observable surface
  touches) + substrate-hygiene grep gates per drive/calibration/grep-library.md.
- Slice DoR + team-overlay walk: all canonical items met except slice plan
  (handed off to drive-plan-slice next). Linear linkage uses parent TML-2584
  per no-sub-issues rule.

Grounded in a surface inventory gathered via explore subagent: file paths,
type signatures, call-site counts, instanceof brand-check sites,
PostgresEnumStorageEntry import count (45 files), postgres-enum literal
count (32 files), SQLite rejection-path imports (3 files). Inventory itself
not committed (transient research artifact); spec embeds the load-bearing
facts.

Next step: hand off to drive-plan-slice for the two-dispatch decomposition
under projects/contract-ir-planes/slices/substrate/plan.md.

Refs: TML-2584
Signed-off-by: Will Madden <madden@prisma.io>
Two-dispatch decomposition per the project plan; refines the high-level
"two dispatches" framing into DoR-ready briefs with sizing + gates + tier
routing.

D1 — Framework type primitives. Add EntityCoordinate + Storage
elementCoordinates() (additive); add Contract.domain plane (type-only,
unpopulated); narrow Namespace.kind from optional on IRNode to required on
Namespace. Bounded cascade (4 brand-check sites audited; none access
.kind). Re-decomposition trigger named: > 30 files cascaded halts and
re-plans as D1a/D1b/D1c. M sized.

D2 — Descriptor mechanism + Postgres postgresEnums registration. Extend
AuthoringEntityTypeDescriptor with storageSlotKey + hydrate +
validatorSchema. SQL + Mongo validators compose contributed schemas
additively (no-op when nothing contributed; the existing hardcoded SQL
enum schema STAYS — F1 risk pre-named to prevent silent relocation).
SQL family base serializer extends entityTypeRegistry with a parallel
Map for pack-contributed slot hydration. Postgres pack registers
postgresEnums; serializer enum branch refactored to delegate to the
registry. No on-disk contract changes. M sized.

Tier routing per drive/calibration/model-tier.md: both dispatches are
substrate / spec-interpretation (table row 1) → Opus. With sonnet/low
banned and sonnet/mid + opus/medium not selectable from the orchestrator,
both dispatches route to claude-opus-4-7-thinking-high.

Every slice-spec edge case mapped to a covering dispatch (or explicit
deferral); slice-DoD conditions reachable from the dispatch sequence;
per-dispatch DoR + team overlay walked.

Refs: TML-2584
Signed-off-by: Will Madden <madden@prisma.io>
D1 (framework primitives) brief per drive brief-discipline (8 required
sections). Lives at dispatches/01-framework-primitives.md per the team
plan-shape convention.

Tier: Opus (substrate change per drive/calibration/model-tier.md row 1).
Sized M with re-decomposition trigger at > 30 cascaded files or > 90 min
wall-clock.

Edge cases pulled forward from slice spec D1-portion (8 with dispositions,
including F2 directly-relevant + F5 destructive-git forbidden). Grep
gates pulled from drive/calibration/grep-library.md § IR substrate
hygiene + an F2-territory check specific to this dispatch (rg .kind
?? packages/).

Per-slice reviews/code-review.md scaffolded but gitignored repo-wide
(projects/**/reviews/ in .gitignore — code-review.md is iteration-loop
scratch, not a shipped artifact).

Next: spawn implementer subagent with the brief, in background, model
opus-high.

Refs: TML-2584
Signed-off-by: Will Madden <madden@prisma.io>
R1 attempted to add elementCoordinates() as a required member on
the framework Storage interface. That broke structural
assignability of emitted contract.d.ts literals against
Contract<SqlStorage> consumers (the printed literal carries
storageHash / namespaces / types? but no method members), cascading
into ~56 typecheck diagnostics across 29 fixture files and a
pnpm fixtures:check byte-stability violation.

R2 reframes the walk as a free elementCoordinates(storage)
function dispatched on Namespace.kind via an inline lookup table
in @prisma-next/framework-components/ir. The Storage interface is
unchanged; emitted literals keep satisfying every consumer; the
byte-stability gate holds. D2 replaces the inline table with the
pack-contributed descriptor registry.

The brief now enumerates: Step 0 revert of R1's 8 unstaged files
(done by orchestrator), Steps 1–8 verbatim edits, Step 9
audit-only grep gates. Model tier moves from Opus 4.7 to
Composer-2.5 because the redirect makes the dispatch strictly
mechanical with no design latitude; hard escalation triggers
re-route to Opus if any boundary breaks.

Refs TML-2584.

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

D1 (projects/contract-ir-planes/slices/substrate/spec.md, TML-2584): ship
EntityCoordinate, the free elementCoordinates(storage) generator with a
hardcoded sql/mongo slot-key table, Contract.domain?, and canonicalizer
TOP_LEVEL_ORDER — no Storage interface members, no fixture drift.

Signed-off-by: Will Madden <madden@prisma.io>
D1 (projects/contract-ir-planes/slices/substrate/spec.md, TML-2584): require
kind on Namespace/NamespaceBase, flip four namespace-payload declarations,
tighten BuiltStorage and mongo contract-builder literals, remove mongo-emitter
F2 fallback, and align type-test fixtures with the narrowed substrate.

Signed-off-by: Will Madden <madden@prisma.io>
D1 (projects/contract-ir-planes/slices/substrate/spec.md, TML-2584): avoid
enumerable kind on mongo contract-ts storage hash input; resolve namespace
kind for d.ts emission when plain builder envelopes omit it.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden and others added 20 commits May 21, 2026 15:23
…hrough

Remove FamilyDescriptor.reservedStorageSlotKeys and control-stack assembly
options; update entityTypes merge tests for discriminator-keyed surface.
TML-2584; projects/contract-ir-planes/slices/substrate/spec.md D3.

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

Remove namespaceSlotHydrationRegistry; hydrate namespace envelopes structurally
and dispatch entries through entityTypeRegistry keyed by entry.kind. TML-2584;
projects/contract-ir-planes/slices/substrate/spec.md D3.

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

Fold contributed validatorSchema fragments by entry kind; hardcoded SQL types?
slot stays for F1 additive coexistence. TML-2584;
projects/contract-ir-planes/slices/substrate/spec.md D3.

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

Drop storageSlotKey from postgresAuthoringEntityTypes; collect hydrate and
validator fragments by discriminator. Make PostgresSchema.kind non-enumerable
for the structural walk invariant. TML-2584;
projects/contract-ir-planes/slices/substrate/spec.md D3.

Signed-off-by: Will Madden <madden@prisma.io>
Assert elementCoordinates yields table coordinates for Postgres-promoted
namespaces whose runtime kind is schema, not sql-namespace. TML-2584;
projects/contract-ir-planes/slices/substrate/spec.md D3.

Signed-off-by: Will Madden <madden@prisma.io>
…/spec/plan edits, deferred-to-tickets)

Lands the substrate-altitude outputs of the artefact-review architect +
principal-engineer passes (under projects/contract-ir-planes/reviews/
artifacts-pre-d3/):

- ADR Decision 1 (A05): frame symmetric extensibility - both domain and
  storage planes are pack-extensible through the same descriptor
  surface; the current asymmetry is content, not mechanism.
- ADR Decision 3 (A03): extend EntityCoordinate with a plane axis
  (domain | storage); honest about what the coordinate addresses;
  domain -> storage directional invariant lives in a separate validator,
  not the coord shape.
- ADR Decision 5 (A07 + A08): record the rejected storageSlotKey?
  alternative + slot-key naming convention (essence + singular: enum,
  policy, role, ...). Future pack contributions follow the convention
  from S1.B onward.
- Project plan (F09): add Risk #5 (inherited "decided" fields propagate
  without challenge); name brief-assembly pre-flight questions for S1.B+.
- Project spec OQ1 (A08): lock to essence + singular.
- All references to postgresEnums in the project + slice spec/plan
  flipped to enum (the slot S1.B introduces from the start).
- Slice spec/plan: add D4 dispatch (coord plane axis code change + 1
  test); explicitly deferred A01 (slot rename) and A02b (.entries lift)
  to standalone tickets.
- D4 brief at slices/substrate/dispatches/04-coord-plane-axis.md.

Standalone tickets filed for the deferred cleanup work:
- TML-2634 (A01 substrate rename: tables -> table, etc.)
- TML-2636 (A02b namespace concretion .entries lift)

Both relatedTo TML-2584; both in the "Target-Extensible IR + Namespaces"
project per the no-sub-issues rule.

Refs: TML-2584
Related: TML-2634, TML-2636
Signed-off-by: Will Madden <madden@prisma.io>
Extend EntityCoordinate with plane: 'domain' | 'storage' as the first field
and populate plane: 'storage' in elementCoordinates(storage) yields.

TML-2584 (S1.A substrate D4)

Signed-off-by: Will Madden <madden@prisma.io>
…storage' for SQL/Mongo/Postgres concretions

Add element-coordinates.test.ts covering SqlStorage, MongoStorage, and
PostgresSchema namespace walks. Wire dist path aliases for cross-family
fixtures without framework→target package dependencies.

TML-2584 (S1.A substrate D4)

Signed-off-by: Will Madden <madden@prisma.io>
…del-routing over-correction

Two findings from S1.A:

1. F7 candidate: Composer-2.5 D4 hit an unbriefed structural blocker
   (Turbo build cycle on cross-family devDep) and worked around it with
   dist-path aliases including a framework→target alias that hides a
   forbidden dep edge from lint:deps. The 5-file refusal trigger fired
   (diff was 6 files); the orchestrator accepted the deviation post-hoc
   instead of treating the fire as the halt signal. Reviewer caught it,
   returned NEEDS-REWORK option (2). The meta-lesson: refusal triggers
   are calibration instruments; post-hoc acceptance retunes them
   downward each time. Suggested action: pre-survey the dep graph for
   any single-test-imports-from-siblings dispatch; treat trigger fires
   as halt-and-reconfirm, not accept-after-the-fact.

2. friction: orchestrator over-routed D3 to Opus despite the brief
   being fully-settled mechanical retirement (Composer-sized).
   Operator corrected ("Can you use the Composer-2.5 executor
   please?"). Suggested action: the dispatch brief Model tier
   paragraph must name the calibration check explicitly; if the
   one-sentence justification reads as "substrate work" without
   naming a specific design-judgment site, the tier is probably
   over-routed.

Both upstream-candidate.

TML-2584 (S1.A substrate)

Signed-off-by: Will Madden <madden@prisma.io>
…; move Postgres-promoted coverage to the postgres package (D4-R2)

The framework→target path alias hid a forbidden framework→target dependency behind resolution. Postgres-promoted namespace coverage belongs in target-postgres, which already asserts plane: storage for that case.

TML-2584

Signed-off-by: Will Madden <madden@prisma.io>
… test aliases (CI typecheck on fresh checkout)

CI runs typecheck on a fresh checkout where sibling dist/ artefacts do not
exist yet. tsconfig path aliases to those dist files create an implicit
build-order dependency Turbo cannot see from package.json alone; declare it
explicitly in turbo.json so typecheck and test wait for sql-contract and
mongo-contract builds. TML-2584.

Signed-off-by: Will Madden <madden@prisma.io>
…thetic Storage literal

The D4-R2 test reached into SqlStorage and MongoStorage to exercise
elementCoordinates against real namespace concretions. That import
direction (framework → family) is a layering violation; we previously
papered over it with dist-path aliases and a turbo.json build-order
declaration. Both rejected on review.

Rewrites the framework-side test to feed elementCoordinates a synthetic
Storage literal — which is actually a stronger test, because it pins
the structural contract rather than a particular family concretion.
Family conformance moves to the family's own test package (mirroring the
Postgres test that already lives in @prisma-next/target-postgres). Deletes
the dist-path aliases and turbo.json that existed only to mask the cycle.

Resolves three layering-violation comments on PR #552.

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

Signed-off-by: Will Madden <madden@prisma.io>
…e family-owned kind

When a pack contributes a `validatorSchema` fragment for the family-owned
`postgres-enum` kind, the fragment must compose with the built-in
`PostgresEnumTypeSchema` rather than replace it. Previously the
fragment-vs-fallback dispatch was substitutive: a registered fragment
for `postgres-enum` bypassed the family schema entirely, letting
family-owned enum invariants drift with pack contributions.

Pass the fallback kind to `namespaceSlotEntrySchema` and run both the
fallback AND the fragment when the entry kind matches the fallback
kind. For non-fallback kinds, the fragment alone validates (the
family fallback is single-kind-specific and would reject any other
shape). For entries with no matching fragment, fall through to the
fallback unchanged.

Addresses CodeRabbit PR #552 comment 3279123062.

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

The serializer's legacy 'types' slot guard threw whenever the map was
non-empty and the postgres-enum factory was unregistered — even if every
entry carried some other contributed discriminator with a matching
hydration factory. That blocked the new discriminator-based extension
path behind an unrelated Postgres-specific check.

Replace the bulk non-empty check with a targeted scan: only throw when
at least one entry has kind === 'postgres-enum' AND the postgres-enum
factory isn't registered. Entries with other kinds flow through the
existing per-property structural dispatch in hydrateSqlNamespaceEntry.

Addresses CodeRabbit PR #552 comment 3279123067.

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

The contract-ts and contract-psl builders previously constructed plain
object literals for namespaces, bypassing MongoNamespacePayload's
non-enumerable kind materialization. The Mongo emitter then needed a
fallback helper to guess 'mongo-namespace' when the plain-literal input
lacked kind.

Flips both builders to `new MongoStorage(...)`, mirroring the SQL pattern
in @prisma-next/sql-contract-ts. The constructor materializes kind
non-enumerably, so JSON.stringify emits the same bytes (fixture
byte-stability preserved) but the runtime IR exposes kind reliably.

MongoStorage.kind is also installed non-enumerably (matching SqlNode) so
in-memory contracts pass arktype reject validation during emit.

Deletes the mongoNamespaceKindForDts helper (now unreferenced) and the
MONGO_NAMESPACE_KIND constant. Updates the mongo contract-schema JSDoc
to note that the optional `kind?` on the validator is intentional —
wire-shape gated, not IR-shape gated.

Closes the Mongo portion of TML-2648; SQLite analogue still pending.

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

The hydrate field's function body was always a typed-input wrapper around
output.factory's body; the family-base serializer can call output.factory
directly with the validated raw value, with ctx synthesized from the pack's
own bootstrap-time {family, target} knowledge.

Same F6 pattern as the retired storageSlotKey? field — a descriptor surface
field carrying information another descriptor field already carries.

Signed-off-by: Will Madden <madden@prisma.io>
…spaceEntry with an arktype schema

The hydration loop's cast-chain (`raw as Record<string, unknown>` +
`obj['id'] as string | undefined ?? nsId` + per-slot `as` casts) is exactly
what arktype exists to replace. A small NamespaceRawSchema narrows `raw`
once; the slot loop iterates typed properties after.

Hydration still runs after the family-base validator, so the schema here is
ergonomics + defence-in-depth — behavior is byte-identical.

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

D6 R2 swapped the `interpreter.test.ts` storage expectation to `new MongoStorage({
  storageHash: expect.stringMatching(/^sha256:/) as unknown as `sha256:${string}`,
  ...
})` but the bridge cast only landed on the template-literal portion of the
storageHash type, not the `StorageHashBase<…>` brand the constructor accepts.
Local + CI typecheck has been failing on `tsc` at `interpreter.test.ts:803`
ever since.

Extend the cast through to `StorageHashBase<`sha256:${string}`>` (and import
the type alongside the existing `@prisma-next/contract/types` imports). Test
output is identical (the matcher still passes the same runtime value through
the constructor); the change is purely the compile-time bridge.

Signed-off-by: Will Madden <wmadden@users.noreply.github.com>
Signed-off-by: Will Madden <madden@prisma.io>
Snapshot the briefs that drove the two in-PR cleanups, for the audit
trail drive-build-workflow expects:

- `06-mongo-builder-class-lift.md` — Mongo TS builder + PSL interpreter
  switch from plain object literals to `new MongoStorage(...)`, deleting
  the `mongoNamespaceKindForDts` emitter helper. Re-dispatched as R2
  after R1 caught a fixtures-check fail from the missed PSL site.
- `07-hydrate-and-arktype-cleanup.md` — drop the redundant
  `AuthoringEntityTypeDescriptor.hydrate?` (Part A) and replace the
  hand-rolled raw narrowing in `hydrateSqlNamespaceEntry` with an
  arktype `NamespaceRawSchema` (Part B).

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2584-s1a-substrate branch from 5baa952 to c2cf4ab Compare May 21, 2026 13:23
@wmadden wmadden merged commit 38bfd1e into main May 21, 2026
17 checks passed
wmadden added a commit that referenced this pull request May 21, 2026
…thetic Storage literal

The D4-R2 test reached into SqlStorage and MongoStorage to exercise
elementCoordinates against real namespace concretions. That import
direction (framework → family) is a layering violation; we previously
papered over it with dist-path aliases and a turbo.json build-order
declaration. Both rejected on review.

Rewrites the framework-side test to feed elementCoordinates a synthetic
Storage literal — which is actually a stronger test, because it pins
the structural contract rather than a particular family concretion.
Family conformance moves to the family's own test package (mirroring the
Postgres test that already lives in @prisma-next/target-postgres). Deletes
the dist-path aliases and turbo.json that existed only to mask the cycle.

Resolves three layering-violation comments on PR #552.

Signed-off-by: Will Madden <madden@prisma.io>
wmadden added a commit that referenced this pull request May 21, 2026
…e family-owned kind

When a pack contributes a `validatorSchema` fragment for the family-owned
`postgres-enum` kind, the fragment must compose with the built-in
`PostgresEnumTypeSchema` rather than replace it. Previously the
fragment-vs-fallback dispatch was substitutive: a registered fragment
for `postgres-enum` bypassed the family schema entirely, letting
family-owned enum invariants drift with pack contributions.

Pass the fallback kind to `namespaceSlotEntrySchema` and run both the
fallback AND the fragment when the entry kind matches the fallback
kind. For non-fallback kinds, the fragment alone validates (the
family fallback is single-kind-specific and would reject any other
shape). For entries with no matching fragment, fall through to the
fallback unchanged.

Addresses CodeRabbit PR #552 comment 3279123062.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden deleted the tml-2584-s1a-substrate branch May 21, 2026 13:49
wmadden added a commit that referenced this pull request May 21, 2026
…m entries

The serializer's legacy 'types' slot guard threw whenever the map was
non-empty and the postgres-enum factory was unregistered — even if every
entry carried some other contributed discriminator with a matching
hydration factory. That blocked the new discriminator-based extension
path behind an unrelated Postgres-specific check.

Replace the bulk non-empty check with a targeted scan: only throw when
at least one entry has kind === 'postgres-enum' AND the postgres-enum
factory isn't registered. Entries with other kinds flow through the
existing per-property structural dispatch in hydrateSqlNamespaceEntry.

Addresses CodeRabbit PR #552 comment 3279123067.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant