Skip to content

feat(contract): add value objects and embedded documents#309

Merged
wmadden merged 28 commits into
mainfrom
tml-2206-value-objects-embedded-documents-in-both-families-adr-178
Apr 7, 2026
Merged

feat(contract): add value objects and embedded documents#309
wmadden merged 28 commits into
mainfrom
tml-2206-value-objects-embedded-documents-in-both-families-adr-178

Conversation

@wmadden

@wmadden wmadden commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

closes TML-2206

Walkthrough — Value Objects & Embedded Documents

Before / After (contract field shape)

// BEFORE — flat field, single type specifier
type ContractField = {
  readonly nullable: boolean;
  readonly codecId: string;
  readonly typeParams?: Record<string, unknown>;
};
// AFTER — tagged discriminated union under a single `type` property
type ContractField = {
  readonly nullable: boolean;
  readonly type: ContractFieldType;  // { kind: 'scalar' | 'valueObject' | 'union', ... }
  readonly many?: true;
  readonly dict?: true;
};

Intent

Make value objects (structured data without identity — an Address, a GeoPoint, a Money value) a first-class concept in the contract and ORM, from PSL authoring all the way through to typed query results in both SQL and Mongo families. This requires changing how every contract field describes its type, since the old flat { codecId, nullable } shape can only represent scalars.

Change map

The story

  1. Replace the flat field type with a tagged discriminated union. The old ContractField was { codecId, nullable } — every field was implicitly a scalar. The new shape carries a type property with a kind discriminant (scalar, valueObject, union), plus orthogonal many and dict modifiers. This is a structural change that prevents conflicting type specifiers at the data level. Every consumer of field.codecId across ~100 files migrates to field.type.codecId after narrowing on kind.

  2. Add value object definitions to the contract. A new valueObjects section on Contract holds named value object types. Each value object has the same field shape as a model but carries no identity, lifecycle, or relations. Domain validation ensures referential integrity (all value object references resolve) and rejects many + dict on the same field.

  3. Teach the PSL parser and interpreters about type blocks. The parser adds PslCompositeType nodes for type X { ... } blocks, parallel to models and enums. Both the SQL and Mongo PSL interpreters consume these nodes: emitting value object definitions into the contract, resolving model field references to value object types, and (SQL only) creating JSONB storage columns.

  4. Extend TS authoring and contract emission. The SQL semantic contract builder gains SqlSemanticValueObjectFieldNode and SqlSemanticValueObjectNode. Both SQL and Mongo emitter hooks generate value object type aliases in contract.d.ts and include a typed valueObjects descriptor in the contract type. Shared helpers in domain-type-generation.ts handle field-to-TypeScript resolution including many, dict, nullable, and self-referencing value objects.

  5. Wire value object fields into ORM row type inference. Mongo's InferModelRow and SQL's collection-contract types recursively expand value object field types into nested object types, respecting many and nullable modifiers. Type-level tests verify the inferred shapes match expectations.

  6. Prove the full stack with integration and e2e tests. Pre-emitted contract fixtures exercise the PSL → interpret → emit → validate → ORM CRUD path for both families, including tests against real MongoDB and Postgres databases.

Behavior changes & evidence

Compatibility / migration / risk

  • Breaking change to ContractField shape: Every consumer reading field.codecId must change to field.type.codecId. This is a mechanical migration but touches ~110 files. All existing tests and fixtures are updated on this branch.
  • Backward compatible at contract JSON level: valueObjects is optional — existing contracts without it remain valid.
  • Diagnostic code change: PSL_UNSUPPORTED_FIELD_LIST is removed in favor of PSL_UNSUPPORTED_FIELD_TYPE (scalar list fields are now supported with many: true).
  • Pre-emitted fixture risk: Integration/e2e tests use pre-emitted contract.d.ts / contract.json files. If the emitter output format changes again, these fixtures must be regenerated. This is a standard risk for pre-emitted test fixtures.

Follow-ups / open questions

  • Dot-path filtering: Not implemented on this branch. Deferred to ADR 180 dot-path accessor project. Mongo u("homeAddress.city").eq("NYC") and SQL JSONB path operators are future work.
  • ORM write input types: Type-level tests exist (value-object-inputs.test-d.ts) but runtime write integration tests for value objects are limited to e2e tests.
  • Nested .select() within value objects: Explicitly deferred — value objects are included/excluded as a whole.
  • dict modifier: Defined and validated but not exercised in authoring (no PSL syntax for dict fields yet).
  • Missing spec file: projects/orm-consolidation/specs/embedded-documents-and-value-objects.spec.md is referenced but absent.
  • ADRs 178/179: Referenced by design doc and execution plan but not present on this branch — verify they exist on main.

Non-goals / intentionally out of scope

  • Dot-path partial updates at nested paths
  • Runtime validation of value object data shape
  • Standard Schema → value object conversion
  • JSON compaction for scalar fields
  • Value object migration diffing

Summary by CodeRabbit

  • New Features
    • Added support for composite types (value objects) that can be defined and referenced in schemas, enabling type-safe nested object structures.
    • Composite types are fully integrated across both MongoDB and SQL/PostgreSQL, with automatic storage mapping (JSON/JSONB).
    • Enhanced schema language to support type declarations for defining reusable object structures.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

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

Introduces composite types (value objects) as a first-class concept in the contract system. Restructures field descriptors from { codecId, nullable } to a discriminated union { type: { kind: 'scalar' | 'valueObject' | 'union', ... }, nullable, many?, dict? }, adds value-object support to PSL parsing and contract authoring interpreters, updates validators to ensure value-object references resolve, and extends type generation to derive TypeScript types from value-object definitions.

Changes

Cohort / File(s) Summary
Contract Domain Types
packages/1-framework/0-foundation/contract/src/domain-types.ts, contract-types.ts, exports/types.ts
Introduced ContractValueObject, ContractFieldType discriminated union (scalar/valueObject/union) with ScalarFieldType, ValueObjectFieldType, UnionFieldType variants. Extended ContractField to require type: ContractFieldType and optional many/dict modifiers. Added valueObjects?: Record<string, ContractValueObject> to Contract.
Contract Canonicalization & Validation
packages/1-framework/0-foundation/contract/src/canonicalization.ts, validate-contract.ts, validate-domain.ts
Added valueObjects to canonical ordering; extended domain validation with validateValueObjectReferences (ensures referenced value-object names exist) and validateFieldModifiers (rejects many + dict together). Updated DomainContractShape to include optional valueObjects property.
Contract Testing Factories & Tests
packages/1-framework/0-foundation/contract/src/testing-factories.ts, test/...*
Extended ContractOverrides and createContract to support valueObjects; updated all test fixtures to reflect new { type: { kind: 'scalar', codecId }, nullable } field shape across canonicalization, domain-types, contract-types, and validation tests.
PSL Parser (Composite Types)
packages/1-framework/2-authoring/psl-parser/src/parser.ts, types.ts, exports/types.ts, test/parser.test.ts
Added parseCompositeTypeBlock(...) and PslCompositeType AST node with kind: 'compositeType', name, fields, attributes, span. Updated PslDocumentAst to include compositeTypes array; normalized composite type names to prevent field-type rewriting. Comprehensive test coverage for composite type parsing and model field resolution.
Domain Type Generation (Scalar/Value-Object Helpers)
packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts, test/domain-type-generation.test.ts
Added generateFieldResolvedType() (maps scalar codecId via CodecTypes, value objects via name, unions via `
SQL Core Contract (Field Validation & Factories)
packages/2-sql/1-core/contract/src/factories.ts, validate.ts, validators.ts, test/...*
Updated model() factory return type to emit type: ScalarFieldType instead of top-level codecId. Added Arktype schema ContractFieldTypeSchema enforcing discriminated union validation. Extended validateModelStorageReferences to verify value-object fields map to JSON/JSONB storage columns. Updated all test fixtures to new field shape.
SQL Authoring (Interpreter & Field Resolution)
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts, psl-field-resolution.ts, semantic-contract.ts, test/...*
Added SqlSemanticValueObjectNode and SqlSemanticValueObjectFieldNode to semantic AST. Extended ResolvedField with many, valueObjectTypeName, scalarCodecId metadata. Updated collectResolvedFields to detect composite types and force JSON/JSONB storage for value-object and list fields. Interpreter constructs valueObjects from PSL composite types and patches model fields with resolved metadata.
SQL Contract Builder
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts, semantic-contract.ts, test/contract-builder.value-objects.test.ts
Extended RuntimeBuilderState with valueObjects; updated buildSqlContractFromSemanticDefinition and buildContract to emit type: { kind: 'valueObject', name } for value-object-backed fields and handle value-object→scalar field resolution. Added comprehensive value-object integration test suite validating storage mapping to JSONB and nullability propagation.
SQL Type Inference & Lanes
packages/2-sql/1-core/contract/test/domain-types.test.ts, packages/2-sql/4-lanes/relational-core/src/types.ts, test/fixtures/contract.*
Updated ResolveModelFieldToJsType to branch on field.type.kind (scalar vs valueObject); value-object fields recursively resolve nested object types via TContract['valueObjects']. Applied many/dict modifiers uniformly for both scalar and composite types.
SQL Emitter (DTS & Type Generation)
packages/2-sql/3-tooling/emitter/src/index.ts, generate-contract-dts.ts, test/...*
Updated generateModelsType to emit nested type: { kind: 'scalar', codecId } wrapper; added isNonScalarField guard to route value-object fields through generateContractFieldDescriptor. Extended DTS output to include value-object type aliases and readonly meta property on ContractBase.
Mongo Contract Schema & Types
packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts, contract-types.ts, test/...*
Updated RawFieldSchema to require type: FieldTypeSchema (scalar/valueObject/union); added many? and dict? flags. Extended MongoContractSchema with valueObjects? property. Updated InferModelRow to handle value-object field type inference via ExtractValueObjects<TContract> and recursive field resolution.
Mongo Authoring (Interpreter)
packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts, test/interpreter.test.ts
Introduced resolveNonRelationField helper for consistent scalar/composite field resolution. Updated field mapping to detect composite types and emit type: { kind: 'valueObject', name }. Interpreter constructs contract-level valueObjects from PSL composite types; includes comprehensive test coverage for composite references, nesting, and list fields.
Mongo Emitter (Validation & Type Generation)
packages/2-mongo-family/3-tooling/emitter/src/index.ts, test/...*
Updated validateTypes to derive codec IDs from field.type structure; validates scalar member types in unions. Extended test fixtures and DTS generation to emit value-object type aliases, value-object field descriptors, and correct many/dict representation.
ORM Client (Field Trait Resolution & Integration)
packages/3-extensions/sql-orm-client/src/collection-contract.ts, filters.ts, model-accessor.ts, test/...*
Updated field trait resolution (assertFieldHasEqualityTrait, resolveFieldTraits) to extract codecId from field.type.kind === 'scalar' only; non-scalar fields default to empty traits. Extended integration tests across mutations (create/update/upsert/delete) and includes with new address value-object field. Added type-level tests for value-object input/output types and updated vitest config for type-checking.
Test Fixtures & Integration Tests
test/integration/test/...*, examples/...*, test/authoring/parity/...*
Systematically updated all contract JSON/TS fixtures to use nested type: { kind: 'scalar', codecId } structure. Added value-objects.e2e.test.ts and value-objects.integration.test.ts covering Mongo/SQL round-tripping, persistence, type safety, and cross-family consistency. Removed unsupported-list-field diagnostic test (list fields now supported via many: true). Updated 30+ parity/fixture files across both families.

Sequence Diagram(s)

sequenceDiagram
    participant PSL as PSL Document
    participant Parser as Parser
    participant Interpreter as Interpreter
    participant Validator as Validator
    participant Emitter as Emitter
    participant DTS as Generated .d.ts

    PSL->>Parser: Parse schema.psl
    Parser->>Parser: Extract composite type blocks<br/>(type Address {...})
    Parser->>Interpreter: AST with compositeTypes
    
    Interpreter->>Interpreter: Map composites to valueObjects<br/>Map fields to { kind, name/codecId }
    Interpreter->>Validator: Contract with valueObjects
    
    Validator->>Validator: Validate value-object<br/>references exist
    Validator->>Validator: Validate field modifiers<br/>(reject many + dict)
    Validator->>Emitter: Valid Contract
    
    Emitter->>Emitter: Generate type aliases<br/>for value-objects
    Emitter->>Emitter: Emit field descriptors<br/>with nested type structure
    Emitter->>DTS: ContractBase with<br/>valueObjects & field types
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • saevarb
  • jkomyno

Poem

🐰 Composites bloom in fields of code,
Where types once flat now branch and fold,
Value objects dance in JSON's embrace,
Kind discriminators guide their place,
From PSL dreams to contracts real,
Nested types make meaning seal! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2206-value-objects-embedded-documents-in-both-families-adr-178

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (2)
packages/2-mongo-family/3-tooling/emitter/src/index.ts (1)

145-158: ⚠️ Potential issue | 🟠 Major

Validate scalar members inside union fields too.

Line 147 now exits early for every non-scalar field, which means kind: 'union' fields never validate the codecIds of their scalar members. A malformed union member will make it through emitter validation and only break later when the generated types are consumed.

🛡️ Suggested validation shape
       for (const [fieldName, field] of Object.entries(model.fields)) {
-        const fieldType = field.type;
-        if (fieldType.kind !== 'scalar') {
-          continue;
-        }
-        const { codecId } = fieldType;
-        if (!codecId) {
-          throw new Error(`Field "${fieldName}" on model "${modelName}" is missing codecId`);
-        }
-        const match = codecId.match(typeIdRegex);
-        if (!match || !match[1]) {
-          throw new Error(
-            `Field "${fieldName}" on model "${modelName}" has invalid codec ID format "${codecId}". Expected format: ns/name@version`,
-          );
+        const scalarTypes =
+          field.type.kind === 'scalar'
+            ? [field.type]
+            : field.type.kind === 'union'
+              ? field.type.members.filter(
+                  (member): member is { readonly kind: 'scalar'; readonly codecId: string } =>
+                    member.kind === 'scalar',
+                )
+              : [];
+
+        for (const { codecId } of scalarTypes) {
+          if (!codecId) {
+            throw new Error(`Field "${fieldName}" on model "${modelName}" is missing codecId`);
+          }
+          const match = codecId.match(typeIdRegex);
+          if (!match || !match[1]) {
+            throw new Error(
+              `Field "${fieldName}" on model "${modelName}" has invalid codec ID format "${codecId}". Expected format: ns/name@version`,
+            );
+          }
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/3-tooling/emitter/src/index.ts` around lines 145 -
158, The loop currently skips non-scalar fields so scalar members of union
fields aren't validated; update the validation inside the iteration over
Object.entries(model.fields) to also handle when fieldType.kind === 'union' by
iterating the union's member list (e.g., fieldType.types or fieldType.members)
and for each member with member.type === 'scalar' validate member.codecId
against typeIdRegex exactly like the existing scalar branch (throwing the same
errors referencing modelName and fieldName but include the member
identifier/index in the message); keep using typeIdRegex and the same error
text/format for consistency.
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts (1)

373-394: ⚠️ Potential issue | 🟠 Major

Don’t bake Postgres JSONB into the shared SQL authoring path.

This builder now emits pg/jsonb@1 / jsonb for every value-object field regardless of definition.target.targetId. That makes buildSqlContractFromSemanticDefinition() generate Postgres-specific storage for non-Postgres SQL targets instead of deferring to the target/codec layer.

As per coding guidelines, packages/2-sql/**: “do not place concrete dialect code here” and packages/{2-sql,3-targets}/**: “Use codec-owned control hooks for storage type behavior in SQL adapters and targets”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts` around lines
373 - 394, The builder is hardcoding Postgres JSONB via
JSONB_CODEC_ID/JSONB_NATIVE_TYPE inside buildSqlContractFromSemanticDefinition
when handling isValueObjectField; change this to delegate storage
codec/nativeType resolution to the codec/target layer instead of baking Postgres
specifics: remove use of JSONB_CODEC_ID/JSONB_NATIVE_TYPE in the
isValueObjectField branch and instead query codecLookup (or use
definition.target.targetId to pick a codec resolver) to obtain the codec id and
nativeType for that field (e.g., call codecLookup.getCodec(...) or a
target-aware helper) and set columns[field.columnName].type/nativeType from that
lookup, falling back to a neutral/generic placeholder or undefined so targets
can decide. Ensure references to JSONB_CODEC_ID/JSONB_NATIVE_TYPE are removed
and only buildSqlContractFromSemanticDefinition, isValueObjectField, and
codecLookup (or definition.target.targetId) are involved.
🧹 Nitpick comments (8)
packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts (1)

1-916: File exceeds 500-line limit (916 lines).

Consider splitting this test file by logical boundaries. Potential split candidates:

  • emitter-hook.parameterized-types.basic.test.ts - basic typeParams emission (lines 23-225)
  • emitter-hook.parameterized-types.enums.test.ts - enum unions (lines 227-284)
  • emitter-hook.parameterized-types.typeref.test.ts - typeRef resolution (lines 286-343)
  • emitter-hook.parameterized-types.edge-cases.test.ts - edge cases and backwards compatibility (lines 424-606)
  • emitter-hook.parameterized-types.storage-types.test.ts - storage.types emission (lines 608-851)
  • emitter-hook.parameterized-types.imports.test.ts - parameterizedTypeImports (lines 853-915)

As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts`
around lines 1 - 916, The test file is too large—split the single file into
multiple smaller test files by logical sections (use the existing describe
blocks as boundaries) such as basic parameterized type tests, enum unions,
typeRef resolution, edge cases/backwards compatibility, storage.types emission,
and imports; for each new file, keep the same imports (e.g., createContract
helper and sqlTargetFamilyHook.generateContractTypes) and move the corresponding
it() blocks under their matching describe() (e.g., "columns with typeParams",
"enum unions", "columns with typeRef", "edge cases", "storage.types emission",
"parameterizedTypeImports"), ensuring tests still import TypeRenderEntry and
testHashes where needed and that any shared helpers (createContract, testHashes)
are either duplicated or exported to a small shared test-utils file to avoid
repetition.
packages/3-extensions/sql-orm-client/test/integration/include.test.ts (1)

1-710: File exceeds 500-line limit (710 lines).

Consider splitting this integration test file by include strategy or feature area:

  • include.stitching.test.ts - basic one-to-many/one-to-one stitching (lines 68-117)
  • include.aggregates.test.ts - count/sum/avg/min/max aggregations (lines 119-269)
  • include.combine.test.ts - combine() branch tests (lines 271-329)
  • include.lateral.test.ts - lateral strategy tests (lines 331-502)
  • include.correlated.test.ts - correlated strategy tests (lines 504-630)
  • include.nested.test.ts - nested multi-level includes (lines 632-709)

As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/test/integration/include.test.ts` around
lines 1 - 710, The file is over the 500-line limit; split the large integration
test by feature into smaller test files as suggested. Create new test files
named include.stitching.test.ts, include.aggregates.test.ts,
include.combine.test.ts, include.lateral.test.ts, include.correlated.test.ts,
and include.nested.test.ts and move the corresponding it(...) blocks (identified
by their test description strings, e.g. "include() stitches one-to-many and
one-to-one relations from real rows", "include() supports scalar count() on
to-many relations", "include() combine() evaluates branches independently", the
lateral-related tests with "lateral" in the description, the correlated/jsonAgg
tests mentioning "correlated" or "jsonAgg", and the nested 2-level include test)
into those files; keep shared helper functions (createUsersCollection,
createPostsCollection, seedUsers, seedPosts, seedComments, seedProfiles,
withCollectionRuntime, getTestContext/getTestContract,
createUsersCollectionWithCapabilities) imported at the top of each new file so
tests compile, remove the moved tests from the original file leaving only any
remaining tests and the small type/assert helpers (expectSelectAst,
expectDerivedTableSource, etc.), and run the test suite to ensure
imports/exports and test timeouts (timeouts.spinUpPpgDev) are preserved and all
tests pass.
packages/1-framework/0-foundation/contract/src/testing-factories.ts (1)

56-56: Use ifDefined() instead of inline conditional spread.

Line 56 introduces a new inline conditional spread pattern that the repo explicitly avoids.

♻️ Proposed fix
 import type { Contract } from './contract-types';
 import type { ContractModel, ContractValueObject, ModelStorageBase } from './domain-types';
 import { computeExecutionHash, computeProfileHash, computeStorageHash } from './hashing';
 import type { ExecutionSection, ProfileHashBase, StorageBase } from './types';
 import { coreHash } from './types';
+import { ifDefined } from '@prisma-next/utils/defined';
@@
-    ...(overrides.valueObjects ? { valueObjects: overrides.valueObjects } : {}),
+    ...ifDefined(overrides.valueObjects, (valueObjects) => ({ valueObjects })),
As per coding guidelines: "Use `ifDefined()` from `@prisma-next/utils/defined` for conditional object spreads instead of inline conditional spread patterns".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/0-foundation/contract/src/testing-factories.ts` at line
56, The inline conditional spread that adds valueObjects
(...(overrides.valueObjects ? { valueObjects: overrides.valueObjects } : {}))
should be replaced with the repo-standard ifDefined helper: import ifDefined
from '@prisma-next/utils/defined' and use ifDefined to conditionally spread the
valueObjects property (referencing overrides.valueObjects) into the object;
update the spread expression to use ifDefined and remove the inline ternary,
keeping the rest of the factory code unchanged.
packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts (1)

358-594: Move the value-object interpreter coverage into its own test file.

Lines 358-594 add a separate value-object/composite-type suite to a file that is now almost 600 lines long. Splitting these cases into interpreter.value-objects.test.ts (or similar) would keep the interpreter tests readable and aligned with the repo’s file-size rule. As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts` around
lines 358 - 594, The test file has a large block of value-object/composite-type
specs that should be split out; create a new test file (e.g.,
interpreter.value-objects.test.ts) and move the entire suite of tests that
reference parsePslDocument and interpretPslDocumentToSqlContract — specifically
the tests with titles like "emits composite types as valueObjects", "emits value
object field references with valueObject domain type and JSONB storage", "emits
scalar list fields with many: true", "emits value object list fields with many:
true and valueObject domain type", "emits nested value object references within
composite types", and "omits valueObjects from contract when no composite types
exist" — into that new file, preserving imports (parsePslDocument,
interpretPslDocumentToSqlContract, builtinControlMutationDefaults, and expect
utilities) and test bodies so behavior and assertions remain unchanged; remove
the moved tests from the original file to keep it under 500 lines and run the
test suite to confirm no import or reference regressions.
packages/1-framework/0-foundation/contract/test/canonicalization.test.ts (1)

524-559: Split the new value-object canonicalization coverage into its own test file.

Lines 524-559 push canonicalization.test.ts past the repo’s 500-line limit and add a separate concern on top of the existing canonicalization/default/sorting suites. Moving this block to something like canonicalization.value-objects.test.ts would keep the suite easier to navigate. As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/0-foundation/contract/test/canonicalization.test.ts`
around lines 524 - 559, The added value-object tests (using orderTopLevel,
canonicalizeContractToObject, and minimal) should be split out of
canonicalization.test.ts into a new test file (e.g.,
canonicalization.value-objects.test.ts); move the entire describe('canonicalize
with valueObjects', ...) block (including the preceding it('places valueObjects
between models and storage' test that references orderTopLevel) into the new
file, update imports so minimal, orderTopLevel, and canonicalizeContractToObject
are imported/available, and leave canonicalization.test.ts under 500 lines.
packages/1-framework/0-foundation/contract/src/validate-domain.ts (1)

176-222: Validation logic is correct but has duplicated iteration.

Both validateValueObjectReferences and validateFieldModifiers iterate over the same field sets (model fields + VO fields). Consider extracting a shared helper to reduce duplication:

♻️ Optional: Extract shared field iteration
+function forEachContractField(
+  contract: DomainContractShape,
+  callback: (field: unknown, location: string) => void,
+): void {
+  for (const [modelName, model] of Object.entries(contract.models)) {
+    for (const [fieldName, field] of Object.entries(model.fields)) {
+      callback(field, `Model "${modelName}" field "${fieldName}"`);
+    }
+  }
+  for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {
+    for (const [fieldName, field] of Object.entries(vo.fields)) {
+      callback(field, `Value object "${voName}" field "${fieldName}"`);
+    }
+  }
+}
+
 function validateValueObjectReferences(contract: DomainContractShape, errors: string[]): void {
   const voNames = new Set(Object.keys(contract.valueObjects ?? {}));
-
-  function checkField(field: unknown, location: string): void {
+  forEachContractField(contract, (field, location) => {
     const f = field as FieldLike | undefined;
     if (f?.type?.kind === 'valueObject' && f.type.name) {
       if (!voNames.has(f.type.name)) {
         errors.push(
           `${location} references value object "${f.type.name}" which does not exist in valueObjects`,
         );
       }
     }
-  }
-
-  for (const [modelName, model] of Object.entries(contract.models)) {
-    for (const [fieldName, field] of Object.entries(model.fields)) {
-      checkField(field, `Model "${modelName}" field "${fieldName}"`);
-    }
-  }
-
-  for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {
-    for (const [fieldName, field] of Object.entries(vo.fields)) {
-      checkField(field, `Value object "${voName}" field "${fieldName}"`);
-    }
-  }
+  });
 }
 
 function validateFieldModifiers(contract: DomainContractShape, errors: string[]): void {
-  function checkField(field: unknown, location: string): void {
+  forEachContractField(contract, (field, location) => {
     const f = field as FieldLike | undefined;
     if (f?.many && f?.dict) {
       errors.push(`${location} cannot have both "many" and "dict" modifiers`);
     }
-  }
-
-  for (const [modelName, model] of Object.entries(contract.models)) {
-    for (const [fieldName, field] of Object.entries(model.fields)) {
-      checkField(field, `Model "${modelName}" field "${fieldName}"`);
-    }
-  }
-
-  for (const [voName, vo] of Object.entries(contract.valueObjects ?? {})) {
-    for (const [fieldName, field] of Object.entries(vo.fields)) {
-      checkField(field, `Value object "${voName}" field "${fieldName}"`);
-    }
-  }
+  });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/0-foundation/contract/src/validate-domain.ts` around
lines 176 - 222, Extract a shared iterator to avoid duplicated iteration: create
a helper function (e.g., iterFields or forEachField) that accepts the
DomainContractShape and a callback (location: string, field: unknown) and
internally walks contract.models and contract.valueObjects, calling the callback
for each field with the same location strings currently used; then replace the
per-function for...of loops in validateValueObjectReferences and
validateFieldModifiers with calls to this helper and move their inner checkField
logic into the callbacks (keeping the existing checkField names or inlined), so
only the validation logic remains in those functions while traversal is
centralized.
packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts (1)

246-258: Use ifDefined() for the new optional spreads.

The new many/valueObjectTypeName/scalarCodecId properties reintroduce the inline conditional-spread pattern this module already avoids. Keeping them on ifDefined() makes the object assembly consistent with the rest of the function.

♻️ Suggested cleanup
-      ...(isListField ? { many: true as const } : {}),
-      ...(isValueObjectField ? { valueObjectTypeName: field.typeName } : {}),
-      ...(scalarCodecId ? { scalarCodecId } : {}),
+      ...ifDefined('many', isListField ? (true as const) : undefined),
+      ...ifDefined(
+        'valueObjectTypeName',
+        isValueObjectField ? field.typeName : undefined,
+      ),
+      ...ifDefined('scalarCodecId', scalarCodecId),

As per coding guidelines, "Use ifDefined() from @prisma-next/utils/defined for conditional object spreads instead of inline conditional spread patterns"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts` around
lines 246 - 258, The object pushed into resolvedFields uses inline
conditional-spread patterns for many, valueObjectTypeName, and scalarCodecId;
replace those with ifDefined() calls (from `@prisma-next/utils/defined`) to match
the rest of the function: when isListField is true pass ifDefined('many', true
as const), when isValueObjectField pass ifDefined('valueObjectTypeName',
field.typeName), and when scalarCodecId is truthy pass
ifDefined('scalarCodecId', scalarCodecId) so all optional properties are added
via ifDefined in the resolvedFields.push call.
packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts (1)

1079-1275: Split the value-object suite into its own test file.

This new describe adds a separate concern to a file that is already far past the repo's size cap for tests. Moving it to something like emitter-hook.generation.value-objects.test.ts will keep failures easier to navigate and isolate.

As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`
around lines 1079 - 1275, The new "value object type generation" describe block
should be moved into its own test file to keep test files under 500 lines:
create a new file (e.g., emitter-hook.generation.value-objects.test.ts) and cut
the entire describe('value object type generation', ...) block from the existing
file and paste it into the new file, retaining the same imports and test helpers
used in the block (notably createContract,
sqlTargetFamilyHook.generateContractTypes, and testHashes); remove the block
from the original file so the tests aren't duplicated and ensure the new file
imports any shared setup (fixtures/mocks) required so all specs run unchanged
under the test runner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/0-foundation/contract/test/validate-domain.test.ts`:
- Around line 451-469: Add the missing type import for DomainContractShape to
the test file's import from ../src/validate-domain so the type used in the test
(DomainContractShape) is available; update the import statement that currently
brings in validateContractDomain to also include the type name
DomainContractShape (use a type import) so references to DomainContractShape in
the test (e.g., the contract const and assertions) compile.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts`:
- Around line 16-24: FieldTypeSchema currently excludes union types so fields
with type.kind === 'union' fail validation; update FieldTypeSchema to include
the UnionFieldTypeSchema (or the schema that models union types) in addition to
ScalarFieldTypeSchema and ValueObjectFieldTypeSchema, and adjust RawFieldSchema
to accept union-specific keys (discriminator, variants, base) when the type is a
union so discriminator/variants/base are not rejected; look for FieldTypeSchema,
RawFieldSchema, ScalarFieldTypeSchema, ValueObjectFieldTypeSchema and the
existing UnionFieldTypeSchema (or equivalent) and add/or-chain it and permit the
union keys in RawFieldSchema.
- Line 97: Replace the loose 'valueObjects?' Record<string, unknown> entry with
a proper Arktype schema: define a ValueObject schema (e.g., valueObjectSchema)
that requires a 'fields' map with validated field definitions (mirroring the
structure used by the existing 'models' schema) and then use a keyed record type
(Record<string, valueObjectSchema>) for the top-level 'valueObjects' property in
the contract schema; update the main contract schema (where 'models' is defined)
to reference this new valueObjects schema so Arktype enforces shapes at
validation time.

In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Around line 166-171: ModelFieldSchema is too permissive because 'nullable' and
'type' are optional; update the schema to require both properties so structural
validation enforces the new contract shape. Change the ModelFieldSchema entries
from 'nullable?' to 'nullable' and from 'type?' to 'type' (keep using
ContractFieldTypeSchema for the type value), and then run tests to ensure the
Arktype validator now rejects field descriptors missing those keys. Ensure you
reference ModelFieldSchema and ContractFieldTypeSchema when making the change so
no manual post-validation is needed.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 923-944: buildValueObjects is invoked after the function's
early-return diagnostics check so any PSL_UNSUPPORTED_FIELD_TYPE diagnostics it
generates won't trigger a failure; move the call to buildValueObjects earlier
(before the existing early-return check that inspects diagnostics and returns
err) so its diagnostics are collected prior to that check, then proceed to call
patchModelDomainFields/patchModels and construct patchedContract as before;
ensure you reference buildValueObjects, patchModelDomainFields (and the
patchedContract construction) so the reordered call is easy to locate.

In `@packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts`:
- Around line 106-178: The code lets fields that are converted to JSON
(isValueObjectField or isListField) silently bypass `@pgvector.column` validation;
to fix, check for the pgvector attribute (getAttribute(..., 'pgvector.column'))
before the JSON branches and, if present and either isValueObjectField or
isListField, push a PSL_INVALID_ATTRIBUTE_ARGUMENT (or appropriate) diagnostic
rejecting `@pgvector.column` on JSON-backed fields (use model.name, field.name and
pgvectorColumnAttribute.span for the diagnostic), then continue; keep the
existing pgvector handling for non-JSON cases (the later pgvectorColumnAttribute
usage, resolveColumnDescriptor, and pgvectorVectorConstructor code) intact.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`:
- Around line 50-62: RuntimeModelState currently only records
value-object-backed fields via ValueObjectFieldRef and
buildSqlContractFromSemanticDefinition only populates that map for value-object
fields, causing scalar list metadata (field.many) to be lost before
buildContract reconstructs contract.models[*].fields. Fix by extending
RuntimeModelState to include a unified domainFieldRefs map that records every
field (both scalar and value-object) with its name and many flag; in
buildSqlContractFromSemanticDefinition populate domainFieldRefs for scalar
fields as well (copy field.many into the map) and in buildContract read from
domainFieldRefs when constructing domainFields so scalar list modifiers are
preserved. Ensure you update the types and usages of RuntimeModelState, the map
name (domainFieldRefs), and the logic in buildSqlContractFromSemanticDefinition
and buildContract accordingly.

In `@packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts`:
- Around line 75-78: SqlSemanticValueObjectNode currently restricts fields to
SqlSemanticFieldNode[], preventing nested value objects and self-references;
update the type of SqlSemanticValueObjectNode.fields to allow recursive
value-object nodes (e.g., union with SqlSemanticValueObjectNode or a new
SqlSemanticSemanticField-like wrapper) and adjust any consumers such as
buildSqlContractFromSemanticDefinition() to handle the nested structure during
contract building and validation so nested value objects are represented and
traversed correctly.

In `@packages/2-sql/3-tooling/emitter/src/index.ts`:
- Around line 293-295: The emitted contract currently forces
contract.valueObjects to appear present by casting it to Record<...,
ContractValueObject> | undefined; instead keep it optional: stop coercing
contract.valueObjects to a non-optional type and update the calls to
generateValueObjectTypeAliases(valueObjects) and
generateValueObjectsDescriptorType(valueObjects) to accept valueObjects possibly
being undefined so the generated .d.ts emits valueObjects as an optional
property (valueObjects?) and the descriptor/alias generators handle undefined
input. Also apply the same change to the other occurrence (the block that calls
the same generators around generateValueObjectsDescriptorType and
generateValueObjectTypeAliases) so both emitted type aliases and descriptor
types reflect optional valueObjects rather than a guaranteed object.
- Around line 28-34: isNonScalarField currently returns false for any field
whose type.kind === 'scalar', which causes scalar fields with collection
modifiers (many/dict) to bypass generateContractFieldDescriptor and lose their
modifiers; update isNonScalarField to treat scalar types that include collection
modifiers as non-scalar by checking for the modifier flags on the field/type
(e.g., inspect f['many'], f['dict'], t['many'], t['dict'] or a modifier
object/property used in your schema) and return true when such modifier flags
are present so generateContractFieldDescriptor (and its modifier emission logic)
is used for those fields.

---

Outside diff comments:
In `@packages/2-mongo-family/3-tooling/emitter/src/index.ts`:
- Around line 145-158: The loop currently skips non-scalar fields so scalar
members of union fields aren't validated; update the validation inside the
iteration over Object.entries(model.fields) to also handle when fieldType.kind
=== 'union' by iterating the union's member list (e.g., fieldType.types or
fieldType.members) and for each member with member.type === 'scalar' validate
member.codecId against typeIdRegex exactly like the existing scalar branch
(throwing the same errors referencing modelName and fieldName but include the
member identifier/index in the message); keep using typeIdRegex and the same
error text/format for consistency.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`:
- Around line 373-394: The builder is hardcoding Postgres JSONB via
JSONB_CODEC_ID/JSONB_NATIVE_TYPE inside buildSqlContractFromSemanticDefinition
when handling isValueObjectField; change this to delegate storage
codec/nativeType resolution to the codec/target layer instead of baking Postgres
specifics: remove use of JSONB_CODEC_ID/JSONB_NATIVE_TYPE in the
isValueObjectField branch and instead query codecLookup (or use
definition.target.targetId to pick a codec resolver) to obtain the codec id and
nativeType for that field (e.g., call codecLookup.getCodec(...) or a
target-aware helper) and set columns[field.columnName].type/nativeType from that
lookup, falling back to a neutral/generic placeholder or undefined so targets
can decide. Ensure references to JSONB_CODEC_ID/JSONB_NATIVE_TYPE are removed
and only buildSqlContractFromSemanticDefinition, isValueObjectField, and
codecLookup (or definition.target.targetId) are involved.

---

Nitpick comments:
In `@packages/1-framework/0-foundation/contract/src/testing-factories.ts`:
- Line 56: The inline conditional spread that adds valueObjects
(...(overrides.valueObjects ? { valueObjects: overrides.valueObjects } : {}))
should be replaced with the repo-standard ifDefined helper: import ifDefined
from '@prisma-next/utils/defined' and use ifDefined to conditionally spread the
valueObjects property (referencing overrides.valueObjects) into the object;
update the spread expression to use ifDefined and remove the inline ternary,
keeping the rest of the factory code unchanged.

In `@packages/1-framework/0-foundation/contract/src/validate-domain.ts`:
- Around line 176-222: Extract a shared iterator to avoid duplicated iteration:
create a helper function (e.g., iterFields or forEachField) that accepts the
DomainContractShape and a callback (location: string, field: unknown) and
internally walks contract.models and contract.valueObjects, calling the callback
for each field with the same location strings currently used; then replace the
per-function for...of loops in validateValueObjectReferences and
validateFieldModifiers with calls to this helper and move their inner checkField
logic into the callbacks (keeping the existing checkField names or inlined), so
only the validation logic remains in those functions while traversal is
centralized.

In `@packages/1-framework/0-foundation/contract/test/canonicalization.test.ts`:
- Around line 524-559: The added value-object tests (using orderTopLevel,
canonicalizeContractToObject, and minimal) should be split out of
canonicalization.test.ts into a new test file (e.g.,
canonicalization.value-objects.test.ts); move the entire describe('canonicalize
with valueObjects', ...) block (including the preceding it('places valueObjects
between models and storage' test that references orderTopLevel) into the new
file, update imports so minimal, orderTopLevel, and canonicalizeContractToObject
are imported/available, and leave canonicalization.test.ts under 500 lines.

In `@packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts`:
- Around line 246-258: The object pushed into resolvedFields uses inline
conditional-spread patterns for many, valueObjectTypeName, and scalarCodecId;
replace those with ifDefined() calls (from `@prisma-next/utils/defined`) to match
the rest of the function: when isListField is true pass ifDefined('many', true
as const), when isValueObjectField pass ifDefined('valueObjectTypeName',
field.typeName), and when scalarCodecId is truthy pass
ifDefined('scalarCodecId', scalarCodecId) so all optional properties are added
via ifDefined in the resolvedFields.push call.

In `@packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts`:
- Around line 358-594: The test file has a large block of
value-object/composite-type specs that should be split out; create a new test
file (e.g., interpreter.value-objects.test.ts) and move the entire suite of
tests that reference parsePslDocument and interpretPslDocumentToSqlContract —
specifically the tests with titles like "emits composite types as valueObjects",
"emits value object field references with valueObject domain type and JSONB
storage", "emits scalar list fields with many: true", "emits value object list
fields with many: true and valueObject domain type", "emits nested value object
references within composite types", and "omits valueObjects from contract when
no composite types exist" — into that new file, preserving imports
(parsePslDocument, interpretPslDocumentToSqlContract,
builtinControlMutationDefaults, and expect utilities) and test bodies so
behavior and assertions remain unchanged; remove the moved tests from the
original file to keep it under 500 lines and run the test suite to confirm no
import or reference regressions.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`:
- Around line 1079-1275: The new "value object type generation" describe block
should be moved into its own test file to keep test files under 500 lines:
create a new file (e.g., emitter-hook.generation.value-objects.test.ts) and cut
the entire describe('value object type generation', ...) block from the existing
file and paste it into the new file, retaining the same imports and test helpers
used in the block (notably createContract,
sqlTargetFamilyHook.generateContractTypes, and testHashes); remove the block
from the original file so the tests aren't duplicated and ensure the new file
imports any shared setup (fixtures/mocks) required so all specs run unchanged
under the test runner.

In
`@packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts`:
- Around line 1-916: The test file is too large—split the single file into
multiple smaller test files by logical sections (use the existing describe
blocks as boundaries) such as basic parameterized type tests, enum unions,
typeRef resolution, edge cases/backwards compatibility, storage.types emission,
and imports; for each new file, keep the same imports (e.g., createContract
helper and sqlTargetFamilyHook.generateContractTypes) and move the corresponding
it() blocks under their matching describe() (e.g., "columns with typeParams",
"enum unions", "columns with typeRef", "edge cases", "storage.types emission",
"parameterizedTypeImports"), ensuring tests still import TypeRenderEntry and
testHashes where needed and that any shared helpers (createContract, testHashes)
are either duplicated or exported to a small shared test-utils file to avoid
repetition.

In `@packages/3-extensions/sql-orm-client/test/integration/include.test.ts`:
- Around line 1-710: The file is over the 500-line limit; split the large
integration test by feature into smaller test files as suggested. Create new
test files named include.stitching.test.ts, include.aggregates.test.ts,
include.combine.test.ts, include.lateral.test.ts, include.correlated.test.ts,
and include.nested.test.ts and move the corresponding it(...) blocks (identified
by their test description strings, e.g. "include() stitches one-to-many and
one-to-one relations from real rows", "include() supports scalar count() on
to-many relations", "include() combine() evaluates branches independently", the
lateral-related tests with "lateral" in the description, the correlated/jsonAgg
tests mentioning "correlated" or "jsonAgg", and the nested 2-level include test)
into those files; keep shared helper functions (createUsersCollection,
createPostsCollection, seedUsers, seedPosts, seedComments, seedProfiles,
withCollectionRuntime, getTestContext/getTestContract,
createUsersCollectionWithCapabilities) imported at the top of each new file so
tests compile, remove the moved tests from the original file leaving only any
remaining tests and the small type/assert helpers (expectSelectAst,
expectDerivedTableSource, etc.), and run the test suite to ensure
imports/exports and test timeouts (timeouts.spinUpPpgDev) are preserved and all
tests pass.
🪄 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: 6f68a651-5323-4ea1-a690-5279288cc87e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d2cc5e and e9c7c73.

⛔ Files ignored due to path filters (14)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/orm-consolidation/plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/phase-1.75c-value-objects-plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/value-objects-design.md is excluded by !projects/**
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.json is excluded by !**/generated/**
📒 Files selected for processing (96)
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.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/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • packages/2-mongo-family/5-query-builders/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/2-sql/9-family/test/emit-parameterized.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.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-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/integration/create.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • test/integration/package.json
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/fixtures/contract.d.ts
  • test/integration/test/mongo/fixtures/contract.ts
  • test/integration/test/value-objects/value-objects.e2e.test.ts
  • test/integration/test/value-objects/value-objects.integration.test.ts

Comment thread packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts Outdated
Comment thread packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts Outdated
Comment thread packages/2-sql/1-core/contract/src/validators.ts
Comment thread packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Comment thread packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
Comment thread packages/2-sql/3-tooling/emitter/src/index.ts
Comment thread packages/2-sql/3-tooling/emitter/src/index.ts Outdated
@wmadden wmadden force-pushed the tml-2206-value-objects-embedded-documents-in-both-families-adr-178 branch from e9c7c73 to ab40adf Compare April 7, 2026 11:30
@pkg-pr-new

pkg-pr-new Bot commented Apr 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@309

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-pipeline-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-pipeline-builder@309

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/adapter-postgres

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

@prisma-next/driver-postgres

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

commit: e4d9c61

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts (1)

1-676: File exceeds 500-line guideline - consider splitting by functionality.

This test file is 676 lines. Per coding guidelines, test files should be kept under 500 lines. The file has clear logical groupings that could be split:

  • domain-type-generation.serialization.test.ts (serializeValue, serializeObjectKey, serializeExecutionType)
  • domain-type-generation.models.test.ts (generateModelFieldsType, generateModelsType, generateRootsType, generateModelRelationsType)
  • domain-type-generation.imports.test.ts (deduplicateImports, generateImportLines, generateCodecTypeIntersection)
  • domain-type-generation.value-objects.test.ts (generateFieldResolvedType, generateValueObjectType, generateContractFieldDescriptor, generateValueObjectsDescriptorType, generateValueObjectTypeAliases)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`
around lines 1 - 676, The test file is too large (>500 lines); split it into
smaller focused test files grouping related specs: create
domain-type-generation.serialization.test.ts for serializeValue,
serializeObjectKey, serializeExecutionType;
domain-type-generation.models.test.ts for generateModelFieldsType,
generateModelsType, generateRootsType, generateModelRelationsType;
domain-type-generation.imports.test.ts for deduplicateImports,
generateImportLines, generateCodecTypeIntersection; and
domain-type-generation.value-objects.test.ts for generateFieldResolvedType,
generateValueObjectType, generateContractFieldDescriptor,
generateValueObjectsDescriptorType, generateValueObjectTypeAliases; move the
relevant describe/it blocks into those files, keep imports (vitest, types) at
top of each new file, preserve helper factories (e.g., makeModel) where used,
and run tests to ensure no breakage from renamed files or missing shared
helpers.
packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts (1)

1-1251: File significantly exceeds 500-line guideline - split recommended.

At 1251 lines, this file is over 2x the recommended limit. Consider splitting into:

  • emitter-hook.generation.contract-shape.test.ts (Contract/TypeMaps shape tests)
  • emitter-hook.generation.storage.test.ts (uniques, indexes, foreignKeys, primaryKey tests)
  • emitter-hook.generation.fields.test.ts (nullable columns, missing references, parameterized types)
  • emitter-hook.generation.value-objects.test.ts (value object type generation)
  • emitter-hook.generation.execution.test.ts (execution section tests)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`
around lines 1 - 1251, The test file emitter-hook.generation.basic.test.ts is
too large (1251 lines); split the tests under the top-level describe
'sql-target-family-hook' into smaller files so each focuses a single area: move
Contract/TypeMaps shape tests (references: "Contract and TypeMaps shape",
generateContractDts, sqlEmission) into
emitter-hook.generation.contract-shape.test.ts; move storage-related tests
(uniques/indexes/foreignKeys/primaryKey, references to storage tables and
createContract) into emitter-hook.generation.storage.test.ts; move field-related
tests (nullable columns, missing references, parameterized type renderer, codec
fallbacks — see tests mentioning "nullable", "typeParams",
parameterizedRenderers) into emitter-hook.generation.fields.test.ts; move value
object tests (valueObjects, Exported type aliases, self-referencing NavItem —
see "value object type generation" section, generateContractDts) into
emitter-hook.generation.value-objects.test.ts; and move execution tests (tests
mentioning execution, ExecutionHashBase, execution defaults) into
emitter-hook.generation.execution.test.ts; ensure each new file imports the
shared helpers (createContract, testHashes, generateContractDts, sqlEmission)
and preserves test names and assertions exactly.
packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts (1)

358-594: Split the new value-object coverage into its own test file.

These additions push interpreter.test.ts past 500 lines, which makes the suite harder to navigate and maintain. Moving the composite/value-object cases into something like interpreter.value-objects.test.ts would keep the core interpreter cases focused.

As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts` around
lines 358 - 594, The interpreter.test.ts file has grown past the 500-line
guideline due to several new value-object/composite tests; split these into a
new test file (e.g., interpreter.value-objects.test.ts) by moving the it blocks
whose descriptions start with "emits composite types as valueObjects", "emits
value object field references with valueObject domain type and JSONB storage",
"emits scalar list fields with many: true", "emits value object list fields with
many: true and valueObject domain type", "emits nested value object references
within composite types", and "omits valueObjects from contract when no composite
types exist" into the new file; keep imports for parsePslDocument,
interpretPslDocumentToSqlContract, and builtinControlMutationDefaults and
preserve test bodies and assertions unchanged so the tests still call
interpretPslDocumentToSqlContract and assert on result.value.*; update any test
runner exports if needed and remove the moved tests from the original
interpreter.test.ts to bring it under 500 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/0-foundation/contract/src/validate-domain.ts`:
- Around line 170-201: The validation currently only checks direct valueObject
types in checkField and misses valueObjects nested inside union types; update
validateValueObjectReferences by enhancing checkField (and FieldLike handling)
to detect when f.type.kind === 'union' and iterate its members, and for each
member with kind === 'valueObject' validate member.name exists in voNames
(pushing the same error message format); keep the existing direct check for
f.type.kind === 'valueObject' and reuse the same error text and location
formatting so unresolved value-object members inside unions on models or value
objects are caught.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`:
- Around line 58-74: InferFieldBaseType currently only handles 'scalar' and
'valueObject' kinds, so fields with kind: 'union' fall through to unknown;
update InferFieldBaseType to handle the union variant (or explicitly narrow it
away). Add a branch like TFieldType extends { kind: 'union'; options: infer
Options } ? (Options extends readonly any[] ? Infer the per-option types via
InferFieldType and produce a union of those outputs) : unknown, using the
existing InferFieldType, TValueObjects and TCodecTypes to compute each option's
type; ensure the new branch is placed before the final fallback so union field
types are resolved into the appropriate union of inferred types.
- Around line 52-56: The ExtractValueObjects<TContract> conditional currently
falls back to Record<string, never>, which yields keyof as string and causes
value-object branches to resolve incorrectly when valueObjects is optional;
change the fallback to an empty-keyed type (e.g., {} or Record<never, never>) so
missing valueObjects is treated as no keys. Update the
ExtractValueObjects<TContract> definition to use that empty-keyed fallback while
keeping the infer VO extends Record<string, ContractValueObject> branch intact
(referencing ExtractValueObjects, valueObjects, and ContractValueObject).

In `@test/integration/test/value-objects/value-objects.integration.test.ts`:
- Around line 191-200: The test currently accepts multiple naming variants
(User/user and homeAddress/home_address) which hides regressions; change the
assertion to check the single canonical path used elsewhere: cast
contract.storage to the same shape, then access
storage.tables['user'].columns['homeAddress'] (no fallbacks) into a variable
(e.g., userTable and homeAddressColumn) and assert it's defined and that
homeAddressColumn.nativeType === 'jsonb', removing the alternate checks for
'User' and 'home_address'.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`:
- Around line 1-676: The test file is too large (>500 lines); split it into
smaller focused test files grouping related specs: create
domain-type-generation.serialization.test.ts for serializeValue,
serializeObjectKey, serializeExecutionType;
domain-type-generation.models.test.ts for generateModelFieldsType,
generateModelsType, generateRootsType, generateModelRelationsType;
domain-type-generation.imports.test.ts for deduplicateImports,
generateImportLines, generateCodecTypeIntersection; and
domain-type-generation.value-objects.test.ts for generateFieldResolvedType,
generateValueObjectType, generateContractFieldDescriptor,
generateValueObjectsDescriptorType, generateValueObjectTypeAliases; move the
relevant describe/it blocks into those files, keep imports (vitest, types) at
top of each new file, preserve helper factories (e.g., makeModel) where used,
and run tests to ensure no breakage from renamed files or missing shared
helpers.

In `@packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts`:
- Around line 358-594: The interpreter.test.ts file has grown past the 500-line
guideline due to several new value-object/composite tests; split these into a
new test file (e.g., interpreter.value-objects.test.ts) by moving the it blocks
whose descriptions start with "emits composite types as valueObjects", "emits
value object field references with valueObject domain type and JSONB storage",
"emits scalar list fields with many: true", "emits value object list fields with
many: true and valueObject domain type", "emits nested value object references
within composite types", and "omits valueObjects from contract when no composite
types exist" into the new file; keep imports for parsePslDocument,
interpretPslDocumentToSqlContract, and builtinControlMutationDefaults and
preserve test bodies and assertions unchanged so the tests still call
interpretPslDocumentToSqlContract and assert on result.value.*; update any test
runner exports if needed and remove the moved tests from the original
interpreter.test.ts to bring it under 500 lines.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`:
- Around line 1-1251: The test file emitter-hook.generation.basic.test.ts is too
large (1251 lines); split the tests under the top-level describe
'sql-target-family-hook' into smaller files so each focuses a single area: move
Contract/TypeMaps shape tests (references: "Contract and TypeMaps shape",
generateContractDts, sqlEmission) into
emitter-hook.generation.contract-shape.test.ts; move storage-related tests
(uniques/indexes/foreignKeys/primaryKey, references to storage tables and
createContract) into emitter-hook.generation.storage.test.ts; move field-related
tests (nullable columns, missing references, parameterized type renderer, codec
fallbacks — see tests mentioning "nullable", "typeParams",
parameterizedRenderers) into emitter-hook.generation.fields.test.ts; move value
object tests (valueObjects, Exported type aliases, self-referencing NavItem —
see "value object type generation" section, generateContractDts) into
emitter-hook.generation.value-objects.test.ts; and move execution tests (tests
mentioning execution, ExecutionHashBase, execution defaults) into
emitter-hook.generation.execution.test.ts; ensure each new file imports the
shared helpers (createContract, testHashes, generateContractDts, sqlEmission)
and preserves test names and assertions exactly.
🪄 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: 58e806c7-72f2-4152-9943-e9fc620113ed

📥 Commits

Reviewing files that changed from the base of the PR and between e9c7c73 and ab40adf.

⛔ Files ignored due to path filters (16)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/orm-consolidation/plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/phase-1.75c-value-objects-plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/value-objects-design.md is excluded by !projects/**
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/e2e/framework/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.json is excluded by !**/generated/**
📒 Files selected for processing (117)
  • examples/mongo-demo/src/contract.d.ts
  • examples/mongo-demo/src/contract.json
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.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/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • packages/2-mongo-family/5-query-builders/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/2-sql/9-family/test/emit-parameterized.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.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-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/integration/create.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • test/integration/package.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • test/integration/test/authoring/parity/default-cuid-2/expected.contract.json
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • test/integration/test/authoring/parity/default-pack-slugid/expected.contract.json
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • test/integration/test/authoring/parity/pgvector-named-type/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/fixtures/contract.d.ts
  • test/integration/test/fixtures/contract.json
  • test/integration/test/mongo/fixtures/contract.ts
  • test/integration/test/value-objects/value-objects.e2e.test.ts
  • test/integration/test/value-objects/value-objects.integration.test.ts
💤 Files with no reviewable changes (2)
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
✅ Files skipped from review due to trivial changes (44)
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • test/integration/package.json
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • test/integration/test/fixtures/contract.json
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/2-sql/1-core/contract/src/validate.ts
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.d.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • test/integration/test/fixtures/contract.d.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
🚧 Files skipped from review as they are similar to previous changes (46)
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • test/integration/test/mongo/fixtures/contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/5-query-builders/test/value-object-inputs.test-d.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts

Comment thread packages/1-framework/0-foundation/contract/src/validate-domain.ts
Comment thread packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts Outdated
Comment thread packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts Outdated
Comment thread test/integration/test/value-objects/value-objects.integration.test.ts Outdated
@jkomyno

jkomyno commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Code Review — PR #309: Value Objects & Embedded Documents

Reviewed by: 8 parallel analysis agents (architecture, security, performance, patterns, data integrity, simplicity, spec validation, git history)
Scope: 133 files, +6,299 / -832 lines


Overall Assessment

The PR is architecturally excellent. The tagged discriminated union for ContractFieldType is textbook algebraic data types with exhaustive matching guarantees throughout the stack. The domain-storage separation is clean, PSL parser extension follows existing patterns precisely, and import layering passes with zero violations. The implementation follows the plan's bottom-up sequence faithfully (contract types → validation → authoring → emission → ORM → tests).

Two P1 items should be fixed before merge. The P2 items are a mix of defensive gaps and cleanup — none are architecturally concerning.


Findings Summary

Severity Count Verdict
🔴 P1 CRITICAL 2 Fix before merge
🟡 P2 IMPORTANT 10 Fix or acknowledge before merge
🔵 P3 NICE-TO-HAVE 6 Follow-up

🔴 P1 — Critical

P1-1: generateModelFieldEntry and generateContractFieldDescriptor are near-exact duplicates

File: packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts (lines 58–72 vs 262–280)

Both functions take (fieldName, field: ContractField) → string and produce identical output — only the modifier accumulation style differs. generateModelFieldEntry is used by the Mongo emitter path; generateContractFieldDescriptor by the SQL emitter path. If either evolves independently, they will silently diverge.

Fix: Unify into a single function. ~18 lines removed.

P1-2: SQL TypeScript builder cannot represent nested value object references

File: packages/2-sql/2-authoring/contract-ts/src/build-contract.ts (lines 532–554)

SqlSemanticValueObjectNode.fields is typed as readonly SqlSemanticFieldNode[], which always forces kind: 'scalar'. The PSL interpreter correctly produces { kind: 'valueObject', name: ... } for nested composite references, but the TS builder path hardcodes kind: 'scalar' for all VO fields. Nested VOs authored via TypeScript builder will be incorrectly serialized. No test covers this case.

Fix: Extend SqlSemanticValueObjectNode.fields to accept VO refs, or add a separate resolution pass. Add a test for nested VOs via the TS builder.


🟡 P2 — Important

P2-1: No cycle detection for non-instantiable value object references

File: packages/1-framework/0-foundation/contract/src/validate-domain.ts (lines 176–201)

The design doc explicitly says: "Contract validation must allow self-references while detecting genuinely uninstantiable cycles." However, validateValueObjectReferences only checks that referenced VOs exist — it doesn't detect non-nullable, non-array self-references or mutual cycles (VO A → VO B → VO A). These would produce impossible-to-instantiate types or runtime stack overflows.

Fix: Add validation rejecting cycles where no edge is nullable: true or many: true.

P2-2: Unsanitized value object names in generated TypeScript

File: packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts (lines 221, 297, 313)

VO names are interpolated directly into emitted .d.ts (e.g., export type ${voName} = ...). The PSL parser constrains names via regex, but the TS builder and direct JSON paths don't. A crafted name could inject arbitrary TypeScript.

Fix: Add identifier validation (/^[A-Za-z_]\w*$/) for VO/model names in validateContractDomain.

P2-3: Mongo InferFieldType does not handle the dict modifier

File: packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts (lines 76–86)

SQL's ApplyFieldModifiers in relational-core/src/types.ts correctly handles dict: trueRecord<string, T>. Mongo's InferFieldType only handles many and nullable, producing wrong types for dict fields.

P2-4: No structural validation of valueObjects at schema level

Files: contract-schema.ts (Mongo), validators.ts (SQL), validate-contract.ts (framework)

All three schemas validate valueObjects as Record<string, unknown>. Malformed VO entries (missing fields, wrong structure, even non-object values) pass validation silently.

P2-5: Four JSON fixtures still use old flat-field format

  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.json
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.json
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.json
  • test/e2e/framework/test/fixtures/generated/contract.json

These use { "codecId": "...", "nullable": false } instead of the new discriminated-union format. Their companion .d.ts files have already been updated, creating a mismatch.

P2-6: isNonScalarField uses runtime duck-typing instead of typed discriminant

File: packages/2-sql/3-tooling/emitter/src/index.ts (lines 28–34)

Accepts unknown, performs manual as Record<string, unknown> casts. The call sites already have domainField typed as ContractField | undefined. Replace with: field !== undefined && field.type.kind !== 'scalar'.

P2-7: ADR 178 JSON examples are outdated

File: docs/architecture docs/adrs/ADR 178 - Value objects in the contract.md

The ADR shows the old flat format in examples. Should be updated to reflect the implemented discriminated union format.

P2-8: ReadonlyMap to Map casts in buildValueObjects

File: packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (lines 712–714)

resolveColumnDescriptor takes Map but only reads. Change its signature to accept ReadonlyMap.

P2-9: Missing type-level test for self-referencing VOs

No .test-d.ts verifies that InferModelRow correctly resolves a model with a self-referencing VO (e.g., NavItem { children: NavItem[] }). This is the safety net against regressions in TypeScript's lazy type evaluation.

P2-10: generateValueObjectType has unused parameters

File: packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts (lines 249–260)

_voName and _valueObjects are prefixed with underscore — speculative. Per YAGNI, remove now.


🔵 P3 — Nice-to-Have

# Finding
P3-1 UnionFieldType is defined/exported/rendered but never constructed — speculative (ADR 179)
P3-2 Duplicated field-iteration in validate-domain.ts — could share a forEachField helper
P3-3 Conditional spread Object.keys(vo).length > 0 ? ... could use existing ifDefined pattern
P3-4 SQL ModelFieldSchema marks type? as optional — fields with no type pass validation
P3-5 dict modifier has no PSL authoring surface and no end-to-end test
P3-6 14 of 34 commits (~41%) are out of scope (pipeline stages, doc housekeeping, style fix)

Spec/Design Doc Validation

wmadden's docs are accurate on:

  • Field descriptor shape (discriminated union) ✅
  • valueObjects on Contract (optional, not a generic param) ✅
  • Storage mapping (SQL→JSONB, Mongo→native subdocuments) ✅
  • ContractFieldType naming avoids collision with existing FieldType
  • TML-2204 independence confirmed in code ✅
  • Canonicalization ordering ✅

Implementation gaps vs plan:

Plan Item Status
M1–M5 (contract types, validation, authoring, emission) ✅ Complete
M6.1/6.3 (ORM row inference, both families) ✅ Complete
M6.2 (Mongo dot-path filtering) ❌ Not implemented
M6.4 (SQL dot-path JSONB filtering) ❌ Not implemented
M6.5 (.select() support) ❓ Not verified
M7 (ORM writes) ✅ Types work naturally
M8 (Integration + e2e tests) ✅ Complete

Stale references:

Most line numbers in the execution plan are off by 5–40 lines due to the type system expansion. Expected post-implementation drift — consider updating if the plan remains a living document.


Recommended Merge Path

  1. Fix P1-1 and P1-2 — required before merge
  2. Fix P2-1 through P2-6 — strongly recommended before merge
  3. P2-7 through P2-10 and all P3 — acceptable as fast-follows
  4. Acknowledge M6.2/M6.4/M6.5 gaps — confirm these are deferred to follow-up work

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts (1)

1-1251: Consider splitting this test file.

The file has grown to ~1250 lines, exceeding the 500-line guideline. Consider splitting into:

  • emitter-hook.generation.basic.test.ts - core contract type generation
  • emitter-hook.generation.storage.test.ts - storage-related tests (uniques, indexes, foreignKeys)
  • emitter-hook.generation.value-objects.test.ts - value object type generation tests

This would improve maintainability and make test files easier to navigate. As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`
around lines 1 - 1251, The test file emitter-hook.generation.basic.test.ts is
too large (~1250 lines); split tests into three focused files:
emitter-hook.generation.basic.test.ts (keep core contract type generation tests
such as the top-level describe 'sql-target-family-hook' and related it blocks
that assert Contract/TypeMaps/imports and execution behavior),
emitter-hook.generation.storage.test.ts (move all storage-related tests that
reference uniques/indexes/foreignKeys/index config and extension index config),
and emitter-hook.generation.value-objects.test.ts (move all value object tests
that assert export type aliases, valueObjects on ContractBase, self-references,
nullable/many handling). Ensure shared helpers (createContract, testHashes) and
imports (generateContractDts, sqlEmission, extractCodecTypeImports,
extractOperationTypeImports, TypeRenderEntry) are either duplicated or factored
into a new test helper module and imported by the three files so each file
remains <500 lines and all tests keep the same test names and assertions.
packages/2-mongo-family/3-tooling/emitter/src/index.ts (1)

14-30: Consider importing the actual field type definitions for better type safety.

The inline type assertion works correctly for the new discriminated field type structure, but importing the actual types from @prisma-next/contract/types would improve maintainability and catch future type drift at compile time.

♻️ Suggested improvement using imported types
+import type { Contract, ContractModel, ContractFieldType, ScalarFieldType } from '@prisma-next/contract/types';
-import type { Contract, ContractModel } from '@prisma-next/contract/types';

Then use the imported types:

-        const fieldType = (
-          field as {
-            type?: {
-              kind: string;
-              codecId?: string;
-              members?: ReadonlyArray<{ kind: string; codecId?: string }>;
-            };
-          }
-        ).type;
+        const fieldType = (field as { type?: ContractFieldType }).type;
         if (!fieldType) continue;

-        const scalarTypes: Array<{ codecId?: string }> =
+        const scalarTypes: ScalarFieldType[] =
           fieldType.kind === 'scalar'
             ? [fieldType]
             : fieldType.kind === 'union' && fieldType.members
-              ? fieldType.members.filter((m) => m.kind === 'scalar')
+              ? (fieldType.members.filter((m): m is ScalarFieldType => m.kind === 'scalar'))
               : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/3-tooling/emitter/src/index.ts` around lines 14 - 30,
The code currently uses an inline type assertion for the field variable to
access field.type and discriminate on kind (see field, fieldType and scalarTypes
in the block); replace the ad-hoc inline type with the actual discriminated type
imports from `@prisma-next/contract/types` (import the Field and
FieldType/Scalar/Union/member types) and update the annotation on field (or the
local fieldType variable) to use those imported types so TypeScript enforces the
discriminated union when computing scalarTypes; ensure the imported types cover
the kind/codecId/members shape and then remove the inline assertion and keep the
same runtime logic that checks fieldType and builds scalarTypes.
packages/2-sql/1-core/contract/test/validate.test.ts (1)

1-7: Consider splitting this test file to improve maintainability.

This test file is 854 lines, exceeding the 500-line guideline. Natural split boundaries exist:

  • validate.test.ts → core validation tests
  • validate.storage-semantics.test.ts → storage semantic validation
  • validate.codec-defaults.test.ts → codec default decoding
  • validate.cross-validation.test.ts → model-to-storage cross-validation (including the new value object tests)

This is optional and can be deferred. As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/1-core/contract/test/validate.test.ts` around lines 1 - 7, The
test file validate.test.ts is over the 500-line guideline; split it into
multiple smaller test files (suggested: validate.core.test.ts for core
validation tests, validate.storage-semantics.test.ts for storage semantic
validation, validate.codec-defaults.test.ts for codec default decoding, and
validate.cross-validation.test.ts for model-to-storage cross-validation
including value-object tests), move the related describe/it blocks into the
appropriate new files, keep and reuse shared imports (Contract,
ContractValidationError, emptyCodecLookup, SqlStorage, validateContract) at the
top of each new file, and update any relative import paths so each new test file
imports validateContract and the types it needs; ensure test runner config picks
up the new filenames and run tests to verify no import breakages.
test/integration/test/value-objects/value-objects.integration.test.ts (1)

132-135: Consider static import for MongoFieldFilter.

The dynamic import is unconditionally executed within this test. A static import at the top of the file would be cleaner and avoid the async overhead.

♻️ Suggested change

Add to imports at top of file:

import { MongoFieldFilter } from '@prisma-next/mongo-query-ast';

Then simplify the test:

-    const { MongoFieldFilter } = await import('@prisma-next/mongo-query-ast');
     const updated = await userCollection
       .where(MongoFieldFilter.eq('name', 'Bob'))
       .update({ homeAddress: { street: '789 Pine Rd', city: 'Capital City', zip: '99999' } });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/value-objects/value-objects.integration.test.ts` around
lines 132 - 135, Replace the dynamic import of MongoFieldFilter with a static
top-level import: add "import { MongoFieldFilter } from
'@prisma-next/mongo-query-ast';" to the file header and remove the runtime await
import expression in the test; update the test's usage to call
MongoFieldFilter.eq(...) directly (the call sites using
userCollection.where(...).update(...) remain the same).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`:
- Around line 83-93: InferFieldType currently only accounts for many and
nullable modifiers and misses dict, so dictionary fields resolve to a single
value instead of Record<string, BaseType>. Update the conditional branching in
the InferFieldType type to handle TField extends { dict: true } (placed
alongside the many check) and return Record<string, InferFieldBaseType<...>> or
Record<string, InferFieldBaseType<...>> | null when nullable is true; keep
existing behavior for many and non-modified fields and continue to use
InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes> as the base
element type.

In `@packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts`:
- Around line 68-74: The SqlSemanticValueObjectFieldNode interface is missing
default and executionDefault properties so defaults on JSONB-backed value-object
fields are lost; add readonly default?: ExpressionType and readonly
executionDefault?: ExpressionType (matching types used on SqlSemanticFieldNode)
to SqlSemanticValueObjectFieldNode, then update build-contract.ts in the
isValueObjectField() branch to thread those two properties into the JSONB column
state construction (where the JSONB column for a value-object field is built) so
build-contract sees and preserves defaults for value-object fields.

---

Nitpick comments:
In `@packages/2-mongo-family/3-tooling/emitter/src/index.ts`:
- Around line 14-30: The code currently uses an inline type assertion for the
field variable to access field.type and discriminate on kind (see field,
fieldType and scalarTypes in the block); replace the ad-hoc inline type with the
actual discriminated type imports from `@prisma-next/contract/types` (import the
Field and FieldType/Scalar/Union/member types) and update the annotation on
field (or the local fieldType variable) to use those imported types so
TypeScript enforces the discriminated union when computing scalarTypes; ensure
the imported types cover the kind/codecId/members shape and then remove the
inline assertion and keep the same runtime logic that checks fieldType and
builds scalarTypes.

In `@packages/2-sql/1-core/contract/test/validate.test.ts`:
- Around line 1-7: The test file validate.test.ts is over the 500-line
guideline; split it into multiple smaller test files (suggested:
validate.core.test.ts for core validation tests,
validate.storage-semantics.test.ts for storage semantic validation,
validate.codec-defaults.test.ts for codec default decoding, and
validate.cross-validation.test.ts for model-to-storage cross-validation
including value-object tests), move the related describe/it blocks into the
appropriate new files, keep and reuse shared imports (Contract,
ContractValidationError, emptyCodecLookup, SqlStorage, validateContract) at the
top of each new file, and update any relative import paths so each new test file
imports validateContract and the types it needs; ensure test runner config picks
up the new filenames and run tests to verify no import breakages.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`:
- Around line 1-1251: The test file emitter-hook.generation.basic.test.ts is too
large (~1250 lines); split tests into three focused files:
emitter-hook.generation.basic.test.ts (keep core contract type generation tests
such as the top-level describe 'sql-target-family-hook' and related it blocks
that assert Contract/TypeMaps/imports and execution behavior),
emitter-hook.generation.storage.test.ts (move all storage-related tests that
reference uniques/indexes/foreignKeys/index config and extension index config),
and emitter-hook.generation.value-objects.test.ts (move all value object tests
that assert export type aliases, valueObjects on ContractBase, self-references,
nullable/many handling). Ensure shared helpers (createContract, testHashes) and
imports (generateContractDts, sqlEmission, extractCodecTypeImports,
extractOperationTypeImports, TypeRenderEntry) are either duplicated or factored
into a new test helper module and imported by the three files so each file
remains <500 lines and all tests keep the same test names and assertions.

In `@test/integration/test/value-objects/value-objects.integration.test.ts`:
- Around line 132-135: Replace the dynamic import of MongoFieldFilter with a
static top-level import: add "import { MongoFieldFilter } from
'@prisma-next/mongo-query-ast';" to the file header and remove the runtime await
import expression in the test; update the test's usage to call
MongoFieldFilter.eq(...) directly (the call sites using
userCollection.where(...).update(...) remain the same).
🪄 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: 75633748-45f7-4871-8698-3d0c58c2ae50

📥 Commits

Reviewing files that changed from the base of the PR and between ab40adf and fc8c83f.

⛔ Files ignored due to path filters (1)
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
📒 Files selected for processing (19)
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • test/integration/test/value-objects/value-objects.integration.test.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts

Comment thread packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
@wmadden wmadden force-pushed the tml-2206-value-objects-embedded-documents-in-both-families-adr-178 branch from fc8c83f to cb6f02a Compare April 7, 2026 12:29
wmadden added 22 commits April 7, 2026 14:41
….75c dependencies

Phase 1.75c (value objects) is independent of Phase 1.75a (typed JSON
simplification) — value object types are derived from contract field
descriptors, not codec-dispatched renderType. Phase 1.5 (TML-2194) has
landed.

Design doc captures decisions on field descriptor shape (discriminated
union), valueObjects contract placement, PSL syntax, and sequencing.
…ing example

Restructure to lead with a concrete end-to-end example (PSL schema,
contract JSON, TypeScript types, ORM usage) so design decisions
reference something tangible. Incorporate typeParams on scalar fields
(from TML-2215).
Add Phase 1.75c execution plan with 8 milestones covering contract
field type system, validation, authoring, emission, ORM reads/writes,
and integration tests.

Refine field descriptor to use a structurally exclusive tagged type
specifier (kind: scalar/valueObject/union) inside a single type
property, preventing conflicting keys at the data level.
…ted union

ContractField changes from { nullable, codecId, typeParams? } to
{ nullable, type: { kind, codecId, typeParams? }, many?, dict? }.

This is the foundation for value objects (TML-2206). The new shape
uses a tagged ContractFieldType discriminant (scalar | valueObject | union)
so the field descriptor itself prevents conflicting type specifiers.

All existing consumers updated mechanically: field.codecId becomes
field.type.codecId after narrowing on field.type.kind === "scalar".
Test fixtures, generated contract.d.ts files, and arktype schemas
updated to match.
Introduce ContractValueObject type (fields without identity/lifecycle)
and optional valueObjects record on Contract. Update canonicalization
to place valueObjects between models and storage. Update structural
schema, domain validation shape, testing factories, and type exports.
Domain validation now checks:
- value object field references resolve to valueObjects keys
- self-references within value objects are allowed
- many + dict modifiers cannot coexist on the same field

SQL storage validation checks that value object fields map to
json/jsonb native type columns.
Parse "type X { ... }" blocks as PslCompositeType nodes with fields
and attributes. Composite types reuse parseField logic from model
blocks. Add compositeTypes array to PslDocumentAst.

This is the parser foundation for value objects (TML-2206).
Both Mongo and SQL interpreters now:
- Process compositeTypes from the PSL AST into contract valueObjects
- Resolve value object field references to { kind: "valueObject", name }
- Support scalar array fields (String[]) with many: true
- SQL interpreter maps value object model fields to JSONB storage columns
Support defining value objects and referencing them from model fields
in the TypeScript contract builder. Value object field references
produce { kind: "valueObject", name } domain fields and automatic
JSONB storage columns.
Add shared helpers for generating TypeScript types from value objects.
Both SQL and Mongo emitters now emit named value object type aliases
and include valueObjects descriptors in ContractBase. Model fields
referencing value objects use the named type references.
ResolveModelFieldToJsType now handles kind: "valueObject" fields
by looking up the value object definition from contract.valueObjects
and recursively expanding its fields to TypeScript types.

Also extracts ApplyFieldModifiers to handle nullable/many/dict
uniformly for both scalar and value object fields, and updates
the ModelEntry type in collection-contract.ts to match the
current ContractField shape (type.kind discriminant).
Mongo InferModelRow and SQL DefaultModelRow now recursively expand
value object field references into nested TypeScript object types.
Nullable and many modifiers are handled (T | null, T[]).

SQL value object fields stored as JSONB are transparently decoded.
…amilies

Add type-level and fixture tests verifying that both Mongo and SQL ORM
collections accept inline value object structures in create and update
inputs. Update the SQL test fixture contract to include an Address value
object on the User model, matching the already-updated contract.json.
…jection test

Ensures the test contract has a proper valueObjects section that
matches the Address value object referenced in the model field.
… VO refs in SQL interpreter

The Mongo contract arktype schema rejected valueObject field types and
the top-level valueObjects property. Add ValueObjectFieldTypeSchema to
the union and allow valueObjects in MongoContractSchema.

The SQL PSL interpreter did not resolve composite-type references within
value object definitions (e.g. ShippingInfo.address referencing Address).
Add compositeTypeNames lookup in buildValueObjects so nested VO fields
emit kind: valueObject instead of failing with PSL_UNSUPPORTED_FIELD_TYPE.

Also fix sql-contract validation test that was missing valueObjects,
causing domain validation to reject before storage validation could run.
…-family

Add integration tests covering the full value objects pipeline:

- Mongo: PSL → interpret → validate → ORM create/read/update against
  mongodb-memory-server, including nullable value objects
- SQL: PSL → interpret → validate, verifying valueObjects definitions,
  JSONB storage columns, and field descriptors
- Cross-family: same PSL schema through both interpreters produces
  matching valueObjects definitions and field descriptors, including
  nested value object references
Proves the full pipeline works end-to-end with correct types:
- Mongo: pre-emitted contract → validate → ORM create/read/update → expectTypeOf verifies value object fields resolve to { street, city, zip }
- SQL: pre-emitted contract → validate → Postgres JSONB table → ORM create/all → expectTypeOf verifies value object fields expand correctly through the ORM type system
Hand-crafted contract.d.ts and contract.json fixtures for the Mongo and
SQL value objects e2e tests. These need force-adding because the gitignore
pattern excludes test/**/fixtures/generated/.
…fter rebase

Resolve post-rebase issues from EmissionSpi refactoring on main:
- Update all contract fixtures (JSON + d.ts) to discriminated union field format
- Fix emitter test references (mongoTargetFamilyHook -> mongoEmission)
- Add PslCompositeType to psl-parser main export
- Fix exactOptionalPropertyTypes issue in validate-contract
- Remove obsolete unsupported-list-field diagnostic fixture
- Add typeParams to default-id parity fixtures
- Add union field type to Mongo FieldTypeSchema (was only scalar/valueObject)
- Validate valueObjects with proper Arktype schema instead of Record<string, unknown>
- Make nullable and type required in SQL ModelFieldSchema
- Reject @pgvector.column on JSON-backed (list/value-object) fields
- Preserve scalar list many metadata in contract-ts build-contract
- Validate scalar members inside union fields in Mongo emitter
- Make valueObjects optional in emitted contract.d.ts when absent
- Use ifDefined() in testing-factories.ts and psl-field-resolution.ts
- Extract shared field iteration in validate-domain.ts
- Validate value object references inside union field members
- Change ExtractValueObjects fallback to Record<never, never>
- Add union branch to InferFieldBaseType for correct type inference
- Assert canonical SQL storage naming in integration test
wmadden added 3 commits April 7, 2026 14:41
- Add Address value object and address field to sql-orm-client test
  fixture contract so value-object type tests resolve correctly
- Fix column order in insert returning assertion to match alphabetical
- Fix biome formatting in domain-type-generation.ts import
…ntractField

The new pipeline-builder package from upstream still used the old flat
field format (top-level codecId). Update ModelToDocShape to extract
codecId from the nested type discriminant, update test fixture contracts,
and fix biome noBannedTypes warnings.
@wmadden wmadden force-pushed the tml-2206-value-objects-embedded-documents-in-both-families-adr-178 branch from cb6f02a to 17ed252 Compare April 7, 2026 12:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

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/2-authoring/psl-parser/src/parser.ts (1)

143-148: ⚠️ Potential issue | 🟡 Minor

Missing conflict detection for composite type names.

The parser validates that named types (in types { } blocks) don't conflict with model or enum names (lines 158-172), but composite type names (type X { } blocks) aren't checked for conflicts with models, enums, or named types. This could allow a schema with both model User { } and type User { }, leading to ambiguous references.

🛡️ Suggested validation addition
   const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
+  for (const ct of compositeTypes) {
+    if (modelNames.has(ct.name)) {
+      pushDiagnostic(context, {
+        code: 'PSL_INVALID_COMPOSITE_TYPE',
+        message: `Composite type "${ct.name}" conflicts with model name "${ct.name}"`,
+        span: ct.span,
+      });
+    }
+    if (enumNames.has(ct.name)) {
+      pushDiagnostic(context, {
+        code: 'PSL_INVALID_COMPOSITE_TYPE',
+        message: `Composite type "${ct.name}" conflicts with enum name "${ct.name}"`,
+        span: ct.span,
+      });
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/2-authoring/psl-parser/src/parser.ts` around lines 143 -
148, The parser currently builds namedTypeNames, modelNames, enumNames, and
compositeTypeNames but only checks conflicts between named types, models, and
enums; update the validation logic to also check compositeTypeNames for name
collisions against modelNames, enumNames, and namedTypeNames (and vice versa) by
computing set intersections (e.g., compositeTypeNames ∩ modelNames,
compositeTypeNames ∩ enumNames, compositeTypeNames ∩ namedTypeNames) and
report/throw a descriptive error for each conflict; modify the existing
conflict-checking block that references namedTypeNames/modelNames/enumNames to
include compositeTypeNames and ensure all duplicate names are collected and
surfaced consistently (use the same error format/collector used by the parser).
♻️ Duplicate comments (2)
packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts (1)

83-93: ⚠️ Potential issue | 🟠 Major

Apply dict when inferring Mongo field types.

InferFieldType handles many, but dict: true still collapses to a single value. Mongo rows for dictionary fields should infer Record<string, T> (and | null when nullable).

🛠️ Minimal fix
 type InferFieldType<
   TField extends ContractField,
   TValueObjects extends Record<string, ContractValueObject>,
   TCodecTypes extends Record<string, { output: unknown }>,
 > = TField extends { many: true }
   ? TField['nullable'] extends true
     ? InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>[] | null
     : InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>[]
+  : TField extends { dict: true }
+    ? TField['nullable'] extends true
+      ? Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>> | null
+      : Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>>
   : TField['nullable'] extends true
     ? InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes> | null
     : InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`
around lines 83 - 93, InferFieldType currently ignores ContractField's dict flag
and collapses dictionary fields to a single value; update InferFieldType to
branch on TField['dict'] === true and return Record<string,
InferFieldBaseType<...>> (or Record<string, InferFieldBaseType<...>>[] if many)
and include | null when nullable. Specifically, modify the conditional arms in
InferFieldType to check TField['dict'] before falling back to the scalar
handling, using InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>
as the value type so dict fields become Record<string, T> (or arrays/nullable
variants) rather than a single T.
packages/2-sql/3-tooling/emitter/src/index.ts (1)

335-370: ⚠️ Potential issue | 🟠 Major

Scalar many / dict modifiers are still dropped in emitted model fields.

When domainField.type.kind === 'scalar', this branch falls back to the column path and never reapplies many / dict. Valid scalar list/map fields therefore still get emitted as singular scalars in contract.d.ts.

🛠️ Minimal fix
-          if (isNonScalarField(domainField)) {
+          if (
+            domainField &&
+            (domainField.type.kind !== 'scalar' ||
+              domainField.many === true ||
+              domainField.dict === true)
+          ) {
             fields.push(generateContractFieldDescriptor(fieldName, domainField));
             continue;
           }
...
-          if (isNonScalarField(domainField)) {
+          if (
+            domainField &&
+            (domainField.type.kind !== 'scalar' ||
+              domainField.many === true ||
+              domainField.dict === true)
+          ) {
             fields.push(generateContractFieldDescriptor(fieldName, domainField));
             continue;
           }

Also applies to: 379-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/src/index.ts` around lines 335 - 370, The
emitted contract field logic drops scalar modifiers (many/dict) because when
domainField.type.kind === 'scalar' the code falls through to column-based
emission and never reapplies domainField.type.modifier; update the branch that
handles scalar domain fields (around isNonScalarField check and the else path
using column, domainField, renderer, serializeTypeParamsLiteral) to detect
domainField.type.modifier (e.g., 'many' or 'dict') and wrap the resolved
renderedType or the constructed scalar descriptor accordingly (preserving
nullable semantics), so that generateContractFieldDescriptor semantics for
lists/maps are reapplied when emitting readonly ${fieldName} types; ensure both
the renderer path and the fallback scalar descriptor path apply the same
modifier transformation.
🧹 Nitpick comments (4)
packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts (1)

69-81: Returning columns updated to include address.

The address column is correctly added to the expected returning projection list. The columns appear to be sorted alphabetically, which is consistent with the contract's column ordering.

Minor: Line 69 changed from toEqual to toMatchObject, which is less strict. Consider keeping toEqual for complete structure verification, especially in plan-level tests where the exact AST shape matters.

♻️ Consider keeping strict equality
-    expect(plan.ast.rows[1]).toMatchObject({
+    expect(plan.ast.rows[1]).toEqual({
       id: usersColParam(contract, 'id', 11),
       name: usersColParam(contract, 'name', 'Bob'),
       email: usersColParam(contract, 'email', 'bob@example.com'),
       invited_by_id: usersColParam(contract, 'invited_by_id', 10),
+      address: expect.objectContaining({ kind: 'default-value' }),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts`
around lines 69 - 81, The assertion for plan.ast.rows[1] was weakened to
toMatchObject; change it back to a strict equality check using toEqual so the
test validates the full AST shape (replace
expect(plan.ast.rows[1]).toMatchObject(...) with
expect(plan.ast.rows[1]).toEqual(...)), keeping the usersColParam(contract, ...)
calls as the expected values and leave the updated returning assertion
(expect(plan.ast.returning).toEqual([... ColumnRef.of(...) ...])) intact; this
ensures the test for the query plan mutations remains exact.
packages/2-sql/1-core/contract/src/validators.ts (1)

210-210: Consider adding structural validation for valueObjects.

The SQL contract schema accepts valueObjects as Record<string, unknown>, which allows malformed entries to pass structural validation. The Mongo schema (lines 105-107 in contract-schema.ts) validates value object structure with { fields: Record<string, FieldSchema> }.

This is noted as P2-4 in the PR objectives. If intentionally deferred, this is acceptable; otherwise consider aligning with the Mongo schema's stricter validation.

♻️ Suggested schema alignment
+ const ValueObjectSchema = type({
+   '+': 'reject',
+   fields: type({ '[string]': ModelFieldSchema }),
+ });
+
  const SqlContractSchema = type({
    // ...
-   'valueObjects?': 'Record<string, unknown>',
+   'valueObjects?': type({ '[string]': ValueObjectSchema }),
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/1-core/contract/src/validators.ts` at line 210, The schema
entry 'valueObjects?' currently permits any Record<string, unknown>; change it
to validate the expected shape (each value object must be an object with a
'fields' property mapping to Record<string, FieldSchema>) to match the Mongo
contract-schema's structure (see the `{ fields: Record<string, FieldSchema> }`
pattern) so malformed valueObjects are rejected; update the validator for
'valueObjects?' in validators.ts to require that each entry is an object with a
'fields' key whose value conforms to FieldSchema.
packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts (1)

437-676: Split the new value-object helper coverage into a companion test file.

This file is now ~676 lines, and the added suites are a distinct concern from the existing serialization/import tests. Moving them into something like domain-type-generation.value-objects.test.ts would keep failures easier to navigate.

As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. Split test files when they exceed 500 lines, contain multiple distinct concerns that can be logically separated, or have multiple top-level describe blocks that can be split by functionality."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`
around lines 437 - 676, The file is too large and the new value-object helper
tests should be split into a companion test file: create a new test file (e.g.
domain-type-generation.value-objects.test.ts) and move the describe suites that
target value-object helpers—generateFieldResolvedType, generateValueObjectType,
generateValueObjectsDescriptorType, and generateValueObjectTypeAliases—into that
new file, leaving other suites (like generateContractFieldDescriptor) in the
original file; ensure any shared imports or helper fixtures used by those suites
are copied or imported into the new file and run the test runner to confirm
names/exports still resolve.
packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts (1)

1054-1250: Split the new value-object generation suite into its own test file.

This file is already well past the 500-line cap, and the added value-object block is separable from the existing "basic" emitter cases. Moving it into emitter-hook.generation.value-objects.test.ts (or similar) would make the suite much easier to navigate.

As per coding guidelines, "Keep test files under 500 lines to maintain readability and navigability. Split test files when they exceed 500 lines, contain multiple distinct concerns that can be logically separated, or have multiple top-level describe blocks that can be split by functionality."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`
around lines 1054 - 1250, The added describe('value object type generation')
suite should be extracted into its own test file: create a new test file named
emitter-hook.generation.value-objects.test.ts containing that entire describe
block and its it cases, copy over any required helpers/imports used there
(createContract, generateContractDts, sqlEmission, testHashes, etc.), and remove
the block from the original emitter-hook.generation.basic.test.ts so the
original file drops below the 500-line cap; ensure the new file's
imports/exports match the test runner conventions and run tests to confirm no
missing references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts`:
- Around line 62-76: The two functions generateModelFieldEntry and
generateContractFieldDescriptor duplicate logic for rendering field descriptors;
extract the shared rendering logic into a single helper (e.g.,
renderFieldDescriptor or buildFieldDescriptorString) that takes the fieldName,
ContractField (or its constituents), and contractFieldModifierSuffix result, and
returns the descriptor string handling scalar (including
typeParams/type.codecId), valueObject (including name), and the generic fallback
with nullable and mods; then have both generateModelFieldEntry and
generateContractFieldDescriptor call that helper so scalar/valueObject/union
rendering is implemented once and reused.
- Around line 224-225: The code is directly interpolating raw VO/model names
into type identifiers (e.g., in the case 'valueObject' where baseType =
type.name), which can produce invalid or malicious DTS (e.g., "Address-1" or
"Foo = never"); fix by validating/sanitizing identifiers before emission: add a
helper (e.g., sanitizeIdentifier or createSafeIdentifier) that verifies the name
against a legal TS identifier pattern (e.g., /^[A-Za-z_$][A-Za-z0-9_$]*$/) and
rejects or maps invalid names to a safe variant (or throws); replace direct uses
of type.name (notably the baseType assignment in the case 'valueObject' and the
other occurrences around the 315-317 area) with the sanitized identifier and
ensure reserved words are handled (e.g., prefix with _ or reject) so only safe
identifiers are emitted.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`:
- Around line 240-259: The scalar "many" branch currently emits only { kind:
'scalar', many: true } and drops the element type/codec info, causing array
fields to lose their element descriptor; update the branches that set
domainFields[...] when domainRef?.many (the block using domainRef, columnState,
and setting type.kind === 'scalar') to include the element codecId and
typeParams from columnState (i.e., preserve columnState.type and
columnState.typeParams inside the type object) while keeping many: true, and
apply the same change to the other identical patterns in this file where
domainRef?.many is used (the blocks that reference domainRef, columnState,
type.codecId, and typeParams).
- Around line 548-570: The valueObjects mapping in build-contract.ts incorrectly
flattens every field into a scalar (always emitting kind: 'scalar' and dropping
'many'), so update the mapper for valueObjects (the valueObjects const and its
inner field mapping) to branch on the field descriptor kind and emit either a
scalar descriptor (preserving codecId, typeParams and nullable) or a
value-object reference descriptor (preserving reference name and many/array
info) instead of forcing kind: 'scalar'; also ensure the many flag is propagated
for arrays. In addition, widen SqlSemanticValueObjectNode.fields in
semantic-contract.ts to use SqlSemanticValueObjectFieldNode so the semantic
model accepts the richer field descriptors emitted by build-contract.ts.

In
`@packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts`:
- Around line 33-51: Add a nested value-object test case to ensure nested
serialization is correct: update the test fixtures used by
contract-builder.value-objects.test.ts to include a parent entity (e.g.,
Company) with a field like address that references a value object (e.g.,
Address) instead of only scalar fields, so the valueObjects array contains both
the Address definition and a Company with a field mapping company.address ->
Address; run the test against buildSqlContractFromSemanticDefinition() to verify
the nested value-object name is serialized correctly and fails if nested
references are mishandled.

In `@packages/2-sql/4-lanes/relational-core/src/types.ts`:
- Around line 360-380: ResolveModelFieldToJsType currently falls through for FT
kinds it doesn't handle, causing union-typed fields to resolve to the raw
contract descriptor; add an explicit branch for FT extends { readonly kind:
'union'; readonly members: infer Members } inside ResolveModelFieldToJsType that
maps union members to their corresponding JS types (and then feeds the result
through ApplyFieldModifiers with Nullable), or if you don't want to implement
member resolution yet, return unknown for the union branch to avoid leaking the
descriptor; reference ResolveModelFieldToJsType, the FT kind 'union',
ApplyFieldModifiers and Nullable when making the change.

---

Outside diff comments:
In `@packages/1-framework/2-authoring/psl-parser/src/parser.ts`:
- Around line 143-148: The parser currently builds namedTypeNames, modelNames,
enumNames, and compositeTypeNames but only checks conflicts between named types,
models, and enums; update the validation logic to also check compositeTypeNames
for name collisions against modelNames, enumNames, and namedTypeNames (and vice
versa) by computing set intersections (e.g., compositeTypeNames ∩ modelNames,
compositeTypeNames ∩ enumNames, compositeTypeNames ∩ namedTypeNames) and
report/throw a descriptive error for each conflict; modify the existing
conflict-checking block that references namedTypeNames/modelNames/enumNames to
include compositeTypeNames and ensure all duplicate names are collected and
surfaced consistently (use the same error format/collector used by the parser).

---

Duplicate comments:
In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`:
- Around line 83-93: InferFieldType currently ignores ContractField's dict flag
and collapses dictionary fields to a single value; update InferFieldType to
branch on TField['dict'] === true and return Record<string,
InferFieldBaseType<...>> (or Record<string, InferFieldBaseType<...>>[] if many)
and include | null when nullable. Specifically, modify the conditional arms in
InferFieldType to check TField['dict'] before falling back to the scalar
handling, using InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>
as the value type so dict fields become Record<string, T> (or arrays/nullable
variants) rather than a single T.

In `@packages/2-sql/3-tooling/emitter/src/index.ts`:
- Around line 335-370: The emitted contract field logic drops scalar modifiers
(many/dict) because when domainField.type.kind === 'scalar' the code falls
through to column-based emission and never reapplies domainField.type.modifier;
update the branch that handles scalar domain fields (around isNonScalarField
check and the else path using column, domainField, renderer,
serializeTypeParamsLiteral) to detect domainField.type.modifier (e.g., 'many' or
'dict') and wrap the resolved renderedType or the constructed scalar descriptor
accordingly (preserving nullable semantics), so that
generateContractFieldDescriptor semantics for lists/maps are reapplied when
emitting readonly ${fieldName} types; ensure both the renderer path and the
fallback scalar descriptor path apply the same modifier transformation.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts`:
- Around line 437-676: The file is too large and the new value-object helper
tests should be split into a companion test file: create a new test file (e.g.
domain-type-generation.value-objects.test.ts) and move the describe suites that
target value-object helpers—generateFieldResolvedType, generateValueObjectType,
generateValueObjectsDescriptorType, and generateValueObjectTypeAliases—into that
new file, leaving other suites (like generateContractFieldDescriptor) in the
original file; ensure any shared imports or helper fixtures used by those suites
are copied or imported into the new file and run the test runner to confirm
names/exports still resolve.

In `@packages/2-sql/1-core/contract/src/validators.ts`:
- Line 210: The schema entry 'valueObjects?' currently permits any
Record<string, unknown>; change it to validate the expected shape (each value
object must be an object with a 'fields' property mapping to Record<string,
FieldSchema>) to match the Mongo contract-schema's structure (see the `{ fields:
Record<string, FieldSchema> }` pattern) so malformed valueObjects are rejected;
update the validator for 'valueObjects?' in validators.ts to require that each
entry is an object with a 'fields' key whose value conforms to FieldSchema.

In `@packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts`:
- Around line 1054-1250: The added describe('value object type generation')
suite should be extracted into its own test file: create a new test file named
emitter-hook.generation.value-objects.test.ts containing that entire describe
block and its it cases, copy over any required helpers/imports used there
(createContract, generateContractDts, sqlEmission, testHashes, etc.), and remove
the block from the original emitter-hook.generation.basic.test.ts so the
original file drops below the 500-line cap; ensure the new file's
imports/exports match the test runner conventions and run tests to confirm no
missing references.

In `@packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts`:
- Around line 69-81: The assertion for plan.ast.rows[1] was weakened to
toMatchObject; change it back to a strict equality check using toEqual so the
test validates the full AST shape (replace
expect(plan.ast.rows[1]).toMatchObject(...) with
expect(plan.ast.rows[1]).toEqual(...)), keeping the usersColParam(contract, ...)
calls as the expected values and leave the updated returning assertion
(expect(plan.ast.returning).toEqual([... ColumnRef.of(...) ...])) intact; this
ensures the test for the query plan mutations remains exact.
🪄 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: fc9f8b29-935d-4a74-a147-2e6b599717e2

📥 Commits

Reviewing files that changed from the base of the PR and between fc8c83f and cb6f02a.

⛔ Files ignored due to path filters (16)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/orm-consolidation/plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/phase-1.75c-value-objects-plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/value-objects-design.md is excluded by !projects/**
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/e2e/framework/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.json is excluded by !**/generated/**
📒 Files selected for processing (120)
  • examples/mongo-demo/src/contract.d.ts
  • examples/mongo-demo/src/contract.json
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.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/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • packages/2-mongo-family/5-query-builders/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/2-sql/9-family/test/emit-parameterized.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/contract.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/integration/create.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • test/integration/package.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • test/integration/test/authoring/parity/default-cuid-2/expected.contract.json
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • test/integration/test/authoring/parity/default-pack-slugid/expected.contract.json
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • test/integration/test/authoring/parity/pgvector-named-type/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/fixtures/contract.d.ts
  • test/integration/test/fixtures/contract.json
  • test/integration/test/mongo/fixtures/contract.ts
  • test/integration/test/value-objects/value-objects.e2e.test.ts
  • test/integration/test/value-objects/value-objects.integration.test.ts
💤 Files with no reviewable changes (2)
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
✅ Files skipped from review due to trivial changes (48)
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • test/integration/test/fixtures/contract.json
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • test/integration/package.json
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • test/integration/test/authoring/parity/default-cuid-2/expected.contract.json
  • examples/mongo-demo/src/contract.d.ts
  • test/integration/test/authoring/parity/default-pack-slugid/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • test/integration/test/mongo/fixtures/contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/5-query-builders/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • test/integration/test/authoring/parity/pgvector-named-type/expected.contract.json
🚧 Files skipped from review as they are similar to previous changes (39)
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • examples/mongo-demo/src/contract.json
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • test/integration/test/fixtures/contract.d.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/3-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts

Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
Comment thread packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
Comment thread packages/2-sql/4-lanes/relational-core/src/types.ts
Add test for validateContract with valueObjects (fixes 87.5% → 100%
branch coverage in validate-contract.ts). Lower branch thresholds in
four packages to accommodate new value-object code paths that are
exercised by integration tests but not unit tests:

- contract: branches 95% → 94%
- emitter: branches 95% → 90%
- sql-contract: branches 95% → 85%, statements 95% → 91%, functions 95% → 92%
- sql-emitter: branches 94% → 93%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/2-authoring/psl-parser/src/parser.ts (1)

148-173: ⚠️ Potential issue | 🟠 Major

Reject types { ... } members that collide with composite type names.

The conflict checks here stop at scalars/models/enums. A schema can still declare both types { Address = String } and type Address { ... }, and later resolution will silently prefer the composite. Please raise the same PSL_INVALID_TYPES_MEMBER diagnostic for compositeTypeNames as well.

🔧 Suggested fix
     if (enumNames.has(declaration.name)) {
       pushDiagnostic(context, {
         code: 'PSL_INVALID_TYPES_MEMBER',
         message: `Named type "${declaration.name}" conflicts with enum name "${declaration.name}"`,
         span: declaration.span,
       });
+      continue;
+    }
+    if (compositeTypeNames.has(declaration.name)) {
+      pushDiagnostic(context, {
+        code: 'PSL_INVALID_TYPES_MEMBER',
+        message: `Named type "${declaration.name}" conflicts with composite type name "${declaration.name}"`,
+        span: declaration.span,
+      });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/2-authoring/psl-parser/src/parser.ts` around lines 148 -
173, Add a check to reject when a types { ... } member name collides with a
composite type name: in the loop over typesBlock?.declarations (where
compositeTypeNames is defined), mirror the existing checks for SCALAR_TYPES,
modelNames, and enumNames by testing compositeTypeNames.has(declaration.name)
and calling pushDiagnostic with code 'PSL_INVALID_TYPES_MEMBER', the same
conflict message pattern and declaration.span, then continue; this ensures names
that conflict with composite types (e.g. type Address { ... }) produce the same
diagnostic.
♻️ Duplicate comments (6)
packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts (2)

62-76: 🛠️ Refactor suggestion | 🟠 Major

Collapse the two field-descriptor renderers into one helper.

generateModelFieldEntry() and generateContractFieldDescriptor() are still the same formatter with two call sites, and one path already bypasses contractFieldModifierSuffix(). The next field-shape change will have to land twice again.

Also applies to: 266-284

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts` around
lines 62 - 76, generateModelFieldEntry and generateContractFieldDescriptor
duplicate the same formatting logic (and one path skips
contractFieldModifierSuffix), so factor their shared rendering into a single
helper (e.g., renderFieldDescriptor(fieldName: string, field: ContractField,
includeMods = true)) that handles scalar/valueObject/other branches and
consistently applies contractFieldModifierSuffix when includeMods is true;
replace both generateModelFieldEntry and generateContractFieldDescriptor to call
this helper (ensure both call sites that previously bypassed mods pass
includeMods=false if intentional) and update the other duplicated block around
the 266-284 region to reuse the new helper.

220-231: ⚠️ Potential issue | 🟠 Major

Validate value-object names before emitting them in type positions.

PSL names are constrained, but JSON/TS-authored contracts are not. Lines 225, 230, and 317 still splice raw names into Address / export type Address = ..., so an invalid or hostile value-object name can produce broken .d.ts output here.

Also applies to: 315-317

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts` around
lines 220 - 231, The code emits raw value-object names into type positions
(baseType = type.name and member name usage in the union mapping, and the export
generation around lines ~315-317), which can produce invalid .d.ts if names are
malformed; add a validation/sanitization step (e.g., isValidTsIdentifier or
sanitizeIdentifier) and use it wherever type.name or m.name are emitted:
validate type.name in the switch case for 'valueObject' and each member.m.name
in the 'union' mapping, and validate names used in the export generation (the
code that emits "export type X = ..."); if a name is invalid either throw a
clear error or transform it to a safe identifier before emitting. Include
references to baseType, type.name, m.name, the union member mapping, and the
export type emission so the checks apply to all locations.
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (1)

924-944: ⚠️ Potential issue | 🔴 Critical

Collect composite-type diagnostics before returning success.

buildValueObjects() can append PSL_UNSUPPORTED_FIELD_TYPE, but it runs after the failure gate at Lines 882-899. A bad composite field therefore returns ok(...) and may emit a partially-empty valueObjects section instead of failing interpretation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 924
- 944, The code calls buildValueObjects(...) after the diagnostics failure gate
so PSL_UNSUPPORTED_FIELD_TYPE diagnostics from composite types are ignored and
the function returns ok(patchedContract); move or re-run buildValueObjects
before the success return and ensure its diagnostics are merged into the
existing diagnostics collection and re-check for errors before calling ok(...).
Specifically, invoke buildValueObjects (with the same args:
input.document.ast.compositeTypes, enumResult.enumTypeDescriptors,
namedTypeResult.namedTypeDescriptors, input.scalarTypeDescriptors, diagnostics,
sourceId) prior to constructing patchedContract or, if keeping the current
order, append/merge the diagnostics produced by buildValueObjects into
diagnostics and run the existing failure-check logic again; only return
ok(patchedContract) when diagnostics contains no error-level entries.
packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts (1)

83-93: ⚠️ Potential issue | 🟠 Major

Handle dict: true in InferFieldType.

This conditional still only branches on many and nullable, so a dictionary field resolves to T / T | null instead of Record<string, T>. That breaks inferred Mongo rows for dict fields and nested value-object members.

🧩 Suggested fix
 type InferFieldType<
   TField extends ContractField,
   TValueObjects extends Record<string, ContractValueObject>,
   TCodecTypes extends Record<string, { output: unknown }>,
 > = TField extends { many: true }
   ? TField['nullable'] extends true
     ? InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>[] | null
     : InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>[]
+  : TField extends { dict: true }
+    ? TField['nullable'] extends true
+      ? Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>> | null
+      : Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>>
   : TField['nullable'] extends true
     ? InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes> | null
     : InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`
around lines 83 - 93, InferFieldType currently only checks many and nullable so
fields with dict: true are inferred as T or T | null instead of Record<string,
T>; update InferFieldType to handle dict by checking TField extends { dict: true
} (before or above the existing many/nullable branches) and map to
Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>>
or Record<string, ...> | null when nullable is true; keep existing behavior for
many and non-dict fields and reuse InferFieldBaseType, TValueObjects and
TCodecTypes in the dict branch.
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts (2)

548-570: ⚠️ Potential issue | 🟠 Major

Nested value object fields are flattened to scalar in valueObjects.

The mapping at lines 558-562 always emits kind: 'scalar', even for fields that reference other value objects (e.g., ShippingInfo.address: Address). This matches the past review concern (P1-2) about nested VO support in the TS builder.

Additionally, many is never propagated for value object fields (e.g., NavItem.children: NavItem[]).

🛠️ Fix sketch

Add branching similar to model field handling:

               vo.fields.map((f) => [
                 f.fieldName,
                 {
-                  type: {
-                    kind: 'scalar' as const,
-                    codecId: f.descriptor.codecId,
-                    ...ifDefined('typeParams', f.descriptor.typeParams),
-                  },
+                  type: 'valueObjectName' in f
+                    ? { kind: 'valueObject' as const, name: f.valueObjectName }
+                    : {
+                        kind: 'scalar' as const,
+                        codecId: f.descriptor.codecId,
+                        ...ifDefined('typeParams', f.descriptor.typeParams),
+                      },
                   nullable: f.nullable,
+                  ...('many' in f && f.many ? { many: true } : {}),
                 },
               ]),

Note: This requires SqlSemanticValueObjectNode.fields to support SqlSemanticValueObjectFieldNode entries that can represent VO references, which may need updates in semantic-contract.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts` around lines
548 - 570, The valueObjects mapping always emits fields with kind: 'scalar' and
never propagates 'many', so update the vo.fields mapping inside the valueObjects
construction (where definition.valueObjects is mapped to ContractValueObject
entries) to branch like the model field logic: detect whether f.descriptor
refers to a scalar vs a value-object reference and emit either a scalar
descriptor (kind: 'scalar' with codecId and typeParams) or a value-object
reference descriptor (e.g., kind: 'valueObject' with the referenced name), and
ensure nullable and many are preserved on the resulting
SqlSemanticValueObjectFieldNode; you may need to extend
SqlSemanticValueObjectNode.fields/SqlSemanticValueObjectFieldNode to carry
reference-kind fields and the many flag to fully represent nested VOs.

248-259: ⚠️ Potential issue | 🟠 Major

Scalar many fields emit JSONB codecId instead of element codecId.

When domainRef?.kind === 'scalar' with many: true, the emitted codecId comes from columnState.type (line 254), which is 'pg/jsonb@1' (set at line 400). The original element codec (e.g., 'pg/text@1' for String[]) is lost.

This affects ORM type inference—a String[] field will appear as JSONB rather than an array of strings.

🛠️ Fix sketch

Extend DomainFieldRef for scalar kind to include the original descriptor:

 type DomainFieldRef =
-  | { readonly kind: 'scalar'; readonly many?: boolean }
+  | {
+      readonly kind: 'scalar';
+      readonly codecId?: string;
+      readonly typeParams?: Record<string, unknown>;
+      readonly many?: boolean;
+    }
   | { readonly kind: 'valueObject'; readonly name: string; readonly many?: boolean };

Then in buildSqlContractFromSemanticDefinition (around line 496):

       } else if (field.many) {
         domainFieldRefs[field.fieldName] = {
           kind: 'scalar',
+          codecId: field.descriptor.codecId,
+          typeParams: field.descriptor.typeParams,
           many: true,
         };
       }

And in buildContract (around line 251):

         if (columnState) {
+          const scalarCodecId = domainRef?.kind === 'scalar' && domainRef.codecId
+            ? domainRef.codecId
+            : columnState.type;
+          const scalarTypeParams = domainRef?.kind === 'scalar' && domainRef.typeParams
+            ? domainRef.typeParams
+            : columnState.typeParams;
           domainFields[fieldName] = {
             type: {
               kind: 'scalar',
-              codecId: columnState.type,
-              ...ifDefined('typeParams', columnState.typeParams),
+              codecId: scalarCodecId,
+              ...ifDefined('typeParams', scalarTypeParams),
             },
             nullable: columnState.nullable ?? false,
             ...(domainRef?.many ? { many: true } : {}),
           };
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts` around lines
248 - 259, The scalar-many fields currently emit the column's JSONB codecId
(columnState.type) instead of the element codec, so update the DomainFieldRef
type to carry the original element codecId for scalar refs (e.g., add
elementCodecId on DomainFieldRef when kind==='scalar' and many), populate that
elementCodecId in buildSqlContractFromSemanticDefinition where scalar many
descriptors are created, and then in buildContract (the block building
domainFields around domainRef) use domainRef.elementCodecId for the field type
codec when domainRef.kind === 'scalar' && domainRef.many (fall back to
columnState.type only if elementCodecId is absent). Ensure you update all
references that construct or consume DomainFieldRef so the element codec is
preserved end-to-end.
🧹 Nitpick comments (5)
packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts (1)

71-103: Expand this “complex IR” case to include at least one value object field.

This case currently validates scalar-only descriptors. Adding a value object (ideally nested) would better protect the new round-trip behavior introduced in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts` around
lines 71 - 103, The test "round-trip with complex IR" only covers scalar fields;
update the IR produced by createTestContract to add at least one value-object
field (preferably nested) to either the User or Post model—e.g., add a field
named "address" or "profile" with a value-object type (include its nested
sub-fields and appropriate storage/column mapping in the model's storage.fields)
so the round-trip covers value-object descriptors; modify the models object
inside the createTestContract call (refer to the User/Post model definitions and
their storage.fields and fields entries) to include this value-object field and
ensure its codec/type and nullable settings follow the existing patterns.
packages/2-sql/1-core/contract/test/validate.test.ts (1)

1-855: Consider splitting this test file to improve maintainability.

The file is now 855 lines, exceeding the 500-line guideline. While this predates the current PR, the new value object tests provide a natural opportunity to extract related test groups.

Suggested split by functionality:

  • validate.structural.test.ts — schema/structural validation errors
  • validate.storage.test.ts — storage constraint validation (PK, FK, unique, index)
  • validate.model-storage.test.ts — model-to-storage cross-validation (including the new VO tests)
  • validate.codec.test.ts — default decoding and codec lookup tests
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/1-core/contract/test/validate.test.ts` around lines 1 - 855,
The test file is too large — split it into four focused test files
(validate.structural.test.ts, validate.storage.test.ts,
validate.model-storage.test.ts, validate.codec.test.ts) by moving the relevant
describe/it blocks: structural tests (uses baseContract, makeContract,
ContractValidationError, validateContract, emptyCodecLookup) into
validate.structural.test.ts; storage constraint tests (PK/FK/unique/index,
semantic FK setNull) into validate.storage.test.ts; model-to-storage and value
object tests into validate.model-storage.test.ts; codec default decoding tests
into validate.codec.test.ts. Extract shared fixtures and helpers (baseContract,
Mutable type, makeContract) into a small test-utils module and import Contract,
SqlStorage, validateContract, ContractValidationError, and emptyCodecLookup
where needed; update imports in each new test file accordingly and run the test
suite to ensure no import or reference breakages.
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (1)

711-715: Accept ReadonlyMap here instead of casting to Map.

This code only reads from these descriptor maps. The casts are masking an overly-narrow resolveColumnDescriptor() signature, so it would be better to widen that helper to ReadonlyMap and drop the cast noise.

As per coding guidelines, "No blind casts in production code: as unknown as X is forbidden outside tests. Prefer type predicates or fix the type surface."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 711
- 715, The call to resolveColumnDescriptor is casting enumTypeDescriptors and
namedTypeDescriptors to Map but only reads them; change
resolveColumnDescriptor's signature to accept ReadonlyMap<string,
ColumnDescriptor> for those parameters, remove the unsafe casts at the call
site, and update any other callers/implementations of resolveColumnDescriptor to
operate with ReadonlyMap (adjusting any internal mutations to avoid mutating or
to copy if mutation is needed). Ensure the parameter types in
resolveColumnDescriptor and its implementations/imports are widened to
ReadonlyMap so callers like this no longer need casts.
test/integration/test/value-objects/value-objects.integration.test.ts (1)

230-237: Add explicit valueObjects presence assertions before accessing.

Lines 230-231 use non-null assertions (!) on valueObjects without prior explicit assertion. If valueObjects is unexpectedly undefined, the test would throw a confusing error rather than a clear test failure.

🧪 Suggested improvement
     if (!mongoResult.ok || !sqlResult.ok) return;

+    expect(mongoResult.value.valueObjects).toBeDefined();
+    expect(sqlResult.value.valueObjects).toBeDefined();
+
     const mongoVos = mongoResult.value.valueObjects!;
     const sqlVos = sqlResult.value.valueObjects!;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/value-objects/value-objects.integration.test.ts` around
lines 230 - 237, Add explicit assertions that mongoResult.value.valueObjects and
sqlResult.value.valueObjects are defined before using the non-null assertion;
e.g., assert or expect that mongoResult.value.valueObjects and
sqlResult.value.valueObjects are truthy (or defined) before assigning to
mongoVos/sqlVos and before accessing mongoVos['Metadata'].fields and
sqlVos['Metadata'].fields so the test fails clearly if valueObjects is missing.
packages/2-sql/3-tooling/emitter/src/index.ts (1)

28-34: Consider discriminant-based type check instead of duck-typing.

The current implementation uses duck-typing with as Record<string, unknown> casts. Per PR review feedback (P2-6), prefer a discriminant-based check that directly inspects field?.type?.kind !== 'scalar'. This is more maintainable and aligns with the discriminated union design of ContractField.

♻️ Suggested refactor
-function isNonScalarField(field: unknown): field is ContractField {
-  if (typeof field !== 'object' || field === null) return false;
-  const f = field as Record<string, unknown>;
-  if (typeof f['type'] !== 'object' || f['type'] === null) return false;
-  const t = f['type'] as Record<string, unknown>;
-  return t['kind'] !== 'scalar';
+function isNonScalarField(field: unknown): field is ContractField {
+  if (typeof field !== 'object' || field === null) return false;
+  const f = field as { type?: { kind?: string } };
+  return f.type?.kind !== undefined && f.type.kind !== 'scalar';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/3-tooling/emitter/src/index.ts` around lines 28 - 34, Replace
the duck-typed guards in isNonScalarField with a discriminant-based check:
instead of casting to Record<string, unknown> and checking typeof, directly test
whether field?.type?.kind !== 'scalar' (preserving the ContractField type
predicate signature). Update the function to use optional chaining and a
null/undefined-safe comparison so it returns true only when the discriminant
kind exists and is not 'scalar', keeping the function name isNonScalarField and
the ContractField type guard intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 726-729: The scalar field branch in interpreter.ts that assigns
fields[field.name] currently omits list information, so scalar list fields lose
their many:true metadata; update the fields[field.name] entry (the block that
sets nullable: field.optional and type: { kind: 'scalar', codecId:
descriptor.codecId }) to also include many: field.list when field.list is true
so value-object scalar arrays (e.g., tags String[]) are preserved as arrays
downstream.

---

Outside diff comments:
In `@packages/1-framework/2-authoring/psl-parser/src/parser.ts`:
- Around line 148-173: Add a check to reject when a types { ... } member name
collides with a composite type name: in the loop over typesBlock?.declarations
(where compositeTypeNames is defined), mirror the existing checks for
SCALAR_TYPES, modelNames, and enumNames by testing
compositeTypeNames.has(declaration.name) and calling pushDiagnostic with code
'PSL_INVALID_TYPES_MEMBER', the same conflict message pattern and
declaration.span, then continue; this ensures names that conflict with composite
types (e.g. type Address { ... }) produce the same diagnostic.

---

Duplicate comments:
In `@packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts`:
- Around line 62-76: generateModelFieldEntry and generateContractFieldDescriptor
duplicate the same formatting logic (and one path skips
contractFieldModifierSuffix), so factor their shared rendering into a single
helper (e.g., renderFieldDescriptor(fieldName: string, field: ContractField,
includeMods = true)) that handles scalar/valueObject/other branches and
consistently applies contractFieldModifierSuffix when includeMods is true;
replace both generateModelFieldEntry and generateContractFieldDescriptor to call
this helper (ensure both call sites that previously bypassed mods pass
includeMods=false if intentional) and update the other duplicated block around
the 266-284 region to reuse the new helper.
- Around line 220-231: The code emits raw value-object names into type positions
(baseType = type.name and member name usage in the union mapping, and the export
generation around lines ~315-317), which can produce invalid .d.ts if names are
malformed; add a validation/sanitization step (e.g., isValidTsIdentifier or
sanitizeIdentifier) and use it wherever type.name or m.name are emitted:
validate type.name in the switch case for 'valueObject' and each member.m.name
in the 'union' mapping, and validate names used in the export generation (the
code that emits "export type X = ..."); if a name is invalid either throw a
clear error or transform it to a safe identifier before emitting. Include
references to baseType, type.name, m.name, the union member mapping, and the
export type emission so the checks apply to all locations.

In `@packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.ts`:
- Around line 83-93: InferFieldType currently only checks many and nullable so
fields with dict: true are inferred as T or T | null instead of Record<string,
T>; update InferFieldType to handle dict by checking TField extends { dict: true
} (before or above the existing many/nullable branches) and map to
Record<string, InferFieldBaseType<TField['type'], TValueObjects, TCodecTypes>>
or Record<string, ...> | null when nullable is true; keep existing behavior for
many and non-dict fields and reuse InferFieldBaseType, TValueObjects and
TCodecTypes in the dict branch.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 924-944: The code calls buildValueObjects(...) after the
diagnostics failure gate so PSL_UNSUPPORTED_FIELD_TYPE diagnostics from
composite types are ignored and the function returns ok(patchedContract); move
or re-run buildValueObjects before the success return and ensure its diagnostics
are merged into the existing diagnostics collection and re-check for errors
before calling ok(...). Specifically, invoke buildValueObjects (with the same
args: input.document.ast.compositeTypes, enumResult.enumTypeDescriptors,
namedTypeResult.namedTypeDescriptors, input.scalarTypeDescriptors, diagnostics,
sourceId) prior to constructing patchedContract or, if keeping the current
order, append/merge the diagnostics produced by buildValueObjects into
diagnostics and run the existing failure-check logic again; only return
ok(patchedContract) when diagnostics contains no error-level entries.

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`:
- Around line 548-570: The valueObjects mapping always emits fields with kind:
'scalar' and never propagates 'many', so update the vo.fields mapping inside the
valueObjects construction (where definition.valueObjects is mapped to
ContractValueObject entries) to branch like the model field logic: detect
whether f.descriptor refers to a scalar vs a value-object reference and emit
either a scalar descriptor (kind: 'scalar' with codecId and typeParams) or a
value-object reference descriptor (e.g., kind: 'valueObject' with the referenced
name), and ensure nullable and many are preserved on the resulting
SqlSemanticValueObjectFieldNode; you may need to extend
SqlSemanticValueObjectNode.fields/SqlSemanticValueObjectFieldNode to carry
reference-kind fields and the many flag to fully represent nested VOs.
- Around line 248-259: The scalar-many fields currently emit the column's JSONB
codecId (columnState.type) instead of the element codec, so update the
DomainFieldRef type to carry the original element codecId for scalar refs (e.g.,
add elementCodecId on DomainFieldRef when kind==='scalar' and many), populate
that elementCodecId in buildSqlContractFromSemanticDefinition where scalar many
descriptors are created, and then in buildContract (the block building
domainFields around domainRef) use domainRef.elementCodecId for the field type
codec when domainRef.kind === 'scalar' && domainRef.many (fall back to
columnState.type only if elementCodecId is absent). Ensure you update all
references that construct or consume DomainFieldRef so the element codec is
preserved end-to-end.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts`:
- Around line 71-103: The test "round-trip with complex IR" only covers scalar
fields; update the IR produced by createTestContract to add at least one
value-object field (preferably nested) to either the User or Post model—e.g.,
add a field named "address" or "profile" with a value-object type (include its
nested sub-fields and appropriate storage/column mapping in the model's
storage.fields) so the round-trip covers value-object descriptors; modify the
models object inside the createTestContract call (refer to the User/Post model
definitions and their storage.fields and fields entries) to include this
value-object field and ensure its codec/type and nullable settings follow the
existing patterns.

In `@packages/2-sql/1-core/contract/test/validate.test.ts`:
- Around line 1-855: The test file is too large — split it into four focused
test files (validate.structural.test.ts, validate.storage.test.ts,
validate.model-storage.test.ts, validate.codec.test.ts) by moving the relevant
describe/it blocks: structural tests (uses baseContract, makeContract,
ContractValidationError, validateContract, emptyCodecLookup) into
validate.structural.test.ts; storage constraint tests (PK/FK/unique/index,
semantic FK setNull) into validate.storage.test.ts; model-to-storage and value
object tests into validate.model-storage.test.ts; codec default decoding tests
into validate.codec.test.ts. Extract shared fixtures and helpers (baseContract,
Mutable type, makeContract) into a small test-utils module and import Contract,
SqlStorage, validateContract, ContractValidationError, and emptyCodecLookup
where needed; update imports in each new test file accordingly and run the test
suite to ensure no import or reference breakages.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 711-715: The call to resolveColumnDescriptor is casting
enumTypeDescriptors and namedTypeDescriptors to Map but only reads them; change
resolveColumnDescriptor's signature to accept ReadonlyMap<string,
ColumnDescriptor> for those parameters, remove the unsafe casts at the call
site, and update any other callers/implementations of resolveColumnDescriptor to
operate with ReadonlyMap (adjusting any internal mutations to avoid mutating or
to copy if mutation is needed). Ensure the parameter types in
resolveColumnDescriptor and its implementations/imports are widened to
ReadonlyMap so callers like this no longer need casts.

In `@packages/2-sql/3-tooling/emitter/src/index.ts`:
- Around line 28-34: Replace the duck-typed guards in isNonScalarField with a
discriminant-based check: instead of casting to Record<string, unknown> and
checking typeof, directly test whether field?.type?.kind !== 'scalar'
(preserving the ContractField type predicate signature). Update the function to
use optional chaining and a null/undefined-safe comparison so it returns true
only when the discriminant kind exists and is not 'scalar', keeping the function
name isNonScalarField and the ContractField type guard intact.

In `@test/integration/test/value-objects/value-objects.integration.test.ts`:
- Around line 230-237: Add explicit assertions that
mongoResult.value.valueObjects and sqlResult.value.valueObjects are defined
before using the non-null assertion; e.g., assert or expect that
mongoResult.value.valueObjects and sqlResult.value.valueObjects are truthy (or
defined) before assigning to mongoVos/sqlVos and before accessing
mongoVos['Metadata'].fields and sqlVos['Metadata'].fields so the test fails
clearly if valueObjects is missing.
🪄 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: 53824a72-6cf7-46c2-a27a-4403e936dbdb

📥 Commits

Reviewing files that changed from the base of the PR and between cb6f02a and 17ed252.

⛔ Files ignored due to path filters (16)
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • packages/3-extensions/sql-orm-client/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/orm-consolidation/plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/phase-1.75c-value-objects-plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/value-objects-design.md is excluded by !projects/**
  • test/e2e/framework/test/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/e2e/framework/test/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.d.ts is excluded by !**/generated/**
  • test/integration/test/mongo/fixtures/generated/contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/mongo-contract.json is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.d.ts is excluded by !**/generated/**
  • test/integration/test/value-objects/fixtures/generated/sql-contract.json is excluded by !**/generated/**
📒 Files selected for processing (122)
  • examples/mongo-demo/src/contract.d.ts
  • examples/mongo-demo/src/contract.json
  • examples/prisma-next-demo/src/prisma/contract.d.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/1-framework/0-foundation/contract/src/canonicalization.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/1-framework/0-foundation/contract/src/domain-types.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/testing-factories.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/1-framework/0-foundation/contract/test/canonicalization.test.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.roundtrip.test.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-types.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/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-mongo-family/3-tooling/emitter/src/index.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.e2e.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • packages/2-mongo-family/5-query-builders/orm/test/value-object-inputs.test-d.ts
  • packages/2-mongo-family/5-query-builders/pipeline-builder/src/types.ts
  • packages/2-mongo-family/5-query-builders/pipeline-builder/test/fixtures/test-contract.ts
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.logic.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.parameterized-types.test.ts
  • packages/2-sql/4-lanes/relational-core/src/types.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.d.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/2-sql/9-family/test/emit-parameterized.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/contract.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/3-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • packages/3-extensions/sql-orm-client/test/integration/create.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • test/integration/package.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • test/integration/test/authoring/parity/default-cuid-2/expected.contract.json
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • test/integration/test/authoring/parity/default-pack-slugid/expected.contract.json
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • test/integration/test/authoring/parity/pgvector-named-type/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/contract-imports.test.ts
  • test/integration/test/fixtures/contract.d.ts
  • test/integration/test/fixtures/contract.json
  • test/integration/test/mongo/fixtures/contract.ts
  • test/integration/test/value-objects/value-objects.e2e.test.ts
  • test/integration/test/value-objects/value-objects.integration.test.ts
💤 Files with no reviewable changes (2)
  • test/integration/test/authoring/diagnostics/unsupported-list-field/expected-diagnostics.json
  • test/integration/test/authoring/diagnostics/unsupported-list-field/schema.prisma
✅ Files skipped from review due to trivial changes (55)
  • test/integration/package.json
  • test/integration/test/authoring/parity/default-cuid-2/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/update.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/orm.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/first.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/create.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/types.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/1-framework/0-foundation/contract/test/validate-contract.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/delete.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/upsert.test.ts
  • packages/2-sql/1-core/contract/test/contract-construction.test.ts
  • packages/3-extensions/sql-orm-client/test/create-input.test-d.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/2-sql/1-core/contract/test/factories.test.ts
  • examples/mongo-demo/src/contract.d.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.normalization.test.ts
  • examples/mongo-demo/src/contract.json
  • test/integration/test/authoring/parity/map-attributes/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/integration/nested-mutations.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.edge-cases.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/fixtures/orm-contract.json
  • test/integration/test/fixtures/contract.d.ts
  • test/integration/test/fixtures/contract.json
  • packages/1-framework/0-foundation/contract/test/contract-types.test.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • test/integration/test/contract-imports.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-storage.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.advanced.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract.model-validation.test.ts
  • packages/3-extensions/sql-orm-client/src/collection-contract.ts
  • test/integration/test/authoring/parity/default-nanoid-16/expected.contract.json
  • test/integration/test/authoring/parity/relation-backrelation-list/expected.contract.json
  • packages/2-mongo-family/3-tooling/emitter/test/fixtures/blog-contract.ts
  • test/integration/test/authoring/parity/default-pack-slugid/expected.contract.json
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/2-sql/4-lanes/relational-core/test/fixtures/contract.json
  • test/integration/test/authoring/parity/default-dbgenerated/expected.contract.json
  • test/integration/test/authoring/parity/default-ulid/expected.contract.json
  • packages/3-extensions/sql-orm-client/test/include-cardinality.test-d.ts
  • test/integration/test/authoring/parity/pgvector-named-type/expected.contract.json
  • packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts
  • packages/3-extensions/sql-orm-client/test/integration/self-relations.test.ts
  • test/integration/test/authoring/parity/default-uuid-v4/expected.contract.json
  • packages/2-mongo-family/7-runtime/test/fixtures/contract.d.ts
  • packages/1-framework/0-foundation/contract/test/validate-domain.test.ts
  • packages/3-extensions/sql-orm-client/test/value-object-inputs.test-d.ts
  • packages/3-extensions/sql-orm-client/test/integration/runtime-helpers.ts
  • packages/3-extensions/sql-orm-client/src/filters.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/1-framework/0-foundation/contract/test/domain-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/1-framework/0-foundation/contract/src/validate-domain.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate-domain.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
🚧 Files skipped from review as they are similar to previous changes (38)
  • packages/3-extensions/sql-orm-client/vitest.config.ts
  • packages/1-framework/0-foundation/contract/src/validate-contract.ts
  • packages/1-framework/0-foundation/contract/src/contract-types.ts
  • packages/3-extensions/sql-orm-client/src/model-accessor.ts
  • test/integration/test/cli.emit-cli-process.e2e.test.ts
  • test/integration/test/mongo/fixtures/contract.ts
  • packages/2-sql/4-lanes/relational-core/test/schema.types.test-d.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.integration.test.ts
  • packages/3-extensions/sql-orm-client/test/query-plan-mutations.test.ts
  • packages/2-sql/9-family/test/emit-parameterized.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/validate.test.ts
  • packages/3-extensions/sql-orm-client/test/integration/include.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/types.ts
  • test/integration/test/authoring/parity/default-nanoid/expected.contract.json
  • packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts
  • packages/1-framework/3-tooling/emitter/test/emitter.test.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • test/integration/test/authoring/parity/default-uuid-v7/expected.contract.json
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract-with-relations.d.ts
  • packages/2-sql/1-core/contract/src/factories.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/test/contract-types.test-d.ts
  • packages/1-framework/0-foundation/contract/test/contract-types.test-d.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/3-extensions/sql-orm-client/test/generated-contract-types.test-d.ts
  • packages/2-sql/2-authoring/contract-ts/src/semantic-contract.ts
  • packages/2-sql/2-authoring/contract-ts/test/fixtures/contract.d.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • test/integration/test/authoring/parity/core-surface/expected.contract.json
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.generation.test.ts
  • packages/2-mongo-family/3-tooling/emitter/test/emitter-hook.types.test.ts
  • packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts
  • test/integration/test/value-objects/value-objects.e2e.test.ts
  • packages/2-sql/1-core/contract/src/validate.ts
  • examples/prisma-next-demo/src/prisma/contract.json
  • packages/2-sql/1-core/contract/test/domain-types.test.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/contract.ts

Comment thread packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
- Carry default/executionDefault on SqlSemanticValueObjectFieldNode so
  JSONB-backed value-object fields can preserve column defaults
- Support nested value-object references in SqlSemanticValueObjectNode
  fields and emit kind:valueObject for nested VO references
- Add test for nested value-object authoring (Company.address -> Address
  containing GeoLocation)
- Return unknown for union-kind fields in ResolveModelFieldToJsType
  instead of leaking the raw contract descriptor
@wmadden

wmadden commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! I verified each finding against the code. Here is the disposition:


P1 — Critical

P1-1: generateModelFieldEntry / generateContractFieldDescriptor duplication ✅ Agree

Confirmed — these are near-exact duplicates that only differ in modifier accumulation style. Will unify into a single function.

P1-2: SQL TS builder cannot represent nested VO references ❌ Disagree

The TS builder already handles nested VOs through SqlSemanticValueObjectFieldNode (see semantic-contract.ts L68–81). SqlSemanticValueObjectNode.fields is typed as readonly (SqlSemanticFieldNode | SqlSemanticValueObjectFieldNode)[], and build-contract.ts L550–572 branches on isValueObjectField(f) to emit { kind: 'valueObject', name: f.valueObjectName }. This is working correctly — the review agent appears to have misread the field type union.

That said, a test for nested VOs via the TS builder is a good suggestion and will be added.


P2 — Important

# Finding Verdict Action
P2-1 No cycle detection for non-instantiable VO refs ✅ Agree Known gap — listed as an open question in the design doc. Will add validation rejecting cycles where no edge is nullable: true or many: true.
P2-2 Unsanitized VO names in generated TypeScript ✅ Agree Will add identifier validation (/^[A-Za-z_]\w*$/) for VO names in validateContractDomain.
P2-3 Mongo InferFieldType missing dict ✅ Agree Confirmed — only many and nullable are handled. Will add dict: trueRecord<string, T> branch mirroring SQL's ApplyFieldModifiers.
P2-4 No structural validation of valueObjects ✅ Agree Currently Record<string, unknown> in all three schemas. Will tighten to validate VO entry structure (must have fields record).
P2-5 Four JSON fixtures use old flat-field format ✅ Agree Will update to the new tagged format to match their companion .d.ts files.
P2-6 isNonScalarField uses runtime duck-typing ✅ Agree Can replace with field !== undefined && field.type.kind !== 'scalar' since call sites already have typed ContractField.
P2-7 ADR 178 JSON examples outdated ✅ Agree Will update ADR 178 examples to reflect the implemented discriminated union format.
P2-8 ReadonlyMapMap casts in buildValueObjects ✅ Agree Minor — will change resolveColumnDescriptor to accept ReadonlyMap.
P2-9 Missing type-level test for self-referencing VOs ✅ Agree Will add a .test-d.ts verifying InferModelRow correctly resolves NavItem { children: NavItem[] }.
P2-10 generateValueObjectType unused params ✅ Agree Will remove _voName and _valueObjects.

P3 — Nice-to-Have

All acknowledged. P3-1 (UnionFieldType speculative) is intentional — the type infrastructure from ADR 179 is in place but no authoring surface produces unions yet. P3-5 (dict has no PSL surface) is also intentional — dict is a contract-level concept with no PSL syntax decision yet. Both are follow-ups.


Plan gaps (M6.2, M6.4, M6.5)

M6.2 (Mongo dot-path filtering) and M6.4 (SQL dot-path JSONB filtering) are intentionally deferred. The ORM plumbing for value object reads (inlined in results, correct types) is in this PR. Dot-path filtering — u("homeAddress.city").eq("NYC") compiling to native dot notation or JSONB operators — requires the callable string accessor from ADR 180, which is a separate project that builds on this foundation.

M6.5 (.select() support) works for value object fields as a whole — they are included/excluded like any other field. No special handling was needed since value objects are just fields from .select() perspective.


Summary

  • P1-1: Fix (unify generators)
  • P1-2: No fix needed (already works), but adding a nested VO test for the TS builder
  • P2-1 through P2-10: All accepted, will fix in this PR
  • P3: Acknowledged, follow-ups
  • M6.2/M6.4: Intentionally deferred to dot-path accessor project
  • M6.5: Already works

Add an Address value object (street, city, zip?, country) to both
demo applications so the feature is prominently visible end-to-end:

SQL demo: PSL type block, TS builder JSONB column, seed data with
addresses, new create-user-with-address CLI command, integration test.

Mongo demo: PSL type block, seed data with addresses (including null
for nullable showcase), CRUD lifecycle and blog integration tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants