Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions coverage.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
{
"package": "3-targets/3-targets/sqlite",
"reason": "runner.ts, statement-builders.ts, codecs.ts, default-normalizer.ts, and other foundational files moved into target-sqlite to break a workspace cycle. They are exhaustively exercised by full target↔adapter↔driver tests in adapter-sqlite/test/migrations and the e2e framework's sqlite migrations suite, but those run in a separate vitest config so the per-package coverage report doesn't see them. Mirrors the equivalent Postgres situation.",
"addedDate": "2026-04-27",
"addedDate": "2026-07-27",
"expiryDays": 90,
"assignee": null,
"linear": null,
"notes": "See packages/3-targets/6-adapters/sqlite/test/migrations/* (runner.basic, runner.errors, runner.idempotency, planner-introspection.integration, render-typescript.roundtrip) and test/e2e/framework/test/sqlite/migrations/* for the integration coverage. Recovery requires either reorganizing coverage scopes or adding unit-level tests inside target-sqlite."
"notes": "See packages/3-targets/6-adapters/sqlite/test/migrations/* (runner.basic, runner.errors, runner.idempotency, planner-introspection.integration, render-typescript.roundtrip) and test/e2e/framework/test/sqlite/migrations/* for the integration coverage. Recovery requires either reorganizing coverage scopes or adding unit-level tests inside target-sqlite. Renewed 2026-07-27 (lapsed mid-flight on TML-3099): the cross-config coverage situation is unchanged; recovery options above still apply."
},
{
"package": "3-extensions/sql-orm-client",
Expand Down
34 changes: 27 additions & 7 deletions docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Namespaces:
| `CLI` | Command-line argument and invocation errors |
| `CONTRACT` | Contract authoring, emission, validation, and the contract↔database relationship (markers, schema verification) |
| `PSL` | The PSL source text itself (parse/format) |
| `ORM` | ORM client API misuse |
| `ORM` | ORM client and query-builder DSL API misuse |
| `RUNTIME` | Query execution and runtime wiring |
| `DRIVER` | Database driver connection and protocol failures |
| `MIGRATION` | Migration authoring, planning, checking, and execution |
Expand Down Expand Up @@ -175,7 +175,7 @@ An entity attached to the contract declares a framework-managed namespace entry

### CONTRACT.ENTITY_KIND_UNKNOWN

An entity handle passed to the contract has an `entityKind` that no composed pack registers, so the builder cannot lower it. Raised during SQL contract lowering. Meta: `entityKind`.
An entity handle passed to the contract has an `entityKind` that no composed pack registers, so the builder cannot lower it — or contract entries carry a kind no composed pack recognizes when the framework hydrates entities from an emitted contract. Raised during SQL contract lowering and entity hydration. Meta: `entityKind`.

### CONTRACT.ENUM_CODEC_NOT_IN_PACK_STACK

Expand All @@ -189,6 +189,10 @@ An enum declaration is malformed: it has no members, a duplicate member name or

A Mongo field references an enum that is not declared in `defineContract({ enums })`. Raised by the Mongo contract builder. Meta: `modelName`, `fieldName`, `enumName`.

### CONTRACT.EXPORT_INVALID

The TS contract module's export is not a plain JSON-serializable contract object: a non-object export, circular references, getters, or function-valued properties. Raised by the CLI while loading a TypeScript contract source. Meta: `path`, `reason`, `key`.

### CONTRACT.FIELD_UNKNOWN

An index or column mapping references a field the model does not declare (unknown field in the contract definition, or a Mongo model index over an undeclared field). Raised while lowering/building the contract. Meta: `modelName`, `fieldName`, `indexSignature`.
Expand Down Expand Up @@ -305,6 +309,14 @@ A role entity is declared more than once in the entities list, or a role name is

Schema verification found that the live database schema does not satisfy the contract — missing/extra/mismatched tables, columns, or other elements. Produced by `verify`-family CLI operations; the full verification result rides in meta. Fix path: `prisma-next db update` or adjust the contract. Meta: `verificationResult`, `issues`.

### CONTRACT.SOURCE_IMPORT_DISALLOWED

The TypeScript contract module imports something outside the contract-source import allowlist; contract sources must stay pure so they can be bundled and evaluated deterministically. Raised by the CLI while loading a TS contract source. Meta: `allowlist`, `disallowed`.

### CONTRACT.SOURCE_LOAD_FAILED

Bundling or evaluating the TypeScript contract module failed (esbuild bundle error, or the module threw on import). Raised by the CLI while loading a TS contract source; the underlying failure is attached as `cause`. Meta: `path`, `stage` (`bundle` or `import`).

### CONTRACT.TABLE_AMBIGUOUS

A storage table name resolves in more than one namespace of the contract and needs namespace qualification to disambiguate. Raised whenever a bare table name is resolved against contract storage (authoring and runtime paths). Meta: `tableName`, `candidates`.
Expand Down Expand Up @@ -339,6 +351,10 @@ An authored wire-name prefix (an index name, or an RLS policy prefix) exceeds th

## PSL

### PSL.FORMAT_OPTION_INVALID

`resolveFormatOptions` was given an invalid formatting option — a non-positive/non-integer `indent` or an unrecognized `newline` value. Raised before any PSL source is read. Meta: `option`, `received`.

### PSL.PARSE_FAILED

`format()` was asked to format PSL source that has parse errors; formatting refuses to run on an unparseable document. The message carries the first diagnostic and a count of the rest; the CLI `format` command wraps this into a structured failure telling the user to fix the parse errors. Meta: `diagnostics`.
Expand All @@ -355,15 +371,15 @@ An `aggregate()` or `groupBy().aggregate()` selector is not a valid aggregation

### ORM.ARGUMENT_INVALID

A method argument on the ORM client is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, or a custom collection registered as an instance / against a nonexistent model in `orm({ collections })`. Meta: `method`, `argument`, `model`, `column`, `key`.
A method argument on the ORM client — or on the `sql()` / Mongo query-builder DSLs — is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, a custom collection registered as an instance / against a nonexistent model in `orm({ collections })`, invalid builder argument shapes, `$and`/`$or` with no expressions, non-integer limit/skip, or malformed lookup/group/update specs. Meta: `method`, `argument`, `model`, `column`, `key`.

### ORM.CAPABILITY_MISSING

The requested operation requires a contract capability the contract does not declare — currently the `returning` capability needed for mutations that read back the affected row. Meta: `capability`, `action`.
The requested operation requires a contract capability the contract does not declare — currently the `returning` capability needed for mutations that read back the affected row. Raised by the ORM client and the `sql()` builder. Meta: `capability`, `action`.

### ORM.COLUMN_UNKNOWN

A mutation payload or where-expression references a column that does not exist on the resolved table. Thrown while compiling the query plan or binding the where clause. Meta: `namespaceId`, `tableName`, `column`.
A mutation payload or where-expression references a column that does not exist on the resolved table. Thrown while compiling the query plan or binding the where clause — by the ORM client and by the `sql()` builder DSL. Meta: `namespaceId`, `tableName`, `column`.

### ORM.CURSOR_VALUE_MISSING

Expand Down Expand Up @@ -599,11 +615,11 @@ A parameterized codec's `paramsSchema` rejected the `typeParams` carried by a co

### DRIVER.ALREADY_CONNECTED

Calling `connect(binding)` on a driver — or `connect()` on a target facade client (Postgres, SQLite, Mongo) — that is already connected. Close with `close()` before reconnecting with a new binding. Meta: `bindingKind`.
Calling `connect(binding)` on a driver — or `connect()` on a target facade client (Postgres, SQLite, Mongo) or the CLI control client — that is already connected. Close with `close()` before reconnecting with a new binding. Meta: `bindingKind`.

### DRIVER.NOT_CONNECTED

Using a driver or a target facade client before `connect(...)` has been called (or after it was closed) — surfaces from `execute`, `executePrepared`, `acquireConnection`, `query`, or `explain`, including lazily when iterating an execute result.
Using a driver, a target facade client, or the CLI control client before `connect(...)` has been called (or after it was closed) — surfaces from `execute`, `executePrepared`, `acquireConnection`, `query`, or `explain`, including lazily when iterating an execute result.

## MIGRATION

Expand Down Expand Up @@ -719,6 +735,10 @@ A migration object's `endContract`/`startContract` accessor was read, but the in

At migration authoring/emit time, a `dataTransform(endContract, …)` produced a query plan whose storage hash does not match the contract passed to `dataTransform` — the query builder was configured with a different contract reference than the migration itself. Make both use the same imported `endContract`. Meta: `dataTransformName`, `expected`, `actual`.

### MIGRATION.DESCRIBE_INVALID

A migration author class's `describe()` result is unusable: it carries neither an `endContractJson` nor an override, or the returned metadata fails validation. Raised while loading/describing an authored migration. Meta: `reason`.

### MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH

An extension descriptor publishes a `contractSpace` whose `headRef.hash` does not match the hash recomputed from its `contractJson` — the descriptor was published with a stale head hash, typically because the contract was bumped without rerunning the extension's emit pipeline. Meta: `extensionId`, `recomputedHash`, `headRefHash`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
import { structuredError } from '@prisma-next/utils/structured-error';

export type ContractCode = `CONTRACT.${ContractSubcode}`;

type ContractSubcode = 'NAMESPACE_INVALID';

export function contractError(
code: ContractCode,
message: string,
options?: StructuredErrorOptions,
): StructuredError {
return structuredError(code, message, options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,3 @@ export class ContractValidationError extends Error {
this.phase = phase;
}
}

export class DomainNamespaceResolutionError extends Error {
constructor(message: string) {
super(message);
this.name = 'DomainNamespaceResolutionError';
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DomainNamespaceResolutionError } from './contract-validation-error';
import { contractError } from './contract-errors';

/**
* Reserved sentinel domain namespace id for the late-bound application-domain
Expand All @@ -24,12 +24,16 @@ export function soleDomainNamespaceId(domain: {
}): string {
const [soleNamespaceId, ...rest] = Object.keys(domain.namespaces);
if (soleNamespaceId === undefined) {
throw new DomainNamespaceResolutionError('domain has no namespaces');
throw contractError('CONTRACT.NAMESPACE_INVALID', 'domain has no namespaces', {
meta: { reason: 'no-domain-namespaces' },
});
}
if (rest.length > 0) {
const all = [soleNamespaceId, ...rest];
throw new DomainNamespaceResolutionError(
throw contractError(
'CONTRACT.NAMESPACE_INVALID',
`bare-name resolution requires exactly one domain namespace, found ${all.length} (${all.join(', ')}); select a namespace explicitly`,
{ meta: { reason: 'multiple-domain-namespaces', namespaceIds: all } },
);
}
return soleNamespaceId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DomainNamespaceResolutionError } from './contract-validation-error';
import { contractError } from './contract-errors';
import { soleDomainNamespaceId } from './default-namespace';
import type { ApplicationDomain } from './domain-envelope';
import type { ContractModelBase, ContractValueObject } from './domain-types';
Expand All @@ -14,8 +14,10 @@ export function domainModelsAtDefaultNamespace(
const namespaceId = soleDomainNamespaceId(domain);
const domainNamespace = domain.namespaces[namespaceId];
if (domainNamespace === undefined) {
throw new DomainNamespaceResolutionError(
throw contractError(
'CONTRACT.NAMESPACE_INVALID',
`domain namespace "${namespaceId}" is not present on the contract`,
{ meta: { reason: 'domain-namespace-missing', namespaceId } },
);
}
return domainNamespace.models;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export {
ContractValidationError,
type ContractValidationPhase,
DomainNamespaceResolutionError,
} from '../contract-validation-error';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export type {
ContractExecutionSection,
ContractValueObjectDefinitions,
} from '../contract-types';
export { DomainNamespaceResolutionError } from '../contract-validation-error';
export type { ControlPolicy } from '../control-policy';
export { effectiveControlPolicy } from '../control-policy';
export type { CrossReference } from '../cross-reference';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import { isStructuredError } from '@prisma-next/utils/structured-error';
import { describe, expect, it } from 'vitest';
import { DomainNamespaceResolutionError } from '../src/contract-validation-error';
import { soleDomainNamespaceId, UNBOUND_DOMAIN_NAMESPACE_ID } from '../src/default-namespace';

function capture(run: () => unknown): unknown {
try {
run();
} catch (error) {
return error;
}
return expect.unreachable('expected the call to throw');
}

describe('UNBOUND_DOMAIN_NAMESPACE_ID', () => {
it('is the late-bound domain sentinel', () => {
expect(UNBOUND_DOMAIN_NAMESPACE_ID).toBe('__unbound__');
});
});

describe('soleDomainNamespaceId', () => {
it('throws when the domain declares no namespaces', () => {
expect(() => soleDomainNamespaceId({ namespaces: {} })).toThrow(DomainNamespaceResolutionError);
it('throws CONTRACT.NAMESPACE_INVALID when the domain declares no namespaces', () => {
const error = capture(() => soleDomainNamespaceId({ namespaces: {} }));
expect(isStructuredError(error)).toBe(true);
expect(error).toMatchObject({
code: 'CONTRACT.NAMESPACE_INVALID',
message: 'domain has no namespaces',
});
});

it('returns the namespace when exactly one is declared', () => {
expect(soleDomainNamespaceId({ namespaces: { auth: {} } })).toBe('auth');
});

it('throws when more than one namespace is declared rather than guessing', () => {
expect(() => soleDomainNamespaceId({ namespaces: { auth: {}, public: {} } })).toThrow(
DomainNamespaceResolutionError,
);
it('throws CONTRACT.NAMESPACE_INVALID when more than one namespace is declared rather than guessing', () => {
const error = capture(() => soleDomainNamespaceId({ namespaces: { auth: {}, public: {} } }));
expect(isStructuredError(error)).toBe(true);
expect(error).toMatchObject({ code: 'CONTRACT.NAMESPACE_INVALID' });
});
});
9 changes: 5 additions & 4 deletions packages/1-framework/0-foundation/utils/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { blindCast } from './casts';
import { InternalError } from './internal-error';

/**
* Represents a successful result containing a value.
Expand Down Expand Up @@ -60,15 +61,15 @@ class ResultImpl<T, F> {

get value(): T {
if (!this.ok) {
throw new Error('Cannot access value on NotOk result');
throw new InternalError('Cannot access value on NotOk result');
}
// biome-ignore lint/style/noNonNullAssertion: must be present if ok is true
return this._value!;
}

get failure(): F {
if (this.ok) {
throw new Error('Cannot access failure on Ok result');
throw new InternalError('Cannot access failure on Ok result');
}
// biome-ignore lint/style/noNonNullAssertion: must be present if ok is false
return this._failure!;
Expand Down Expand Up @@ -100,7 +101,7 @@ class ResultImpl<T, F> {
*/
assertOk(this: Result<T, F>): T {
if (!this.ok) {
throw new Error('Expected Ok result but got NotOk');
throw new InternalError('Expected Ok result but got NotOk');
}
return this.value;
}
Expand All @@ -111,7 +112,7 @@ class ResultImpl<T, F> {
*/
assertNotOk(this: Result<T, F>): F {
if (this.ok) {
throw new Error('Expected NotOk result but got Ok');
throw new InternalError('Expected NotOk result but got Ok');
}
return this.failure;
}
Expand Down
14 changes: 14 additions & 0 deletions packages/1-framework/1-core/config/src/config-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
import { structuredError } from '@prisma-next/utils/structured-error';

export type ConfigCode = `CONFIG.${ConfigSubcode}`;

type ConfigSubcode = 'VALIDATION_FAILED';

export function configError(
code: ConfigCode,
message: string,
options?: StructuredErrorOptions,
): StructuredError {
return structuredError(code, message, options);
}
3 changes: 2 additions & 1 deletion packages/1-framework/1-core/config/src/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ControlTargetDescriptor,
} from '@prisma-next/framework-components/control';
import { type } from 'arktype';
import { configError } from './config-errors';
import type { ContractSourceProvider } from './contract-source-types';

/**
Expand Down Expand Up @@ -173,7 +174,7 @@ export function defineConfig<TFamilyId extends string = string, TTargetId extend
const validated = PrismaNextConfigSchema(config);
if (validated instanceof type.errors) {
const messages = validated.map((p: { message: string }) => p.message).join('; ');
throw new Error(`Config validation failed: ${messages}`);
throw configError('CONFIG.VALIDATION_FAILED', `Config validation failed: ${messages}`);
}

// Normalize contract config if present
Expand Down
7 changes: 4 additions & 3 deletions packages/1-framework/1-core/config/src/config-validation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { configError } from './config-errors';
import type { PrismaNextConfig } from './config-types';
import { ConfigValidationError } from './errors';

function throwValidation(field: string, why?: string): never {
throw new ConfigValidationError(field, why);
const message = why ?? `Config must have a "${field}" field`;
throw configError('CONFIG.VALIDATION_FAILED', message, { why: message, meta: { field } });
}

function validateContractConfig(contract: Record<string, unknown>): void {
Expand Down Expand Up @@ -54,7 +55,7 @@ function validateContractConfig(contract: Record<string, unknown>): void {
* This is pure validation logic with no file I/O or CLI awareness.
*
* @param config - Config object to validate
* @throws ConfigValidationError if config structure is invalid
* @throws a `CONFIG.VALIDATION_FAILED` structured error if config structure is invalid
*/
export function validateConfig(config: unknown): asserts config is PrismaNextConfig {
if (!config || typeof config !== 'object') {
Expand Down
11 changes: 0 additions & 11 deletions packages/1-framework/1-core/config/src/errors.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { validateConfig } from '../config-validation';
export { ConfigValidationError } from '../errors';
Loading
Loading