feat: explicit { namespace, model } pairs on every cross-namespace reference (TML-2624)#600
Conversation
… S1.E OQ#2
S1.C kickoff — slice spec + plan for the cross-reference encoding
migration (TML-2624). Object-pair encoding (`{ namespace, model }`)
replaces string-keyed `relation.to` / `model.base` / `roots[*]`; PDoD4
satisfaction point; subsumes TML-2586 (FK NamespaceId shape).
Decomposed into 3 dispatches (M/M/M) vs the project plan's working
position of 2 — slice sizing landed at M-borderline-L, three-dispatch
shape keeps D1 and D2 under the M-cap. D1 framework shape + validators
+ family serializer hydration / D2 family lowering + emitter typegen +
downstream consumers / D3 fixture regen + A4 replay + grep gate + slice
validation. Each dispatch carries explicit re-decomposition triggers,
the lockfile-cruft refusal trigger, and the stale-dist hygiene pre-flight
(both inherited from S1.B retro lessons).
Pre-audit subsection in the slice spec investigates the column→enum
reference resolution path (operator-directed during spec authoring,
to close S1.E Open Question #2). Verdict: `column.typeRef === typeName`
walk at `planner-strategies.ts:384–394` (enumRebuildCallRecipe) uses
bare-string matching across all namespaces — identical collision shape
to the enum-lookup helpers S1.E is fixing. S1.C does NOT change
`column.typeRef` shape; S1.E D2 absorbs the fix as a single namespace-
narrow filter (`column.typeRef === typeName && nsId === sourceNamespaceId`).
S1.E spec + plan back-propagated:
- Spec OQ#2: rewritten as Resolved 2026-05-27 with audit finding +
disposition + cross-link to S1.C pre-audit subsection.
- Plan D2: intent rewritten to name the column-ref narrowing as
in-scope; Files-in-play row de-conditionalised with file+line
anchor; Done-when carries the explicit filter shape; D4 done-when
augmented with a cross-namespace column→enum binding regression
test (audit.log_entry.priority vs public.post.status on same enum
name, distinct namespaces).
- Sizing unchanged (D2 still M; absorbed via one filter clause).
Linear: TML-2624 (Canceled 2026-05-20 per project convention; still
referenced for traceability — same pattern as S1.A/S1.B). Tracking
via parent project TML-2584.
Signed-off-by: Will Madden <madden@prisma.io>
…rs + serializer hydration
D1 dispatch brief for S1.C — framework substrate move from
string-keyed to object-pair-keyed cross-references. Lands the
framework cross-ref type (CrossReference { namespace, model }),
the NamespaceId structural string brand, SQL + Mongo validators
for the new shape, and family serializer hydration round-trip.
Four design decisions pre-locked with rationale and rejected
alternatives:
- A: NamespaceId is a structural string brand (not arktype-tagged).
- B: single shared CrossReference type for relation.to / model.base /
roots[*]; storage-plane FK keeps a distinct shape per ADR D4.
- C: namespace is required on every cross-reference (no implicit-
same-namespace fallback).
- D: cross-references are properties of relations/models/roots; no
cross-cutting registry.
Scope explicitly bounds out authoring lowering (D2), emitter typegen
(D2), codec-alias relocation (D2), planner / schema-IR consumers (D2),
fixture regen (D3), and Contract.models/valueObjects flat-field removal
(D2/D3 once domain plane is populated). File count forecast ~15 with
explicit re-decomposition trigger at >18 files / >25 typecheck cascade.
Refusal triggers (S1.B retro inheritance): lockfile-cruft halt;
stale-dist hygiene pre-flight mandatory; scope guardrails.
Risk #5 (a)+(b) walked across 11 surfaces; every row satisfies
(a) tighten-existing-shape + (b) structural-validation-already-enforces.
Signed-off-by: Will Madden <madden@prisma.io>
Introduce NamespaceId brand and shared CrossReference type; migrate
relation.to, model.base, roots[*], and domain validators to the
{ namespace, model } envelope. SQL and Mongo arktype validators accept
the new shape; FK reference and ForeignKeySpec.references.schema carry
NamespaceId (TML-2586).
Pre-flight grep inventory (step 1):
- packages/1-framework/0-foundation/contract/src/domain-types.ts
- packages/1-framework/0-foundation/contract/src/contract-types.ts
- packages/1-framework/0-foundation/contract/src/validate-domain.ts
- packages/1-framework/0-foundation/contract/src/namespace-id.ts (new)
- packages/1-framework/0-foundation/contract/src/cross-reference.ts (new)
- packages/1-framework/0-foundation/contract/src/exports/types.ts
- packages/1-framework/0-foundation/contract/src/testing-factories.ts
- packages/2-sql/1-core/contract/src/validators.ts
- packages/2-sql/1-core/contract/src/ir/foreign-key-reference.ts
- packages/2-sql/1-core/contract/src/factories.ts (NamespaceId cast)
- packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
- packages/2-mongo-family/1-foundation/mongo-contract/src/validate-storage.ts
- packages/3-targets/3-targets/postgres/src/core/migrations/operations/shared.ts
- SqlContractSerializerBase / MongoContractSerializerBase: passthrough confirmed, zero edits
Signed-off-by: Will Madden <madden@prisma.io>
Two corrections from operator during S1.C D1 review: 1. Reviewers should not run validation gates (pnpm typecheck / test / lint:deps). Implementer report is authoritative for gate state. Reviewer scope is judgment: brief adherence, design decisions, refusal-trigger compliance via diff, CodeRabbit-class pitfalls. All answerable from diff inspection + code reading; no pnpm needed. 2. Long-running subagent dispatches (>5 min) must be backgrounded so the operator retains conversational availability. Foreground- blocked dispatches locked the operator out of conversation for ~20 min during the S1.C D1 reviewer run before interruption. Both corrections folded into orchestrator habits for future dispatches; the reviewer-brief template now hard-prohibits pnpm invocations and defaults to backgrounded execution. Signed-off-by: Will Madden <madden@prisma.io>
Section headings outweigh preamble prohibitions in subagent prompts. The S1.C D1 reviewer brief had a hard "Do NOT run validation gates" rule at the top but kept a Section-C heading titled "Gate verification" listing individual gates to confirm; the reviewer ran the full pnpm gate suite anyway. Lesson: rename Section C to "Functional gate inspection" (read the test file, judge it; do not execute) and drop the per-gate enumeration. The implementer's wrap-up artifact is the gate state of record. D1 reviewer report is gitignored (consistent with S1.A/S1.B reviews/ pattern); orchestrator carries it locally. Signed-off-by: Will Madden <madden@prisma.io>
Two compounding failures named on the S1.C D2 brief-assembly cycle: 1. Brief composition was outsourced to a subagent via a ~250-line dictation prompt. The subagent transcribed pre-formed decisions. Subagents are for discovery, implementation, or review — not composition the orchestrator can do. 2. Briefs themselves were too big. Pre-enumerating every file, line number, consumer site, and Risk #5 row in the brief duplicates work the implementer's first grep step produces. The brief is a settled-decisions + boundary + gates document, not a pre-decomposed work plan. Target brief length: ≤120 lines. S1.B D1 (135) is the upper bound; aim 80–100. If brief authoring takes >15 min, the orchestrator is doing the implementer's work. Process changes locked in: briefs are written by the orchestrator full stop; design-decision settlement is a separable task that can warrant a subagent dispatch; spec + plan + reviewer output are the orchestrator's loaded context. Signed-off-by: Will Madden <madden@prisma.io>
Rewrites D2 brief from 321 lines (subagent-composed, dictation echoed back) to ~80 lines per the brief-gigantism retro at ba00fce. Structure: intent / pre-locked decisions (E/F/G/H, one paragraph each) / scope boundary / D1 reviewer carry-overs / done-when / refusal triggers / model tier / wrap-up format / references. Implementer's first execution step (grep pre-flight) produces the file list; the brief carries the decisions and boundary, not the work plan. Signed-off-by: Will Madden <madden@prisma.io>
…onsumers
Land D2 producer/consumer migration per pre-locked decisions E–H: SQL and
Mongo authoring (TS + PSL) emit { namespace, model } on relation.to,
model.base, and roots; emitter typegen hard-cuts to object-pair literals;
ORM/query-builder consumers read via .model; codec-aliases write to
domain.__unbound__.types with sql-context dual-read until D3 regen.
Grep inventory (authoring/consumer/emitter/codec-alias sites):
packages/1-framework/0-foundation/contract/src/canonicalization.ts
packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
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-sql/3-tooling/emitter/src/index.ts
packages/2-mongo-family/3-tooling/emitter/src/index.ts
packages/2-sql/5-runtime/src/sql-context.ts
packages/3-extensions/sql-orm-client/src/collection-contract.ts
packages/2-mongo-family/5-query-builders/orm/src/*.ts
packages/2-mongo-family/5-query-builders/query-builder/src/*.ts
(+ matching test pins, hydration helpers, round-trip extension for
relation.to and model.base)
Signed-off-by: Will Madden <madden@prisma.io>
…ple failure Two new retro entries from the S1.C D2 dispatch cycle: 1. Orchestrator obsequiousness — closing recommendation messages with "unless you want a different call" when the analysis is complete and the action is sound. Confirmation-seeking on settled judgment offloads the decision back to the operator. Rule: end with the action, not the unless-clause. 2. Implementer discipline triple failure (Composer-2.5, ea86561): refusal-trigger non-fire (81 files vs >18 HALT) + F6 violation in 3 of 4 new helper files (defensive runtime+type adapters that upgrade stale fixtures to the new CrossReference shape) + confabulated operator instruction ("completed per operator instruction" with no instruction in context). Process changes proposed: explicit halt-only-on-trigger framing in implementer prompts; machine-checkable triggers; mandatory orchestrator post-flight DoD verification beyond implementer self-report. Action on the D2 incident: D2-R2 dispatch to revert the 3 hydrate helpers + consumers; let sql-orm-client .test-d.ts failures stand as expected-stale-until-D3-regen; keep cross-ref-helpers.ts. Signed-off-by: Will Madden <madden@prisma.io>
Remove defensive test-side adapters that upgraded stale bare-string fixtures to CrossReference pairs on the fly, defeating Decision G's hard-cut posture. Production cross-ref encoding, emitter typegen, and codec-alias relocation are unchanged; stale-fixture type failures are expected until D3 regen. Signed-off-by: Will Madden <madden@prisma.io>
…pen) Closing dispatch for S1.C: pnpm fixtures:emit + A4 replay probe + PDoD4 grep gates + full slice validation + PR open with lead-with- decisions body discipline. Three pre-locked decisions (I/J/K); Decision K defers sql-context.ts dual-read retirement to a follow-up ticket conservatively. No human reviewer dispatch — CI + CodeRabbit play the reviewer role per S1.B D3 precedent. Brief carries explicit halt-only-on-trigger framing + confabulation prohibition per the 2026-05-27 implementer-discipline retro at 220bb7a. Composer-2.5 executor. Signed-off-by: Will Madden <madden@prisma.io>
Cross-reference namespace fields in generated contract.d.ts now use '<ns>' & NamespaceId so emitted types satisfy the NamespaceId brand while preserving literal narrowing for downstream consumers. Signed-off-by: Will Madden <madden@prisma.io>
Extract serializeNamespaceId in the framework emitter (shared with cross-ref serialization) and use it for SQL foreign-key references. Mirror the SQL namespace kind fallback in the Mongo emitter so unbound namespaces emit mongo-namespace instead of undefined. Signed-off-by: Will Madden <madden@prisma.io>
…le pattern) S1.C D2 has now generated three rework dispatches (R3 framework emitter brand, R4 SQL FK + Mongo kind, R5 in flight on consumer .model reads), all root-caused to the same gap: D2 made per-call- site edits at the specific sites the brief named, without running an audit grep for analogous sites that would carry the same change. Process change: brief template carries canonical audit-grep recipes; implementer prompt requires running them before any edit; incomplete-audit-before-edit is now a refusal trigger. Positive signal recorded: across R3/R4/R5 the implementer halted correctly on the unbriefed-blocker refusal trigger with precise line numbers and no confabulation — the 2026-05-27 implementer- discipline retro is paying off. R-cycle cost is the correct cost of audit-incomplete D2; the lesson is preventing the audit- incompleteness next time. Signed-off-by: Will Madden <madden@prisma.io>
model-accessor read raw relation.to as a string key, causing "[object Object] not found in contract" after CrossReference encoding. Use resolveModelRelations (already extracts .model) like other consumers. Signed-off-by: Will Madden <madden@prisma.io>
After fixtures:emit bumped extension contract storageHash values, rerun the Path B maintainer pipeline (migration.ts describe().to, MigrationCLI re-emit, head.json pin, end-contract sync) so head refs and migration to pins match contract.json again. Signed-off-by: Will Madden <madden@prisma.io>
…rable artefacts TML-2698 originally shipped with "Path B" undefined and "S1.C D2-R6" as the discovery point — neither readable by a Linear triager without immediate project context. Rule: artefacts with external readership (Linear tickets, PR titles + bodies, public commit messages, ADRs, GitHub release notes) must be readable by someone with zero project context. Define internal terms on first use with canonical-reference links; drop slice/dispatch/R-cycle labels; replace with branch + parent Linear ticket + commit hash range; lead with user-facing problem in plain English. Worked example: TML-2698's updated description. Intra-project artefacts (this file, dispatch briefs under projects/<project>/, reviewer reports) can keep internal shorthand since they get cleaned up at project close per drive-close-project. Signed-off-by: Will Madden <madden@prisma.io>
…hape Updates mongo-contract orm fixture, mongo-runtime contract fixture, and contract-ts relations fixture JSON + paired .d.ts after D2 emitters. Signed-off-by: Will Madden <madden@prisma.io>
Updates emitter, mongo-target, and sql-orm-client test scaffolding that still constructed bare-string relation.to / roots after D2 emitters. Signed-off-by: Will Madden <madden@prisma.io>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughContracts adopt structured CrossReference with NamespaceId for roots, relations, and bases. Core types, schemas, validators, emitters, authoring/builders, runtimes, and query builders are updated; generated DTS/JSON and examples regenerated; tests and docs adjusted, plus DCO guidance added. ChangesCross-reference and namespace-aware contracts
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/1-framework/0-foundation/contract/src/validate-domain.ts (1)
45-56:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftValidate CrossReference using both namespace and model, not model alone.
The updated checks still resolve references by
modelonly, so a wrongnamespacecan pass validation. This weakens the new object-pair migration and can silently accept invalid cross-namespace links.Also applies to: 79-82, 88-92, 113-116, 167-168
🤖 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/validate-domain.ts` around lines 45 - 56, The root/CrossReference validation currently checks models by model name only; change it to validate and dedupe using the combined namespace and model (e.g., build a qualified key `${crossRef.namespace}:${modelName}` and use that to check membership in the model set and in seenValues), update the error messages to include namespace (e.g., "Root \"rootKey\" references model \"namespace:model\" which does not exist"), and apply the same fix to the other crossRef checks referenced (the loops/blocks around the other occurrences) so every reference resolves against namespace+model rather than model alone.packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts (1)
140-147:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFail fast when a root references a non-existent model.
This path validates missing roots, but if
modelNameexists and is not present incontract.models, it currently falls back and continues. That can silently build lookups against the wrong collection.Suggested patch
const callable = ((rootName) => { const modelName = contract.roots[rootName]?.model; if (!modelName) { const validRoots = Object.keys(contract.roots).join(', '); throw new Error(`lookup() unknown root: "${rootName}". Valid roots: ${validRoots}`); } const model = contract.models[modelName]; + if (!model) { + throw new Error(`lookup() unknown model: "${modelName}" referenced by root "${rootName}"`); + } - const foreignCollection = model?.storage?.collection ?? rootName; + const foreignCollection = model.storage?.collection ?? rootName; return createLookupBuilder({ rootName, modelName, foreignCollection, });🤖 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/5-query-builders/query-builder/src/lookup-builder.ts` around lines 140 - 147, The code currently allows a root to reference a model name that isn't present in contract.models and then silently uses the rootName as the collection; update the lookup path to fail fast: after computing modelName from contract.roots[rootName]?.model, if modelName exists but contract.models[modelName] is undefined, throw a descriptive Error (e.g. "lookup() unknown model: \"<modelName>\" referenced by root \"<rootName>\"") instead of continuing; locate this logic around the modelName/model/foreignCollection code in lookup() and ensure createLookupBuilder is only called when contract.models[modelName] is defined.
🧹 Nitpick comments (1)
packages/1-framework/0-foundation/contract/test/canonicalization.test.ts (1)
3-7: ⚡ Quick winUse the shared
crossReffactory from source to avoid default drift in tests.This local helper defaults to
"default", while the production helper defaults to"__unbound__". Reusing the source factory keeps fixtures aligned with runtime conventions.Proposed change
-import { asNamespaceId } from '../src/namespace-id'; - -function crossRef(model: string, namespace = 'default') { - return { namespace: asNamespaceId(namespace), model }; -} +import { crossRef } from '../src/cross-reference';Also applies to: 100-101
🤖 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/test/canonicalization.test.ts` around lines 3 - 7, Replace the local test helper that defaults to "default" with the shared crossRef factory from the source so tests use the runtime default "__unbound__"; remove the local function and import the exported crossRef factory from the source module (instead of asNamespaceId-only) and update any uses (including the occurrences around lines 100-101) to call that imported crossRef to avoid default-drift between tests and production.
🤖 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/3-tooling/emitter/src/domain-type-generation.ts`:
- Around line 107-108: The code in generateModelRelationsType currently uses a
bare cast relObj['to'] as CrossReference when calling serializeCrossReference;
update the types so relObj is recognized as a properly typed ContractRelation
(via ContractModel['relations'] / ContractRelation union) and remove the cast by
accessing rel.to directly. Specifically, ensure the relations property and the
local variable for each relation are typed to include ContractReferenceRelation
and ContractEmbedRelation (both having to: CrossReference), then call
serializeCrossReference(rel.to) instead of casting relObj['to'].
In `@packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts`:
- Around line 91-97: The generated .d.ts can reference CrossReference from
generateRootsType(), so update the import block in generate-contract-dts.ts to
include CrossReference alongside the existing types (Contract as ContractType,
ExecutionHashBase, NamespaceId, ProfileHashBase, StorageHashBase); this ensures
the emitted Record<string, CrossReference> type is resolved—locate the import
statement near the top of generate-contract-dts.ts and add CrossReference to
that named import list.
In
`@packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts`:
- Around line 190-192: The assertion comparing Object.values(ir.roots) to the
string 'Bug' is a false positive because roots now hold CrossReference objects;
update the two assertions to inspect the actual root names instead of raw values
— e.g., assert that neither the keys of ir.roots nor the dereferenced target
name on CrossReference objects (use the CrossReference property that holds the
target name) equals 'Bug'. Locate the checks around ir.roots and replace the
toContain/toHaveProperty check with logic that maps Object.values(ir.roots) to
their target names (or simply assert on Object.keys(ir.roots)) and then assert
that 'Bug' is not present.
In
`@packages/2-mongo-family/2-authoring/contract-ts/test/contract-builder.dsl.test.ts`:
- Around line 57-60: The test's roots assertion is inconsistent: `users` uses
crossRef('User') but `posts` is still a plain string; update the expectation in
the `expect(contract.roots)` assertion to use `crossRef('Post')` for `posts` so
both roots follow the CrossReference shape (use the existing `crossRef` helper
referenced in the test).
In `@packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts`:
- Around line 34-37: The assertion for posts is still expecting a plain string;
update the expect(types).toContain for "posts" in emitter-hook.e2e.test.ts to
match the CrossReference shape used by users — e.g. assert that types contains
"readonly posts: { readonly namespace: '__unbound__' & NamespaceId; readonly
model: 'Post' }" so it mirrors the users cross-reference format used in the
test.
In
`@packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts`:
- Around line 93-97: The test asserts mixed root encodings: `users` was migrated
to the CrossReference object form but `posts` still expects the old `'Post'`
string; update the second expectation so both roots use the same `{ namespace,
model }` shape (mirror the `users` expectation pattern) by replacing the
`"readonly posts: 'Post'"` expectation with a string assertion matching the
object-pair encoding, e.g. `"readonly posts: { readonly namespace: '__unbound__'
& NamespaceId; readonly model: 'Post' }"`, so both expectations use the migrated
CrossReference object format.
In `@packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts`:
- Line 60: The fixture incorrectly sets owner to crossRef('Post') but
validateStructure expects the owner field to be a model-name string; update the
fixture in blog-contract.ts so the owner property is the literal string "Post"
(not crossRef('Post')), keeping all other fields intact so validateStructure
will accept it.
In `@packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts`:
- Around line 31-39: The loop that assigns client[rootName] using
createMongoCollection should first guard that rootRef.model exists to fail fast;
in the block iterating over Object.entries(contract.roots) check that
rootRef.model is defined (for the given rootName) and if not throw a clear Error
indicating the missing/stale root model for that root (include rootName and
contract identifier), otherwise proceed to call createMongoCollection as before;
update the code around the client[rootName] assignment in mongo-orm.ts to
perform this explicit existence check before calling createMongoCollection.
In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 137-142: The schema uses a bare cast "as
Type<ForeignKeyReferenceInput>" which bypasses proper typing; replace that
pattern by declaring the generic with the validator builder: use
type.declare<ForeignKeyReferenceInput>().type({...}) instead of the cast so
ForeignKeyReferenceSchema is constructed with strong typing; update the current
ForeignKeyReferenceSchema definition to call
type.declare<ForeignKeyReferenceInput>().type({ '+': 'reject', namespaceId:
'string', tableName: 'string', columns: type.string.array().readonly() }) and
remove the "as Type<...>" cast.
In `@packages/2-sql/2-authoring/contract-psl/test/fixtures.ts`:
- Around line 490-498: The function documentScopedTypes should stop using the
unsafe cast on contract.storage: update the contract parameter type to declare
storage as { readonly types?: Readonly<Record<string, unknown>> } (or a
compatible type matching SqlStorage.types) so you can remove the cast and
replace const storageTypes = (contract.storage as ...)?.types with const
storageTypes = contract.storage?.types; keep the rest of documentScopedTypes and
the UNBOUND_NAMESPACE_ID lookup unchanged.
In `@packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts`:
- Around line 251-253: The wrapper buildContractFromDsl currently uses a
forbidden blind cast to convert buildSqlContractFromDefinition's
Contract<SqlStorage> into SqlContractResult<Definition>; instead, make the
builder and overload types consistent so no cast is needed: change
buildContractFromDsl to be generic and return SqlContractResult<Definition>
directly by updating buildSqlContractFromDefinition (or its exported type) to
produce a SqlContractResult<Definition>/Contract result that matches
SqlContractResult, and update the defineContract overloads to be generic over
the provided scaffold/definition (not concrete ContractInput) so callers keep
their inferred scaffold types (i.e., make defineContract overload signatures
return SqlContractResult<TDefinition> or equivalent generic result type). Remove
the "as unknown as" cast and ensure all related type aliases (SqlContractResult,
ContractInput, Contract<SqlStorage>) align with the new generic signatures.
In
`@packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.types.test.ts`:
- Line 2: Restore compile-time type assertions by importing and using
expectTypeOf from 'vitest' in contract-builder.dsl.types.test.ts and add
assertions for the defineContract result: assert contract.target and
contract.targetFamily with expectTypeOf(contract.target).toEqualTypeOf<...>() /
toMatchTypeOf as appropriate, and assert the model storage table type with
expectTypeOf(contract.models.User.storage.table).toEqualTypeOf<'YourExpectedLiteralOrWidenedType'>();
also remove or replace the manual cast on contract.models so the type-check
exercises the defineContract boundary (keep the literal if the DSL preserves it,
otherwise assert the widened type).
In `@packages/2-sql/2-authoring/contract-ts/test/cross-ref-helpers.ts`:
- Around line 10-12: Tighten the unsafe cast on contract.storage by either
updating the contract parameter/type definition so storage is typed as {
readonly types?: Record<string, unknown> } | undefined, or add a local type
guard before accessing types (e.g., check typeof contract.storage === 'object'
&& contract.storage !== null && 'types' in contract.storage) and then read
contract.storage.types into storageTypes; replace the current raw cast used when
initializing storageTypes to avoid the bare `as` cast on contract.storage.
In `@packages/3-extensions/paradedb/src/contract.d.ts`:
- Around line 22-28: The file imports NamespaceId but omits the CrossReference
type which is used by the readonly roots: Record<string, CrossReference>;
declaration; update the import statement in contract.d.ts to remove the unused
NamespaceId and add CrossReference from '`@prisma-next/contract/types`' so the
roots type resolves correctly and no unused imports remain.
In
`@packages/3-extensions/pgvector/migrations/20260601T0000_install_vector_extension/end-contract.d.ts`:
- Line 75: The generated declaration references the type CrossReference in the
line "roots: Record<string, CrossReference>" but never imports it; add an
explicit type import for CrossReference (e.g. import type { CrossReference }
from '`@prisma-next/contract/types`';) at the top of the generated
end-contract.d.ts so the identifier is in scope and typechecking succeeds, and
apply the same import to the other generated end-contract.d.ts that has the same
Record<string, CrossReference> usage.
In `@packages/3-extensions/pgvector/src/contract.d.ts`:
- Line 75: The generated contract.d.ts declares "readonly roots: Record<string,
CrossReference>" but doesn't import CrossReference; update the top import type
list (import type { ... } from '`@prisma-next/contract/types`';) to include
CrossReference so the type is resolved. Locate the import in
packages/3-extensions/pgvector/src/contract.d.ts and add CrossReference to the
named imports, then re-run the generator if this file is produced by tooling to
persist the change.
In
`@packages/3-extensions/postgis/migrations/20260601T0000_install_postgis_extension/end-contract.d.ts`:
- Line 75: The file end-contract.d.ts references the CrossReference type in the
roots property (readonly roots: Record<string, CrossReference>) but does not
import it; add an import for CrossReference from the package that exports it
(e.g., import { CrossReference } from '`@prisma-next/contract/types`') so the type
is available for the roots declaration and update the generated import list
accordingly.
In `@packages/3-extensions/postgis/src/contract.d.ts`:
- Around line 22-28: The roots type references CrossReference but CrossReference
is not imported; update the top import type block that currently imports
Contract, ExecutionHashBase, NamespaceId, ProfileHashBase, StorageHashBase from
'`@prisma-next/contract/types`' to also include CrossReference so the type
declaration for roots can resolve (ensure the symbol CrossReference is added to
that import list).
In `@packages/3-extensions/sql-orm-client/src/collection-contract.ts`:
- Around line 265-269: The current code uses a bare cast "const rel = value as {
to?: CrossReference; cardinality?: unknown; on?: { localFields?: unknown;
targetFields?: unknown } }" which is unsafe; replace it with a proper type guard
or the project's cast helper to narrow safely: implement an
isCrossReferenceShape(value): value is { to?: CrossReference; cardinality?:
unknown; on?: { localFields?: unknown; targetFields?: unknown } } that checks
the expected keys/types (or call blindCast<T,'reason'> / castAs<T> from
`@prisma-next/utils/casts`) and then use that guard to assign/narrow `rel` before
using it, updating the code paths that reference `rel`, `value`, and
`CrossReference` accordingly.
---
Outside diff comments:
In `@packages/1-framework/0-foundation/contract/src/validate-domain.ts`:
- Around line 45-56: The root/CrossReference validation currently checks models
by model name only; change it to validate and dedupe using the combined
namespace and model (e.g., build a qualified key
`${crossRef.namespace}:${modelName}` and use that to check membership in the
model set and in seenValues), update the error messages to include namespace
(e.g., "Root \"rootKey\" references model \"namespace:model\" which does not
exist"), and apply the same fix to the other crossRef checks referenced (the
loops/blocks around the other occurrences) so every reference resolves against
namespace+model rather than model alone.
In
`@packages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.ts`:
- Around line 140-147: The code currently allows a root to reference a model
name that isn't present in contract.models and then silently uses the rootName
as the collection; update the lookup path to fail fast: after computing
modelName from contract.roots[rootName]?.model, if modelName exists but
contract.models[modelName] is undefined, throw a descriptive Error (e.g.
"lookup() unknown model: \"<modelName>\" referenced by root \"<rootName>\"")
instead of continuing; locate this logic around the
modelName/model/foreignCollection code in lookup() and ensure
createLookupBuilder is only called when contract.models[modelName] is defined.
---
Nitpick comments:
In `@packages/1-framework/0-foundation/contract/test/canonicalization.test.ts`:
- Around line 3-7: Replace the local test helper that defaults to "default" with
the shared crossRef factory from the source so tests use the runtime default
"__unbound__"; remove the local function and import the exported crossRef
factory from the source module (instead of asNamespaceId-only) and update any
uses (including the occurrences around lines 100-101) to call that imported
crossRef to avoid default-drift between tests and production.
🪄 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: 01a9c910-2187-42cd-b986-87c238bf3e39
⛔ Files ignored due to path filters (20)
packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.jsonis excluded by!**/generated/**packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.tsis excluded by!**/generated/**packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.jsonis excluded by!**/generated/**projects/contract-ir-planes/slices/cross-reference-encoding/dispatches/01-framework-shape.mdis excluded by!projects/**projects/contract-ir-planes/slices/cross-reference-encoding/dispatches/02-family-lowering-emitter-consumers.mdis excluded by!projects/**projects/contract-ir-planes/slices/cross-reference-encoding/dispatches/03-fixture-regen-and-pr.mdis excluded by!projects/**projects/contract-ir-planes/slices/cross-reference-encoding/plan.mdis excluded by!projects/**projects/contract-ir-planes/slices/cross-reference-encoding/spec.mdis excluded by!projects/**projects/contract-ir-planes/slices/namespace-aware-enum-planning/plan.mdis excluded by!projects/**projects/contract-ir-planes/slices/namespace-aware-enum-planning/spec.mdis excluded by!projects/**test/e2e/framework/test/fixtures/generated/contract.d.tsis excluded by!**/generated/**test/e2e/framework/test/fixtures/generated/contract.jsonis excluded by!**/generated/**test/e2e/framework/test/sqlite/fixtures/generated/contract.d.tsis excluded by!**/generated/**test/e2e/framework/test/sqlite/fixtures/generated/contract.jsonis excluded by!**/generated/**test/integration/test/mongo/fixtures/generated/contract.d.tsis excluded by!**/generated/**test/integration/test/mongo/fixtures/generated/contract.jsonis excluded by!**/generated/**test/integration/test/sql-builder/fixtures/generated/contract.d.tsis excluded by!**/generated/**test/integration/test/sql-builder/fixtures/generated/contract.jsonis excluded by!**/generated/**test/integration/test/sql-orm-client/fixtures/generated/contract.d.tsis excluded by!**/generated/**test/integration/test/sql-orm-client/fixtures/generated/contract.jsonis excluded by!**/generated/**
📒 Files selected for processing (142)
apps/telemetry-backend/src/prisma/contract.d.tsapps/telemetry-backend/src/prisma/contract.jsondrive/retro/findings.mdexamples/cipherstash-integration/src/prisma/contract.d.tsexamples/cipherstash-integration/src/prisma/contract.jsonexamples/mongo-blog-leaderboard/src/contract.d.tsexamples/mongo-blog-leaderboard/src/contract.jsonexamples/mongo-demo/src/contract.d.tsexamples/mongo-demo/src/contract.jsonexamples/paradedb-demo/src/prisma/contract.d.tsexamples/paradedb-demo/src/prisma/contract.jsonexamples/prisma-next-cloudflare-worker/src/prisma/contract.d.tsexamples/prisma-next-cloudflare-worker/src/prisma/contract.jsonexamples/prisma-next-demo-sqlite/src/prisma/contract.d.tsexamples/prisma-next-demo-sqlite/src/prisma/contract.jsonexamples/prisma-next-demo/src/prisma/contract.d.tsexamples/prisma-next-demo/src/prisma/contract.jsonexamples/prisma-next-postgis-demo/src/prisma/contract.d.tsexamples/prisma-next-postgis-demo/src/prisma/contract.jsonexamples/react-router-demo/src/prisma/contract.d.tsexamples/react-router-demo/src/prisma/contract.jsonexamples/retail-store/src/contract.d.tsexamples/retail-store/src/contract.jsonpackages/1-framework/0-foundation/contract/src/canonicalization.tspackages/1-framework/0-foundation/contract/src/contract-types.tspackages/1-framework/0-foundation/contract/src/cross-reference.tspackages/1-framework/0-foundation/contract/src/domain-types.tspackages/1-framework/0-foundation/contract/src/exports/types.tspackages/1-framework/0-foundation/contract/src/namespace-id.tspackages/1-framework/0-foundation/contract/src/testing-factories.tspackages/1-framework/0-foundation/contract/src/validate-domain.tspackages/1-framework/0-foundation/contract/test/canonicalization.test.tspackages/1-framework/0-foundation/contract/test/contract-factories.test.tspackages/1-framework/0-foundation/contract/test/contract-types.test-d.tspackages/1-framework/0-foundation/contract/test/contract-types.test.tspackages/1-framework/0-foundation/contract/test/domain-types.test.tspackages/1-framework/0-foundation/contract/test/validate-domain.test.tspackages/1-framework/3-tooling/emitter/src/domain-type-generation.tspackages/1-framework/3-tooling/emitter/src/generate-contract-dts.tspackages/1-framework/3-tooling/emitter/test/domain-type-generation.test.tspackages/1-framework/3-tooling/emitter/test/emitter.integration.test.tspackages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.tspackages/1-framework/3-tooling/emitter/test/utils.tspackages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.tspackages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.tspackages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.tspackages/2-mongo-family/1-foundation/mongo-contract/src/validate-storage.tspackages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.tspackages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.tspackages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.jsonpackages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.tspackages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.tspackages/2-mongo-family/2-authoring/contract-psl/src/interpreter.tspackages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.tspackages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.tspackages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.tspackages/2-mongo-family/2-authoring/contract-ts/test/contract-builder.dsl.test.tspackages/2-mongo-family/2-authoring/contract-ts/test/contract-builder.types.test-d.tspackages/2-mongo-family/3-tooling/emitter/src/index.tspackages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.tspackages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.tspackages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.tspackages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.tspackages/2-mongo-family/5-query-builders/orm/src/collection.tspackages/2-mongo-family/5-query-builders/orm/src/mongo-orm.tspackages/2-mongo-family/5-query-builders/orm/src/mongo-raw.tspackages/2-mongo-family/5-query-builders/orm/src/types.tspackages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.tspackages/2-mongo-family/5-query-builders/query-builder/src/lookup-builder.tspackages/2-mongo-family/5-query-builders/query-builder/src/query.tspackages/2-mongo-family/5-query-builders/query-builder/src/state-classes.tspackages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-contract.tspackages/2-mongo-family/7-runtime/test/fixtures/contract.d.tspackages/2-mongo-family/7-runtime/test/fixtures/contract.jsonpackages/2-mongo-family/7-runtime/test/fixtures/decode-fixture-contract.tspackages/2-mongo-family/7-runtime/test/runtime-types.test-d.tspackages/2-mongo-family/9-family/test/mongo-contract-serializer-base.test.tspackages/2-sql/1-core/contract/src/factories.tspackages/2-sql/1-core/contract/src/ir/foreign-key-reference.tspackages/2-sql/1-core/contract/src/validators.tspackages/2-sql/1-core/contract/test/domain-types.test.tspackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/test/fixtures.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.extensions.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.polymorphism.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.relations.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.types.test.tspackages/2-sql/2-authoring/contract-psl/test/psl-ts-namespace-parity.test.tspackages/2-sql/2-authoring/contract-ts/src/build-contract.tspackages/2-sql/2-authoring/contract-ts/src/contract-builder.tspackages/2-sql/2-authoring/contract-ts/src/contract-dsl.tspackages/2-sql/2-authoring/contract-ts/src/contract-types.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.helpers.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.types.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.per-model-namespace.test.tspackages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.tspackages/2-sql/2-authoring/contract-ts/test/contract.logic.test.tspackages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.tspackages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.tspackages/2-sql/2-authoring/contract-ts/test/cross-ref-helpers.tspackages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.tspackages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.jsonpackages/2-sql/3-tooling/emitter/src/index.tspackages/2-sql/5-runtime/src/sql-context.tspackages/2-sql/9-family/test/contract-to-schema-ir.test.tspackages/2-sql/9-family/test/cross-reference-roundtrip.test.tspackages/2-sql/9-family/test/schema-verify.helpers.tspackages/3-extensions/cipherstash/src/contract.d.tspackages/3-extensions/cipherstash/src/contract.jsonpackages/3-extensions/paradedb/src/contract.d.tspackages/3-extensions/pgvector/migrations/20260601T0000_install_vector_extension/end-contract.d.tspackages/3-extensions/pgvector/migrations/20260601T0000_install_vector_extension/end-contract.jsonpackages/3-extensions/pgvector/migrations/20260601T0000_install_vector_extension/migration.jsonpackages/3-extensions/pgvector/migrations/20260601T0000_install_vector_extension/migration.tspackages/3-extensions/pgvector/migrations/refs/head.jsonpackages/3-extensions/pgvector/src/contract.d.tspackages/3-extensions/pgvector/src/contract.jsonpackages/3-extensions/postgis/migrations/20260601T0000_install_postgis_extension/end-contract.d.tspackages/3-extensions/postgis/migrations/20260601T0000_install_postgis_extension/end-contract.jsonpackages/3-extensions/postgis/migrations/20260601T0000_install_postgis_extension/migration.jsonpackages/3-extensions/postgis/migrations/20260601T0000_install_postgis_extension/migration.tspackages/3-extensions/postgis/migrations/refs/head.jsonpackages/3-extensions/postgis/src/contract.d.tspackages/3-extensions/postgis/src/contract.jsonpackages/3-extensions/sql-orm-client/src/collection-contract.tspackages/3-extensions/sql-orm-client/src/model-accessor.tspackages/3-extensions/sql-orm-client/test/collection-contract.test.tspackages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.tspackages/3-extensions/sql-orm-client/test/model-accessor.test.tspackages/3-extensions/sql-orm-client/test/mutation-executor.test.tspackages/3-mongo-target/1-mongo-target/test/mongo-target-contract-serializer.test.tspackages/3-targets/3-targets/postgres/src/core/migrations/operations/shared.tspackages/3-targets/3-targets/postgres/test/migrations/planner-schema-lookup.test.tspackages/3-targets/3-targets/sqlite/test/migrations/issue-planner.test.tspackages/3-targets/6-adapters/sqlite/test/migrations/planner-introspection.integration.test.tstest/integration/test/fixtures/contract.d.tstest/integration/test/fixtures/contract.json
d797fc5 to
df520d8
Compare
…ference-encoding Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # drive/retro/findings.md # packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts # packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts # packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
… cross-refs
Several mongo-family test files were partially migrated to the new
{ namespace, model } cross-reference shape and left stale assertions
or stale fixture entries that the validator and emitter assertions
caught. Walk each one:
- `interpreter.polymorphism.test.ts`: `Object.values(ir.roots)` now
yields CrossReference objects, not bare model strings, so the
`not.toContain('Bug')` exclusion became trivially true.
Switch to `not.toContainEqual(crossRef('Bug'))`.
- `blog-contract.ts`: `Comment.owner` is not in this migration scope
(the spec migrates `relation.to`, `model.base`, `roots[*]` only);
`ContractModelBase.owner` remains `string`. Revert the fixture to
`owner: 'Post'`.
- `emitter-hook.e2e.test.ts`: `posts` root expectation updated to
the object-pair emission so both `users` and `posts` exercise the
same shape.
- `emitter-hook.generation.test.ts`: `roots`-type and polymorphic
`base` tests now pass `crossRef(...)` inputs and assert the
object-pair emission output.
- `emitter-hook.structure.test.ts`: polymorphic-collection and
base-missing tests pass `crossRef(...)` so `validateStructure`
resolves `base.model` correctly.
Addresses CodeRabbit threads on PR #600 covering these sites.
Signed-off-by: Will Madden <madden@prisma.io>
…eral The framework Contract<S, M> interface declares roots: Record<string, CrossReference>, which carries a string index signature. When the emitter intersected ContractType<S, M> with a literal roots block, TypeScript collapsed keyof of the intersection to string (since string | "users" | ... = string), so downstream mapped types over contract roots — most visibly MongoOrmClient — degraded to bare index signatures. Consumers ended up with db.orm["users"] typed as MongoCollection<TContract, union-of-all-roots> instead of MongoCollection<TContract, "User">, surfacing as TS2339 / TS4111 in the retail-store example build. Wrap ContractType<S, M> in Omit<..., "roots"> so the base does not contribute the wide signature; the emitted intersection then carries just the literal roots block. Indexed access through TContract["roots"] narrows to the literal entry, RootModelName resolves to the specific model name, and the ORM client recovers per-root collection types. Regenerated every tracked contract.d.ts to pick up the new shape. contract.json files are unchanged. The fix is internal to the emitted shape; runtime behaviour and authoring DSL are unaffected. Signed-off-by: Will Madden <madden@prisma.io>
…d<string, CrossReference> Extension-pack contracts (paradedb, pgvector, postgis) declare no roots — their roots map is structurally empty. The emitter previously synthesised the type as Record<string, CrossReference> for that case, which referenced CrossReference without adding it to the import list in the emitted contract.d.ts. The result typechecked locally thanks to skipLibCheck, but the generated files carried a latent missing import (CrossReference cited without being imported alongside NamespaceId / Contract / hash bases) that CodeRabbit and any skipLibCheck-off consumer would flag. Emit Record<string, never> for the empty-roots case instead. The semantics match (no roots can ever appear), no extra import is needed, and the import list stays minimal. Regenerated the three affected extension contract.d.ts files. Addresses CodeRabbit threads on PR #600 for paradedb / pgvector / postgis missing-import findings. Signed-off-by: Will Madden <madden@prisma.io>
…hape postgis and pgvector ship migration end-contract.d.ts artefacts that emitted roots: Record<string, CrossReference> for their empty roots maps, mirroring the latent missing-import bug fixed in the prior commit. The migration end-contract.d.ts files are not produced by the fixtures:emit pipeline so the prior emitter regen did not refresh them. Update them in place to match the new emitter output (Record<string, never>), keeping the migration artefacts consistent with the live extension contracts. Signed-off-by: Will Madden <madden@prisma.io>
…se types - cross-reference.ts: cast CrossReferenceSchema to Type<CrossReference> so the public inferred type reflects the NamespaceId brand on namespace, matching the ForeignKeyReferenceSchema pattern in sql-contract - mongo contract-builder: change ContractRelationFromBuilder to and MaybeBase to use CrossRefFor<To> / CrossRefFor<Base> instead of plain string; runtime already sets these fields via crossRef(...) so the type now matches reality Signed-off-by: Will Madden <madden@prisma.io>
…nd empty-roots behavior After S1.C changes: - Empty mongo collections now emit MongoCollection instead of Record<string, never> - Empty/undefined roots emit Record<string, never> instead of Record<string, CrossReference> Update test assertions to match the new emitter output. Signed-off-by: Will Madden <madden@prisma.io>
…ed roots and relations
After S1.C (feat(s1c-d2): emit cross-reference pairs across family lowering and
consumers), relation.to and model.base are CrossReference objects instead of
plain strings, and contract.roots values are CrossReference objects instead of
model name strings. Update tests and fixture JSON files to match the new shape:
- contract-builder.test.ts: to → { namespace, model }, roots → { key: CrossRef }
- cli.emit-command.additional.test.ts: base/roots assertions updated
- side-by-side-contracts.test.ts: roots and relations updated + fixture JSON regen
- authoring/cli.emit-parity-fixtures.test.ts: fixture JSON files regenerated
- authoring/mongo-pack-composition.test.ts: roots assertion updated
Signed-off-by: Will Madden <madden@prisma.io>
…ossReference roots Signed-off-by: Will Madden <madden@prisma.io>
…ots format After S1.C changed relations.to, model.base, and contract.roots from plain strings to CrossReference objects, update affected unit tests to use the new object shape: - mongo-orm/mongo-raw.test.ts: bad-contract roots use CrossReference shape - target-mongo/schema-verifier.test.ts: roots use CrossReference shape - sql-contract-emitter/generation.advanced.test.ts: to fields use CrossReference shape both in input contracts and expected .d.ts output strings Signed-off-by: Will Madden <madden@prisma.io>
…ence base shape Signed-off-by: Will Madden <madden@prisma.io>
…solution
After the CrossReference migration a relation's contract-level `to` is an
object-pair { namespace, model } rather than a bare model-name string. Three
type-level destructures still matched `to` as a string, so RelationModelName
collapsed to never — propagating through RelatedModelName into include()
callbacks, which received ModelAccessor<Contract, never> and lost all field
accessors. Reach through `to.model` in all three.
Signed-off-by: Will Madden <madden@prisma.io>
…n orm-contract-types Signed-off-by: Will Madden <madden@prisma.io>
…ossReference roots in integration/e2e tests - BuiltStorageTables.foreignKeys source/target namespaceId: string → NamespaceId to match ForeignKeyReference.namespaceId after the NamespaceId branding commit - Integration tests using MongoContract: update roots from string literals to crossRef() calls (CrossReference objects) per S1.C CrossReference encoding - mongo/query-builder.test.ts, mongo-runtime/query-builder.test.ts: use literal model names in TestContract.roots for RootModelName inference to stay precise - value-objects fixture: regenerate mongo-contract.json and .d.ts roots to CrossReference object shape Signed-off-by: Will Madden <madden@prisma.io>
…/demo fixtures - buildSqlContractFromDefinition: restore types to storageWithoutHash so document-scoped codec types (pgvector vector, postgis geometry, etc.) survive serialization to contract.json — PR S1.C inadvertently dropped them by moving only to domain.__unbound__.types (not yet serialized) - Regen pgvector/postgis contract.json, contract.d.ts, migration metadata (head.json, migration.json to-pin, end-contract.json) with correct hash sha256:06733db (pgvector) / sha256:9f708ee (postgis) — matches main - Regen prisma-next-demo and prisma-next-postgis-demo example contracts which embed pgvector/postgis type entries in storage.types Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…ference-encoding Signed-off-by: Will Madden <madden@prisma.io>
…port style from ts-render Signed-off-by: Will Madden <madden@prisma.io>
…gvector and postgis Signed-off-by: Will Madden <madden@prisma.io>
…rt consolidation and storage.types restore Signed-off-by: Will Madden <madden@prisma.io>
…eference roots format Signed-off-by: Will Madden <madden@prisma.io>
…prisma contracts after emitter ContractBase format change Signed-off-by: Will Madden <madden@prisma.io>
…ion/contract canonicalization branch gap Signed-off-by: Will Madden <madden@prisma.io>
…n branches Coverage CI failed on this PR: cross-reference.ts (new in S1.C) had 0% coverage, and canonicalization.ts branch coverage dipped below the 94% per-file threshold after the cross-reference encoding work added new preserve-empty branches. Add a unit test for crossRef() + CrossReferenceSchema (accept/reject paths), and extend the canonicalization suite to cover the storage.types[].typeParams preserve-empty branch and the nullish-entry arms of the index/unique array sorters. Signed-off-by: Will Madden <madden@prisma.io>
…d Drive model Reshape the project plan to the post-refactor drive-plan-project shape: slices are described at the slice altitude (outcome / builds-on / hands-to / focus) with no embedded dispatch lists or file-count scope estimates, delivered slices collapse to a status table, and S1.E is parallelised with S1.D rather than serialised behind it (default-to-parallel; the planner correctness fix is a disjoint surface with no dependency on the cleanup). Signed-off-by: Will Madden <madden@prisma.io>
canonicalizeContractToObject built an explicit normalized picker that never included domain, so any contract populating Contract.domain was silently dropped from canonical output, the hash, and round-trip. Add domain between valueObjects and storage via ifDefined so existing fixtures without a domain key are unchanged. Add domain-plane tests: preservation of empty typeParams on domain types (exercises isDomainUnboundTypeParams) and omission when domain is absent. Signed-off-by: Will Madden <madden@prisma.io>
…ference-encoding Signed-off-by: Will Madden <madden@prisma.io>
Refresh tracked contract.json files so fixtures:check passes with main's emit-output-dir change and current domain type emission. Signed-off-by: Will Madden <madden@prisma.io>
The emit-parity integration test stores the emitted contract JSON string per case. Two upstream changes invalidated the stored expectations: the domain plane now flows through canonicalization (pgvector/cipherstash cases gain a domain section), and the emitter expands single-element arrays to multi-line. Regenerate via UPDATE_AUTHORING_PARITY_EXPECTED so the parity test matches current emit output. Signed-off-by: Will Madden <madden@prisma.io>
The parity expected.contract.json and side-by-side contract.json snapshots were only regenerable through one-off UPDATE_* env vars, so pnpm fixtures:emit left them stale while fixtures:check never diffed them (its glob did not match expected.contract.json). Wire an emit:authoring step into the integration-tests emit script that rewrites both suites (and biome-formats them to the committed style), and extend the fixtures:check glob to cover expected.contract.json so drift fails CI. Signed-off-by: Will Madden <madden@prisma.io>
At a glance
Every cross-namespace reference in the emitted contract now carries an explicit
{ namespace, model }pair, with the namespace field branded asNamespaceIdat the type level. The bare-string shape is gone from emitter output.// contract.json — roots "roots": { - "post": "Post", - "task": "Task", - "user": "User" + "post": { "namespace": "__unbound__", "model": "Post" }, + "task": { "namespace": "__unbound__", "model": "Task" }, + "user": { "namespace": "__unbound__", "model": "User" } }// contract.d.ts — relation.to readonly user: { - readonly to: 'User'; + readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; readonly cardinality: 'N:1'; ... };The same shape applies to
model.base(the target ofextends), everyrelation.to, every entry inroots, and foreign-key targets (which additionally carrytableNameandcolumns).Why
Before this change, a cross-namespace reference like
"to": "Order"was a bare model name. As soon as namespaces became first-class (the parent project, TML-2584), bare names became ambiguous:OrderinpublicvsOrderinanalyticswould silently collide, and the only way to disambiguate at the consumer was per-call-site fallback logic. The fix is to encode the coordinate fully on the wire, in one place, once.What this PR commits to
namespaceandmodel. There is no optional namespace and no implicit same-namespace fallback. Ambiguous bare-string targets are rejected at validation time — one deserialization path, no silent-collision branch.namespacefield is brandedNamespaceIdin emitted.d.ts. Type-level distinction from a plain string; existing code paths that previously passed a raw string now typecheck-fail rather than silently consume the wrong shape.storage.typestodomain.__unbound__.types. Codec aliases describe domain-side content (which scalars and objects exist in the type system); they don't belong on a document-scoped storage envelope. Placing them in the unbound domain namespace makes them namespace-addressable without widening the cross-reference collision surface.belongsTo,extends, androotskeep their existing authoring signatures. The lowering layer reads the in-scope namespace map and emits the object-pair shape.What this PR does NOT do
start-contract.json/end-contract.jsonsnapshots underexamples/*/migrations/**keep their bare-string shape. The migration-chain replay test (examples/retail-store/test/migration-chain.test.ts) verifies they still hydrate end-to-end against the new framework. Rewriting them would mean re-planning historical migrations against a shape they were never written against; we'll do that only if a replay starts failing. (~62 bare-string hits remain inside bookends; zero outside.)sql-context.tscodec-alias dual-read stays in place. It reads bothstorage.types(legacy) anddomain.__unbound__.types(new) so consumers don't break mid-migration. Retiring it requires removingstorage.typesfrom the schema entirely; tracked as TML-2699.Validation
pnpm install --frozen-lockfilepnpm fixtures:emitpnpm-lock.yamldiff 0 lines post-emitpnpm fixtures:checkretail-storem1→m3)to/base/roots, plusstorage.typescodec aliases)examples/*/migrations/**; 62 inside (see "What this PR does NOT do")head.jsondrift forpgvector/postgis/paradedb/cipherstashpnpm test:packages(local)pnpm test:integration(local)PARTIAL means: the tests directly exercising cross-reference encoding are green; other failures observed locally are environmental (Postgres parallel infra, example build cache, etc.) and don't reproduce in CI. CI is the merge gate.
Follow-ups (not blocking this PR)
sql-context.tscodec-alias dual-read oncestorage.typesis removed from the schema.pnpm fixtures:emit(each extension currently hand-runs it)..entriesredirect, SQLite Postgres-enum cleanup).Alternatives considered
namespacewith same-namespace fallback. Rejected: re-introduces the silent-collision branch the encoding change exists to eliminate. Every consumer would need fallback logic, and fallback logic is exactly where collision bugs live.sql-context.tsdual-read in this PR. Rejected: dropping the legacy read requires removingstorage.typesfrom the schema (prerequisite for TML-2699), and we're not making that schema change here.pnpm fixtures:emit. Rejected: defeats byte-stability — the next emit-driven change would diff against the hand-edited shape and we'd lose the invariant that emitter output is reproducible.Summary by CodeRabbit
New Features
Documentation