@prisma-next/framework-components 0.11.0 → 0.12.0

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 (47) hide show
  1. package/dist/codec-BFOsuHKK.d.mts.map +1 -1
  2. package/dist/codec.d.mts.map +1 -1
  3. package/dist/codec.mjs.map +1 -1
  4. package/dist/components.d.mts +33 -1
  5. package/dist/components.d.mts.map +1 -0
  6. package/dist/components.mjs +64 -1
  7. package/dist/components.mjs.map +1 -0
  8. package/dist/control.d.mts +85 -110
  9. package/dist/control.d.mts.map +1 -1
  10. package/dist/control.mjs +1 -15
  11. package/dist/control.mjs.map +1 -1
  12. package/dist/emission-types-CMv_053d.d.mts.map +1 -1
  13. package/dist/execution.d.mts.map +1 -1
  14. package/dist/execution.mjs.map +1 -1
  15. package/dist/framework-authoring-BPPe9C9D.d.mts.map +1 -1
  16. package/dist/framework-authoring-DcEZ5Lin.mjs.map +1 -1
  17. package/dist/framework-components-CuoUhyB5.d.mts.map +1 -1
  18. package/dist/framework-components-FdqmlGUj.mjs.map +1 -1
  19. package/dist/ir.d.mts +22 -12
  20. package/dist/ir.d.mts.map +1 -1
  21. package/dist/ir.mjs +22 -1
  22. package/dist/ir.mjs.map +1 -1
  23. package/dist/psl-ast-BDXL7iCg.d.mts.map +1 -1
  24. package/dist/psl-ast.mjs.map +1 -1
  25. package/dist/runtime.d.mts +11 -0
  26. package/dist/runtime.d.mts.map +1 -1
  27. package/dist/runtime.mjs +6 -2
  28. package/dist/runtime.mjs.map +1 -1
  29. package/dist/utils.d.mts.map +1 -1
  30. package/dist/utils.mjs.map +1 -1
  31. package/package.json +18 -7
  32. package/src/control/contract-serializer.ts +19 -0
  33. package/src/control/control-capabilities.ts +1 -26
  34. package/src/control/control-instances.ts +3 -3
  35. package/src/control/control-migration-types.ts +59 -108
  36. package/src/control/control-result-types.ts +7 -0
  37. package/src/execution/runtime-core.ts +13 -2
  38. package/src/execution/runtime-middleware.ts +11 -0
  39. package/src/exports/components.ts +1 -0
  40. package/src/exports/control.ts +2 -7
  41. package/src/exports/ir.ts +1 -0
  42. package/src/ir/domain.ts +23 -0
  43. package/src/ir/ir-node.ts +1 -1
  44. package/src/ir/namespace.ts +4 -4
  45. package/src/ir/storage-type.ts +1 -1
  46. package/src/ir/storage.ts +7 -5
  47. package/src/shared/capabilities.ts +90 -0
@@ -14,26 +14,12 @@ import type { ImportRequirement } from '@prisma-next/ts-render';
14
14
  import type { Result } from '@prisma-next/utils/result';
15
15
  import type { TargetBoundComponentDescriptor } from '../shared/framework-components';
16
16
  import type { ControlDriverInstance, ControlFamilyInstance } from './control-instances';
17
+ import type { OperationContext } from './control-result-types';
17
18
 
18
19
  // ============================================================================
19
20
  // Migration Package Metadata
20
21
  // ============================================================================
21
22
 
22
- /**
23
- * Planner provenance recorded inside {@link MigrationMetadata}.
24
- *
25
- * `used` / `applied` track which migration hints the planner consulted
26
- * vs. which it actually applied during emission; `plannerVersion`
27
- * pins the planner build that produced the migration so future
28
- * verification passes can recognise plans authored against an older
29
- * planner.
30
- */
31
- export interface MigrationHints {
32
- readonly used: readonly string[];
33
- readonly applied: readonly string[];
34
- readonly plannerVersion: string;
35
- }
36
-
37
23
  /**
38
24
  * In-memory migration metadata envelope. Every migration is
39
25
  * content-addressed: the `migrationHash` is a hash over the metadata
@@ -63,8 +49,6 @@ export interface MigrationMetadata {
63
49
  readonly migrationHash: string;
64
50
  readonly from: string | null;
65
51
  readonly to: string;
66
- readonly hints: MigrationHints;
67
- readonly labels: readonly string[];
68
52
  /**
69
53
  * Sorted, deduplicated list of `invariantId`s declared by the
70
54
  * migration's data-transform ops. Always present; an empty array
@@ -216,11 +200,10 @@ export interface MigrationPlan {
216
200
  /**
217
201
  * Contract space this plan applies to. Runners cross-check
218
202
  * `options.space` against `plan.spaceId` so the marker row gets keyed
219
- * by the right space when applying via `executeAcrossSpaces`.
203
+ * by the right space when applying via {@link MigrationRunner.execute}.
220
204
  *
221
- * Optional for backward compatibility with single-space callers that
222
- * pre-date the contract-space aggregate; when present, runners
223
- * enforce that it matches `options.space`.
205
+ * Optional because not every plan carries a space id; when present,
206
+ * runners enforce that it matches `options.space`.
224
207
  */
225
208
  readonly spaceId?: string;
226
209
  /**
@@ -316,13 +299,25 @@ export type MigrationPlannerResult = MigrationPlannerSuccessResult | MigrationPl
316
299
  // ============================================================================
317
300
 
318
301
  /**
319
- * Success value for migration runner execution.
302
+ * Per-space success payload returned inside
303
+ * {@link MigrationRunnerSuccessValue.perSpaceResults}.
320
304
  */
321
- export interface MigrationRunnerSuccessValue {
305
+ export interface MigrationRunnerPerSpaceSuccessValue {
322
306
  readonly operationsPlanned: number;
323
307
  readonly operationsExecuted: number;
324
308
  }
325
309
 
310
+ /**
311
+ * Success value for migration runner execution across one or more contract
312
+ * spaces.
313
+ */
314
+ export interface MigrationRunnerSuccessValue {
315
+ readonly perSpaceResults: ReadonlyArray<{
316
+ readonly space: string;
317
+ readonly value: MigrationRunnerPerSpaceSuccessValue;
318
+ }>;
319
+ }
320
+
326
321
  /**
327
322
  * Failure details for migration runner execution.
328
323
  */
@@ -335,6 +330,11 @@ export interface MigrationRunnerFailure {
335
330
  readonly why?: string;
336
331
  /** Optional metadata for debugging and UX (e.g., schema issues, SQL state). */
337
332
  readonly meta?: Record<string, unknown>;
333
+ /**
334
+ * Identifier of the space whose plan caused the rollback when
335
+ * {@link MigrationRunner.execute} processes multiple spaces.
336
+ */
337
+ readonly failingSpace?: string;
338
338
  }
339
339
 
340
340
  /**
@@ -445,64 +445,20 @@ export interface MigrationPlanner<
445
445
  * @template TFamilyId - The family ID (e.g., 'sql', 'document')
446
446
  * @template TTargetId - The target ID (e.g., 'postgres', 'mysql')
447
447
  */
448
- export interface MigrationRunner<
449
- TFamilyId extends string = string,
450
- TTargetId extends string = string,
451
- > {
452
- /**
453
- * Execute a migration plan against the configured driver.
454
- *
455
- * The `plan` parameter is trusted input. Callers are responsible for
456
- * upstream verification of the originating migration package — typically
457
- * by obtaining the package via `readMigrationPackage` from
458
- * `@prisma-next/migration-tools/io`, which performs hash-integrity checks
459
- * at the load boundary. Runners do not re-verify the plan and assume the
460
- * `(metadata, ops)` pair on disk has not been tampered with since emit.
461
- */
462
- execute(options: {
463
- readonly plan: MigrationPlan;
464
- readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
465
- readonly destinationContract: unknown;
466
- readonly policy: MigrationOperationPolicy;
467
- readonly callbacks?: {
468
- onOperationStart?(op: MigrationPlanOperation): void;
469
- onOperationComplete?(op: MigrationPlanOperation): void;
470
- };
471
- /**
472
- * Execution-time checks configuration.
473
- * All checks default to `true` (enabled) when omitted.
474
- */
475
- readonly executionChecks?: MigrationRunnerExecutionChecks;
476
- /**
477
- * Active framework components participating in this composition.
478
- * Families/targets can interpret this list to derive family-specific metadata.
479
- * All components must have matching familyId and targetId.
480
- */
481
- readonly frameworkComponents: ReadonlyArray<
482
- TargetBoundComponentDescriptor<TFamilyId, TTargetId>
483
- >;
484
- }): Promise<MigrationRunnerResult>;
485
- }
486
-
487
- // ============================================================================
488
- // Multi-space runner protocol (extension contract spaces, TML-2397)
489
- // ============================================================================
490
-
491
448
  /**
492
- * Per-space input for {@link MultiSpaceCapableRunner.executeAcrossSpaces}.
449
+ * Per-space input for {@link MigrationRunner.execute}.
493
450
  *
494
- * Mirrors the single-space `MigrationRunner.execute` options, extended with a
495
- * required `space` identifier. Each entry's `driver` must reference the same
496
- * connection the outer transaction is opened on (typically the same value as
497
- * the top-level `driver` on `executeAcrossSpaces`).
451
+ * Each entry's `driver` must reference the same connection the outer
452
+ * transaction is opened on (typically the same value as the top-level
453
+ * `driver` on `execute`). An apply that targets one space passes a
454
+ * one-element `perSpaceOptions` list.
498
455
  *
499
456
  * Family-specific runners (e.g. the SQL family's `SqlMigrationRunner`) define
500
457
  * a richer per-space option shape that is structurally compatible with this
501
- * one — additional optional fields (e.g. SQL's `strictVerification`,
502
- * `schemaName`, `callbacks`) are tolerated by the underlying runner without
503
- * affecting cross-target wiring.
458
+ * one — additional optional fields (e.g. SQL's `schemaName`, `callbacks`) are
459
+ * tolerated by the underlying runner without affecting cross-target wiring.
504
460
  */
505
- export interface MultiSpaceRunnerPerSpaceOptions<
461
+ export interface MigrationRunnerPerSpaceOptions<
506
462
  TFamilyId extends string = string,
507
463
  TTargetId extends string = string,
508
464
  > {
@@ -513,45 +469,40 @@ export interface MultiSpaceRunnerPerSpaceOptions<
513
469
  readonly policy: MigrationOperationPolicy;
514
470
  readonly executionChecks?: MigrationRunnerExecutionChecks;
515
471
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
472
+ /**
473
+ * When `false`, schema verification tolerates objects owned by sibling
474
+ * contract spaces. Aggregate apply passes `false` per space because each
475
+ * `destinationContract` describes only that space's slice.
476
+ */
477
+ readonly strictVerification?: boolean;
478
+ /**
479
+ * Paths and metadata forwarded to schema verification diagnostics.
480
+ */
481
+ readonly context?: OperationContext;
516
482
  }
517
483
 
518
- export interface MultiSpaceRunnerSuccessValue {
519
- readonly perSpaceResults: ReadonlyArray<{
520
- readonly space: string;
521
- readonly value: MigrationRunnerSuccessValue;
522
- }>;
523
- }
524
-
525
- export interface MultiSpaceRunnerFailure extends MigrationRunnerFailure {
526
- /** Identifier of the space whose plan caused the rollback. */
527
- readonly failingSpace: string;
528
- }
529
-
530
- export type MultiSpaceRunnerResult = Result<MultiSpaceRunnerSuccessValue, MultiSpaceRunnerFailure>;
531
-
532
- /**
533
- * Optional capability for runners that can apply a list of per-space plans.
534
- * Atomicity semantics differ by family:
535
- *
536
- * - SQL (`SqlMigrationRunner`) opens one outer transaction across every
537
- * space; a failure on any space rolls back every space's writes.
538
- * - Mongo (`mongoTargetDescriptor`) cannot wrap most DDL ops in a session
539
- * transaction (TML-2408), so it iterates per-space without an outer
540
- * transaction and relies on per-space-internal verify-gated marker
541
- * atomicity. Earlier-advanced markers are not rolled back when a later
542
- * space fails; re-running resumes from the failing space.
543
- *
544
- * The capability is declared at the framework layer so CLI utilities can
545
- * route through it without importing any specific family directly.
546
- */
547
- export interface MultiSpaceCapableRunner<
484
+ export interface MigrationRunner<
548
485
  TFamilyId extends string = string,
549
486
  TTargetId extends string = string,
550
487
  > {
551
- executeAcrossSpaces(options: {
488
+ /**
489
+ * Apply one or more per-space migration plans against the configured driver.
490
+ *
491
+ * Each plan is trusted input. Callers are responsible for upstream
492
+ * verification of the originating migration package — typically by
493
+ * obtaining the package via `readMigrationPackage` from
494
+ * `@prisma-next/migration-tools/io`, which performs hash-integrity checks
495
+ * at the load boundary. Runners do not re-verify plans and assume the
496
+ * `(metadata, ops)` pairs on disk have not been tampered with since emit.
497
+ *
498
+ * Atomicity semantics differ by family: SQL targets open one outer
499
+ * transaction across every space; Mongo iterates per-space without an
500
+ * outer transaction and relies on per-space verify-gated marker atomicity.
501
+ */
502
+ execute(options: {
552
503
  readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
553
- readonly perSpaceOptions: ReadonlyArray<MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>>;
554
- }): Promise<MultiSpaceRunnerResult>;
504
+ readonly perSpaceOptions: ReadonlyArray<MigrationRunnerPerSpaceOptions<TFamilyId, TTargetId>>;
505
+ }): Promise<MigrationRunnerResult>;
555
506
  }
556
507
 
557
508
  // ============================================================================
@@ -85,6 +85,13 @@ export interface BaseSchemaIssue {
85
85
 
86
86
  export interface EnumValuesChangedIssue {
87
87
  readonly kind: 'enum_values_changed';
88
+ /**
89
+ * Namespace coordinate of the enum type that changed values. Populated by
90
+ * family verifiers that have the coordinate in scope when constructing the
91
+ * issue. Downstream planners trust this field as the authoritative subject
92
+ * coordinate and do not re-derive it by name lookup.
93
+ */
94
+ readonly namespaceId: string;
88
95
  readonly typeName: string;
89
96
  readonly addedValues: readonly string[];
90
97
  readonly removedValues: readonly string[];
@@ -119,6 +119,17 @@ export abstract class RuntimeCore<
119
119
  // satisfy `signal?: AbortSignal`).
120
120
  const codecCtx: CodecCallContext = signal === undefined ? {} : { signal };
121
121
 
122
+ // Per-execute middleware context. Spread the stored runtime-level
123
+ // template and mint a fresh `planExecutionId` so every hook in this
124
+ // call observes the same value, and two executions of the same plan
125
+ // observe distinct values. ADR 220. The same reference is threaded
126
+ // through `runBeforeExecuteChain` and `runWithMiddleware`; the plan
127
+ // itself flows through unchanged.
128
+ const execCtx: RuntimeMiddlewareContext = {
129
+ ...self.ctx,
130
+ planExecutionId: crypto.randomUUID(),
131
+ };
132
+
122
133
  async function* generator(): AsyncGenerator<Row, void, unknown> {
123
134
  // Pre-check the signal at entry so an already-aborted caller observes
124
135
  // RUNTIME.ABORTED on the first `next()` without any work being done.
@@ -130,13 +141,13 @@ export abstract class RuntimeCore<
130
141
  // plan before opening the row source. Families that need
131
142
  // pre-encode mutator visibility (SQL) override `execute` to
132
143
  // inject the same chain at the equivalent point.
133
- await runBeforeExecuteChain<TExec>(exec, self.middleware, self.ctx);
144
+ await runBeforeExecuteChain<TExec>(exec, self.middleware, execCtx);
134
145
  // The driver yields raw `Record<string, unknown>`; we cast to `Row` here.
135
146
  // The Row contract is enforced by the caller via `plan._row`.
136
147
  yield* runWithMiddleware<TExec, Row>(
137
148
  exec,
138
149
  self.middleware,
139
- self.ctx,
150
+ execCtx,
140
151
  () => self.runDriver(exec) as AsyncIterable<Row>,
141
152
  );
142
153
  }
@@ -79,6 +79,17 @@ export interface RuntimeMiddlewareContext {
79
79
  * scope. Existing middleware that ignore the field are unaffected.
80
80
  */
81
81
  readonly scope: 'runtime' | 'connection' | 'transaction';
82
+ /**
83
+ * Identity for one `execute()` call. The runtime mints a fresh value via
84
+ * `crypto.randomUUID()` when it constructs the per-execute context, and
85
+ * the same context reference is threaded through every middleware phase
86
+ * (`beforeExecute`, `intercept`, `onRow`, `afterExecute`). Every hook in
87
+ * one execute call therefore observes the same `planExecutionId`; two
88
+ * executions of the same plan observe distinct values. Use this to
89
+ * correlate observations across the lifecycle of a single execute call
90
+ * (tracing, timing, audit). See ADR 220.
91
+ */
92
+ readonly planExecutionId: string;
82
93
  }
83
94
 
84
95
  export interface AfterExecuteResult {
@@ -1,3 +1,4 @@
1
+ export { mergeCapabilityMatrices } from '../shared/capabilities';
1
2
  export type {
2
3
  AdapterDescriptor,
3
4
  AdapterInstance,
@@ -8,7 +8,6 @@ export type {
8
8
  } from '../control/control-capabilities';
9
9
  export {
10
10
  hasMigrations,
11
- hasMultiSpaceRunner,
12
11
  hasOperationPreview,
13
12
  hasPslContractInfer,
14
13
  hasSchemaView,
@@ -28,7 +27,6 @@ export type {
28
27
  ControlTargetInstance,
29
28
  } from '../control/control-instances';
30
29
  export type {
31
- MigrationHints,
32
30
  MigrationMetadata,
33
31
  MigrationOperationClass,
34
32
  MigrationOperationPolicy,
@@ -43,14 +41,11 @@ export type {
43
41
  MigrationRunner,
44
42
  MigrationRunnerExecutionChecks,
45
43
  MigrationRunnerFailure,
44
+ MigrationRunnerPerSpaceOptions,
45
+ MigrationRunnerPerSpaceSuccessValue,
46
46
  MigrationRunnerResult,
47
47
  MigrationRunnerSuccessValue,
48
48
  MigrationScaffoldContext,
49
- MultiSpaceCapableRunner,
50
- MultiSpaceRunnerFailure,
51
- MultiSpaceRunnerPerSpaceOptions,
52
- MultiSpaceRunnerResult,
53
- MultiSpaceRunnerSuccessValue,
54
49
  OpFactoryCall,
55
50
  SerializedQueryPlan,
56
51
  TargetMigrationsCapability,
package/src/exports/ir.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { domainElementCoordinates } from '../ir/domain';
1
2
  export type { IRNode } from '../ir/ir-node';
2
3
  export { freezeNode, IRNodeBase } from '../ir/ir-node';
3
4
  export type { Namespace } from '../ir/namespace';
@@ -0,0 +1,23 @@
1
+ import type { ApplicationDomain } from '@prisma-next/contract/types';
2
+ import type { EntityCoordinate } from './storage';
3
+
4
+ /**
5
+ * Lazy walk over every named domain entity in a {@link ApplicationDomain},
6
+ * yielded as {@link EntityCoordinate} tuples with `plane: 'domain'`.
7
+ *
8
+ * Same structural rules as {@link elementCoordinates} over storage: skip
9
+ * scalar `id`; each other object-valued property is an entity-kind slot.
10
+ */
11
+ export function* domainElementCoordinates(
12
+ domain: Pick<ApplicationDomain, 'namespaces'>,
13
+ ): Generator<EntityCoordinate> {
14
+ for (const [namespaceId, ns] of Object.entries(domain.namespaces)) {
15
+ for (const [entityKind, slot] of Object.entries(ns)) {
16
+ if (entityKind === 'id') continue;
17
+ if (slot === null || typeof slot !== 'object') continue;
18
+ for (const entityName of Object.keys(slot)) {
19
+ yield { plane: 'domain', namespaceId, entityKind, entityName };
20
+ }
21
+ }
22
+ }
23
+ }
package/src/ir/ir-node.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  * through a union of leaves where each leaf carries a literal kind, so
18
18
  * requiring `kind` at the base would be unearned. Future leaves that
19
19
  * earn polymorphic dispatch override with a required literal at that
20
- * leaf (e.g. `override readonly kind = 'postgres-enum' as const`).
20
+ * leaf (e.g. `override readonly kind = 'pack-contributed-kind' as const`).
21
21
  *
22
22
  * `IRNodeBase` carries no methods: the freeze-and-assign affordance
23
23
  * lives in the free `freezeNode` helper below. Keeping `freezeNode` out
@@ -1,3 +1,4 @@
1
+ import type { StorageNamespace } from '@prisma-next/contract/types';
1
2
  import { type IRNode, IRNodeBase } from './ir-node';
2
3
 
3
4
  /**
@@ -44,8 +45,8 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
44
45
  * own native idiom). Generic consumers walking "all named entries" go
45
46
  * through a family-typed namespace, not the framework `Namespace`.
46
47
  *
47
- * Every namespace concretion (e.g. `SqlNamespacePayload`,
48
- * `MongoNamespacePayload`, target-promoted namespaces like
48
+ * Every namespace concretion (e.g. family-built SQL namespaces,
49
+ * `MongoUnboundNamespace`, target-promoted namespaces like
49
50
  * `PostgresSchema`) carries exactly: `id` (enumerable string), `kind`
50
51
  * (non-enumerable string discriminator set via `Object.defineProperty`),
51
52
  * and one or more entity-kind slot maps — each an own-enumerable property
@@ -57,8 +58,7 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
57
58
  * on this invariant to enumerate entities structurally without
58
59
  * family-specific knowledge.
59
60
  */
60
- export interface Namespace extends IRNode {
61
- readonly id: string;
61
+ export interface Namespace extends IRNode, StorageNamespace {
62
62
  readonly kind: string;
63
63
  }
64
64
 
@@ -6,7 +6,7 @@ import type { IRNode } from './ir-node';
6
6
  * The slot is polymorphic at the framework level: a family or target can
7
7
  * persist either a JSON-clean codec-triple object literal (carrying
8
8
  * `kind: 'codec-instance'`) or a class-instance IR node with a narrower
9
- * kind discriminator (e.g. `'postgres-enum'`). Hydration walkers,
9
+ * kind discriminator (e.g. `'<kind>'`). Hydration walkers,
10
10
  * verifiers, and planners dispatch on the `kind` literal to recover the
11
11
  * precise variant.
12
12
  *
package/src/ir/storage.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { StorageBase } from '@prisma-next/contract/types';
1
2
  import type { IRNode } from './ir-node';
2
3
  import type { Namespace } from './namespace';
3
4
 
@@ -5,10 +6,9 @@ import type { Namespace } from './namespace';
5
6
  * Canonical address for a named entity in Contract IR / Schema IR.
6
7
  *
7
8
  * `plane` is `'domain' | 'storage'`: which top-level contract plane the
8
- * entity lives on. Domain-side walks (once domain content is populated)
9
- * yield `plane: 'domain'`; {@link elementCoordinates} over storage yields
10
- * `plane: 'storage'`. A sibling `elementCoordinates(domain)` is not wired
11
- * yet — domain-plane content lands in S1.C; the sibling walk lands there.
9
+ * entity lives on. Domain-side walks yield `plane: 'domain'` via
10
+ * {@link domainElementCoordinates}; {@link elementCoordinates} over storage
11
+ * yields `plane: 'storage'`.
12
12
  *
13
13
  * Cross-plane references obey a directional invariant: domain → storage is
14
14
  * allowed; storage → domain is forbidden. That rule is enforced by a
@@ -36,7 +36,9 @@ export interface EntityCoordinate {
36
36
  * property whose value is a non-null object, yields one coordinate per
37
37
  * entry key in that map. No family-specific slot vocabulary is required.
38
38
  */
39
- export function* elementCoordinates(storage: Storage): Generator<EntityCoordinate> {
39
+ export function* elementCoordinates(
40
+ storage: Pick<StorageBase, 'namespaces'>,
41
+ ): Generator<EntityCoordinate> {
40
42
  for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
41
43
  for (const [entityKind, slot] of Object.entries(ns)) {
42
44
  if (entityKind === 'id') continue;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Capability matrix merge primitive shared by emit-time and runtime stack composition.
3
+ *
4
+ * The CLI's `enrichContract` and the SQL runtime's `createExecutionContext` both need
5
+ * to fold a stack of component descriptors' `capabilities` declarations into a single
6
+ * matrix keyed by namespace. Keeping the primitive here lets both call sites stay
7
+ * byte-for-byte consistent without one depending on the other.
8
+ */
9
+
10
+ import { blindCast } from '@prisma-next/utils/casts';
11
+
12
+ type CapabilityMatrix = Record<string, Record<string, boolean>>;
13
+
14
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ function sortDeep(value: unknown): unknown {
19
+ if (Array.isArray(value)) {
20
+ return value.map(sortDeep);
21
+ }
22
+ if (!isPlainObject(value)) {
23
+ return value;
24
+ }
25
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
26
+ const next: Record<string, unknown> = {};
27
+ for (const [key, child] of entries) {
28
+ next[key] = sortDeep(child);
29
+ }
30
+ return next;
31
+ }
32
+
33
+ function extractCapabilityMatrix(value: unknown): CapabilityMatrix {
34
+ if (!isPlainObject(value)) return {};
35
+
36
+ const out: CapabilityMatrix = {};
37
+ for (const [namespace, maybeCaps] of Object.entries(value)) {
38
+ if (!isPlainObject(maybeCaps)) continue;
39
+ const caps: Record<string, boolean> = {};
40
+ for (const [key, flag] of Object.entries(maybeCaps)) {
41
+ if (typeof flag === 'boolean') {
42
+ caps[key] = flag;
43
+ }
44
+ }
45
+ if (Object.keys(caps).length > 0) {
46
+ out[namespace] = caps;
47
+ }
48
+ }
49
+
50
+ return out;
51
+ }
52
+
53
+ /**
54
+ * Merge an ordered list of contributor capability declarations into a base matrix.
55
+ *
56
+ * Behaviour:
57
+ * - `base` and each contributor's `capabilities` are filtered through the same
58
+ * structural extraction: non-plain-object namespace blocks are dropped,
59
+ * non-boolean leaves inside a namespace block are dropped, and a namespace
60
+ * block that ends up with zero boolean leaves is omitted entirely (so a
61
+ * later contributor with a malformed namespace cannot erase a namespace
62
+ * already present in `base`).
63
+ * - Non-plain-object `capabilities` on a contributor (including `undefined`,
64
+ * `null`, arrays, primitives) are skipped silently — the contributor
65
+ * contributes nothing.
66
+ * - Later contributors win on `(namespace, key)` collisions.
67
+ * - The returned object is fresh — neither `base` nor any contributor is mutated.
68
+ * - Output keys are sorted lexicographically at every plain-object level.
69
+ */
70
+ export function mergeCapabilityMatrices(
71
+ base: Record<string, Record<string, boolean>>,
72
+ contributors: ReadonlyArray<{ readonly capabilities?: unknown }>,
73
+ ): Record<string, Record<string, boolean>> {
74
+ const merged: CapabilityMatrix = extractCapabilityMatrix(base);
75
+
76
+ for (const contributor of contributors) {
77
+ const extracted = extractCapabilityMatrix(contributor.capabilities);
78
+ for (const [namespace, capabilities] of Object.entries(extracted)) {
79
+ merged[namespace] = {
80
+ ...(merged[namespace] ?? {}),
81
+ ...capabilities,
82
+ };
83
+ }
84
+ }
85
+
86
+ return blindCast<
87
+ CapabilityMatrix,
88
+ "sortDeep preserves the matrix shape but the recursive generic relationship can't be expressed to TS"
89
+ >(sortDeep(merged));
90
+ }