@prisma-next/framework-components 0.7.0 → 0.8.0-dev.1

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 (43) hide show
  1. package/dist/authoring.d.mts +2 -2
  2. package/dist/authoring.mjs +2 -2
  3. package/dist/components.d.mts +1 -1
  4. package/dist/control.d.mts +108 -21
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +8 -5
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/execution.d.mts +1 -1
  9. package/dist/{framework-authoring-DGIQbNPt.d.mts → framework-authoring-Bb_GEnzj.d.mts} +43 -3
  10. package/dist/framework-authoring-Bb_GEnzj.d.mts.map +1 -0
  11. package/dist/{framework-authoring-DxXcjyJX.mjs → framework-authoring-DcEZ5Lin.mjs} +26 -5
  12. package/dist/framework-authoring-DcEZ5Lin.mjs.map +1 -0
  13. package/dist/{framework-components-DwkHnl-e.d.mts → framework-components-DQRRmQOH.d.mts} +2 -2
  14. package/dist/{framework-components-DwkHnl-e.d.mts.map → framework-components-DQRRmQOH.d.mts.map} +1 -1
  15. package/dist/ir.d.mts +161 -0
  16. package/dist/ir.d.mts.map +1 -0
  17. package/dist/ir.mjs +39 -0
  18. package/dist/ir.mjs.map +1 -0
  19. package/dist/runtime.d.mts +336 -1
  20. package/dist/runtime.d.mts.map +1 -1
  21. package/dist/runtime.mjs +140 -1
  22. package/dist/runtime.mjs.map +1 -1
  23. package/package.json +10 -9
  24. package/src/annotations.ts +322 -0
  25. package/src/control/contract-serializer.ts +37 -0
  26. package/src/control/control-descriptors.ts +10 -0
  27. package/src/control/control-instances.ts +7 -15
  28. package/src/control/control-schema-view.ts +3 -3
  29. package/src/control/control-stack.ts +24 -10
  30. package/src/control/schema-verifier.ts +51 -0
  31. package/src/execution/runtime-middleware.ts +49 -0
  32. package/src/exports/authoring.ts +7 -0
  33. package/src/exports/control.ts +7 -1
  34. package/src/exports/ir.ts +6 -0
  35. package/src/exports/runtime.ts +11 -0
  36. package/src/ir/ir-node.ts +62 -0
  37. package/src/ir/namespace.ts +40 -0
  38. package/src/ir/storage-type.ts +23 -0
  39. package/src/ir/storage.ts +41 -0
  40. package/src/meta-builder.ts +101 -0
  41. package/src/shared/framework-authoring.ts +116 -12
  42. package/dist/framework-authoring-DGIQbNPt.d.mts.map +0 -1
  43. package/dist/framework-authoring-DxXcjyJX.mjs.map +0 -1
@@ -0,0 +1,37 @@
1
+ import type { JsonObject } from '@prisma-next/utils/json';
2
+
3
+ /**
4
+ * Framework SPI for moving a contract between its canonical on-disk JSON
5
+ * form and its in-memory class-hierarchy form. Both directions live on the
6
+ * same SPI so the conceptual seam — "the boundary where the contract
7
+ * crosses between persisted JSON and live class instances" — has a single
8
+ * named home.
9
+ *
10
+ * Both faces are needed by the framework today (round-trip property tests,
11
+ * drift detection, future canonicalization), not eventually. For most
12
+ * targets `serializeContract` is identity over JSON-clean class instances;
13
+ * the method exists because the seam is real, not as a convention placeholder.
14
+ *
15
+ * Implementers compose this SPI as a named property on their target
16
+ * descriptor (`descriptor.contractSerializer`); the descriptor itself
17
+ * remains the aggregator of all per-target SPIs.
18
+ */
19
+ export interface ContractSerializer<TContract> {
20
+ /**
21
+ * Validate the JSON shape and construct typed class instances. Throws on
22
+ * structural / domain / storage validation failures. Returns the typed
23
+ * contract on success.
24
+ */
25
+ deserializeContract(json: unknown): TContract;
26
+
27
+ /**
28
+ * Serialize a typed contract to its canonical JSON shape. Returns
29
+ * `JsonObject` so callers can stringify, hash, or feed the result into
30
+ * another SPI without re-asserting JSON-cleanness. Targets whose contract
31
+ * fields are JSON-clean by construction return the contract unchanged
32
+ * (the symmetric pair to `deserializeContract`); targets that need to
33
+ * canonicalize on the way out (key ordering, dropping computed-only
34
+ * fields, normalizing numeric encodings) do that work here.
35
+ */
36
+ serializeContract(contract: TContract): JsonObject;
37
+ }
@@ -1,3 +1,4 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
1
2
  import type {
2
3
  AdapterDescriptor,
3
4
  DriverDescriptor,
@@ -5,6 +6,7 @@ import type {
5
6
  FamilyDescriptor,
6
7
  TargetDescriptor,
7
8
  } from '../shared/framework-components';
9
+ import type { ContractSerializer } from './contract-serializer';
8
10
  import type {
9
11
  ControlAdapterInstance,
10
12
  ControlDriverInstance,
@@ -33,7 +35,15 @@ export interface ControlTargetDescriptor<
33
35
  TFamilyId,
34
36
  TTargetId
35
37
  >,
38
+ TContract extends Contract = Contract,
36
39
  > extends TargetDescriptor<TFamilyId, TTargetId> {
40
+ /**
41
+ * JSON ⇄ class boundary for this target's contract. Every target
42
+ * ships the SPI: framework consumers reach the serializer through
43
+ * `descriptor.contractSerializer` rather than importing a per-target
44
+ * `validateContract` helper. The descriptor IS the aggregator.
45
+ */
46
+ readonly contractSerializer: ContractSerializer<TContract>;
37
47
  create(): TTargetInstance;
38
48
  }
39
49
 
@@ -25,27 +25,19 @@ export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
25
25
  readonly configPath?: string;
26
26
  }): Promise<VerifyDatabaseResult>;
27
27
 
28
- schemaVerify(options: {
29
- readonly driver: ControlDriverInstance<TFamilyId, string>;
30
- readonly contract: unknown;
31
- readonly strict: boolean;
32
- readonly contractPath: string;
33
- readonly configPath?: string;
34
- readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, string>>;
35
- }): Promise<VerifyDatabaseSchemaResult>;
36
-
37
28
  /**
38
29
  * Verify a contract against an already-introspected schema slice.
39
30
  *
40
- * Difference from {@link schemaVerify}: no `driver`, no introspection
41
- * the caller hands over the schema directly. Used by the aggregate
42
- * verifier to invoke the family's verification logic per member,
43
- * with the schema **pre-projected** to that member's claimed slice
44
- * via {@link import('@prisma-next/migration-tools/aggregate').projectSchemaToSpace}.
31
+ * Callers that need to verify against the live database compose
32
+ * {@link introspect} + `verifySchema` directly; the family
33
+ * interface deliberately exposes the introspection step so callers
34
+ * can pre-project the schema (e.g. the aggregate verifier projects
35
+ * each member's claimed slice via
36
+ * {@link import('@prisma-next/migration-tools/aggregate').projectSchemaToSpace}).
45
37
  *
46
38
  * Synchronous — no I/O. Idempotent.
47
39
  */
48
- schemaVerifyAgainstSchema(options: {
40
+ verifySchema(options: {
49
41
  readonly contract: unknown;
50
42
  readonly schema: TSchemaIR;
51
43
  readonly strict: boolean;
@@ -8,7 +8,7 @@
8
8
  * core view via the `toSchemaView` method on `FamilyInstance`.
9
9
  */
10
10
 
11
- export type SchemaNodeKind =
11
+ export type SchemaViewNodeKind =
12
12
  | 'root'
13
13
  | 'namespace'
14
14
  | 'collection'
@@ -22,7 +22,7 @@ export interface SchemaTreeVisitor<R> {
22
22
  }
23
23
 
24
24
  export interface SchemaTreeNodeOptions {
25
- readonly kind: SchemaNodeKind;
25
+ readonly kind: SchemaViewNodeKind;
26
26
  readonly id: string;
27
27
  readonly label: string;
28
28
  readonly meta?: Record<string, unknown>;
@@ -30,7 +30,7 @@ export interface SchemaTreeNodeOptions {
30
30
  }
31
31
 
32
32
  export class SchemaTreeNode {
33
- readonly kind: SchemaNodeKind;
33
+ readonly kind: SchemaViewNodeKind;
34
34
  readonly id: string;
35
35
  readonly label: string;
36
36
  readonly meta?: Record<string, unknown>;
@@ -2,11 +2,13 @@ import type { Codec } from '../shared/codec';
2
2
  import type { CodecLookup, CodecMeta } from '../shared/codec-types';
3
3
  import type {
4
4
  AuthoringContributions,
5
+ AuthoringEntityTypeNamespace,
5
6
  AuthoringFieldNamespace,
6
7
  AuthoringTypeNamespace,
7
8
  } from '../shared/framework-authoring';
8
9
  import {
9
10
  assertNoCrossRegistryCollisions,
11
+ isAuthoringEntityTypeDescriptor,
10
12
  isAuthoringFieldPresetDescriptor,
11
13
  isAuthoringTypeConstructorDescriptor,
12
14
  mergeAuthoringNamespaces,
@@ -29,6 +31,7 @@ import type {
29
31
  export interface AssembledAuthoringContributions {
30
32
  readonly field: AuthoringFieldNamespace;
31
33
  readonly type: AuthoringTypeNamespace;
34
+ readonly entityTypes: AuthoringEntityTypeNamespace;
32
35
  }
33
36
 
34
37
  export interface ControlStack<
@@ -147,6 +150,7 @@ export function assembleAuthoringContributions(
147
150
  ): AssembledAuthoringContributions {
148
151
  const field = {} as Record<string, unknown>;
149
152
  const type = {} as Record<string, unknown>;
153
+ const entityTypes = {} as Record<string, unknown>;
150
154
 
151
155
  for (const descriptor of descriptors) {
152
156
  if (descriptor.authoring?.field) {
@@ -158,25 +162,35 @@ export function assembleAuthoringContributions(
158
162
  'field',
159
163
  );
160
164
  }
161
- if (!descriptor.authoring?.type) {
162
- continue;
165
+ if (descriptor.authoring?.type) {
166
+ mergeAuthoringNamespaces(
167
+ type,
168
+ descriptor.authoring.type,
169
+ [],
170
+ isAuthoringTypeConstructorDescriptor,
171
+ 'type',
172
+ );
173
+ }
174
+ if (descriptor.authoring?.entityTypes) {
175
+ mergeAuthoringNamespaces(
176
+ entityTypes,
177
+ descriptor.authoring.entityTypes,
178
+ [],
179
+ isAuthoringEntityTypeDescriptor,
180
+ 'entity',
181
+ );
163
182
  }
164
- mergeAuthoringNamespaces(
165
- type,
166
- descriptor.authoring.type,
167
- [],
168
- isAuthoringTypeConstructorDescriptor,
169
- 'type',
170
- );
171
183
  }
172
184
 
173
185
  const fieldNamespace = field as AuthoringFieldNamespace;
174
186
  const typeNamespace = type as AuthoringTypeNamespace;
175
- assertNoCrossRegistryCollisions(typeNamespace, fieldNamespace);
187
+ const entityTypeNamespace = entityTypes as AuthoringEntityTypeNamespace;
188
+ assertNoCrossRegistryCollisions(typeNamespace, fieldNamespace, entityTypeNamespace);
176
189
 
177
190
  return {
178
191
  field: fieldNamespace,
179
192
  type: typeNamespace,
193
+ entityTypes: entityTypeNamespace,
180
194
  };
181
195
  }
182
196
 
@@ -0,0 +1,51 @@
1
+ import type { SchemaIssue } from './control-result-types';
2
+
3
+ /**
4
+ * Framework SPI for verifying that an introspected schema matches the
5
+ * contract that authored it. The implementer walks the target's IR
6
+ * natively — concrete classes, target-only kinds — and reports issues.
7
+ *
8
+ * The framework verifier (per FR6) walks the contract-space aggregate,
9
+ * dispatches to the right target's verifier (`descriptor.schemaVerifier`)
10
+ * per space, and wraps the per-target results into a unified
11
+ * `VerifyDatabaseSchemaResult` with timings, summary, and per-node
12
+ * verification tree.
13
+ *
14
+ * Family-level abstract bases (e.g. `SqlSchemaVerifierBase`) carry the
15
+ * shared SQL/Mongo walk logic and expose protected hooks for target
16
+ * extensions; concrete target verifiers (`PostgresSchemaVerifier extends
17
+ * SqlSchemaVerifierBase`) own the dispatch on target-specific kinds.
18
+ *
19
+ * Target-specific issue kinds (RLS-policy-mismatch, namespace-mismatch,
20
+ * future function-shape-mismatch) are widened target-side; the framework
21
+ * `SchemaIssue` union (in `control-result-types.ts`) is intentionally not
22
+ * generic over target kinds in this project — see the spec's
23
+ * "Forward note: SchemaIssue layering" for the layering observation
24
+ * captured for a future project.
25
+ */
26
+ export interface SchemaVerifier<TContract, TSchema> {
27
+ verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult;
28
+ }
29
+
30
+ /**
31
+ * Minimal per-target verifier input. Family abstract bases extend this
32
+ * shape with family-specific options (`strict`, `frameworkComponents`,
33
+ * codec hooks, …) — the framework SPI itself stays at the contract +
34
+ * schema pair every implementer needs.
35
+ */
36
+ export interface SchemaVerifyOptions<TContract, TSchema> {
37
+ readonly contract: TContract;
38
+ readonly schema: TSchema;
39
+ }
40
+
41
+ /**
42
+ * Per-target verifier result. The framework verifier wraps these into the
43
+ * existing `VerifyDatabaseSchemaResult` envelope (with timings, summary,
44
+ * per-node verification tree); the SPI itself returns just the
45
+ * core ok/issues pair so the seam between target-walk and
46
+ * framework-aggregation is explicit.
47
+ */
48
+ export interface SchemaVerifyResult {
49
+ readonly ok: boolean;
50
+ readonly issues: readonly SchemaIssue[];
51
+ }
@@ -58,6 +58,27 @@ export interface RuntimeMiddlewareContext {
58
58
  * cancels.
59
59
  */
60
60
  readonly signal?: AbortSignal;
61
+ /**
62
+ * Identifies the queryable scope this execution is running under.
63
+ *
64
+ * - `'runtime'` — top-level `runtime.execute(plan)`. The default scope
65
+ * used by the standard read/write paths.
66
+ * - `'connection'` — `connection.execute(plan)` after
67
+ * `runtime.connection()` checked out a connection from the pool.
68
+ * - `'transaction'` — `transaction.execute(plan)` inside an explicit
69
+ * transaction, or a query routed through `withTransaction`.
70
+ *
71
+ * Middleware that should only act at the top level read this field to
72
+ * bypass non-runtime scopes. The cache middleware uses it to skip
73
+ * caching inside transactions (where read-after-write coherence is the
74
+ * caller's expectation) and dedicated connections (where the user has
75
+ * explicitly stepped outside the shared cache surface). Observers that
76
+ * don't care about the scope can ignore the field.
77
+ *
78
+ * Family runtimes populate this at context-construction time per
79
+ * scope. Existing middleware that ignore the field are unaffected.
80
+ */
81
+ readonly scope: 'runtime' | 'connection' | 'transaction';
61
82
  }
62
83
 
63
84
  export interface AfterExecuteResult {
@@ -201,6 +222,33 @@ export interface RuntimeMiddleware<
201
222
  ): Promise<void>;
202
223
  }
203
224
 
225
+ /**
226
+ * Cross-family middleware — one that doesn't constrain `familyId` or
227
+ * `targetId` and is therefore compatible with any family runtime's
228
+ * middleware array (`SqlMiddleware[]`, `MongoMiddleware[]`, etc.).
229
+ *
230
+ * The intersection `RuntimeMiddleware & { familyId?: undefined; targetId?: undefined }`
231
+ * pins both optional properties to exactly `undefined` (intersecting
232
+ * `string | undefined` with `undefined` collapses to `undefined`). Under
233
+ * `exactOptionalPropertyTypes: true`, the plain `RuntimeMiddleware` shape
234
+ * — with `familyId?: string` — is *not* assignable to `SqlMiddleware`
235
+ * (which narrows `familyId?: 'sql'`) because `string` is wider than
236
+ * `'sql'`. Pinning the property to `undefined` makes the value a subtype
237
+ * of every narrowed variant: `undefined` extends both `'sql' | undefined`
238
+ * and `'mongo' | undefined`, so a `CrossFamilyMiddleware` value drops
239
+ * into a SQL or Mongo middleware slot without a cast.
240
+ *
241
+ * Cross-family middleware factories (`createCacheMiddleware`, future
242
+ * `audit` / OTel middleware) declare this as their return type so the
243
+ * cross-family typing is named once rather than re-spelled at every call
244
+ * site.
245
+ */
246
+ export type CrossFamilyMiddleware<TPlan extends QueryPlan = QueryPlan> =
247
+ RuntimeMiddleware<TPlan> & {
248
+ readonly familyId?: undefined;
249
+ readonly targetId?: undefined;
250
+ };
251
+
204
252
  /**
205
253
  * Optional per-`execute` options accepted by every family runtime.
206
254
  *
@@ -212,6 +260,7 @@ export interface RuntimeMiddleware<
212
260
  */
213
261
  export interface RuntimeExecuteOptions {
214
262
  readonly signal?: AbortSignal;
263
+ readonly scope?: 'runtime' | 'connection' | 'transaction';
215
264
  }
216
265
 
217
266
  /**
@@ -3,6 +3,11 @@ export type {
3
3
  AuthoringArgumentDescriptor,
4
4
  AuthoringColumnDefaultTemplate,
5
5
  AuthoringContributions,
6
+ AuthoringEntityContext,
7
+ AuthoringEntityTypeDescriptor,
8
+ AuthoringEntityTypeFactoryOutput,
9
+ AuthoringEntityTypeNamespace,
10
+ AuthoringEntityTypeTemplateOutput,
6
11
  AuthoringFieldNamespace,
7
12
  AuthoringFieldPresetDescriptor,
8
13
  AuthoringFieldPresetOutput,
@@ -14,9 +19,11 @@ export type {
14
19
  export {
15
20
  assertNoCrossRegistryCollisions,
16
21
  hasRegisteredFieldNamespace,
22
+ instantiateAuthoringEntityType,
17
23
  instantiateAuthoringFieldPreset,
18
24
  instantiateAuthoringTypeConstructor,
19
25
  isAuthoringArgRef,
26
+ isAuthoringEntityTypeDescriptor,
20
27
  isAuthoringFieldPresetDescriptor,
21
28
  isAuthoringTypeConstructorDescriptor,
22
29
  mergeAuthoringNamespaces,
@@ -1,4 +1,5 @@
1
1
  export type { ImportRequirement } from '@prisma-next/ts-render';
2
+ export type { ContractSerializer } from '../control/contract-serializer';
2
3
  export type {
3
4
  MigratableTargetDescriptor,
4
5
  OperationPreviewCapable,
@@ -78,9 +79,9 @@ export {
78
79
  } from '../control/control-result-types';
79
80
  export type {
80
81
  CoreSchemaView,
81
- SchemaNodeKind,
82
82
  SchemaTreeNodeOptions,
83
83
  SchemaTreeVisitor,
84
+ SchemaViewNodeKind,
84
85
  } from '../control/control-schema-view';
85
86
  export { SchemaTreeNode } from '../control/control-schema-view';
86
87
  export type {
@@ -105,6 +106,11 @@ export {
105
106
  extractComponentIds,
106
107
  extractQueryOperationTypeImports,
107
108
  } from '../control/control-stack';
109
+ export type {
110
+ SchemaVerifier,
111
+ SchemaVerifyOptions,
112
+ SchemaVerifyResult,
113
+ } from '../control/schema-verifier';
108
114
  export type {
109
115
  ControlMutationDefaultEntry,
110
116
  ControlMutationDefaultRegistry,
@@ -0,0 +1,6 @@
1
+ export type { IRNode } from '../ir/ir-node';
2
+ export { freezeNode, IRNodeBase } from '../ir/ir-node';
3
+ export type { Namespace } from '../ir/namespace';
4
+ export { NamespaceBase, UNSPECIFIED_NAMESPACE_ID } from '../ir/namespace';
5
+ export type { Storage } from '../ir/storage';
6
+ export type { StorageType } from '../ir/storage-type';
@@ -1,3 +1,11 @@
1
+ export type {
2
+ AnnotationHandle,
3
+ AnnotationValue,
4
+ DefineAnnotationOptions,
5
+ OperationKind,
6
+ ValidAnnotations,
7
+ } from '../annotations';
8
+ export { assertAnnotationsApplicable, defineAnnotation } from '../annotations';
1
9
  export { AsyncIterableResult } from '../execution/async-iterable-result';
2
10
  export { runBeforeExecuteChain } from '../execution/before-execute-chain';
3
11
  export type { ExecutionPlan, QueryPlan, ResultType } from '../execution/query-plan';
@@ -14,6 +22,7 @@ export {
14
22
  } from '../execution/runtime-error';
15
23
  export type {
16
24
  AfterExecuteResult,
25
+ CrossFamilyMiddleware,
17
26
  InterceptResult,
18
27
  ParamRefMutator,
19
28
  RuntimeExecuteOptions,
@@ -23,3 +32,5 @@ export type {
23
32
  RuntimeMiddlewareContext,
24
33
  } from '../execution/runtime-middleware';
25
34
  export { checkMiddlewareCompatibility } from '../execution/runtime-middleware';
35
+ export type { LaneMetaBuilder, MetaBuilder } from '../meta-builder';
36
+ export { createMetaBuilder } from '../meta-builder';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Framework-level IR alphabet.
3
+ *
4
+ * The framework's contribution to Contract IR / Schema IR is a common
5
+ * root for the IR class hierarchy and a freeze affordance. Family
6
+ * abstract bases (e.g. `SqlNode`, `MongoSchemaIRNode`) refine the alphabet
7
+ * for their family shape; targets ship the concrete classes.
8
+ *
9
+ * `kind` is an optional discriminator on the base. Families and leaves
10
+ * that benefit from discriminated-union dispatch declare their own
11
+ * literal `kind` at the level that earns it — Mongo leaves carry
12
+ * per-class literals (`readonly kind = 'mongo-collection' as const`)
13
+ * because Mongo IR has polymorphic walkers; SQL declares a single
14
+ * family-level `kind = 'sql'` on `SqlNode` because SQL IR has no
15
+ * polymorphic dispatch today. No framework consumer dispatches on
16
+ * `IRNode.kind` at the BASE type — every dispatch site narrows
17
+ * through a union of leaves where each leaf carries a literal kind, so
18
+ * requiring `kind` at the base would be unearned. Future leaves that
19
+ * earn polymorphic dispatch override with a required literal at that
20
+ * leaf (e.g. `override readonly kind = 'postgres-enum' as const`).
21
+ *
22
+ * `IRNodeBase` carries no methods: the freeze-and-assign affordance
23
+ * lives in the free `freezeNode` helper below. Keeping `freezeNode` out
24
+ * of the class type means an emitted contract literal type
25
+ * (`{ readonly kind: 'mongo-collection', ... }` or an unkeyed literal
26
+ * like `{ nativeType, codecId, nullable }`) is structurally assignable
27
+ * to its class type — a `protected freeze()` instance method would
28
+ * otherwise leak into the public type surface and require the literal
29
+ * to carry it too.
30
+ *
31
+ * Subclasses construct fields then call `freezeNode(this)` to seal the
32
+ * instance. Frozen instances + plain readonly fields keep IR nodes
33
+ * JSON-clean by construction, so `JSON.stringify(node)` produces canonical
34
+ * JSON without a `toJSON()` method. The `ContractSerializer` SPI handles
35
+ * round-trip from canonical JSON back to typed class instances.
36
+ *
37
+ * The name (`IRNode` / `IRNodeBase`) reflects the dual-hierarchy reality:
38
+ * this base is the common root for both Contract IR and Schema IR class
39
+ * hierarchies, not a Schema-IR-specific alphabet.
40
+ */
41
+
42
+ export interface IRNode {
43
+ readonly kind?: string;
44
+ }
45
+
46
+ export abstract class IRNodeBase implements IRNode {
47
+ abstract readonly kind?: string;
48
+ }
49
+
50
+ /**
51
+ * Seal an IR class instance after its constructor has assigned all
52
+ * fields. The free-helper form (rather than a `protected freeze()`
53
+ * instance method) keeps the class type structurally narrow so emitted
54
+ * contract literal types remain assignable to their class types.
55
+ *
56
+ * The helper name stays `freezeNode` — it operates on IR nodes
57
+ * regardless of root naming.
58
+ */
59
+ export function freezeNode<T extends IRNode>(node: T): T {
60
+ Object.freeze(node);
61
+ return node;
62
+ }
@@ -0,0 +1,40 @@
1
+ import { type IRNode, IRNodeBase } from './ir-node';
2
+
3
+ /**
4
+ * Reserved sentinel namespace id meaning
5
+ * "no namespace bound at authoring time; resolve from connection context".
6
+ *
7
+ * Materialised target-side as a singleton subclass of the target's
8
+ * `NamespaceBase` concretion that overrides the namespace's
9
+ * qualifier-emission methods to elide the prefix entirely. Call sites
10
+ * stay polymorphic and never branch on `id === UNSPECIFIED_NAMESPACE_ID`
11
+ * — the singleton's overrides drop the qualifier so emitted SQL / Mongo
12
+ * commands look unqualified.
13
+ *
14
+ * Encoded as an exported const (rather than scattered string literals)
15
+ * so the sentinel-id invariant is single-sourced: any production-source
16
+ * site that constructs an unspecified-namespace singleton imports this
17
+ * constant.
18
+ */
19
+ export const UNSPECIFIED_NAMESPACE_ID = '__unspecified__' as const;
20
+
21
+ /**
22
+ * Framework-level building block for a "namespace" — the database-level
23
+ * grouping under which storage objects (tables, collections, enums, …)
24
+ * reside. Each target's namespace concretion maps the framework concept to
25
+ * a target-native binding:
26
+ *
27
+ * - Postgres: a schema (`CREATE SCHEMA …`); rendered as `"<schema>"`.
28
+ * - SQLite: the singleton `UNSPECIFIED_NAMESPACE_ID`; emitted SQL has no qualifier.
29
+ * - Mongo: the connection's `db` field; addressed as a database name.
30
+ *
31
+ * See `UNSPECIFIED_NAMESPACE_ID` above for the sentinel id and the
32
+ * singleton-subclass pattern that materialises it.
33
+ */
34
+ export interface Namespace extends IRNode {
35
+ readonly id: string;
36
+ }
37
+
38
+ export abstract class NamespaceBase extends IRNodeBase implements Namespace {
39
+ abstract readonly id: string;
40
+ }
@@ -0,0 +1,23 @@
1
+ import type { IRNode } from './ir-node';
2
+
3
+ /**
4
+ * Framework-level alphabet for entries in a storage `types` slot.
5
+ *
6
+ * The slot is polymorphic at the framework level: a family or target can
7
+ * persist either a JSON-clean codec-triple object literal (carrying
8
+ * `kind: 'codec-instance'`) or a class-instance IR node with a narrower
9
+ * kind discriminator (e.g. `'postgres-enum'`). Hydration walkers,
10
+ * verifiers, and planners dispatch on the `kind` literal to recover the
11
+ * precise variant.
12
+ *
13
+ * The `kind` field is required at this layer (in contrast with
14
+ * `IRNode.kind` which is optional) because the slot's downstream
15
+ * consumers dispatch on it — without a guaranteed discriminator the
16
+ * polymorphic walk cannot pick the right reader. Concrete variants
17
+ * narrow `kind` to their literal and add their own field set;
18
+ * downstream consumers use `kind`-discriminator helpers to recover
19
+ * the precise shape.
20
+ */
21
+ export interface StorageType extends IRNode {
22
+ readonly kind: string;
23
+ }
@@ -0,0 +1,41 @@
1
+ import type { IRNode } from './ir-node';
2
+ import type { Namespace } from './namespace';
3
+
4
+ /**
5
+ * Framework-level promise that every Contract IR / Schema IR carries a
6
+ * collection of namespaces keyed by namespace id. Family storage
7
+ * concretions (`SqlStorage`, `MongoStorage`) refine the shape with
8
+ * family-specific fields (tables, collections, enums, …); target
9
+ * concretions add target fields where the family vocabulary doesn't
10
+ * reach.
11
+ *
12
+ * Keeping `namespaces` at the framework layer enforces that every storage
13
+ * object — across any target — is namespace-scoped. The framework can
14
+ * therefore walk the namespace map without knowing the family alphabet, and
15
+ * the `(namespace.id, name)` keying that the verifier and planner depend on
16
+ * is honest at every layer.
17
+ *
18
+ * Extends `IRNode` so the framework's IR-walking surfaces (verifiers,
19
+ * serializers) can dispatch on `Storage`-typed slots through the same
20
+ * IR-node alphabet as every other node — the structural dual already
21
+ * holds in code (every concrete storage class extends an IR-node base);
22
+ * the interface promotion makes the typing honest.
23
+ *
24
+ * **Persisted envelope shape is target-owned, not framework-promised.**
25
+ * Whether the `namespaces` map appears in the on-disk JSON envelope is
26
+ * a per-target decision made by `ContractSerializer.serializeContract`.
27
+ * Some targets emit a JSON-clean namespace shape that round-trips
28
+ * through `JSON.stringify` cleanly (SQL today via the family-layer
29
+ * identity serializer); others ship runtime-only fields on their
30
+ * namespace concretions and override `serializeContract` to strip
31
+ * them (Mongo). Future open (F16): extend the per-target
32
+ * `ContractSerializer` integration-test surface with an explicit
33
+ * envelope-shape assertion for each target, so the strip-vs-pass-through
34
+ * choice is locked at test time rather than implied by the override
35
+ * presence/absence. Earned by PR2's per-target namespace lift, when
36
+ * `PostgresSchema` / `SqliteUnspecifiedDatabase` start carrying
37
+ * target-specific fields.
38
+ */
39
+ export interface Storage extends IRNode {
40
+ readonly namespaces: Readonly<Record<string, Namespace>>;
41
+ }