@prisma-next/framework-components 0.13.0-dev.2 → 0.13.0-dev.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/authoring.d.mts +2 -2
  2. package/dist/authoring.mjs +1 -1
  3. package/dist/{codec-DCQAerzB.d.mts → codec-types-29q8imKF.d.mts} +84 -67
  4. package/dist/codec-types-29q8imKF.d.mts.map +1 -0
  5. package/dist/codec.d.mts +23 -2
  6. package/dist/codec.d.mts.map +1 -1
  7. package/dist/codec.mjs +2 -1
  8. package/dist/codec.mjs.map +1 -1
  9. package/dist/components.d.mts +1 -1
  10. package/dist/control.d.mts +12 -24
  11. package/dist/control.d.mts.map +1 -1
  12. package/dist/control.mjs +11 -1
  13. package/dist/control.mjs.map +1 -1
  14. package/dist/execution.d.mts +1 -1
  15. package/dist/{framework-authoring-R0TYCkvG.d.mts → framework-authoring-BXiebZGn.d.mts} +92 -9
  16. package/dist/framework-authoring-BXiebZGn.d.mts.map +1 -0
  17. package/dist/{framework-authoring-CnwPJCO4.mjs → framework-authoring-Dv5F3EFC.mjs} +51 -24
  18. package/dist/framework-authoring-Dv5F3EFC.mjs.map +1 -0
  19. package/dist/{framework-components-DDQXmW0b.d.mts → framework-components-B-ABhSOs.d.mts} +3 -3
  20. package/dist/{framework-components-DDQXmW0b.d.mts.map → framework-components-B-ABhSOs.d.mts.map} +1 -1
  21. package/dist/{psl-ast-Cn50B-UG.d.mts → psl-ast-CHgjnZ3h.d.mts} +4 -4
  22. package/dist/{psl-ast-Cn50B-UG.d.mts.map → psl-ast-CHgjnZ3h.d.mts.map} +1 -1
  23. package/dist/psl-ast.d.mts +4 -4
  24. package/dist/psl-ast.mjs +11 -9
  25. package/dist/psl-ast.mjs.map +1 -1
  26. package/dist/resolve-codec-DR7uyr_c.mjs +47 -0
  27. package/dist/resolve-codec-DR7uyr_c.mjs.map +1 -0
  28. package/dist/runtime-error-B2gWOtgH.mjs +37 -0
  29. package/dist/runtime-error-B2gWOtgH.mjs.map +1 -0
  30. package/dist/runtime.d.mts +12 -10
  31. package/dist/runtime.d.mts.map +1 -1
  32. package/dist/runtime.mjs +1 -33
  33. package/dist/runtime.mjs.map +1 -1
  34. package/package.json +7 -7
  35. package/src/control/control-migration-types.ts +5 -22
  36. package/src/control/control-stack.ts +20 -3
  37. package/src/control/psl-ast.ts +4 -1
  38. package/src/control/psl-extension-block-validator.ts +11 -9
  39. package/src/execution/runtime-error.ts +5 -55
  40. package/src/exports/authoring.ts +1 -0
  41. package/src/exports/codec.ts +2 -0
  42. package/src/exports/control.ts +0 -1
  43. package/src/shared/codec-types.ts +19 -1
  44. package/src/shared/framework-authoring.ts +118 -35
  45. package/src/shared/psl-extension-block.ts +56 -5
  46. package/src/shared/resolve-codec.ts +64 -0
  47. package/src/shared/runtime-error.ts +50 -0
  48. package/dist/codec-DCQAerzB.d.mts.map +0 -1
  49. package/dist/framework-authoring-CnwPJCO4.mjs.map +0 -1
  50. package/dist/framework-authoring-R0TYCkvG.d.mts.map +0 -1
@@ -1,9 +1,8 @@
1
- export interface RuntimeErrorEnvelope extends Error {
2
- readonly code: string;
3
- readonly category: 'PLAN' | 'CONTRACT' | 'LINT' | 'BUDGET' | 'RUNTIME';
4
- readonly severity: 'error';
5
- readonly details?: Record<string, unknown>;
6
- }
1
+ import type { RuntimeErrorEnvelope } from '../shared/runtime-error';
2
+ import { runtimeError } from '../shared/runtime-error';
3
+
4
+ export type { RuntimeErrorEnvelope } from '../shared/runtime-error';
5
+ export { isRuntimeError, runtimeError } from '../shared/runtime-error';
7
6
 
8
7
  /**
9
8
  * Stable code emitted by the runtime when an in-flight `execute()`
@@ -30,55 +29,6 @@ export type RuntimeAbortedPhase =
30
29
  | 'afterExecute'
31
30
  | 'onRow';
32
31
 
33
- /**
34
- * Type guard for the runtime-error envelope produced by `runtimeError`.
35
- *
36
- * Prefer this over duck-typing on `error.code` directly so consumers stay
37
- * insulated from the envelope's internal shape.
38
- */
39
- export function isRuntimeError(error: unknown): error is RuntimeErrorEnvelope {
40
- return (
41
- error instanceof Error &&
42
- 'code' in error &&
43
- typeof (error as { code?: unknown }).code === 'string' &&
44
- 'category' in error &&
45
- 'severity' in error
46
- );
47
- }
48
-
49
- export function runtimeError(
50
- code: string,
51
- message: string,
52
- details?: Record<string, unknown>,
53
- ): RuntimeErrorEnvelope {
54
- const error = new Error(message) as RuntimeErrorEnvelope;
55
- Object.defineProperty(error, 'name', {
56
- value: 'RuntimeError',
57
- configurable: true,
58
- });
59
-
60
- return Object.assign(error, {
61
- code,
62
- category: resolveCategory(code),
63
- severity: 'error' as const,
64
- message,
65
- details,
66
- });
67
- }
68
-
69
- function resolveCategory(code: string): RuntimeErrorEnvelope['category'] {
70
- const prefix = code.split('.')[0] ?? 'RUNTIME';
71
- switch (prefix) {
72
- case 'PLAN':
73
- case 'CONTRACT':
74
- case 'LINT':
75
- case 'BUDGET':
76
- return prefix;
77
- default:
78
- return 'RUNTIME';
79
- }
80
- }
81
-
82
32
  /**
83
33
  * Construct a `RUNTIME.ABORTED` envelope. Phase distinguishes where the
84
34
  * abort was observed — codec call sites (`encode` / `decode` / `stream`)
@@ -3,6 +3,7 @@ export type {
3
3
  AuthoringArgumentDescriptor,
4
4
  AuthoringColumnDefaultTemplate,
5
5
  AuthoringContributions,
6
+ AuthoringDiagnosticSink,
6
7
  AuthoringEntityContext,
7
8
  AuthoringEntityTypeDescriptor,
8
9
  AuthoringEntityTypeFactoryOutput,
@@ -16,6 +16,7 @@ export type {
16
16
  CodecLookup,
17
17
  CodecMeta,
18
18
  CodecRef,
19
+ CodecRegistry,
19
20
  CodecTrait,
20
21
  } from '../shared/codec-types';
21
22
  export { emptyCodecLookup, voidParamsSchema } from '../shared/codec-types';
@@ -26,3 +27,4 @@ export type {
26
27
  ColumnTypeDescriptor,
27
28
  } from '../shared/column-spec';
28
29
  export { column } from '../shared/column-spec';
30
+ export { materializeCodec, validateCodecTypeParams } from '../shared/resolve-codec';
@@ -47,7 +47,6 @@ export type {
47
47
  MigrationRunnerSuccessValue,
48
48
  MigrationScaffoldContext,
49
49
  OpFactoryCall,
50
- SerializedQueryPlan,
51
50
  TargetMigrationsCapability,
52
51
  } from '../control/control-migration-types';
53
52
  export type {
@@ -36,7 +36,7 @@ export interface CodecCallContext {
36
36
  /**
37
37
  * Codec-id-keyed read surface threaded into emit and authoring paths.
38
38
  *
39
- * - `get(id)` returns the runtime {@link Codec} instance for the codec id (used by `family.deserializeContract` for `decodeJson` of literal column defaults).
39
+ * - `get(id)` returns a representative {@link Codec} instance for the codec id (used by `family.deserializeContract` for `decodeJson` of literal column defaults). For parameterized codecs whose factory requires concrete params, this may return `undefined` — use `CodecRegistry.forCodecRef` instead.
40
40
  * - `targetTypesFor(id)` exposes the codec-id-keyed `targetTypes` metadata the runtime instance no longer carries (TML-2357). Returns the same array `CodecDescriptor.targetTypes` would; for Mongo (whose registration doesn't yet resolve through the unified descriptor map — TML-2324) the family-side assembly populates this directly from the contributor's codec metadata.
41
41
  * - `metaFor(id)` exposes the codec-id-keyed `meta` (e.g. SQL-side `db.sql.postgres.nativeType`) the runtime instance no longer carries.
42
42
  * - `renderOutputTypeFor(id, params)` exposes the codec-id-keyed `renderOutputType` renderer the runtime instance no longer carries. Returns `undefined` when the codec doesn't render a custom type or when the codec id is unknown.
@@ -48,6 +48,24 @@ export interface CodecLookup {
48
48
  renderOutputTypeFor(id: string, params: Record<string, unknown>): string | undefined;
49
49
  }
50
50
 
51
+ /**
52
+ * Full codec registry — the read surface of {@link CodecLookup} plus codec resolution by ref or
53
+ * column coordinate. Built once by `extractCodecLookup` and passed by reference to adapters and
54
+ * other consumers that need to materialise codecs at runtime.
55
+ *
56
+ * - `forCodecRef(ref)` materialises a codec from a {@link CodecRef}. Throws
57
+ * `RUNTIME.CODEC_DESCRIPTOR_MISSING` for unknown ids and `RUNTIME.TYPE_PARAMS_INVALID` on param
58
+ * schema rejection.
59
+ * - `forColumn(namespaceId, table, column)` returns the codec for a specific column coordinate, or
60
+ * `undefined` when no column-to-codec mapping is present. This registry is contract-free so it
61
+ * always returns `undefined` — the method exists so the object structurally satisfies the SQL
62
+ * `ContractCodecRegistry` interface.
63
+ */
64
+ export interface CodecRegistry extends CodecLookup {
65
+ forCodecRef(ref: CodecRef): Codec;
66
+ forColumn(namespaceId: string, table: string, column: string): Codec | undefined;
67
+ }
68
+
51
69
  export const emptyCodecLookup: CodecLookup = {
52
70
  get: () => undefined,
53
71
  targetTypesFor: () => undefined,
@@ -10,6 +10,7 @@ import {
10
10
  import { blindCast } from '@prisma-next/utils/casts';
11
11
  import { ifDefined } from '@prisma-next/utils/defined';
12
12
  import type { Type } from 'arktype';
13
+ import type { CodecLookup } from './codec-types';
13
14
  import type { PslBlockParam } from './psl-extension-block';
14
15
 
15
16
  export type AuthoringArgRef = {
@@ -109,9 +110,30 @@ export type AuthoringFieldNamespace = {
109
110
  * discover what the factory actually needs to read (codec lookup,
110
111
  * namespace registry, …).
111
112
  */
113
+ /**
114
+ * A write-only sink that a factory may push authoring-time diagnostics into.
115
+ * The concrete type pushed must be structurally compatible with whatever the
116
+ * consumer accumulates (typically `ContractSourceDiagnostic[]`); the framework
117
+ * layer deliberately does not depend on that concrete type.
118
+ */
119
+ export interface AuthoringDiagnosticSink {
120
+ push(d: {
121
+ readonly code: string;
122
+ readonly message: string;
123
+ readonly sourceId: string;
124
+ readonly span?: unknown;
125
+ }): void;
126
+ }
127
+
112
128
  export interface AuthoringEntityContext {
113
129
  readonly family: string;
114
130
  readonly target: string;
131
+ /** Codec registry available to factories that need to validate or decode values. */
132
+ readonly codecLookup?: CodecLookup;
133
+ /** Source file identifier threaded into diagnostics emitted by the factory. */
134
+ readonly sourceId?: string;
135
+ /** Push channel for authoring-time diagnostics emitted by the factory. */
136
+ readonly diagnostics?: AuthoringDiagnosticSink;
115
137
  }
116
138
 
117
139
  export interface AuthoringEntityTypeTemplateOutput {
@@ -186,6 +208,21 @@ export interface AuthoringPslBlockDescriptor {
186
208
  readonly discriminator: string;
187
209
  readonly name: { readonly required: boolean };
188
210
  readonly parameters: Record<string, PslBlockParam>;
211
+ /**
212
+ * When `true`, the block body accepts a variadic tail of parameters beyond
213
+ * the declared set. The block body may contain: fields (model-style),
214
+ * `key = value` parameters, and `@@` attributes. With `variadicParameters`,
215
+ * bare identifiers (keys without a `= value`) and undeclared `key = value`
216
+ * pairs flow into the variadic tail — their semantics belong to the
217
+ * lowering, not the parser.
218
+ *
219
+ * A key that IS declared in `parameters` must still be supplied as
220
+ * `key = value`; a bare occurrence of a declared key is a diagnostic.
221
+ *
222
+ * When `false` (default), the validator emits `PSL_EXTENSION_UNKNOWN_PARAMETER`
223
+ * for keys absent from `parameters`.
224
+ */
225
+ readonly variadicParameters?: boolean;
189
226
  }
190
227
 
191
228
  export type AuthoringPslBlockDescriptorNamespace = {
@@ -329,13 +366,29 @@ export function hasRegisteredFieldNamespace(
329
366
  return !isAuthoringFieldPresetDescriptor(contributions.field[namespace]);
330
367
  }
331
368
 
332
- function isPlainNamespaceObject(value: unknown): value is Record<string, unknown> {
333
- return typeof value === 'object' && value !== null && !Array.isArray(value);
369
+ function isCopyableNamespaceObject(value: unknown): value is Record<string, unknown> {
370
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
371
+ const proto: unknown = Object.getPrototypeOf(value);
372
+ return proto === Object.prototype || proto === null;
373
+ }
374
+
375
+ function deepCopyNamespace(
376
+ source: Record<string, unknown>,
377
+ isLeafDescriptor: (value: unknown) => boolean,
378
+ ): Record<string, unknown> {
379
+ const copy: Record<string, unknown> = {};
380
+ for (const [key, value] of Object.entries(source)) {
381
+ copy[key] =
382
+ isCopyableNamespaceObject(value) && !isLeafDescriptor(value)
383
+ ? deepCopyNamespace(value, isLeafDescriptor)
384
+ : value;
385
+ }
386
+ return copy;
334
387
  }
335
388
 
336
389
  /**
337
390
  * Merges `source` into `target` recursively at the descriptor-namespace
338
- * level. `leafGuard` decides which values are descriptors (terminal
391
+ * level. `isLeafDescriptor` decides which values are descriptors (terminal
339
392
  * merge points; same-path registrations across components are reported
340
393
  * as duplicates) versus sub-namespaces (recursion targets).
341
394
  *
@@ -355,7 +408,7 @@ export function mergeAuthoringNamespaces(
355
408
  target: Record<string, unknown>,
356
409
  source: Record<string, unknown>,
357
410
  path: readonly string[],
358
- leafGuard: (value: unknown) => boolean,
411
+ isLeafDescriptor: (value: unknown) => boolean,
359
412
  label: string,
360
413
  ): void {
361
414
  const assertSafePath = (currentPath: readonly string[]) => {
@@ -376,12 +429,19 @@ export function mergeAuthoringNamespaces(
376
429
  const existingValue = hasExistingValue ? target[key] : undefined;
377
430
 
378
431
  if (!hasExistingValue) {
379
- target[key] = sourceValue;
432
+ // Deep-copy plain-object sub-namespaces so subsequent merges don't mutate
433
+ // objects owned by source packs. Leaf descriptors and class instances are
434
+ // passed by reference — leaves are identity values; class instances carry
435
+ // prototype getters that spread would destroy.
436
+ target[key] =
437
+ isCopyableNamespaceObject(sourceValue) && !isLeafDescriptor(sourceValue)
438
+ ? deepCopyNamespace(sourceValue, isLeafDescriptor)
439
+ : sourceValue;
380
440
  continue;
381
441
  }
382
442
 
383
- const existingIsLeaf = leafGuard(existingValue);
384
- const sourceIsLeaf = leafGuard(sourceValue);
443
+ const existingIsLeaf = isLeafDescriptor(existingValue);
444
+ const sourceIsLeaf = isLeafDescriptor(sourceValue);
385
445
 
386
446
  if (existingIsLeaf || sourceIsLeaf) {
387
447
  throw new Error(
@@ -389,17 +449,17 @@ export function mergeAuthoringNamespaces(
389
449
  );
390
450
  }
391
451
 
392
- if (!isPlainNamespaceObject(existingValue) || !isPlainNamespaceObject(sourceValue)) {
452
+ if (!isCopyableNamespaceObject(existingValue) || !isCopyableNamespaceObject(sourceValue)) {
393
453
  throw new Error(
394
454
  `Invalid authoring ${label} helper "${currentPath.join('.')}". Expected a sub-namespace object or a recognized descriptor; received a malformed value.`,
395
455
  );
396
456
  }
397
457
 
398
- mergeAuthoringNamespaces(existingValue, sourceValue, currentPath, leafGuard, label);
458
+ mergeAuthoringNamespaces(existingValue, sourceValue, currentPath, isLeafDescriptor, label);
399
459
  }
400
460
  }
401
461
 
402
- function collectAuthoringLeafPaths(
462
+ function collectDescriptorPaths(
403
463
  namespace: Readonly<Record<string, unknown>>,
404
464
  isLeaf: (value: unknown) => boolean,
405
465
  path: readonly string[] = [],
@@ -413,29 +473,25 @@ function collectAuthoringLeafPaths(
413
473
  }
414
474
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
415
475
  paths.push(
416
- ...collectAuthoringLeafPaths(
417
- value as Readonly<Record<string, unknown>>,
418
- isLeaf,
419
- currentPath,
420
- ),
476
+ ...collectDescriptorPaths(value as Readonly<Record<string, unknown>>, isLeaf, currentPath),
421
477
  );
422
478
  }
423
479
  }
424
480
  return paths;
425
481
  }
426
482
 
427
- interface AuthoringLeafEntry {
483
+ interface DescriptorEntry {
428
484
  readonly path: string;
429
485
  readonly discriminator: string;
430
486
  }
431
487
 
432
- function collectAuthoringLeafDiscriminators(
488
+ function collectDescriptorEntries(
433
489
  namespace: Readonly<Record<string, unknown>>,
434
490
  isLeaf: (value: unknown) => boolean,
435
491
  label: string,
436
492
  path: readonly string[] = [],
437
- ): AuthoringLeafEntry[] {
438
- const entries: AuthoringLeafEntry[] = [];
493
+ ): DescriptorEntry[] {
494
+ const entries: DescriptorEntry[] = [];
439
495
  for (const [key, value] of Object.entries(namespace)) {
440
496
  const currentPath = [...path, key];
441
497
  if (isLeaf(value)) {
@@ -479,7 +535,7 @@ function collectAuthoringLeafDiscriminators(
479
535
  );
480
536
  }
481
537
  }
482
- entries.push(...collectAuthoringLeafDiscriminators(record, isLeaf, label, currentPath));
538
+ entries.push(...collectDescriptorEntries(record, isLeaf, label, currentPath));
483
539
  }
484
540
  }
485
541
  return entries;
@@ -491,7 +547,7 @@ function collectAuthoringLeafDiscriminators(
491
547
  * lowering factory lookup dispatches by discriminator, so one would silently
492
548
  * shadow the other. Catch duplicates before building any dispatch map.
493
549
  */
494
- function assertUniqueDiscriminators(entries: readonly AuthoringLeafEntry[], label: string): void {
550
+ function assertUniqueDiscriminators(entries: readonly DescriptorEntry[], label: string): void {
495
551
  const seen = new Map<string, string>();
496
552
  for (const { path, discriminator } of entries) {
497
553
  const existing = seen.get(discriminator);
@@ -504,23 +560,50 @@ function assertUniqueDiscriminators(entries: readonly AuthoringLeafEntry[], labe
504
560
  }
505
561
  }
506
562
 
563
+ function collectPslBlockDescriptorEntries(
564
+ namespace: Readonly<Record<string, unknown>>,
565
+ path: readonly string[] = [],
566
+ ): DescriptorEntry[] {
567
+ const entries: DescriptorEntry[] = [];
568
+ for (const [key, value] of Object.entries(namespace)) {
569
+ const currentPath = [...path, key];
570
+ if (isAuthoringPslBlockDescriptor(value)) {
571
+ entries.push({
572
+ path: currentPath.join('.'),
573
+ discriminator: value.discriminator,
574
+ });
575
+ continue;
576
+ }
577
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
578
+ const record = blindCast<
579
+ Readonly<Record<string, unknown>>,
580
+ 'walker descends into psl block namespace'
581
+ >(value);
582
+ const hasKind = record['kind'] === 'pslBlock';
583
+ const hasKeyword = typeof record['keyword'] === 'string';
584
+ const hasDiscriminator = typeof record['discriminator'] === 'string';
585
+ if (hasKind || (hasKeyword && hasDiscriminator)) {
586
+ throw new Error(
587
+ `Malformed authoring pslBlock contribution at "${currentPath.join('.')}". The value carries descriptor keys (kind/keyword/discriminator) but does not satisfy the pslBlock descriptor shape. Fix the contribution so it is a complete descriptor, or remove the stray keys if it was meant to be a sub-namespace.`,
588
+ );
589
+ }
590
+ entries.push(...collectPslBlockDescriptorEntries(record, currentPath));
591
+ }
592
+ }
593
+ return entries;
594
+ }
595
+
507
596
  /**
508
- * Every `pslBlockDescriptors` entry needs a matching `entityTypes` factory
509
- * (same discriminator): the parser would otherwise produce an AST node
510
- * nothing can lower to an IR class instance. The link is one-directional
511
- * — an `entityTypes` factory may stand alone (e.g. `enum`, reachable from
512
- * the TypeScript builder without any PSL block).
597
+ * Every `pslBlockDescriptors` entry requires a matching `entityTypes` factory
598
+ * with the same discriminator. An `entityTypes` factory may stand alone (e.g.
599
+ * `enum`, reachable from the TypeScript builder without any PSL block).
513
600
  */
514
601
  function assertPslBlocksHaveFactories(
515
602
  entityTypeNamespace: AuthoringEntityTypeNamespace,
516
603
  pslBlockNamespace: AuthoringPslBlockDescriptorNamespace,
517
604
  ): void {
518
- const blockEntries = collectAuthoringLeafDiscriminators(
519
- pslBlockNamespace,
520
- isAuthoringPslBlockDescriptor,
521
- 'pslBlock',
522
- );
523
- const entityEntries = collectAuthoringLeafDiscriminators(
605
+ const blockEntries = collectPslBlockDescriptorEntries(pslBlockNamespace);
606
+ const entityEntries = collectDescriptorEntries(
524
607
  entityTypeNamespace,
525
608
  isAuthoringEntityTypeDescriptor,
526
609
  'entityType',
@@ -547,13 +630,13 @@ export function assertNoCrossRegistryCollisions(
547
630
  pslBlockNamespace: AuthoringPslBlockDescriptorNamespace = {},
548
631
  ): void {
549
632
  const typePaths = new Set(
550
- collectAuthoringLeafPaths(typeNamespace, isAuthoringTypeConstructorDescriptor),
633
+ collectDescriptorPaths(typeNamespace, isAuthoringTypeConstructorDescriptor),
551
634
  );
552
635
  const fieldPaths = new Set(
553
- collectAuthoringLeafPaths(fieldNamespace, isAuthoringFieldPresetDescriptor),
636
+ collectDescriptorPaths(fieldNamespace, isAuthoringFieldPresetDescriptor),
554
637
  );
555
638
  const entityPaths = new Set(
556
- collectAuthoringLeafPaths(entityTypeNamespace, isAuthoringEntityTypeDescriptor),
639
+ collectDescriptorPaths(entityTypeNamespace, isAuthoringEntityTypeDescriptor),
557
640
  );
558
641
  // Within-registry duplicate detection is handled upstream by the merge
559
642
  // walker (`mergeAuthoringNamespaces` in control-stack.ts and
@@ -70,7 +70,16 @@ export type PslDiagnosticCode =
70
70
  * A `ref`-kind parameter identifier does not resolve to a declared entity of
71
71
  * the required `refKind` within the declared scope.
72
72
  */
73
- | 'PSL_EXTENSION_UNRESOLVED_REF';
73
+ | 'PSL_EXTENSION_UNRESOLVED_REF'
74
+ /**
75
+ * A parameter key appears more than once in an extension block body.
76
+ * The first occurrence is kept; subsequent occurrences emit this diagnostic.
77
+ */
78
+ | 'PSL_EXTENSION_DUPLICATE_PARAMETER'
79
+ /**
80
+ * A `@@`-prefixed block-attribute line inside an extension block has invalid syntax.
81
+ */
82
+ | 'PSL_INVALID_EXTENSION_BLOCK_ATTRIBUTE';
74
83
 
75
84
  /**
76
85
  * Descriptor vocabulary for a single parameter on a declared block.
@@ -118,14 +127,17 @@ export interface PslBlockParamList {
118
127
  /**
119
128
  * The parsed representation of a single parameter value on a uniform
120
129
  * extension-block AST node. Mirrors the `PslBlockParam` descriptor
121
- * vocabulary:
130
+ * vocabulary, plus `bare` for keyonly entries:
122
131
  *
123
132
  * - `ref` → `PslExtensionBlockParamRef` — a raw identifier string
124
133
  * (resolution runs in the validator, not the parser).
125
- * - `value` → `PslExtensionBlockParamValue` — a raw PSL literal string
134
+ * - `value` → `PslExtensionBlockParamScalarValue` — a raw PSL literal string
126
135
  * (codec validation runs in the validator).
127
136
  * - `option` → `PslExtensionBlockParamOption` — the chosen token.
128
137
  * - `list` → `PslExtensionBlockParamList` — ordered list of the above.
138
+ * - `bare` → `PslExtensionBlockParamBare` — a bare identifier line with no
139
+ * `= value` (e.g. `Low` in an enum2 block). The name is the key in
140
+ * `parameters`; the interpreting consumer decides the default value.
129
141
  *
130
142
  * These shapes are intentionally minimal. The validator and lowering refine
131
143
  * and consume them; the generic framework parser produces them.
@@ -134,7 +146,8 @@ export type PslExtensionBlockParamValue =
134
146
  | PslExtensionBlockParamRef
135
147
  | PslExtensionBlockParamScalarValue
136
148
  | PslExtensionBlockParamOption
137
- | PslExtensionBlockParamList;
149
+ | PslExtensionBlockParamList
150
+ | PslExtensionBlockParamBare;
138
151
 
139
152
  export interface PslExtensionBlockParamRef {
140
153
  readonly kind: 'ref';
@@ -160,6 +173,38 @@ export interface PslExtensionBlockParamList {
160
173
  readonly span: PslSpan;
161
174
  }
162
175
 
176
+ /**
177
+ * A bare identifier line inside an extension block — a key with no `= value`.
178
+ * Emitted when a line matches `/^[A-Za-z_]\w*$/` with no assignment. The
179
+ * consumer decides what default value (if any) to apply.
180
+ */
181
+ export interface PslExtensionBlockParamBare {
182
+ readonly kind: 'bare';
183
+ readonly span: PslSpan;
184
+ }
185
+
186
+ /**
187
+ * A positional argument on a block attribute, e.g. the `"pg/text@1"` in
188
+ * `@@type("pg/text@1")`.
189
+ */
190
+ export interface PslExtensionBlockAttributeArg {
191
+ readonly kind: 'positional';
192
+ readonly value: string;
193
+ readonly span: PslSpan;
194
+ }
195
+
196
+ /**
197
+ * A `@@`-prefixed block-level attribute parsed inside an extension block,
198
+ * e.g. `@@type("pg/text@1")`. Block attributes are captured generically
199
+ * — the parser does not validate attribute names or argument shapes; that
200
+ * is a concern of the block's interpreter.
201
+ */
202
+ export interface PslExtensionBlockAttribute {
203
+ readonly name: string;
204
+ readonly args: readonly PslExtensionBlockAttributeArg[];
205
+ readonly span: PslSpan;
206
+ }
207
+
163
208
  /**
164
209
  * Base shape for a uniform extension-contributed top-level PSL block
165
210
  * node, as produced by the generic framework parser and consumed by the
@@ -173,12 +218,18 @@ export interface PslExtensionBlockParamList {
173
218
  * parameter names from the descriptor; values are the parsed parameter
174
219
  * representations. Only parameters present in the source are included
175
220
  * — absence of a required parameter is a validator concern, not a
176
- * parser concern.
221
+ * parser concern. Insertion order is preserved; the first occurrence of a
222
+ * duplicate key is retained and subsequent occurrences emit
223
+ * `PSL_EXTENSION_DUPLICATE_PARAMETER`.
224
+ * - `blockAttributes` are `@@`-prefixed attribute lines inside the block, in
225
+ * declaration order. Captured generically — names and args are not validated
226
+ * by the parser.
177
227
  * - `span` covers the full block from keyword to closing brace.
178
228
  */
179
229
  export interface PslExtensionBlock {
180
230
  readonly kind: string;
181
231
  readonly name: string;
182
232
  readonly parameters: Record<string, PslExtensionBlockParamValue>;
233
+ readonly blockAttributes: readonly PslExtensionBlockAttribute[];
183
234
  readonly span: PslSpan;
184
235
  }
@@ -0,0 +1,64 @@
1
+ import { blindCast } from '@prisma-next/utils/casts';
2
+ import type { Codec } from './codec';
3
+ import type { AnyCodecDescriptor } from './codec-descriptor';
4
+ import type { CodecInstanceContext, CodecRef } from './codec-types';
5
+ import { runtimeError } from './runtime-error';
6
+
7
+ /**
8
+ * Validates `ref.typeParams` against `descriptor.paramsSchema`.
9
+ *
10
+ * Parameterized codecs that omit `typeParams` have it normalized to `{}` before
11
+ * validation (mirrors `ast-codec-resolver.ts` semantics). Throws
12
+ * `RUNTIME.TYPE_PARAMS_INVALID` when the validator returns a `Promise` or
13
+ * reports issues.
14
+ */
15
+ export function validateCodecTypeParams(descriptor: AnyCodecDescriptor, ref: CodecRef): unknown {
16
+ const normalized =
17
+ descriptor.isParameterized && ref.typeParams === undefined ? { ...ref, typeParams: {} } : ref;
18
+
19
+ const result = blindCast<
20
+ { value: unknown } | { issues: ReadonlyArray<{ message: string }> } | Promise<unknown>,
21
+ 'Standard Schema validate returns unknown; the spec guarantees this union shape'
22
+ >(descriptor.paramsSchema['~standard'].validate(normalized.typeParams));
23
+
24
+ if (result instanceof Promise) {
25
+ throw runtimeError(
26
+ 'RUNTIME.TYPE_PARAMS_INVALID',
27
+ `paramsSchema for codec '${ref.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
28
+ { codecId: ref.codecId, typeParams: ref.typeParams },
29
+ );
30
+ }
31
+
32
+ if ('issues' in result && result.issues) {
33
+ const messages = result.issues.map((issue) => issue.message).join('; ');
34
+ throw runtimeError(
35
+ 'RUNTIME.TYPE_PARAMS_INVALID',
36
+ `Invalid typeParams for codec '${ref.codecId}': ${messages}`,
37
+ { codecId: ref.codecId, typeParams: ref.typeParams },
38
+ );
39
+ }
40
+
41
+ return blindCast<{ value: unknown }, 'issues guard above rules out the issues branch'>(result)
42
+ .value;
43
+ }
44
+
45
+ /**
46
+ * Resolves a `Codec` instance: validates `ref.typeParams` via
47
+ * {@link validateCodecTypeParams} then calls `descriptor.factory(validated)(ctx)`.
48
+ *
49
+ * The descriptor's `factory` is typed against its own `P`; the registry erases
50
+ * `P` to `any`, so the factory is narrowed to `(params: unknown) => (ctx) => Codec`
51
+ * at the call boundary. The `paramsSchema` validates the input above before we
52
+ * forward it, so the narrowing is safe by construction.
53
+ */
54
+ export function materializeCodec(
55
+ descriptor: AnyCodecDescriptor,
56
+ ref: CodecRef,
57
+ ctx: CodecInstanceContext,
58
+ ): Codec {
59
+ const validated = validateCodecTypeParams(descriptor, ref);
60
+ return blindCast<
61
+ (params: unknown) => (ctx: CodecInstanceContext) => Codec,
62
+ 'registry erases P to any; paramsSchema validates input before forwarding'
63
+ >(descriptor.factory)(validated)(ctx);
64
+ }
@@ -0,0 +1,50 @@
1
+ export interface RuntimeErrorEnvelope extends Error {
2
+ readonly code: string;
3
+ readonly category: 'PLAN' | 'CONTRACT' | 'LINT' | 'BUDGET' | 'RUNTIME';
4
+ readonly severity: 'error';
5
+ readonly details?: Record<string, unknown>;
6
+ }
7
+
8
+ /**
9
+ * Type guard for the runtime-error envelope produced by `runtimeError`.
10
+ *
11
+ * Prefer this over duck-typing on `error.code` directly so consumers stay
12
+ * insulated from the envelope's internal shape.
13
+ */
14
+ export function isRuntimeError(error: unknown): error is RuntimeErrorEnvelope {
15
+ return (
16
+ error instanceof Error &&
17
+ 'code' in error &&
18
+ typeof (error as { code?: unknown }).code === 'string' &&
19
+ 'category' in error &&
20
+ 'severity' in error
21
+ );
22
+ }
23
+
24
+ export function runtimeError(
25
+ code: string,
26
+ message: string,
27
+ details?: Record<string, unknown>,
28
+ ): RuntimeErrorEnvelope {
29
+ const error = Object.assign(new Error(message), {
30
+ code,
31
+ category: resolveCategory(code),
32
+ severity: 'error' as const,
33
+ ...(details !== undefined ? { details } : {}),
34
+ });
35
+ Object.defineProperty(error, 'name', { value: 'RuntimeError', configurable: true });
36
+ return error;
37
+ }
38
+
39
+ function resolveCategory(code: string): RuntimeErrorEnvelope['category'] {
40
+ const prefix = code.split('.')[0] ?? 'RUNTIME';
41
+ switch (prefix) {
42
+ case 'PLAN':
43
+ case 'CONTRACT':
44
+ case 'LINT':
45
+ case 'BUDGET':
46
+ return prefix;
47
+ default:
48
+ return 'RUNTIME';
49
+ }
50
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"codec-DCQAerzB.d.mts","names":[],"sources":["../src/shared/codec-types.ts","../src/shared/codec-descriptor.ts","../src/shared/codec.ts"],"mappings":";;;;KAIY,UAAA;AAAZ;;;;AAAsB;AAWtB;;;;AAXA,UAWiB,QAAA;EAAA,SACN,OAAA;EAAA,SACA,UAAA,GAAa,SAAS;AAAA;AAAA;AAcjC;;;;AAC+B;AAW/B;;;;;AA1BiC,UAchB,gBAAA;EAAA,SACN,MAAA,GAAS,WAAW;AAAA;;;;;;;;;UAWd,WAAA;EACf,GAAA,CAAI,EAAA,WAAa,KAAA;EACjB,cAAA,CAAe,EAAA;EACf,OAAA,CAAQ,EAAA,WAAa,SAAA;EACrB,mBAAA,CAAoB,EAAA,UAAY,MAAA,EAAQ,MAAA;AAAA;AAAA,cAG7B,gBAAA,EAAkB,WAK9B;;;;AAAA;AASD;;;UAAiB,oBAAA;EAAA,SACN,IAAI;AAAA;;;;UAME,SAAA;EAAA,SACN,EAAA,GAAK,MAAM;AAAA;;;AAMyB;cAAlC,gBAAA,EAAkB,gBAAgB;;;;;;;;AA7Dd;AAcjC;;;;AAC+B;UCFd,eAAA;EDaW;EAAA,SCXjB,OAAA;EDYQ;EAAA,SCVR,MAAA,WAAiB,UAAA;EDac;EAAA,SCX/B,WAAA;EDWqC;EAAA,SCTrC,IAAA,GAAO,SAAA;EDMZ;EAAA,SCJK,YAAA,EAAc,gBAAA,CAAiB,CAAA;EDKxC;EAAA,SCHS,eAAA;EDIT;EAAA,SCFS,gBAAA,IAAoB,MAAA,EAAQ,CAAA;EDEhB;EAAA,SCAZ,OAAA,GAAU,MAAA,EAAQ,CAAA,MAAO,GAAA,EAAK,oBAAA,KAAyB,KAAA;AAAA;;;;ADCD;AAGjE;KCKY,kBAAA,GAAqB,eAAe;;;ADA/C;AASD;;;;uBCAsB,mBAAA,4BAA+C,eAAA,CAAgB,OAAA;EAAA,kBACjE,OAAA;EAAA,kBACA,MAAA,WAAiB,UAAA;EAAA,kBACjB,WAAA;EAAA,SACT,IAAA,GAAO,SAAA;EAAA,kBAEE,YAAA,EAAc,gBAAA,CAAiB,OAAA;EDQtC;EAAA,ICLP,eAAA;;EAKJ,gBAAA,EAAkB,MAAA,EAAQ,OAAA;EDAmB;;;EAAA,SCKpC,OAAA,CACP,MAAA,EAAQ,OAAA,IACN,GAAA,EAAK,oBAAA,KAAyB,KAAA,kBAAuB,UAAA;AAAA;;;;;;;;ADpE1B;AAcjC;;;;AAC+B;AAW/B;;;;;;;;UEViB,KAAA,sDAEU,UAAA,cAAwB,UAAA;EFS7C;EAAA,SEJK,EAAA,EAAI,EAAA;EFKb;EAAA,SEHS,aAAA,GAAgB,OAAA;EFIzB;EEFA,MAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,gBAAA,GAAmB,OAAA,CAAQ,KAAA;EFEjC;EEArB,MAAA,CAAO,IAAA,EAAM,KAAA,EAAO,GAAA,EAAK,gBAAA,GAAmB,OAAA,CAAQ,MAAA;EFChC;EECpB,UAAA,CAAW,KAAA,EAAO,MAAA,GAAS,SAAA;EFDK;EEGhC,UAAA,CAAW,IAAA,EAAM,SAAA,GAAY,MAAA;AAAA;AFA/B;;;;AAKC;AALD,uBEQsB,SAAA,sDAEK,UAAA,cAAwB,UAAA,kDAGtC,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,KAAA,EAAO,MAAA;EAAA,SAMT,UAAA,EAAY,eAAA;;;AFJ3B;cEIe,UAAA,EAAY,eAAA;EAAA,IAEpC,EAAA,IAAM,EAAA;EAAA,SAID,MAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,GAAA,EAAK,gBAAA,GAAmB,OAAA,CAAQ,KAAA;EAAA,SACtD,MAAA,CAAO,IAAA,EAAM,KAAA,EAAO,GAAA,EAAK,gBAAA,GAAmB,OAAA,CAAQ,MAAA;EAAA,SACpD,UAAA,CAAW,KAAA,EAAO,MAAA,GAAS,SAAA;EAAA,SAC3B,UAAA,CAAW,IAAA,EAAM,SAAA,GAAY,MAAA;AAAA"}