@prisma-next/framework-components 0.12.0-dev.7 → 0.12.0-dev.70

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 (63) hide show
  1. package/dist/authoring.d.mts +2 -2
  2. package/dist/authoring.mjs +2 -2
  3. package/dist/{codec-BFOsuHKK.d.mts → codec-DCQAerzB.d.mts} +1 -1
  4. package/dist/{codec-BFOsuHKK.d.mts.map → codec-DCQAerzB.d.mts.map} +1 -1
  5. package/dist/codec.d.mts +1 -1
  6. package/dist/codec.d.mts.map +1 -1
  7. package/dist/components.d.mts +1 -1
  8. package/dist/components.mjs +1 -1
  9. package/dist/components.mjs.map +1 -1
  10. package/dist/control.d.mts +85 -13
  11. package/dist/control.d.mts.map +1 -1
  12. package/dist/control.mjs +89 -7
  13. package/dist/control.mjs.map +1 -1
  14. package/dist/{emission-types-CMv_053d.d.mts → emission-types-vfpSTe63.d.mts} +2 -2
  15. package/dist/{emission-types-CMv_053d.d.mts.map → emission-types-vfpSTe63.d.mts.map} +1 -1
  16. package/dist/emission.d.mts +3 -3
  17. package/dist/execution.d.mts +1 -1
  18. package/dist/execution.d.mts.map +1 -1
  19. package/dist/execution.mjs +1 -1
  20. package/dist/{framework-authoring-DcEZ5Lin.mjs → framework-authoring-CnwPJCO4.mjs} +76 -5
  21. package/dist/framework-authoring-CnwPJCO4.mjs.map +1 -0
  22. package/dist/framework-authoring-Cyde8zSN.d.mts +380 -0
  23. package/dist/framework-authoring-Cyde8zSN.d.mts.map +1 -0
  24. package/dist/{framework-components-FdqmlGUj.mjs → framework-components-DbCS57go.mjs} +1 -1
  25. package/dist/{framework-components-FdqmlGUj.mjs.map → framework-components-DbCS57go.mjs.map} +1 -1
  26. package/dist/{framework-components-CuoUhyB5.d.mts → framework-components-DdqvMc8S.d.mts} +6 -5
  27. package/dist/{framework-components-CuoUhyB5.d.mts.map → framework-components-DdqvMc8S.d.mts.map} +1 -1
  28. package/dist/ir.d.mts +20 -18
  29. package/dist/ir.d.mts.map +1 -1
  30. package/dist/ir.mjs +17 -14
  31. package/dist/ir.mjs.map +1 -1
  32. package/dist/{psl-ast-BDXL7iCg.d.mts → psl-ast-DRzRF9rS.d.mts} +57 -14
  33. package/dist/psl-ast-DRzRF9rS.d.mts.map +1 -0
  34. package/dist/psl-ast.d.mts +37 -2
  35. package/dist/psl-ast.d.mts.map +1 -0
  36. package/dist/psl-ast.mjs +142 -1
  37. package/dist/psl-ast.mjs.map +1 -1
  38. package/dist/runtime.d.mts +1 -1
  39. package/dist/runtime.d.mts.map +1 -1
  40. package/dist/runtime.mjs.map +1 -1
  41. package/dist/{types-import-spec-BxI5cSQy.d.mts → types-import-spec-DRKzrJ20.d.mts} +1 -1
  42. package/dist/{types-import-spec-BxI5cSQy.d.mts.map → types-import-spec-DRKzrJ20.d.mts.map} +1 -1
  43. package/dist/utils.mjs.map +1 -1
  44. package/package.json +9 -9
  45. package/src/control/control-instances.ts +15 -5
  46. package/src/control/control-migration-types.ts +20 -2
  47. package/src/control/control-result-types.ts +4 -1
  48. package/src/control/control-stack.ts +123 -4
  49. package/src/control/psl-ast.ts +73 -27
  50. package/src/control/psl-extension-block-validator.ts +340 -0
  51. package/src/control/verifier-disposition.ts +62 -0
  52. package/src/exports/authoring.ts +16 -0
  53. package/src/exports/control.ts +7 -0
  54. package/src/exports/psl-ast.ts +2 -0
  55. package/src/ir/namespace.ts +15 -13
  56. package/src/ir/storage.ts +8 -7
  57. package/src/shared/framework-authoring.ts +215 -2
  58. package/src/shared/framework-components.ts +2 -0
  59. package/src/shared/psl-extension-block.ts +184 -0
  60. package/dist/framework-authoring-BPPe9C9D.d.mts +0 -183
  61. package/dist/framework-authoring-BPPe9C9D.d.mts.map +0 -1
  62. package/dist/framework-authoring-DcEZ5Lin.mjs.map +0 -1
  63. package/dist/psl-ast-BDXL7iCg.d.mts.map +0 -1
@@ -0,0 +1,62 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
2
+ import type { SchemaVerificationNode } from './control-result-types';
3
+
4
+ export type VerificationStatus = SchemaVerificationNode['status'];
5
+
6
+ export type VerifierOutcome = VerificationStatus | 'suppress';
7
+
8
+ /**
9
+ * Target-neutral classification of a verifier finding, abstracted away from any
10
+ * one storage model's vocabulary. Each family classifies its own concrete issue
11
+ * kinds into these categories; the framework only grades the category against a
12
+ * control policy.
13
+ *
14
+ * - `declaredMissing` — a declared object/element is absent from the database.
15
+ * - `declaredIncompatible` — a declared object/element exists but its shape diverges.
16
+ * - `valueDrift` — the value set of an existing type drifted (e.g. enum values).
17
+ * - `extraNestedElement` — an undeclared element nested inside a declared object
18
+ * (a SQL column, a document field).
19
+ * - `extraAuxiliary` — an undeclared auxiliary attached to a declared object
20
+ * (a SQL constraint/index, a Mongo index/validator).
21
+ * - `extraTopLevelObject` — an undeclared top-level object (a SQL table, a
22
+ * Mongo collection).
23
+ */
24
+ export type VerifierIssueCategory =
25
+ | 'declaredMissing'
26
+ | 'declaredIncompatible'
27
+ | 'valueDrift'
28
+ | 'extraNestedElement'
29
+ | 'extraAuxiliary'
30
+ | 'extraTopLevelObject';
31
+
32
+ /**
33
+ * Grades a target-neutral issue category against a control policy.
34
+ *
35
+ * - `observed` warns on everything.
36
+ * - `tolerated` suppresses only an extra nested element (everything else fails).
37
+ * - `external` suppresses every extra category and value drift (existence and
38
+ * declared-shape divergences still fail).
39
+ * - `managed` (and any other) fails.
40
+ */
41
+ export function dispositionForCategory(
42
+ controlPolicy: ControlPolicy,
43
+ category: VerifierIssueCategory,
44
+ ): VerifierOutcome {
45
+ if (controlPolicy === 'observed') {
46
+ return 'warn';
47
+ }
48
+ if (controlPolicy === 'tolerated' && category === 'extraNestedElement') {
49
+ return 'suppress';
50
+ }
51
+ if (controlPolicy === 'external') {
52
+ if (
53
+ category === 'extraNestedElement' ||
54
+ category === 'extraAuxiliary' ||
55
+ category === 'extraTopLevelObject' ||
56
+ category === 'valueDrift'
57
+ ) {
58
+ return 'suppress';
59
+ }
60
+ }
61
+ return 'fail';
62
+ }
@@ -11,6 +11,8 @@ export type {
11
11
  AuthoringFieldNamespace,
12
12
  AuthoringFieldPresetDescriptor,
13
13
  AuthoringFieldPresetOutput,
14
+ AuthoringPslBlockDescriptor,
15
+ AuthoringPslBlockDescriptorNamespace,
14
16
  AuthoringStorageTypeTemplate,
15
17
  AuthoringTemplateValue,
16
18
  AuthoringTypeConstructorDescriptor,
@@ -25,8 +27,22 @@ export {
25
27
  isAuthoringArgRef,
26
28
  isAuthoringEntityTypeDescriptor,
27
29
  isAuthoringFieldPresetDescriptor,
30
+ isAuthoringPslBlockDescriptor,
28
31
  isAuthoringTypeConstructorDescriptor,
29
32
  mergeAuthoringNamespaces,
30
33
  resolveAuthoringTemplateValue,
31
34
  validateAuthoringHelperArguments,
32
35
  } from '../shared/framework-authoring';
36
+ export type {
37
+ PslBlockParam,
38
+ PslBlockParamList,
39
+ PslBlockParamOption,
40
+ PslBlockParamRef,
41
+ PslBlockParamValue,
42
+ PslExtensionBlock,
43
+ PslExtensionBlockParamList,
44
+ PslExtensionBlockParamOption,
45
+ PslExtensionBlockParamRef,
46
+ PslExtensionBlockParamScalarValue,
47
+ PslExtensionBlockParamValue,
48
+ } from '../shared/psl-extension-block';
@@ -95,6 +95,7 @@ export {
95
95
  assembleControlMutationDefaults,
96
96
  assembleScalarTypeDescriptors,
97
97
  assertUniqueCodecOwner,
98
+ buildExtensionLoadOrder,
98
99
  createControlStack,
99
100
  extractCodecLookup,
100
101
  extractCodecTypeImports,
@@ -106,6 +107,12 @@ export type {
106
107
  SchemaVerifyOptions,
107
108
  SchemaVerifyResult,
108
109
  } from '../control/schema-verifier';
110
+ export type {
111
+ VerificationStatus,
112
+ VerifierIssueCategory,
113
+ VerifierOutcome,
114
+ } from '../control/verifier-disposition';
115
+ export { dispositionForCategory } from '../control/verifier-disposition';
109
116
  export type {
110
117
  ControlMutationDefaultEntry,
111
118
  ControlMutationDefaultRegistry,
@@ -1 +1,3 @@
1
1
  export * from '../control/psl-ast';
2
+ export type { ExtensionBlockRefResolutionContext } from '../control/psl-extension-block-validator';
3
+ export { validateExtensionBlock } from '../control/psl-extension-block-validator';
@@ -41,28 +41,30 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
41
41
  *
42
42
  * The framework promises only the coordinate (`id`) — the named storage
43
43
  * entities a namespace contains are family-typed (SQL contributes
44
- * `tables`, Mongo contributes `collections`, future families pick their
45
- * own native idiom). Generic consumers walking "all named entries" go
46
- * through a family-typed namespace, not the framework `Namespace`.
44
+ * `table` / `type`, Mongo contributes `collection`, future families pick
45
+ * their own native idiom under `entries`). Generic consumers walking "all
46
+ * named entries" go through a family-typed namespace, not the framework
47
+ * `Namespace`.
47
48
  *
48
49
  * Every namespace concretion (e.g. family-built SQL namespaces,
49
50
  * `MongoUnboundNamespace`, target-promoted namespaces like
50
- * `PostgresSchema`) carries exactly: `id` (enumerable string), `kind`
51
- * (non-enumerable string discriminator set via `Object.defineProperty`),
52
- * and one or more entity-kind slot maps each an own-enumerable property
53
- * whose key is the entity kind (`tables`, `types`, `collections`,
54
- * target-pack-contributed slot names) and whose value is a
55
- * `Record<entityName, EntityIRClass>`. No other own-enumerable data lives
56
- * on a namespace; non-entity computed data lives on the surrounding storage
57
- * or contract IR. The framework's `elementCoordinates(storage)` walk relies
58
- * on this invariant to enumerate entities structurally without
59
- * family-specific knowledge.
51
+ * `PostgresSchema`) carries exactly: `id` (enumerable string),
52
+ * `entries` (frozen object holding entity-kind slot maps), and `kind`
53
+ * (non-enumerable string discriminator set via `Object.defineProperty`).
54
+ * Each slot map under `entries` uses a singular essence key (`table`,
55
+ * `type`, `collection`, ) mapping entity names to IR classes. No other
56
+ * own-enumerable data lives on a namespace; non-entity computed data lives
57
+ * on the surrounding storage or contract IR. The framework's
58
+ * `elementCoordinates(storage)` walk relies on this invariant to enumerate
59
+ * entities structurally without family-specific knowledge.
60
60
  */
61
61
  export interface Namespace extends IRNode, StorageNamespace {
62
62
  readonly kind: string;
63
+ readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
63
64
  }
64
65
 
65
66
  export abstract class NamespaceBase extends IRNodeBase implements Namespace {
66
67
  abstract readonly id: string;
68
+ abstract readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
67
69
  abstract override readonly kind: string;
68
70
  }
package/src/ir/storage.ts CHANGED
@@ -30,18 +30,19 @@ export interface EntityCoordinate {
30
30
  * value, yielded as {@link EntityCoordinate} tuples with
31
31
  * `plane: 'storage'` (the parameter type binds the plane).
32
32
  *
33
- * Iterates each namespace's own-enumerable properties structurally.
34
- * Skips `id` (known scalar); `kind` is non-enumerable on namespace
35
- * concretions and does not appear in `Object.entries`. For every other
36
- * property whose value is a non-null object, yields one coordinate per
37
- * entry key in that map. No family-specific slot vocabulary is required.
33
+ * Iterates each namespace's `entries` slot maps structurally. Skips
34
+ * non-object `entries`; `id` and `kind` are not walked (`kind` is
35
+ * non-enumerable on concretions). For every entity-kind key under
36
+ * `entries` whose value is a non-null object, yields one coordinate per
37
+ * entity name in that map. No family-specific slot vocabulary is required.
38
38
  */
39
39
  export function* elementCoordinates(
40
40
  storage: Pick<StorageBase, 'namespaces'>,
41
41
  ): Generator<EntityCoordinate> {
42
42
  for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
43
- for (const [entityKind, slot] of Object.entries(ns)) {
44
- if (entityKind === 'id') continue;
43
+ const entries = ns.entries;
44
+ if (entries === null || typeof entries !== 'object') continue;
45
+ for (const [entityKind, slot] of Object.entries(entries)) {
45
46
  if (slot === null || typeof slot !== 'object') continue;
46
47
  for (const entityName of Object.keys(slot)) {
47
48
  yield { plane: 'storage', namespaceId, entityKind, entityName };
@@ -7,8 +7,10 @@ import {
7
7
  isColumnDefaultLiteralInputValue,
8
8
  isExecutionMutationDefaultValue,
9
9
  } from '@prisma-next/contract/types';
10
+ import { blindCast } from '@prisma-next/utils/casts';
10
11
  import { ifDefined } from '@prisma-next/utils/defined';
11
12
  import type { Type } from 'arktype';
13
+ import type { PslBlockParam } from './psl-extension-block';
12
14
 
13
15
  export type AuthoringArgRef = {
14
16
  readonly kind: 'arg';
@@ -157,10 +159,55 @@ export type AuthoringEntityTypeNamespace = {
157
159
  readonly [name: string]: AuthoringEntityTypeDescriptor | AuthoringEntityTypeNamespace;
158
160
  };
159
161
 
162
+ /**
163
+ * Declarative descriptor for an extension-contributed top-level PSL block.
164
+ *
165
+ * An extension registers one of these per keyword it contributes. The
166
+ * framework owns the generic parser, validator, and printer — no
167
+ * parsing or printing code runs from the extension.
168
+ *
169
+ * - `keyword` is the PSL top-level identifier this descriptor claims
170
+ * (`policy_select`, `role`, …).
171
+ * - `discriminator` is the routing key used by the printer dispatch and
172
+ * the `entityTypes` lowering factory lookup. Convention:
173
+ * `<target-or-family>-<kind>` (`postgres-policy-select`).
174
+ * - `name.required` declares whether the block must have a name token
175
+ * after the keyword. Currently always `true` — anonymous blocks are
176
+ * not part of the closed-grammar premise — but the field is explicit
177
+ * so the type can evolve without a breaking change.
178
+ * - `parameters` maps parameter names to their value-kind descriptors
179
+ * (`ref` / `value` / `option` / `list`). The generic parser and
180
+ * validator interpret these; the extension supplies no parser or
181
+ * printer function.
182
+ */
183
+ export interface AuthoringPslBlockDescriptor {
184
+ readonly kind: 'pslBlock';
185
+ readonly keyword: string;
186
+ readonly discriminator: string;
187
+ readonly name: { readonly required: boolean };
188
+ readonly parameters: Record<string, PslBlockParam>;
189
+ }
190
+
191
+ export type AuthoringPslBlockDescriptorNamespace = {
192
+ readonly [name: string]: AuthoringPslBlockDescriptor | AuthoringPslBlockDescriptorNamespace;
193
+ };
194
+
160
195
  export interface AuthoringContributions {
161
196
  readonly type?: AuthoringTypeNamespace;
162
197
  readonly field?: AuthoringFieldNamespace;
163
198
  readonly entityTypes?: AuthoringEntityTypeNamespace;
199
+ /**
200
+ * Registry of declarative block descriptors this contribution registers,
201
+ * keyed by arbitrary path segments. Each leaf is an
202
+ * {@link AuthoringPslBlockDescriptor} that claims a PSL top-level keyword.
203
+ * The framework owns the generic parser, validator, and printer; the
204
+ * contribution supplies only these declarative descriptors.
205
+ *
206
+ * Contrast with {@link PslNamespace.extensionBlocks}: that field holds
207
+ * the parsed block nodes in a namespace; this field holds the registry
208
+ * of descriptors that teach the parser how to read those blocks.
209
+ */
210
+ readonly pslBlockDescriptors?: AuthoringPslBlockDescriptorNamespace;
164
211
  }
165
212
 
166
213
  export function isAuthoringArgRef(value: unknown): value is AuthoringArgRef {
@@ -228,6 +275,42 @@ export function isAuthoringEntityTypeDescriptor(
228
275
  return typeof factory === 'function' || template !== undefined;
229
276
  }
230
277
 
278
+ export function isAuthoringPslBlockDescriptor(
279
+ value: unknown,
280
+ ): value is AuthoringPslBlockDescriptor {
281
+ if (typeof value !== 'object' || value === null) {
282
+ return false;
283
+ }
284
+ const record = blindCast<
285
+ Record<string, unknown>,
286
+ 'type-guard probing an unknown candidate-descriptor object for known property names'
287
+ >(value);
288
+ if (record['kind'] !== 'pslBlock') {
289
+ return false;
290
+ }
291
+ const keyword = record['keyword'];
292
+ if (typeof keyword !== 'string' || keyword.length === 0) {
293
+ return false;
294
+ }
295
+ const discriminator = record['discriminator'];
296
+ if (typeof discriminator !== 'string' || discriminator.length === 0) {
297
+ return false;
298
+ }
299
+ const name = record['name'];
300
+ if (typeof name !== 'object' || name === null) {
301
+ return false;
302
+ }
303
+ const nameRecord = blindCast<
304
+ Record<string, unknown>,
305
+ 'type-guard probing the name property of a candidate pslBlock descriptor'
306
+ >(name);
307
+ if (typeof nameRecord['required'] !== 'boolean') {
308
+ return false;
309
+ }
310
+ const parameters = record['parameters'];
311
+ return typeof parameters === 'object' && parameters !== null && !Array.isArray(parameters);
312
+ }
313
+
231
314
  /**
232
315
  * Returns true when `namespace` is a non-leaf key in `contributions.field`.
233
316
  *
@@ -341,10 +424,127 @@ function collectAuthoringLeafPaths(
341
424
  return paths;
342
425
  }
343
426
 
427
+ interface AuthoringLeafEntry {
428
+ readonly path: string;
429
+ readonly discriminator: string;
430
+ }
431
+
432
+ function collectAuthoringLeafDiscriminators(
433
+ namespace: Readonly<Record<string, unknown>>,
434
+ isLeaf: (value: unknown) => boolean,
435
+ label: string,
436
+ path: readonly string[] = [],
437
+ ): AuthoringLeafEntry[] {
438
+ const entries: AuthoringLeafEntry[] = [];
439
+ for (const [key, value] of Object.entries(namespace)) {
440
+ const currentPath = [...path, key];
441
+ if (isLeaf(value)) {
442
+ const record = blindCast<
443
+ Record<string, unknown>,
444
+ 'discriminator extraction from a leaf already validated by isLeaf'
445
+ >(value);
446
+ const discriminator = record['discriminator'];
447
+ if (typeof discriminator === 'string' && discriminator.length > 0) {
448
+ entries.push({ path: currentPath.join('.'), discriminator });
449
+ }
450
+ continue;
451
+ }
452
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
453
+ const record = blindCast<
454
+ Readonly<Record<string, unknown>>,
455
+ 'walker inspects a non-leaf value for descriptor-shaped keys before recursing'
456
+ >(value);
457
+ // A value carrying descriptor-shaped keys (`kind`/`keyword`/`discriminator`)
458
+ // but failing `isAuthoringPslBlockDescriptor` (e.g. missing `parameters`) is
459
+ // a malformed declarative descriptor. Descending into it as a sub-namespace
460
+ // would silently skip it, so a half-built contribution would pass validation.
461
+ // Reject it at load time instead, naming the path and what's wrong.
462
+ //
463
+ // A valid sub-namespace whose key happens to be named `kind`, `keyword`, or
464
+ // `discriminator` (but which does not look like a descriptor overall) must
465
+ // still descend normally — the check requires descriptor-shaped keys present
466
+ // AND the leaf guard rejecting it.
467
+ if (
468
+ (record['kind'] !== undefined ||
469
+ record['keyword'] !== undefined ||
470
+ record['discriminator'] !== undefined) &&
471
+ !isLeaf(value)
472
+ ) {
473
+ const hasKind = record['kind'] === 'pslBlock';
474
+ const hasKeyword = typeof record['keyword'] === 'string';
475
+ const hasDiscriminator = typeof record['discriminator'] === 'string';
476
+ if (hasKind || (hasKeyword && hasDiscriminator)) {
477
+ throw new Error(
478
+ `Malformed authoring ${label} contribution at "${currentPath.join('.')}". The value carries descriptor keys (kind/keyword/discriminator) but does not satisfy the ${label} 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.`,
479
+ );
480
+ }
481
+ }
482
+ entries.push(...collectAuthoringLeafDiscriminators(record, isLeaf, label, currentPath));
483
+ }
484
+ }
485
+ return entries;
486
+ }
487
+
488
+ /**
489
+ * Throws when two or more entries in the same namespace share a discriminator.
490
+ * Duplicate discriminators within a namespace make dispatch ambiguous — the
491
+ * lowering factory lookup dispatches by discriminator, so one would silently
492
+ * shadow the other. Catch duplicates before building any dispatch map.
493
+ */
494
+ function assertUniqueDiscriminators(entries: readonly AuthoringLeafEntry[], label: string): void {
495
+ const seen = new Map<string, string>();
496
+ for (const { path, discriminator } of entries) {
497
+ const existing = seen.get(discriminator);
498
+ if (existing !== undefined) {
499
+ throw new Error(
500
+ `Duplicate ${label} discriminator "${discriminator}" registered at both "${existing}" and "${path}". Each ${label} contribution must use a unique discriminator.`,
501
+ );
502
+ }
503
+ seen.set(discriminator, path);
504
+ }
505
+ }
506
+
507
+ /**
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).
513
+ */
514
+ function assertPslBlocksHaveFactories(
515
+ entityTypeNamespace: AuthoringEntityTypeNamespace,
516
+ pslBlockNamespace: AuthoringPslBlockDescriptorNamespace,
517
+ ): void {
518
+ const blockEntries = collectAuthoringLeafDiscriminators(
519
+ pslBlockNamespace,
520
+ isAuthoringPslBlockDescriptor,
521
+ 'pslBlock',
522
+ );
523
+ const entityEntries = collectAuthoringLeafDiscriminators(
524
+ entityTypeNamespace,
525
+ isAuthoringEntityTypeDescriptor,
526
+ 'entityType',
527
+ );
528
+
529
+ assertUniqueDiscriminators(blockEntries, 'pslBlock');
530
+ assertUniqueDiscriminators(entityEntries, 'entityType');
531
+
532
+ const entityDiscriminators = new Set(entityEntries.map((entry) => entry.discriminator));
533
+
534
+ for (const block of blockEntries) {
535
+ if (!entityDiscriminators.has(block.discriminator)) {
536
+ throw new Error(
537
+ `Incomplete extension contribution: pslBlock helper "${block.path}" registers discriminator "${block.discriminator}" but no entityType contribution shares that discriminator. An extension-contributed PSL block requires a matching entityType factory so the parsed AST node can lower to an IR class instance; add an entityType helper with discriminator "${block.discriminator}".`,
538
+ );
539
+ }
540
+ }
541
+ }
542
+
344
543
  export function assertNoCrossRegistryCollisions(
345
544
  typeNamespace: AuthoringTypeNamespace,
346
545
  fieldNamespace: AuthoringFieldNamespace,
347
546
  entityTypeNamespace: AuthoringEntityTypeNamespace = {},
547
+ pslBlockNamespace: AuthoringPslBlockDescriptorNamespace = {},
348
548
  ): void {
349
549
  const typePaths = new Set(
350
550
  collectAuthoringLeafPaths(typeNamespace, isAuthoringTypeConstructorDescriptor),
@@ -360,20 +560,33 @@ export function assertNoCrossRegistryCollisions(
360
560
  // `mergeHelperNamespaces` in composed-authoring-helpers.ts), which throws
361
561
  // on same-path registrations within any single registry before this check
362
562
  // runs. This function only handles the cross-registry case.
563
+ //
564
+ // Cross-registry collisions are checked among `type` / `field` /
565
+ // `entityTypes` only — these three are user-facing helper paths that PSL
566
+ // must resolve unambiguously. `pslBlockDescriptors` is an internal
567
+ // framework index consumed by parser and printer dispatch, not a
568
+ // user-facing helper path; the natural authoring pattern is the same
569
+ // path key in `entityTypes` and `pslBlockDescriptors` for a single
570
+ // contribution. The block→factory link is enforced by
571
+ // `assertPslBlocksHaveFactories` via the discriminator string, not by path.
572
+ const ambiguityHint =
573
+ 'Register each path in only one of authoringContributions.field / authoringContributions.type / authoringContributions.entityTypes.';
363
574
  for (const fieldPath of fieldPaths) {
364
575
  if (typePaths.has(fieldPath)) {
365
576
  throw new Error(
366
- `Ambiguous authoring registry path "${fieldPath}". The same path is registered as both a type constructor and a field preset; PSL resolution would be ambiguous. Register each path in only one of authoringContributions.field / authoringContributions.type / authoringContributions.entityTypes.`,
577
+ `Ambiguous authoring registry path "${fieldPath}". The same path is registered as both a type constructor and a field preset; PSL resolution would be ambiguous. ${ambiguityHint}`,
367
578
  );
368
579
  }
369
580
  }
370
581
  for (const entityPath of entityPaths) {
371
582
  if (typePaths.has(entityPath) || fieldPaths.has(entityPath)) {
372
583
  throw new Error(
373
- `Ambiguous authoring registry path "${entityPath}". The same path is registered as an entity contribution AND as a type constructor or field preset; PSL resolution would be ambiguous. Register each path in only one of authoringContributions.field / authoringContributions.type / authoringContributions.entityTypes.`,
584
+ `Ambiguous authoring registry path "${entityPath}". The same path is registered as an entity contribution AND as a type constructor or field preset; PSL resolution would be ambiguous. ${ambiguityHint}`,
374
585
  );
375
586
  }
376
587
  }
588
+
589
+ assertPslBlocksHaveFactories(entityTypeNamespace, pslBlockNamespace);
377
590
  }
378
591
 
379
592
  export function resolveAuthoringTemplateValue(
@@ -227,6 +227,8 @@ export type TargetPackRef<
227
227
  TTargetId extends string = string,
228
228
  > = PackRefBase<'target', TFamilyId> & {
229
229
  readonly targetId: TTargetId;
230
+ /** The namespace a bare (un-namespaced) entity name resolves to for this target (e.g. Postgres `'public'`). */
231
+ readonly defaultNamespaceId: string;
230
232
  };
231
233
 
232
234
  export type AdapterPackRef<
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Shape-only types for the PSL source-position primitives, diagnostic
3
+ * codes, extension-block descriptor vocabulary, and the uniform
4
+ * extension-block AST node base.
5
+ *
6
+ * These live in the shared plane so an extension's authoring descriptor
7
+ * (`AuthoringPslBlockDescriptor` in `framework-authoring`) can reference
8
+ * them without crossing the shared → migration-plane boundary. The
9
+ * migration-plane `psl-ast.ts` re-exports everything here for consumers
10
+ * that import PSL AST types from the control entrypoint.
11
+ */
12
+
13
+ export interface PslPosition {
14
+ readonly offset: number;
15
+ readonly line: number;
16
+ readonly column: number;
17
+ }
18
+
19
+ export interface PslSpan {
20
+ readonly start: PslPosition;
21
+ readonly end: PslPosition;
22
+ }
23
+
24
+ export type PslDiagnosticCode =
25
+ | 'PSL_UNTERMINATED_BLOCK'
26
+ | 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK'
27
+ | 'PSL_INVALID_NAMESPACE_BLOCK'
28
+ | 'PSL_INVALID_ATTRIBUTE_SYNTAX'
29
+ | 'PSL_INVALID_MODEL_MEMBER'
30
+ | 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE'
31
+ | 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE'
32
+ | 'PSL_INVALID_RELATION_ATTRIBUTE'
33
+ | 'PSL_INVALID_REFERENTIAL_ACTION'
34
+ | 'PSL_INVALID_DEFAULT_VALUE'
35
+ | 'PSL_INVALID_ENUM_MEMBER'
36
+ | 'PSL_INVALID_TYPES_MEMBER'
37
+ | 'PSL_INVALID_QUALIFIED_TYPE'
38
+ /**
39
+ * A malformed line inside an extension-contributed top-level block body, or
40
+ * a structurally invalid element inside a `list` parameter value.
41
+ *
42
+ * Replaces the overloaded `PSL_UNSUPPORTED_TOP_LEVEL_BLOCK` code that the
43
+ * generic framework parser previously used for these two parse-error sites
44
+ * inside extension blocks — keeping `PSL_UNSUPPORTED_TOP_LEVEL_BLOCK` for
45
+ * its original meaning (an unknown keyword at the top level) and giving
46
+ * extension-block parse errors their own code.
47
+ */
48
+ | 'PSL_INVALID_EXTENSION_BLOCK_MEMBER'
49
+ /**
50
+ * An unknown parameter key in an extension-contributed block — a key present
51
+ * in the source block but absent from the descriptor's `parameters` map.
52
+ */
53
+ | 'PSL_EXTENSION_UNKNOWN_PARAMETER'
54
+ /**
55
+ * A required parameter declared in the descriptor is absent from the parsed block.
56
+ */
57
+ | 'PSL_EXTENSION_MISSING_REQUIRED_PARAMETER'
58
+ /**
59
+ * An `option`-kind parameter value is not one of the allowed tokens listed
60
+ * in the descriptor's `values` array.
61
+ */
62
+ | 'PSL_EXTENSION_OPTION_OUT_OF_SET'
63
+ /**
64
+ * A `value`-kind parameter's raw text is not a valid JSON literal, or the
65
+ * parsed JSON value was rejected by the codec's `decodeJson` method, or the
66
+ * codec id is not registered in the lookup.
67
+ */
68
+ | 'PSL_EXTENSION_INVALID_VALUE'
69
+ /**
70
+ * A `ref`-kind parameter identifier does not resolve to a declared entity of
71
+ * the required `refKind` within the declared scope.
72
+ */
73
+ | 'PSL_EXTENSION_UNRESOLVED_REF';
74
+
75
+ /**
76
+ * Descriptor vocabulary for a single parameter on a declared block.
77
+ *
78
+ * Four kinds:
79
+ * - `ref` — the parameter value is an identifier that must resolve to a
80
+ * declared entity of `refKind` within the declared `scope`.
81
+ * - `value` — the parameter value is a PSL literal parsed and printed
82
+ * through the codec identified by `codecId`.
83
+ * - `option` — the parameter value is one of the literal tokens in `values`.
84
+ * Not a codec; not persisted data. A closed authoring-time constraint only.
85
+ * - `list` — a bracketed list whose elements each match the `of` descriptor.
86
+ */
87
+ export type PslBlockParam =
88
+ | PslBlockParamRef
89
+ | PslBlockParamValue
90
+ | PslBlockParamOption
91
+ | PslBlockParamList;
92
+
93
+ export interface PslBlockParamRef {
94
+ readonly kind: 'ref';
95
+ readonly refKind: string;
96
+ readonly scope: 'same-namespace' | 'same-space' | 'cross-space';
97
+ readonly required?: boolean;
98
+ }
99
+
100
+ export interface PslBlockParamValue {
101
+ readonly kind: 'value';
102
+ readonly codecId: string;
103
+ readonly required?: boolean;
104
+ }
105
+
106
+ export interface PslBlockParamOption {
107
+ readonly kind: 'option';
108
+ readonly values: readonly string[];
109
+ readonly required?: boolean;
110
+ }
111
+
112
+ export interface PslBlockParamList {
113
+ readonly kind: 'list';
114
+ readonly of: PslBlockParam;
115
+ readonly required?: boolean;
116
+ }
117
+
118
+ /**
119
+ * The parsed representation of a single parameter value on a uniform
120
+ * extension-block AST node. Mirrors the `PslBlockParam` descriptor
121
+ * vocabulary:
122
+ *
123
+ * - `ref` → `PslExtensionBlockParamRef` — a raw identifier string
124
+ * (resolution runs in the validator, not the parser).
125
+ * - `value` → `PslExtensionBlockParamValue` — a raw PSL literal string
126
+ * (codec validation runs in the validator).
127
+ * - `option` → `PslExtensionBlockParamOption` — the chosen token.
128
+ * - `list` → `PslExtensionBlockParamList` — ordered list of the above.
129
+ *
130
+ * These shapes are intentionally minimal. The validator and lowering refine
131
+ * and consume them; the generic framework parser produces them.
132
+ */
133
+ export type PslExtensionBlockParamValue =
134
+ | PslExtensionBlockParamRef
135
+ | PslExtensionBlockParamScalarValue
136
+ | PslExtensionBlockParamOption
137
+ | PslExtensionBlockParamList;
138
+
139
+ export interface PslExtensionBlockParamRef {
140
+ readonly kind: 'ref';
141
+ readonly identifier: string;
142
+ readonly span: PslSpan;
143
+ }
144
+
145
+ export interface PslExtensionBlockParamScalarValue {
146
+ readonly kind: 'value';
147
+ readonly raw: string;
148
+ readonly span: PslSpan;
149
+ }
150
+
151
+ export interface PslExtensionBlockParamOption {
152
+ readonly kind: 'option';
153
+ readonly token: string;
154
+ readonly span: PslSpan;
155
+ }
156
+
157
+ export interface PslExtensionBlockParamList {
158
+ readonly kind: 'list';
159
+ readonly items: readonly PslExtensionBlockParamValue[];
160
+ readonly span: PslSpan;
161
+ }
162
+
163
+ /**
164
+ * Base shape for a uniform extension-contributed top-level PSL block
165
+ * node, as produced by the generic framework parser and consumed by the
166
+ * validator and lowering factory.
167
+ *
168
+ * - `kind` is the routing discriminant, equal to the descriptor's
169
+ * `discriminator`. The framework parser sets this to
170
+ * `descriptor.discriminator` for every block it parses.
171
+ * - `name` is the block's declared name (the identifier after the keyword).
172
+ * - `parameters` is the descriptor-driven parameter map. Keys are
173
+ * parameter names from the descriptor; values are the parsed parameter
174
+ * representations. Only parameters present in the source are included
175
+ * — absence of a required parameter is a validator concern, not a
176
+ * parser concern.
177
+ * - `span` covers the full block from keyword to closing brace.
178
+ */
179
+ export interface PslExtensionBlock {
180
+ readonly kind: string;
181
+ readonly name: string;
182
+ readonly parameters: Record<string, PslExtensionBlockParamValue>;
183
+ readonly span: PslSpan;
184
+ }