graphddb 0.1.1 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SelectableOf, f as PrimaryKeyOf, E as Executor, g as SegmentedKey, h as SelectBuilderSpec, i as ExecutionPlan, R as ResolvedKey, D as DynamoDBOperation, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, T as TransactWriteExecItem, j as CdcEmulatorOptions, k as ChangeHandler, l as Unsubscribe, F as FaultSpec, m as ConcurrentRecomputeRef, C as ChangeEvent, n as EventLog, o as ReplayOptions, p as ShardId, e as DDBModel, M as ModelStatic, q as PartialQueryKeyOf, r as StrictSelectSpec, s as EntityInput, t as UniqueQueryKeyOf } from './types-CDrWiPxp.js';
2
- export { u as BatchDeleteRequest, v as BatchGetRequest, w as BatchGetResult, x as BatchPutRequest, y as BatchResult, z as BatchWriteRequest, A as CdcMode, G as ChangeBatch, H as ChangeEventName, I as ClockMode, J as Column, K as ColumnMap, L as CondSlot, N as ConditionCheckInput, O as DeleteOptions, Q as FilterInput, V as GsiDefinitionMarker, X as GsiOptions, Y as KeyDefinitionMarker, Z as KeySegment, _ as KeySlot, $ as KeyStructure, a0 as PutOptions, a1 as QueryKeyOf, a2 as QueryResult, a3 as RawCondition, a4 as RelationBuilder, a5 as RelationSelect, a6 as RelationSpec, a7 as SegmentSpec, a8 as SelectBuilder, a9 as SelectOf, aa as StartingPosition, ab as StreamViewType, ac as TransactionContext, ad as Updatable, ae as UpdateOptions, af as attachModelClass, ag as buildDeleteInput, ah as buildPutInput, ai as buildUpdateInput, aj as cond, ak as executeBatchGet, al as executeBatchWrite, am as executeDelete, an as executePut, ao as executeTransaction, ap as executeUpdate, aq as gsi, ar as isColumn, as as isKeySegment, at as k, au as key } from './types-CDrWiPxp.js';
1
+ import { S as SelectableOf, f as PrimaryKeyOf, E as Executor, g as ExecutionPlanSpec, h as SegmentedKey, i as SelectBuilderSpec, R as RawCondition, j as TransactionSpec, k as Manifest, l as ExecutionPlan, m as ResolvedKey, D as DynamoDBOperation, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, T as TransactWriteExecItem, n as CdcEmulatorOptions, o as ChangeHandler, p as Unsubscribe, F as FaultSpec, q as ConcurrentRecomputeRef, C as ChangeEvent, r as EventLog, s as ReplayOptions, t as ShardId, u as TransactionItemSpec, v as Param, w as ParamDescriptor, x as DefinitionMap, O as OperationDefinition, e as DDBModel, M as ModelStatic, y as WriteDefinitionOptions, z as PartialQueryKeyOf, A as StrictSelectSpec, G as EntityInput, H as UniqueQueryKeyOf, I as EntityRef, J as ConditionInput, Q as QueryModelContract, K as QueryMethodSpec, L as CommandModelContract, N as CommandMethodSpec, V as ContractSpec, X as QuerySpec, Y as CommandSpec, Z as ContextSpec, _ as OperationsDocument, $ as AnyOperationDefinition, a0 as BridgeBundle, a1 as ConditionSpec } from './types-DPJ4tPjX.js';
2
+ export { a2 as BatchDeleteRequest, a3 as BatchGetRequest, a4 as BatchGetResult, a5 as BatchPutRequest, a6 as BatchResult, a7 as BatchWriteRequest, a8 as CONTRACT_RANGE_FANOUT_CONCURRENCY, a9 as CdcMode, aa as ChangeBatch, ab as ChangeEventName, ac as ClockMode, ad as Column, ae as ColumnMap, af as CommandContractMethodSpec, ag as CommandInputShape, ah as CommandMethod, ai as CommandPlan, aj as CommandResolutionTarget, ak as CommandResultKind, al as CommandSelectShape, am as CompiledFragment, an as ComposeSpec, ao as CondSlot, ap as ConditionCheckInput, aq as Connection, ar as ContractCallSignature, as as ContractCardinality, at as ContractCommandParams, au as ContractCommandResult, av as ContractComposeNode, aw as ContractFromRef, ax as ContractInputArity, ay as ContractItem, az as ContractKeyFieldRef, aA as ContractKeyInput, aB as ContractKeyRef, aC as ContractKeySpec, aD as ContractKind, aE as ContractMethodOp, aF as ContractParamRef, aG as ContractQueryParams, aH as ContractResolution, aI as DeleteOptions, aJ as DeriveEffect, aK as DerivedEdgeWrite, aL as DerivedUpdate, aM as DescriptorBinding, aN as ENTITY_WRITES_MARKER, aO as EdgeEffect, aP as EffectPath, aQ as EmitEffect, aR as EntityWritesDefinition, aS as EntityWritesShape, aT as ExecutableCommandContract, aU as ExecutableQueryContract, aV as FilterInput, aW as FilterSpec, aX as FragmentInput, aY as GsiDefinitionMarker, aZ as GsiOptions, a_ as IdempotencyEffect, a$ as InProcessWriteDescriptor, b0 as InputArity, b1 as KeyDefinitionMarker, b2 as KeySegment, b3 as KeySlot, b4 as KeyStructure, b5 as KeyedResult, b6 as LIFECYCLE_CONTRACT_MARKER, b7 as LifecycleContract, b8 as LifecycleEffects, b9 as LiteralParam, ba as ManifestEntity, bb as ManifestField, bc as ManifestFieldType, bd as ManifestGsi, be as ManifestKey, bf as ManifestRelation, bg as ManifestTable, bh as ModelRef, bi as MutateMode, bj as MutateOptions, bk as MutateParallelResult, bl as MutateTransactionResult, bm as MutationBody, bn as MutationDescriptorMap, bo as MutationFragment, bp as MutationInputProxy, bq as MutationInputRef, br as MutationIntent, bs as NumberParam, bt as OperationKind, bu as OperationSpec, bv as ParallelOpResult, bw as ParamKind, bx as ParamSpec, by as ParamStructure, bz as PlannedCommandMethod, bA as PutOptions, bB as QueryContractMethodSpec, bC as QueryEnvelopeResult, bD as QueryKeyOf, bE as QueryMethod, bF as QueryResult, bG as RangeConditionSpec, bH as ReadEnvelope, bI as ReadOperationType, bJ as ReadRouteDescriptor, bK as ReadRouteOptions, bL as ReadRouteResult, bM as RecordedCompose, bN as RelationBuilder, bO as RelationSelect, bP as RelationSpec, bQ as RequiresEffect, bR as Resolution, bS as SPEC_VERSION, bT as SegmentSpec, bU as SelectBuilder, bV as SelectOf, bW as StartingPosition, bX as StreamViewType, bY as StringParam, bZ as TransactionContext, b_ as TransactionItemType, b$ as UniqueEffect, c0 as Updatable, c1 as UpdateOptions, c2 as WhenSpec, c3 as WriteDescriptor, c4 as WriteEnvelope, c5 as WriteLifecyclePhase, c6 as WriteOperationType, c7 as WriteRecorder, c8 as WriteResultProjection, c9 as attachModelClass, ca as buildDeleteInput, cb as buildPutInput, cc as buildUpdateInput, cd as compileFragment, ce as compileMutationPlan, cf as compileSingleFragmentPlan, cg as cond, ch as contractOfMethodSpec, ci as definePlan, cj as entityWrites, ck as executeBatchGet, cl as executeBatchWrite, cm as executeCommandMethod, cn as executeDelete, co as executeKeyedBatchGet, cp as executePut, cq as executeQueryMethod, cr as executeRangeFanout, cs as executeTransaction, ct as executeUpdate, cu as from, cv as getEntityWrites, cw as gsi, cx as isColumn, cy as isCommandModelContract, cz as isCommandPlan, cA as isContractComposeNode, cB as isContractFromRef, cC as isContractKeyFieldRef, cD as isContractKeyRef, cE as isContractParamRef, cF as isEntityWritesDefinition, cG as isKeySegment, cH as isLifecycleContract, cI as isMutationFragment, cJ as isMutationInputRef, cK as isParam, cL as isPlannedCommandMethod, cM as isQueryModelContract, cN as k, cO as key, cP as lifecyclePhaseForIntent, cQ as mintContractKeyFieldRef, cR as mintContractParamRef, cS as mutation, cT as param, cU as publicCommandModel, cV as publicQueryModel, cW as query, cX as resolveLifecycle, cY as wholeKeysSentinel } from './types-DPJ4tPjX.js';
3
3
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4
4
  import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
5
5
 
@@ -122,6 +122,60 @@ declare const BATCH_GET_MAX_KEYS = 100;
122
122
  */
123
123
  declare const BATCH_WRITE_MAX_ITEMS = 25;
124
124
 
125
+ /**
126
+ * The single source of truth for **relation execution staging** (issue #70),
127
+ * shared by the static SSoT generator (`src/spec/operations.ts`,
128
+ * {@link buildQuerySpec}) and the TS host runtime
129
+ * (`src/relation/traversal.ts`, {@link resolveRelations}). It is the TS analogue
130
+ * of the Python runtime's `_relation_stages` / `_plan_concurrency`
131
+ * (`python/graphddb_runtime/runtime.py`).
132
+ *
133
+ * ## Why one shared module (anti-drift)
134
+ *
135
+ * #70a records an {@link ExecutionPlanSpec} (`groups` + `concurrency`) in the
136
+ * SSoT; #70b made the Python runtime **honor** it; #70c makes the TS host runtime
137
+ * honor it too. For the two runtimes to make **identical** plan-declared
138
+ * decisions, TS must consume *the same plan facts* the generator serialized — not
139
+ * re-derive parallelism by an independent "parallelize every sibling" walk. The
140
+ * host runtime has no generated JSON to load (it works off live decorator
141
+ * metadata + a `select` tree), so it derives the plan from that live tree using
142
+ * **this exact function** — the same one the generator calls to produce the
143
+ * serialized `executionPlan`. Generator and runtime therefore cannot drift: they
144
+ * are one codepath. {@link deriveExecutionPlan} is the seam.
145
+ *
146
+ * ## How the runtime obtains the plan
147
+ *
148
+ * The generator produces `operations[]` (root + relation ops, each with a
149
+ * `resultPath`) and runs {@link deriveExecutionPlan} over their result paths. The
150
+ * host runtime walks the live `select` tree with {@link relationResultPaths},
151
+ * which emits the **identical** ordered `resultPath` list
152
+ * (`buildRelationOperations` is refactored to use it), and runs the **same**
153
+ * {@link deriveExecutionPlan} over `['$', ...those]`. The resulting
154
+ * {@link ExecutionPlanSpec} is threaded through the recursive relation traversal:
155
+ * at each parent level the traversal honors the plan's grouping (which child ops
156
+ * are an independent, concurrency-eligible stage) and the declared `concurrency`
157
+ * bound — reading them from the plan, never re-deciding them.
158
+ */
159
+
160
+ /**
161
+ * A **resolved** relation execution plan as the host runtime consumes it: the
162
+ * serialized {@link ExecutionPlanSpec} (`groups` over operation indices +
163
+ * `concurrency`) together with the `resultPath` each operation index produces, so
164
+ * the recursive traversal can locate "the operations at this parent level" and
165
+ * read their declared stage from the plan.
166
+ *
167
+ * Built once at a read's entry point ({@link buildRelationExecutionPlan}) and
168
+ * threaded, unchanged, through every recursion level. A read whose `select` has
169
+ * **no** relations (or where the plan would be a single op) carries no plan; the
170
+ * traversal then has nothing to stage.
171
+ */
172
+ interface RelationExecutionPlan {
173
+ /** The serialized staging facts (issue #70a) the traversal honors. */
174
+ readonly plan: ExecutionPlanSpec;
175
+ /** Operation index → the `resultPath` it writes (index 0 = root `$`). */
176
+ readonly resultPaths: readonly string[];
177
+ }
178
+
125
179
  type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
126
180
  interface FieldOptions {
127
181
  format?: 'datetime' | 'date';
@@ -366,22 +420,8 @@ interface ConditionExpressionResult {
366
420
  names: Record<string, string>;
367
421
  values: Record<string, unknown>;
368
422
  }
369
- /**
370
- * Build a DynamoDB ConditionExpression from a condition object (issues #46, #81).
371
- *
372
- * - `{ notExists: true }` → `attribute_not_exists(PK)` (legacy whole-row guard)
373
- * - `{ attributeExists: 'email' }` → `attribute_exists(#cond_email)` (any field,
374
- * incl. PK/SK)
375
- * - `{ attributeNotExists: 'email' }` → `attribute_not_exists(#cond_email)`
376
- * - `{ version: 3 }` → `#cond_version = :cond0`
377
- * - `{ version: 3, status: 'active' }` → `#cond_version = :cond0 AND #cond_status = :cond1`
378
- *
379
- * Attribute name/value keys use the `cond_` prefix to avoid collisions with
380
- * UpdateExpression names/values. The existence primitives go through a `#name`
381
- * placeholder rather than a bare attribute so reserved-word fields (and PK/SK)
382
- * are always legal.
383
- */
384
- declare function buildConditionExpression(condition: Record<string, unknown>): ConditionExpressionResult;
423
+
424
+ declare function buildConditionExpression(condition: Record<string, unknown> | RawCondition): ConditionExpressionResult;
385
425
 
386
426
  /**
387
427
  * Compiled DynamoDB `FilterExpression`. Names are `#`-aliased column
@@ -412,732 +452,6 @@ declare function compileFilterExpression(filter: Record<string, unknown> | undef
412
452
  */
413
453
  declare function evaluateFilter(item: Record<string, unknown>, filter: Record<string, unknown>): boolean;
414
454
 
415
- /**
416
- * Parameter placeholders for the parameterized query / command definition DSL
417
- * (issue #41, Python-bridge Phase 0a).
418
- *
419
- * A `Param<T>` is a **branded placeholder** standing in for a value that is not
420
- * known at definition time but is supplied later (at execution, e.g. from
421
- * Python). It carries:
422
- *
423
- * - the **TypeScript value type** `T` it represents (`string`, `number`, or a
424
- * string-literal union), preserved through the IR and the inferred return
425
- * types of `defineQueries` / `defineCommands`; and
426
- * - a **runtime descriptor** (`kind` + optional `literals`) that the static
427
- * planner (#42) and the code generator read to emit `operations.json`.
428
- *
429
- * Placeholders are created with `param.string()`, `param.number()`, and
430
- * `param.literal(...)`. They are *only* legal at value positions inside a
431
- * parameterized key / changes structure passed to the `define*` entry points —
432
- * never to the live `Model.query` / `Model.put` runtime API, whose types remain
433
- * uncontaminated by params.
434
- */
435
- /** Brand tag preventing a plain object from structurally matching a {@link Param}. */
436
- declare const PARAM_BRAND: unique symbol;
437
- /** Runtime discriminant for a parameter placeholder. */
438
- type ParamKind = 'string' | 'number' | 'literal' | 'array';
439
- /**
440
- * A branded parameter placeholder representing a value of TypeScript type `T`
441
- * that is bound at execution time rather than definition time.
442
- *
443
- * The brand (`[PARAM_BRAND]: T`) keeps `Param<string>` and `Param<number>`
444
- * distinct and prevents a bare value from being mistaken for a placeholder.
445
- *
446
- * @typeParam T - The value type this placeholder stands in for.
447
- */
448
- interface Param<out T> {
449
- /** @internal Type brand carrying the represented value type. */
450
- readonly [PARAM_BRAND]: T;
451
- /** Runtime discriminant: `'string'` | `'number'` | `'literal'` | `'array'`. */
452
- readonly kind: ParamKind;
453
- /**
454
- * For `param.literal(...)`, the allowed literal values (preserved at runtime
455
- * for the generator). `undefined` for `string` / `number`.
456
- */
457
- readonly literals?: readonly T[];
458
- /**
459
- * For `param.array({...})`, the descriptor of each element's fields. Lets the
460
- * transaction planner (#46) type `forEach` element references and emit the
461
- * `{item.<field>}` templates. `undefined` for scalar params.
462
- */
463
- readonly element?: Readonly<Record<string, Param<unknown>>>;
464
- }
465
- /** A `Param` whose represented value type is `string`. */
466
- type StringParam = Param<string>;
467
- /** A `Param` whose represented value type is `number`. */
468
- type NumberParam = Param<number>;
469
- /** A `Param` whose represented value type is the literal union `L`. */
470
- type LiteralParam<L extends string | number> = Param<L>;
471
- /** The element-field descriptor shape accepted by {@link param.array}. */
472
- type ArrayElementShape = Record<string, Param<unknown>>;
473
- /**
474
- * The TypeScript element type implied by an {@link ArrayElementShape} `E`: each
475
- * field carries the value type its placeholder represents.
476
- */
477
- type ElementOf<E extends ArrayElementShape> = {
478
- [K in keyof E]: E[K] extends Param<infer V> ? V : never;
479
- };
480
- /** A `Param` standing in for an **array** whose elements have shape `E`. */
481
- type ArrayParam<E extends ArrayElementShape> = Param<ElementOf<E>[]> & {
482
- readonly element: E;
483
- };
484
- /** Allowed scalar value types a placeholder may stand in for. */
485
- type ParamValue = string | number;
486
- /**
487
- * Placeholder factory. Each call returns a branded {@link Param} carrying both
488
- * the TypeScript value type and a runtime descriptor.
489
- */
490
- declare const param: {
491
- /** A placeholder for a `string` value. */
492
- readonly string: () => StringParam;
493
- /** A placeholder for a `number` value. */
494
- readonly number: () => NumberParam;
495
- /**
496
- * A placeholder constrained to one of the given literal values. The value
497
- * type of the resulting {@link Param} is the **union of the literals**, so it
498
- * is preserved precisely in the IR and the inferred definition types.
499
- *
500
- * @example
501
- * ```ts
502
- * param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
503
- * ```
504
- */
505
- readonly literal: <const L extends readonly [ParamValue, ...ParamValue[]]>(...values: L) => LiteralParam<L[number]>;
506
- /**
507
- * A placeholder for an **array** parameter whose elements have the given field
508
- * shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
509
- * each element's fields to `{item.<field>}` templates.
510
- *
511
- * @example
512
- * ```ts
513
- * param.array({ userId: param.string(), role: param.string() });
514
- * // Param<{ userId: string; role: string }[]>
515
- * ```
516
- */
517
- readonly array: <const E extends ArrayElementShape>(element: E) => ArrayParam<E>;
518
- };
519
- /** Runtime type guard: is `value` a parameter placeholder? */
520
- declare function isParam(value: unknown): value is Param<unknown>;
521
-
522
- /**
523
- * Serializable static operation specs and manifest (issue #42, Python-bridge
524
- * Phase 0b).
525
- *
526
- * Everything in this module is **JSON-serializable** by construction (only
527
- * strings / numbers / booleans / arrays / plain objects — no functions, classes,
528
- * `Date`, `Set`, or `undefined`-valued fields are emitted). These types mirror
529
- * the `graphddb_manifest.json` / `graphddb_operations.json` shapes sketched in
530
- * `docs/python-bridge.md`. The generator (#43) writes them to disk; the
531
- * Python runtime (#44+) interprets them.
532
- *
533
- * ## Templates and placeholders
534
- *
535
- * Key-condition / range / item / change *values* are **template strings** with
536
- * two placeholder forms:
537
- *
538
- * - `{paramName}` — bound from the caller-supplied params at execution time.
539
- * - `{result.sourceField}` — bound from a field of the **prior** operation's
540
- * result item(s), used to chain relation operations.
541
- *
542
- * A value with no `{...}` is a literal (e.g. a fixed sort-key discriminator
543
- * `PROFILE`).
544
- */
545
-
546
- /** The schema version emitted in every manifest / operations document. */
547
- declare const SPEC_VERSION: "1.0";
548
- /** Field type as carried in the manifest (derived from the DynamoDB type). */
549
- type ManifestFieldType = 'string' | 'number' | 'boolean' | 'binary' | 'stringSet' | 'numberSet' | 'list' | 'map';
550
- interface ManifestField {
551
- readonly type: ManifestFieldType;
552
- /**
553
- * Semantic format for `string` fields that carry a serialized temporal value.
554
- * `datetime` → ISO 8601 instant, `date` → `YYYY-MM-DD`. Absent for plain
555
- * fields. The Python runtime uses this to restore `datetime` on hydration,
556
- * mirroring the TS hydrator's `format`-driven `Date` reconstruction.
557
- */
558
- readonly format?: 'datetime' | 'date';
559
- }
560
- /** A key (PK or GSI) template descriptor in the manifest. */
561
- interface ManifestKey {
562
- /** Input field names the key is composed from (in declaration order). */
563
- readonly inputFields: readonly string[];
564
- /** Template for the partition-key value, e.g. `EMAIL#{email}`. */
565
- readonly pkTemplate: string;
566
- /** Template for the sort-key value, or `null` when the key has no sort key. */
567
- readonly skTemplate: string | null;
568
- }
569
- interface ManifestGsi extends ManifestKey {
570
- readonly indexName: string;
571
- readonly unique: boolean;
572
- }
573
- interface ManifestRelation {
574
- readonly type: 'hasMany' | 'hasOne' | 'belongsTo';
575
- /** Target entity (model class) name. */
576
- readonly target: string;
577
- /** target field → source field (on this entity). */
578
- readonly keyBinding: Readonly<Record<string, string>>;
579
- }
580
- interface ManifestEntity {
581
- /** Declared (logical) table name. */
582
- readonly table: string;
583
- /** Physical table name (after `TableMapping` resolution). */
584
- readonly physicalName: string;
585
- /** PK prefix, e.g. `USER#`. */
586
- readonly prefix: string;
587
- readonly fields: Readonly<Record<string, ManifestField>>;
588
- /** Primary key, or `null` when the entity has none. */
589
- readonly key: ManifestKey | null;
590
- readonly gsis: readonly ManifestGsi[];
591
- readonly relations: Readonly<Record<string, ManifestRelation>>;
592
- }
593
- interface ManifestTable {
594
- readonly physicalName: string;
595
- }
596
- interface Manifest {
597
- readonly version: typeof SPEC_VERSION;
598
- readonly tables: Readonly<Record<string, ManifestTable>>;
599
- readonly entities: Readonly<Record<string, ManifestEntity>>;
600
- }
601
- interface ParamSpec {
602
- readonly type: ParamKind;
603
- readonly required: boolean;
604
- /** Allowed literal values for `literal` params; omitted otherwise. */
605
- readonly literals?: readonly (string | number)[];
606
- /**
607
- * For an `array` param (transaction `forEach` source), the per-element field
608
- * descriptors (field name → element param spec). Omitted for scalar params.
609
- */
610
- readonly element?: Readonly<Record<string, ParamSpec>>;
611
- }
612
- /** A `begins_with` range condition on a sort key (templated value). */
613
- interface RangeConditionSpec {
614
- readonly operator: 'begins_with';
615
- readonly key: string;
616
- /** Template value, e.g. `GROUP#` or `GROUP#{result.groupId}`. */
617
- readonly value: string;
618
- }
619
- /** Server-side declarative filter, carried verbatim for the runtime to compile. */
620
- interface FilterSpec {
621
- /** The declarative {@link FilterInput} tree (JSON-safe). */
622
- readonly declarative: unknown;
623
- }
624
- type ReadOperationType = 'Query' | 'GetItem' | 'BatchGetItem';
625
- /**
626
- * A single static read operation. Relation chains are expressed as multiple
627
- * operations whose `resultPath` / templated key conditions wire them together.
628
- */
629
- interface OperationSpec {
630
- readonly type: ReadOperationType;
631
- readonly tableName: string;
632
- readonly indexName?: string;
633
- /**
634
- * Key condition: attribute name → template value. For a `BatchGetItem` this is
635
- * the per-key shape (one key per resolved source item at runtime).
636
- */
637
- readonly keyCondition: Readonly<Record<string, string>>;
638
- readonly rangeCondition?: RangeConditionSpec;
639
- /** Projected fields (entity field names). */
640
- readonly projection: readonly string[];
641
- readonly limit?: number;
642
- readonly filter?: FilterSpec;
643
- /**
644
- * Where the result of this operation is placed in the assembled result.
645
- * `$` is the root; `$.groups.items` a relation connection's items, etc.
646
- */
647
- readonly resultPath: string;
648
- /**
649
- * For relation operations: the source field on the prior result whose value(s)
650
- * drive this operation's `{result.*}` placeholders. Absent on the root op.
651
- */
652
- readonly sourceField?: string;
653
- }
654
- /**
655
- * The **execution plan** for a multi-operation read (issue #70a). It is the
656
- * Single Source of Truth for *how* a query's {@link OperationSpec}[] is staged:
657
- * which operations are independent (may run concurrently) and which must wait for
658
- * a prior operation's result. Every runtime that honors it produces identical
659
- * effects with identical concurrency discipline — the staging is **serialized,
660
- * not re-derived** (cf. the contract decided-facts discipline).
661
- *
662
- * - `groups` is an ordered list of **stages**; each stage is a list of indices
663
- * into the query's `operations[]`. Operations **within** a stage are mutually
664
- * independent and may be issued concurrently; stages run **in order** because a
665
- * later stage reads a `{result.*}` value produced by an earlier one. Stage 0 is
666
- * always `[0]` (the root). Every operation index appears in exactly one stage,
667
- * and the stages are a topological layering of the result-dependency graph.
668
- * - `concurrency` is the declared in-flight bound a runtime applies **within**
669
- * each stage (the shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16): at most
670
- * this many operations of a stage are issued at once.
671
- *
672
- * ## Derivation (see `buildReadOperations` / `deriveExecutionPlan`)
673
- *
674
- * The planner emits `operations[]` in a deterministic pre-order: the root at
675
- * index 0, then each relation child immediately followed by its own descendants.
676
- * A child's key templates reference `{result.<sourceField>}` of the **operation
677
- * whose `resultPath` is the child's parent path** — that producer is the child's
678
- * sole dependency. The stage of an operation is therefore `1 + stage(parent)`:
679
- * the root is stage 0; sibling relations that share a parent path land in the
680
- * **same** stage (no inter-`{result.*}` dependency between siblings, so they are
681
- * independent and concurrency-eligible); a grandchild that reads its parent's
682
- * `{result.*}` lands one stage later. This mirrors exactly the level-by-level
683
- * fan-out the TS relation runtime performs at runtime (`resolveRelations` issues
684
- * sibling relations together under the same bound) — #70a only *records* it.
685
- *
686
- * Absent on a single-operation spec and on specs produced before #70a; a consumer
687
- * without a plan falls back to one-operation-per-stage **sequential** execution
688
- * (the pre-#70 behavior), so an absent plan never changes results.
689
- */
690
- interface ExecutionPlanSpec {
691
- /** Ordered stages; each stage is indices into `operations[]` (stage 0 = `[0]`). */
692
- readonly groups: readonly (readonly number[])[];
693
- /** The declared in-flight bound applied within each stage (16). */
694
- readonly concurrency: number;
695
- }
696
- /** A query (read) definition's full execution spec. */
697
- interface QuerySpec {
698
- readonly params: Readonly<Record<string, ParamSpec>>;
699
- readonly operations: readonly OperationSpec[];
700
- /**
701
- * Result cardinality of the **root** (`$`) operation, derived from the
702
- * definition kind: `'one'` for `defineQuery` (a single entity object, with any
703
- * relations attached directly), `'many'` for `defineList` (a `{ items, cursor }`
704
- * connection). The relation runtime (#45) uses this to shape the root result:
705
- * a `'one'` Query root takes the first matched item rather than returning a
706
- * connection. Absent in specs produced before #45; consumers should treat an
707
- * absent value as `'many'` for a `Query`/`BatchGetItem` root and `'one'` for a
708
- * `GetItem` root (the pre-#45 behavior).
709
- */
710
- readonly cardinality?: 'one' | 'many';
711
- /**
712
- * The staged execution plan (issue #70a): which {@link operations} are
713
- * independent (concurrency-eligible) vs. result-dependent. Present only when the
714
- * query has more than one operation (a relation chain); a single-operation read
715
- * needs no plan. See {@link ExecutionPlanSpec} for the derivation and the
716
- * backward-compatible (plan-absent → sequential) fallback.
717
- */
718
- readonly executionPlan?: ExecutionPlanSpec;
719
- }
720
- type WriteOperationType = 'PutItem' | 'UpdateItem' | 'DeleteItem';
721
- /**
722
- * A condition expression on a write, as a JSON-serializable spec (issues #46,
723
- * #81). The supported subset:
724
- *
725
- * - `{ kind: 'notExists' }` → `attribute_not_exists(PK)` (legacy whole-row guard).
726
- * - `{ kind: 'attributeExists'; field }` → `attribute_exists(<field>)` — the named
727
- * attribute (any field, incl. PK/SK) must be present. The foundation for
728
- * referential-integrity derivation.
729
- * - `{ kind: 'attributeNotExists'; field }` → `attribute_not_exists(<field>)` — the
730
- * named attribute must be absent (field-level uniqueness / first-write guard).
731
- * - `{ kind: 'equals'; fields }` → `#f = :v AND …` field equality (each value a
732
- * `{param}` / literal template).
733
- */
734
- type ConditionSpec = {
735
- readonly kind: 'notExists';
736
- } | {
737
- readonly kind: 'attributeExists';
738
- readonly field: string;
739
- } | {
740
- readonly kind: 'attributeNotExists';
741
- readonly field: string;
742
- } | {
743
- readonly kind: 'equals';
744
- readonly fields: Readonly<Record<string, string>>;
745
- };
746
- interface CommandSpec {
747
- readonly type: WriteOperationType;
748
- readonly tableName: string;
749
- readonly entity: string;
750
- readonly params: Readonly<Record<string, ParamSpec>>;
751
- /**
752
- * Key condition for `UpdateItem` / `DeleteItem` (attribute name → template).
753
- * Absent for `PutItem` (the whole item is built from `item`).
754
- */
755
- readonly keyCondition?: Readonly<Record<string, string>>;
756
- /** The full item template for `PutItem` (field name → template / literal). */
757
- readonly item?: Readonly<Record<string, string>>;
758
- /** Field-level changes for `UpdateItem` (field name → template / literal). */
759
- readonly changes?: Readonly<Record<string, string>>;
760
- /** Optional write condition (subset). */
761
- readonly condition?: ConditionSpec;
762
- }
763
- /**
764
- * A declarative `when` guard on a transaction item (issue #46). Compares a
765
- * templated left-hand value against a templated right-hand value; the item is
766
- * skipped when the comparison does not hold. Both values are template strings
767
- * (`{param}` / `{item.<field>}` / literal), resolved by the runtime before the
768
- * comparison. The runtime compares the resolved **string** values.
769
- */
770
- interface WhenSpec {
771
- readonly op: 'eq' | 'ne';
772
- /** Templated left-hand value (typically an `{item.<field>}` or `{param}`). */
773
- readonly left: string;
774
- /** Templated right-hand value (literal or another reference). */
775
- readonly right: string;
776
- }
777
- /**
778
- * The kind of a transaction item. The three write kinds (`Put` / `Update` /
779
- * `Delete`) mutate an item; `ConditionCheck` (issue #81) is a **read-only
780
- * assertion** on another item — it asserts a {@link ConditionSpec} holds for the
781
- * keyed row without modifying it, and its failure cancels the **whole**
782
- * `TransactWriteItems` atomically. It is the foundation for referential-integrity
783
- * derivation (proposal: `requires <Entity> exists` → a `ConditionCheck` with
784
- * `attribute_exists`).
785
- */
786
- type TransactionItemType = 'Put' | 'Update' | 'Delete' | 'ConditionCheck';
787
- /**
788
- * A single templated item in a transaction. When `forEach` is present, the item
789
- * is expanded **once per element** of the named array param, with each element's
790
- * fields bound to the item's `{item.<field>}` placeholders.
791
- *
792
- * Field presence by `type`:
793
- *
794
- * | type | item | keyCondition | changes | add | condition |
795
- * |----------------|------|--------------|---------|-----|--------------------------|
796
- * | `Put` | ✓ | — | — | — | optional |
797
- * | `Update` | — | ✓ | ✓ / — | ✓ / — | optional |
798
- * | `Delete` | — | ✓ | — | — | optional |
799
- * | `ConditionCheck` | — | ✓ | — | — | **required** (the assert) |
800
- *
801
- * An `Update` carries `changes` (a `SET` of named fields) and/or `add` (an atomic
802
- * `ADD` of named numeric fields, issue #85) — at least one of the two. The two are
803
- * distinct DynamoDB update actions: `SET #f = :v` **overwrites**, while
804
- * `ADD #f :delta` **atomically increments** without a read (concurrency-safe), so
805
- * a derived counter (`User.postCount += 1`) MUST be an `add`, never a `changes`
806
- * `SET` (which would clobber a concurrent increment).
807
- */
808
- interface TransactionItemSpec {
809
- readonly type: TransactionItemType;
810
- readonly tableName: string;
811
- readonly entity: string;
812
- /** Put: the full item template (field → template / literal). */
813
- readonly item?: Readonly<Record<string, string>>;
814
- /** Update / Delete / ConditionCheck: the key template (attribute → template). */
815
- readonly keyCondition?: Readonly<Record<string, string>>;
816
- /** Update: field-level `SET` change templates (overwrite). */
817
- readonly changes?: Readonly<Record<string, string>>;
818
- /**
819
- * Update: field-level atomic `ADD` deltas (issue #85, derived counters). Each
820
- * value is a numeric template (`{param}` / a literal number string); the runtime
821
- * applies `ADD #field :delta`, an atomic increment that needs no prior read and
822
- * is safe under concurrency. A negative delta decrements (e.g. `-1` on a remove).
823
- */
824
- readonly add?: Readonly<Record<string, string>>;
825
- /**
826
- * The write / assertion condition (subset). Optional on `Put` / `Update` /
827
- * `Delete`; **required** on a `ConditionCheck` (the assertion it makes).
828
- */
829
- readonly condition?: ConditionSpec;
830
- /** Optional declarative guard; the item is skipped when it does not hold. */
831
- readonly when?: WhenSpec;
832
- /** When present, expand this item once per element of the named array param. */
833
- readonly forEach?: {
834
- readonly source: string;
835
- };
836
- /**
837
- * Marks a **raw marker-row** write (issue #86 uniqueness guard): the row is NOT a
838
- * modeled entity, so its primary key is carried **literally** rather than derived
839
- * from manifest metadata. When `true`:
840
- *
841
- * - a `Put` writes its `item` record **verbatim** as the stored item — the `item`
842
- * already carries the synthetic `PK` / `SK` templates (`UNIQUE#…`), so the
843
- * runtimes do NOT prepend a model prefix or compute GSI attributes;
844
- * - a `Delete` uses its `keyCondition` (the `PK` / `SK` templates) as the row Key
845
- * verbatim.
846
- *
847
- * The companion {@link entity} is the {@link MARKER_ROW_ENTITY} sentinel (there is
848
- * no manifest entity to resolve). Absent / `false` on every modeled write (a base
849
- * entity / edge / counter / ConditionCheck item), so those paths are unchanged.
850
- */
851
- readonly literalKey?: boolean;
852
- }
853
- /** A declarative transaction definition's full execution spec. */
854
- interface TransactionSpec {
855
- readonly params: Readonly<Record<string, ParamSpec>>;
856
- readonly items: readonly TransactionItemSpec[];
857
- /**
858
- * A static upper bound on the expanded item count when computable (no `forEach`
859
- * present → the exact count; with `forEach` it is left absent because the
860
- * element count is only known at execution — the runtime then enforces the
861
- * DynamoDB ≤25 limit after expansion).
862
- */
863
- readonly maxItems?: number;
864
- }
865
- /**
866
- * Whether a contract is a public **read** (`'query'`) or **write** (`'command'`)
867
- * interface. Mirrors {@link QueryModelContract} / {@link CommandModelContract}'s
868
- * `kind` discriminant from the Contract IR (#58).
869
- */
870
- type ContractKind = 'query' | 'command';
871
- /**
872
- * The resolution kind of a contract query method, **derived** by the planner from
873
- * the internal op (never hand-written; see proposal "N+1 Safety"):
874
- *
875
- * - `'point'` — target keys are known (unique-key `query` / `GetItem`); a key
876
- * array coalesces to one `BatchGetItem`.
877
- * - `'range'` — target key set is unknown (partition `list` / `Query`); one
878
- * request per partition key, so it is only ever safe for a single key.
879
- */
880
- type ContractResolution = 'point' | 'range';
881
- /**
882
- * What input arity a contract method accepts, **derived** from its resolution:
883
- *
884
- * - `'either'` — a single key **or** an array (a `point` read / a known-key
885
- * write; the array form is one `BatchGetItem` / batched write).
886
- * - `'single'` — a single key only (a `range` read; an array would be an N+1
887
- * fan-out and is rejected by construction).
888
- * - `'array'` — an array only (reserved; not produced by the current resolvers).
889
- */
890
- type ContractInputArity = 'single' | 'array' | 'either';
891
- /**
892
- * The per-key result cardinality of a contract query method, **derived** from the
893
- * internal op kind (proposal "Cardinality matrix"):
894
- *
895
- * - `'one'` — at most one item per key (a `point` read).
896
- * - `'many'` — a connection (`{ items, cursor }`) per key (a `range` read).
897
- *
898
- * This is the per-key shape; the input arity (single vs. array) then decides
899
- * whether the overall result is bare or keyed.
900
- */
901
- type ContractCardinality = 'one' | 'many';
902
- /**
903
- * The category of a contract command method's declared result, part of the
904
- * contract (it surfaces in OpenAPI and every binding; proposal "Return values"):
905
- *
906
- * - `'void'` — fire-and-forget write (no body).
907
- * - `'result'` — an outcome / status object (e.g. `{ ok, version }`).
908
- * - `'entity'` — the updated entity (the post-write projection).
909
- */
910
- type ContractCommandResult = 'void' | 'result' | 'entity';
911
- /** The Key of a contract: the field names that compose its access / join key. */
912
- interface ContractKeySpec {
913
- /** The key field names, in declaration order (e.g. `["articleId"]`). */
914
- readonly fields: readonly string[];
915
- }
916
- /**
917
- * One External Query (Query Composition) binding on a contract query method
918
- * (proposal "External Query"). It is a **build-time, in-process** read dependency
919
- * on another contract referenced by name in the same SSoT — there is no protocol
920
- * or transport. The runtime collects every parent key produced at this level and
921
- * resolves the referenced contract **once, batched**, for all of them.
922
- *
923
- * A composed child MUST be `point` (proposal "N+1 Safety"): the parent step may
924
- * yield N records, so a `range` child would be an N+1 fan-out.
925
- */
926
- interface ComposeSpec {
927
- /** The result property the composed value is attached to (e.g. `billing`). */
928
- readonly as: string;
929
- /** The referenced contract name (a symbol in the same SSoT). */
930
- readonly contract: string;
931
- /** The referenced method on that contract (e.g. `get`). */
932
- readonly method: string;
933
- /**
934
- * An optional **logical** owner label (the bounded context that owns the
935
- * referenced contract) — not a network address. Omitted when unlabeled.
936
- */
937
- readonly context?: string;
938
- /**
939
- * The child-key binding: child key field → a `from` source path on the parent
940
- * result, e.g. `{ accountId: "$.billingAccountId" }`.
941
- */
942
- readonly bind: Readonly<Record<string, string>>;
943
- /**
944
- * The composed child's resolution. It MUST be `'point'` in a valid contract — a
945
- * composed child coalesces to one `BatchGetItem` across all parent keys, so a
946
- * `range` child (a partition `Query` that cannot be batched across the parent's
947
- * keys) is an N+1 fan-out. The N+1 static checker (#60,
948
- * {@link assertContractN1Safe}) rejects a `'range'` value on the build path; the
949
- * field is widened to {@link ContractResolution} so the offending node can be
950
- * carried into the assembled {@link ContractSpec} for the checker to inspect and
951
- * reject with a clear message, rather than being silently unrepresentable.
952
- */
953
- readonly resolution: ContractResolution;
954
- /** The composed child's per-key cardinality (`'one'` for a valid `point`). */
955
- readonly cardinality: ContractCardinality;
956
- }
957
- /**
958
- * The **composition execution plan** for one query contract method (issue #70a),
959
- * the contract-layer mirror of {@link ExecutionPlanSpec}. It records how a method's
960
- * External Query compositions (`compose[]`) are staged relative to the parent read
961
- * and to one another, and the batching discipline each composed child obeys:
962
- *
963
- * - `stages` is an ordered list whose entries are indices into the method's
964
- * `compose[]`. The parent read (the referenced `operation`) is the implicit
965
- * stage before all of these. Every compose child binds its child key purely from
966
- * the **parent** result (`from('$.…')` paths), so sibling compositions are
967
- * mutually independent: they form a single stage `[0, 1, …]` and may be resolved
968
- * concurrently. (A composed child cannot, today, bind from another composed
969
- * child — the #63 DSL only exposes parent `from` paths — so there is never a
970
- * cross-composition dependency; the field is a list of stages so the shape can
971
- * carry one if a future primitive introduces it.)
972
- * - `concurrency` is the declared in-flight bound applied **within** a stage (the
973
- * shared {@link RELATION_TRAVERSAL_CONCURRENCY} = 16).
974
- * - `batchChunkSize` is the per-request key cap for a composed child read (100,
975
- * the DynamoDB `BatchGetItem` limit): a composition resolves **one batched
976
- * call** across all parent records (chunked at this size), never an N+1 fan-out
977
- * (the composed child is `point`, enforced by the #60 N+1 checker).
978
- *
979
- * Emitted only when the method declares at least one composition. A method without
980
- * compositions carries no plan, and a runtime without one resolves any
981
- * compositions sequentially (the pre-#70 behavior) — so an absent plan never
982
- * changes results.
983
- */
984
- interface CompositionPlanSpec {
985
- /** Ordered stages of indices into `compose[]` (independent siblings share a stage). */
986
- readonly stages: readonly (readonly number[])[];
987
- /** The declared in-flight bound applied within each composition stage (16). */
988
- readonly concurrency: number;
989
- /** Per-request key cap for a composed child's batched read (100, BatchGetItem limit). */
990
- readonly batchChunkSize: number;
991
- }
992
- /**
993
- * The serialized spec of one **query** contract method. Carries the decided facts
994
- * (`resolution` / `inputArity` / `cardinality`) the planner derived in #58, a
995
- * reference into `queries` by name (the internal op), and any External Query
996
- * compositions. The decided facts are **serialized, not re-derived** — every
997
- * runtime honors them.
998
- */
999
- interface QueryContractMethodSpec {
1000
- /** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
1001
- readonly resolution: ContractResolution;
1002
- /** Derived: `'either'` for `point`, `'single'` for `range`. */
1003
- readonly inputArity: ContractInputArity;
1004
- /** Derived per-key result shape: `'one'` (point) or `'many'` (range). */
1005
- readonly cardinality: ContractCardinality;
1006
- /** The referenced read op name in `queries` (e.g. `ArticleById__get`). */
1007
- readonly operation: string;
1008
- /** External Query compositions on this method; omitted when there are none. */
1009
- readonly compose?: readonly ComposeSpec[];
1010
- /**
1011
- * The composition staging + batch plan (issue #70a); present only when `compose`
1012
- * is. See {@link CompositionPlanSpec}: independent compositions form one
1013
- * concurrency-eligible stage after the parent read, each resolved as one batched
1014
- * call (chunk ≤100). Absent → resolve compositions sequentially (pre-#70).
1015
- */
1016
- readonly compositionPlan?: CompositionPlanSpec;
1017
- }
1018
- /**
1019
- * How a contract command method resolves a single key vs. an array of keys to the
1020
- * underlying write surface (proposal "Consistency with the existing write
1021
- * surface"). A single key → one write op (or one transaction); an array → a
1022
- * batched write — a `TransactWriteItems` when atomicity / per-item conditions are
1023
- * required, or a `BatchWriteItem` when neither is.
1024
- */
1025
- type CommandResolutionTarget =
1026
- /** One write op, referenced by name in `commands`. */
1027
- {
1028
- readonly mode: 'op';
1029
- readonly operation: string;
1030
- }
1031
- /** One transaction, referenced by name in `transactions`. */
1032
- | {
1033
- readonly mode: 'transaction';
1034
- readonly transaction: string;
1035
- }
1036
- /**
1037
- * A `BatchWriteItem` over the per-key write op (no conditions). References the
1038
- * single write op in `commands` that is expanded per key.
1039
- */
1040
- | {
1041
- readonly mode: 'batchWrite';
1042
- readonly operation: string;
1043
- };
1044
- /**
1045
- * The serialized spec of one **command** contract method. Symmetric to
1046
- * {@link QueryContractMethodSpec}: it carries the declared `result` type and the
1047
- * `single` / `batch` resolution targets into the existing write specs.
1048
- */
1049
- interface CommandContractMethodSpec {
1050
- /** Derived input arity: writes accept `'either'` (single key or key array). */
1051
- readonly inputArity: ContractInputArity;
1052
- /** The declared result type (`void` | a `Result` | the updated entity). */
1053
- readonly result: ContractCommandResult;
1054
- /** How a single key resolves (one op / one transaction). */
1055
- readonly single: CommandResolutionTarget;
1056
- /**
1057
- * How an array of keys resolves (a transaction / `BatchWriteItem`); omitted
1058
- * when the contract does not declare a batched write form.
1059
- */
1060
- readonly batch?: CommandResolutionTarget;
1061
- /**
1062
- * The **return projection** of a `mutation`-derived command method (issue #83;
1063
- * proposal §3, "return selection as a read projection"). A JSON-safe boolean
1064
- * field map: after the write commits, the runtime issues a **`GetItem` with
1065
- * `ConsistentRead`** of the written entity's primary key and applies this
1066
- * projection via the existing read-projection machinery, returning the projected
1067
- * item. This is the **uniform** return mechanism for both a single-op command
1068
- * and a future transaction (a `TransactWriteItems` cannot return item images),
1069
- * so TS and Python return an **identical** projected item.
1070
- *
1071
- * The consistent read-back is issued against the read op named
1072
- * `<Contract>__<method>__readback` (a synthesized {@link QuerySpec}: a `GetItem`
1073
- * on the written entity's primary key projecting these fields). Present only on
1074
- * a `.plan(mutation)` method; a hand-written #64 command omits it (its
1075
- * {@link result} stays `'void'` / `'entity'` and it returns no projected item).
1076
- */
1077
- readonly returnSelection?: Readonly<Record<string, boolean>>;
1078
- }
1079
- /**
1080
- * A serialized contract — a keyed public read (`'query'`) or write (`'command'`)
1081
- * interface holding one or more named methods. Each method references an existing
1082
- * operation spec by name and adds the decided contract facts. The existing
1083
- * operation-spec shapes are unchanged, so the current runtime keeps working.
1084
- */
1085
- interface ContractSpec {
1086
- /** Whether this is a read (`'query'`) or write (`'command'`) contract. */
1087
- readonly kind: ContractKind;
1088
- /** The contract Key (the access pattern / join / batch key). */
1089
- readonly key: ContractKeySpec;
1090
- /**
1091
- * The named methods (use cases over the same Key). A query contract's methods
1092
- * are {@link QueryContractMethodSpec}; a command contract's are
1093
- * {@link CommandContractMethodSpec}. The map is keyed by contract `kind`.
1094
- */
1095
- readonly methods: Readonly<Record<string, QueryContractMethodSpec>> | Readonly<Record<string, CommandContractMethodSpec>>;
1096
- }
1097
- /**
1098
- * One bounded context's membership (issue #59, "context ownership"). Lists the
1099
- * Models and Contracts that belong to the same context, so the future boundary
1100
- * lint (#61) can tell **own** from **foreign**: a contract may resolve its own
1101
- * context's Models directly, but may only reach another context through that
1102
- * context's published Contract (direct use of a foreign Model is a build error).
1103
- *
1104
- * This issue only **serializes / emits** this declaration; it does not enforce
1105
- * it. Both lists are entity / contract *names* (matching `manifest.entities` keys
1106
- * and `contracts` keys respectively).
1107
- */
1108
- interface ContextSpec {
1109
- /** Model (entity) names owned by this context (sorted; manifest entity keys). */
1110
- readonly models: readonly string[];
1111
- /** Contract names owned by this context (sorted; `contracts` map keys). */
1112
- readonly contracts: readonly string[];
1113
- }
1114
- interface OperationsDocument {
1115
- readonly version: typeof SPEC_VERSION;
1116
- readonly queries: Readonly<Record<string, QuerySpec>>;
1117
- readonly commands: Readonly<Record<string, CommandSpec>>;
1118
- /** Declarative transactions (issue #46). Absent in pre-#46 specs. */
1119
- readonly transactions?: Readonly<Record<string, TransactionSpec>>;
1120
- /**
1121
- * The CQRS Contract layer (issue #59): contract name → {@link ContractSpec}.
1122
- * Layered **on top of** the existing `queries` / `commands` / `transactions`
1123
- * (each method references one of them by name). **Absent** when the input
1124
- * defines no contracts — so a contract-free input produces a byte-identical
1125
- * pre-#59 operations document (backward compatibility).
1126
- */
1127
- readonly contracts?: Readonly<Record<string, ContractSpec>>;
1128
- /**
1129
- * Context-ownership declarations (issue #59): context name →
1130
- * {@link ContextSpec}. Used by the boundary lint (#61) to tell own from
1131
- * foreign. **Absent** when no contexts are declared (backward compatibility).
1132
- */
1133
- readonly contexts?: Readonly<Record<string, ContextSpec>>;
1134
- }
1135
- /** The full `{ manifest, operations }` bundle produced by the static planner. */
1136
- interface BridgeBundle {
1137
- readonly manifest: Manifest;
1138
- readonly operations: OperationsDocument;
1139
- }
1140
-
1141
455
  /**
1142
456
  * Declarative transaction executor (issue #46, Python-bridge Phase 4).
1143
457
  *
@@ -1221,8 +535,6 @@ interface QueryOptions {
1221
535
  declare function executeQuery(modelClass: Function, key: Record<string, unknown>, selectSpec: Record<string, unknown>, options?: QueryOptions): Promise<Record<string, unknown> | null>;
1222
536
 
1223
537
  interface ListInput {
1224
- /** A plain projection or a `project(...)` builder. */
1225
- select: Record<string, unknown>;
1226
538
  limit?: number;
1227
539
  after?: string;
1228
540
  order?: 'ASC' | 'DESC';
@@ -1250,7 +562,7 @@ interface ListResult {
1250
562
  * only `{ items, cursor }`, so internal-only fields such as `rawItems` never
1251
563
  * cross the public API boundary (no reliance on `as` type narrowing).
1252
564
  */
1253
- declare function executeList(modelClass: Function, key: Record<string, unknown>, options: ListInput): Promise<ListResult>;
565
+ declare function executeList(modelClass: Function, key: Record<string, unknown>, selectSpec: Record<string, unknown>, options?: ListInput): Promise<ListResult>;
1254
566
 
1255
567
  interface ExplainInput {
1256
568
  select?: Record<string, unknown>;
@@ -1497,2591 +809,6 @@ declare class CdcEmulator {
1497
809
  /** Create a CDC emulator (spec §10). */
1498
810
  declare function createCdcEmulator(opts?: CdcEmulatorOptions): CdcEmulator;
1499
811
 
1500
- /**
1501
- * The single source of truth for **relation execution staging** (issue #70),
1502
- * shared by the static SSoT generator (`src/spec/operations.ts`,
1503
- * {@link buildQuerySpec}) and the TS host runtime
1504
- * (`src/relation/traversal.ts`, {@link resolveRelations}). It is the TS analogue
1505
- * of the Python runtime's `_relation_stages` / `_plan_concurrency`
1506
- * (`python/graphddb_runtime/runtime.py`).
1507
- *
1508
- * ## Why one shared module (anti-drift)
1509
- *
1510
- * #70a records an {@link ExecutionPlanSpec} (`groups` + `concurrency`) in the
1511
- * SSoT; #70b made the Python runtime **honor** it; #70c makes the TS host runtime
1512
- * honor it too. For the two runtimes to make **identical** plan-declared
1513
- * decisions, TS must consume *the same plan facts* the generator serialized — not
1514
- * re-derive parallelism by an independent "parallelize every sibling" walk. The
1515
- * host runtime has no generated JSON to load (it works off live decorator
1516
- * metadata + a `select` tree), so it derives the plan from that live tree using
1517
- * **this exact function** — the same one the generator calls to produce the
1518
- * serialized `executionPlan`. Generator and runtime therefore cannot drift: they
1519
- * are one codepath. {@link deriveExecutionPlan} is the seam.
1520
- *
1521
- * ## How the runtime obtains the plan
1522
- *
1523
- * The generator produces `operations[]` (root + relation ops, each with a
1524
- * `resultPath`) and runs {@link deriveExecutionPlan} over their result paths. The
1525
- * host runtime walks the live `select` tree with {@link relationResultPaths},
1526
- * which emits the **identical** ordered `resultPath` list
1527
- * (`buildRelationOperations` is refactored to use it), and runs the **same**
1528
- * {@link deriveExecutionPlan} over `['$', ...those]`. The resulting
1529
- * {@link ExecutionPlanSpec} is threaded through the recursive relation traversal:
1530
- * at each parent level the traversal honors the plan's grouping (which child ops
1531
- * are an independent, concurrency-eligible stage) and the declared `concurrency`
1532
- * bound — reading them from the plan, never re-deciding them.
1533
- */
1534
-
1535
- /**
1536
- * A **resolved** relation execution plan as the host runtime consumes it: the
1537
- * serialized {@link ExecutionPlanSpec} (`groups` over operation indices +
1538
- * `concurrency`) together with the `resultPath` each operation index produces, so
1539
- * the recursive traversal can locate "the operations at this parent level" and
1540
- * read their declared stage from the plan.
1541
- *
1542
- * Built once at a read's entry point ({@link buildRelationExecutionPlan}) and
1543
- * threaded, unchanged, through every recursion level. A read whose `select` has
1544
- * **no** relations (or where the plan would be a single op) carries no plan; the
1545
- * traversal then has nothing to stage.
1546
- */
1547
- interface RelationExecutionPlan {
1548
- /** The serialized staging facts (issue #70a) the traversal honors. */
1549
- readonly plan: ExecutionPlanSpec;
1550
- /** Operation index → the `resultPath` it writes (index 0 = root `$`). */
1551
- readonly resultPaths: readonly string[];
1552
- }
1553
-
1554
- /**
1555
- * Intermediate representation (IR) produced by the parameterized definition DSL
1556
- * (issue #41). This is the **in-memory, typed** representation consumed by the
1557
- * static planner (#42); JSON serialization is #42's responsibility, so nothing
1558
- * here is serialized — the IR keeps full type information (param value types,
1559
- * select projection) so downstream type derivation stays sound.
1560
- *
1561
- * Each `define*` entry point captures, for one definition:
1562
- *
1563
- * - `entity` — the model name + (runtime) class the operation targets;
1564
- * - `operation`— the operation kind (`query` | `list` | `put` | `update`
1565
- * | `delete`);
1566
- * - the operation **structure** with {@link Param} placeholders left in place
1567
- * (`key` / `item` / `changes`, plus `select` for reads); and
1568
- * - `params` — a `name → descriptor` map collected from every placeholder in
1569
- * the structure, with the descriptor preserving the param's value type.
1570
- */
1571
-
1572
- /** The set of operation kinds a definition may describe. */
1573
- type OperationKind = 'query' | 'list' | 'put' | 'update' | 'delete';
1574
- /**
1575
- * A declarative write condition for a parameterized command definition (issues
1576
- * #46, #81). Mirrors the runtime `condition` subset
1577
- * (`expression/condition-expression.ts`):
1578
- *
1579
- * - `{ notExists: true }` → `attribute_not_exists(PK)` (the row must not exist).
1580
- * This legacy whole-row form is kept verbatim for backward compatibility.
1581
- * - `{ attributeExists: '<field>' }` → `attribute_exists(<field>)` — the named
1582
- * attribute (any field, including PK / SK) must be present. The foundation for
1583
- * referential-integrity derivation (proposal "capability gaps": `requires X
1584
- * exists`).
1585
- * - `{ attributeNotExists: '<field>' }` → `attribute_not_exists(<field>)` — the
1586
- * named attribute must be absent (a field-level uniqueness / first-write guard).
1587
- * - `{ field: value, … }` → field equality (`#f = :v AND …`). Each value may be
1588
- * a concrete literal **or** a {@link Param} placeholder (bound at execution).
1589
- *
1590
- * The forms are mutually exclusive: `notExists` / `attributeExists` /
1591
- * `attributeNotExists` are single-primitive conditions and ignore any sibling
1592
- * fields. (When more than one would be present the most specific is taken in the
1593
- * order `notExists` → `attributeExists` → `attributeNotExists`.)
1594
- */
1595
- type ConditionInput = {
1596
- readonly notExists: true;
1597
- } | {
1598
- readonly attributeExists: string;
1599
- } | {
1600
- readonly attributeNotExists: string;
1601
- } | Readonly<Record<string, Param<unknown> | string | number | boolean>>;
1602
- /** Options accepted by the write `define*` entry points (issue #46). */
1603
- interface WriteDefinitionOptions {
1604
- /** Optional declarative write condition (subset). */
1605
- readonly condition?: ConditionInput;
1606
- }
1607
- /**
1608
- * Identifies the entity an operation targets. `name` is the model class name
1609
- * (entity name); `modelClass` is retained so the planner can resolve metadata
1610
- * (keys / GSIs / fields) from the `MetadataRegistry`.
1611
- */
1612
- interface EntityRef {
1613
- readonly name: string;
1614
- readonly modelClass: Function;
1615
- }
1616
- /**
1617
- * A single parameter descriptor in an operation's `params` map. Carries the
1618
- * runtime `kind` (and, for `literal`, the allowed values) and — purely at the
1619
- * type level — the represented value type `T`.
1620
- *
1621
- * @typeParam T - The value type the parameter stands in for.
1622
- */
1623
- interface ParamDescriptor<T = unknown> {
1624
- readonly kind: ParamKind;
1625
- readonly literals?: readonly T[];
1626
- /**
1627
- * For an `array` param (transaction `forEach` source), the per-element field
1628
- * descriptors. `undefined` for scalar params.
1629
- */
1630
- readonly element?: Readonly<Record<string, ParamDescriptor>>;
1631
- /** Parameters are always required (no optional params in this phase). */
1632
- readonly required: true;
1633
- }
1634
- /**
1635
- * Structure with {@link Param} placeholders left intact (key / item / changes).
1636
- * A recursive record whose leaves are either concrete scalar literals (e.g. a
1637
- * fixed sort-key discriminator) or `Param<…>` placeholders.
1638
- */
1639
- type ParamStructure = {
1640
- readonly [key: string]: ParamStructure | Param<unknown> | string | number | boolean;
1641
- };
1642
- /**
1643
- * The typed IR for one definition. Generic over the entity, the parameterized
1644
- * structure(s), the (optional) select projection, and the collected params map
1645
- * so that all type information survives into the value returned by
1646
- * `defineQueries` / `defineCommands`.
1647
- *
1648
- * Field presence by `operation`:
1649
- *
1650
- * | op | key | select | changes | params |
1651
- * |----------|-----|--------|---------|--------|
1652
- * | query | ✓ | ✓ | — | ✓ |
1653
- * | list | ✓ | ✓ | — | ✓ |
1654
- * | put | item (in `key`) | — | — | ✓ |
1655
- * | update | ✓ | — | ✓ | ✓ |
1656
- * | delete | ✓ | — | — | ✓ |
1657
- *
1658
- * @typeParam T - The entity type.
1659
- * @typeParam Op - The operation kind.
1660
- * @typeParam K - The parameterized key / item structure.
1661
- * @typeParam S - The select projection (reads only; `undefined` for writes).
1662
- * @typeParam Ch - The parameterized changes structure (`update` only).
1663
- * @typeParam P - The collected `name → ParamDescriptor` map.
1664
- */
1665
- interface OperationDefinition<T extends DDBModel, Op extends OperationKind, K, S, Ch, P extends Record<string, ParamDescriptor>> {
1666
- /** @internal Marks this object as a definition IR node. */
1667
- readonly __isOperationDefinition: true;
1668
- readonly entity: EntityRef;
1669
- readonly operation: Op;
1670
- /**
1671
- * The parameterized key for `query` / `list` / `update` / `delete`, or the
1672
- * parameterized item for `put`. Placeholders are preserved in place.
1673
- */
1674
- readonly key: K;
1675
- /** The select projection for `query` / `list`; `undefined` otherwise. */
1676
- readonly select: S;
1677
- /** The parameterized changes for `update`; `undefined` otherwise. */
1678
- readonly changes: Ch;
1679
- /**
1680
- * The declarative write condition for `put` / `update` / `delete`; `undefined`
1681
- * when no condition is attached or for reads (issue #46).
1682
- */
1683
- readonly condition?: ConditionInput;
1684
- /** Collected parameters: name → descriptor (value type preserved in `P`). */
1685
- readonly params: P;
1686
- /** @internal Phantom carrier so the entity type `T` is retained on the node. */
1687
- readonly __entityType?: (value: T) => void;
1688
- }
1689
- /** Any operation definition node, regardless of its type parameters. */
1690
- type AnyOperationDefinition = OperationDefinition<any, OperationKind, unknown, unknown, unknown, Record<string, ParamDescriptor>>;
1691
- /**
1692
- * A map of definition name → {@link OperationDefinition}, as accepted by
1693
- * `defineQueries` / `defineCommands` and returned (unchanged in type) from them.
1694
- */
1695
- type DefinitionMap = Record<string, AnyOperationDefinition>;
1696
-
1697
- /**
1698
- * Model write-semantics — the reusable *save contract* `entityWrites` (issue #83,
1699
- * Epic #80; spec `docs/mutation-command-derivation.md` §2).
1700
- *
1701
- * `Model.writes` declares the **write-side invariants / effects required for this
1702
- * entity to be consistent on DynamoDB** — a *reusable save contract*, NOT a
1703
- * mutation and NOT business logic. It is a per-lifecycle map
1704
- * (`{ create?, update?, remove? }`) whose values are {@link LifecycleContract}s
1705
- * built with `w.lifecycle({...})`; each carries the §2 effect arrays
1706
- * (`requires` / `unique` / `edges` / `derive` / `emits` / `idempotency`).
1707
- *
1708
- * ## What #83 does — and deliberately does NOT — with the effects
1709
- *
1710
- * The mutation compiler (#83) resolves a fragment's lifecycle from this save
1711
- * contract (the fragment's intent — `m.create` / `m.update` / `m.remove` — picks
1712
- * `create` / `update` / `remove`) and emits the **base write op** for that
1713
- * lifecycle (create → `PutItem` with `attribute_not_exists(PK)`, update →
1714
- * `UpdateItem`, remove → `DeleteItem`). The §2 effect arrays are **stored
1715
- * opaquely** here and consumed by NONE of #83: deriving `ConditionCheck`
1716
- * (requires, #84), uniqueness guards (#86), adjacency edge `Put` / `Delete` from
1717
- * `edges` (#85), derived counter `UpdateItem`s (#85), outbox events (#87), and
1718
- * the idempotency guard (#87) are each their own follow-up issue. #83 only shapes
1719
- * the declaration and exposes the {@link LifecycleContract.effects} a later issue
1720
- * reads — the **per-fragment hook** the compiler leaves empty (see
1721
- * `compileFragment` in `src/spec/mutation-command.ts`).
1722
- *
1723
- * ## Coexistence with edge writes (`edgeWrites`, #82)
1724
- *
1725
- * This is a **distinct** construct from {@link edgeWrites} (#82, the adjacency
1726
- * edge-only `writes` member). `edgeWrites` declares *only* the edge write side of
1727
- * an adjacency entity; `entityWrites` is the full per-lifecycle save contract a
1728
- * mutation fragment adopts. Both are recognized by their own brand, so a model
1729
- * may carry either form on its static `writes` member without collision; the
1730
- * mutation compiler reads {@link getEntityWrites} (this marker), the edge
1731
- * derivation reads its own.
1732
- *
1733
- * ## Trivial base op when a model declares no `writes`
1734
- *
1735
- * A target model that declares **no** `entityWrites` save contract has no
1736
- * lifecycle to resolve; the fragment then compiles the **trivial base op** for
1737
- * its intent (create → `Put` `attribute_not_exists(PK)`, update → `Update`,
1738
- * remove → `Delete`) directly against the plain model. So
1739
- * `m.create(() => PostModel, { input })` works on a bare model — see
1740
- * {@link resolveLifecycle} in `src/spec/mutation-command.ts`.
1741
- */
1742
- /**
1743
- * The lifecycle phase a save contract entry declares. The mutation fragment's
1744
- * intent (`m.create` / `m.update` / `m.remove`) selects the matching entry.
1745
- */
1746
- type WriteLifecyclePhase = 'create' | 'update' | 'remove';
1747
- /**
1748
- * A path-rooted leaf value in a save-contract effect (proposal §2): every value
1749
- * binds to an explicit path root so its source is unambiguous —
1750
- * `$.input.*` (the mutation input) or `$.entity.*` (the written entity). #83
1751
- * stores these verbatim (it consumes none); a follow-up issue (#84–#87) reads
1752
- * them when deriving the effect's DynamoDB realization.
1753
- */
1754
- type EffectPath = string;
1755
- /**
1756
- * A referential-integrity requirement (proposal §2 `requires`): the named target
1757
- * entity must exist for the keys bound from `$.input.*` / `$.entity.*`. Derived
1758
- * to a `ConditionCheck` (`attribute_exists`) by #84 — **not** by #83, which only
1759
- * stores it.
1760
- */
1761
- interface RequiresEffect {
1762
- readonly kind: 'requires';
1763
- /** Resolves the entity whose existence is asserted. */
1764
- readonly targetFactory: () => new (...args: unknown[]) => unknown;
1765
- /** The key binding: target key field → a path-rooted source value. */
1766
- readonly keys: Readonly<Record<string, EffectPath>>;
1767
- }
1768
- /**
1769
- * A uniqueness guard (proposal §2 `unique`): `{ name, scope, fields }` — a named
1770
- * composite-scope uniqueness invariant. Derived to a `UNIQUE#…` guard `Put` with
1771
- * `attribute_not_exists` by #86 — stored opaquely here.
1772
- */
1773
- interface UniqueEffect {
1774
- readonly kind: 'unique';
1775
- /** The guard name (its key-shape discriminator on DynamoDB). */
1776
- readonly name: string;
1777
- /** The scope the uniqueness is partitioned by (path-rooted values). */
1778
- readonly scope: readonly EffectPath[];
1779
- /** The fields whose combined value must be unique within the scope. */
1780
- readonly fields: readonly EffectPath[];
1781
- }
1782
- /**
1783
- * An edge write effect (proposal §2 `edges`): create / delete the adjacency row
1784
- * that materializes a target relation. Derived to an adjacency `Put` / `Delete`
1785
- * by #82/#85 (the `src/relation/edge-write.ts` primitive) — stored opaquely here.
1786
- */
1787
- interface EdgeEffect {
1788
- readonly kind: 'putEdge' | 'deleteEdge';
1789
- /** Resolves the target model whose relation this edge feeds. */
1790
- readonly targetFactory: () => new (...args: unknown[]) => unknown;
1791
- /** The relation property on the target that reads this edge. */
1792
- readonly relationProperty: string;
1793
- }
1794
- /**
1795
- * A derived (cascading) update (proposal §2 `derive`): e.g. `User.postCount +=
1796
- * 1`. Derived to an `UpdateItem` `ADD` / `SET` in the transaction by #85 —
1797
- * stored opaquely here.
1798
- */
1799
- interface DeriveEffect {
1800
- readonly kind: 'derive';
1801
- /** Resolves the entity whose attribute is derived-updated. */
1802
- readonly targetFactory: () => new (...args: unknown[]) => unknown;
1803
- /** The key binding of the updated row (path-rooted values). */
1804
- readonly keys: Readonly<Record<string, EffectPath>>;
1805
- /** The attribute to increment / set. */
1806
- readonly attribute: string;
1807
- /** The amount to add (for an increment). */
1808
- readonly amount: number;
1809
- }
1810
- /**
1811
- * A domain event (proposal §2 `emits`): an outbox `Put` drained to Streams /
1812
- * `src/cdc/`. Derived by #87 — stored opaquely here.
1813
- */
1814
- interface EmitEffect {
1815
- readonly kind: 'event';
1816
- /** The event name (e.g. `PostCreated`). */
1817
- readonly name: string;
1818
- /** The event payload, each value path-rooted. */
1819
- readonly payload: Readonly<Record<string, EffectPath>>;
1820
- }
1821
- /**
1822
- * An idempotency guard (proposal §2 `idempotency`): a client-token guard item
1823
- * (`attribute_not_exists`). Derived by #87 — stored opaquely here.
1824
- */
1825
- interface IdempotencyEffect {
1826
- readonly kind: 'idempotency';
1827
- /** The path-rooted token the guard keys on (e.g. `$.input.requestId`). */
1828
- readonly token: EffectPath;
1829
- }
1830
- /**
1831
- * The full §2 effect set a {@link LifecycleContract} carries. Every field is
1832
- * optional; an omitted field is the empty effect set. #83 stores this verbatim
1833
- * and consumes none — the per-fragment hook a later issue (#84–#87) reads.
1834
- */
1835
- interface LifecycleEffects {
1836
- /** Referential-integrity requirements (→ `ConditionCheck`, #84). */
1837
- readonly requires?: readonly RequiresEffect[];
1838
- /** Uniqueness guards (→ `UNIQUE#…` guard `Put`, #86). */
1839
- readonly unique?: readonly UniqueEffect[];
1840
- /** Edge write effects (→ adjacency `Put` / `Delete`, #82/#85). */
1841
- readonly edges?: readonly EdgeEffect[];
1842
- /** Derived / cascading updates (→ `UpdateItem` `ADD`/`SET`, #85). */
1843
- readonly derive?: readonly DeriveEffect[];
1844
- /** Domain events (→ outbox `Put`, #87). */
1845
- readonly emits?: readonly EmitEffect[];
1846
- /** Idempotency guard (→ client-token guard `Put`, #87). */
1847
- readonly idempotency?: IdempotencyEffect;
1848
- }
1849
- /**
1850
- * Marker carried by a {@link LifecycleContract} so it is recognized regardless of
1851
- * declaration shape. Built by {@link WriteRecorder.lifecycle}.
1852
- */
1853
- declare const LIFECYCLE_CONTRACT_MARKER: unique symbol;
1854
- /**
1855
- * One lifecycle's save contract — the §2 effect set for a single phase
1856
- * (`create` / `update` / `remove`). #83 emits the lifecycle's **base write op**
1857
- * (selected from the fragment intent) and leaves the {@link effects} as the
1858
- * untouched per-fragment hook a later issue derives.
1859
- */
1860
- interface LifecycleContract {
1861
- readonly [LIFECYCLE_CONTRACT_MARKER]: true;
1862
- /** The §2 effect arrays, stored opaquely by #83. */
1863
- readonly effects: LifecycleEffects;
1864
- }
1865
- /** Runtime guard: is `value` a {@link LifecycleContract} (a `w.lifecycle(...)`)? */
1866
- declare function isLifecycleContract(value: unknown): value is LifecycleContract;
1867
- /**
1868
- * Marker carried by a model's static `writes` member when it is an
1869
- * {@link EntityWritesDefinition} (an `entityWrites(...)` save contract), so it is
1870
- * told apart from an {@link edgeWrites} member (#82) on the same conventional
1871
- * property name.
1872
- */
1873
- declare const ENTITY_WRITES_MARKER: unique symbol;
1874
- /**
1875
- * The per-lifecycle save contract produced by {@link entityWrites}: a map from
1876
- * {@link WriteLifecyclePhase} to its {@link LifecycleContract}. Every phase is
1877
- * optional — a model may declare only the lifecycles it needs (e.g. a `create` +
1878
- * `remove` save contract with no `update`).
1879
- */
1880
- interface EntityWritesDefinition {
1881
- readonly [ENTITY_WRITES_MARKER]: true;
1882
- /** The create-lifecycle save contract, if declared. */
1883
- readonly create?: LifecycleContract;
1884
- /** The update-lifecycle save contract, if declared. */
1885
- readonly update?: LifecycleContract;
1886
- /** The remove-lifecycle save contract, if declared. */
1887
- readonly remove?: LifecycleContract;
1888
- }
1889
- /** Runtime guard: is `value` an {@link EntityWritesDefinition}? */
1890
- declare function isEntityWritesDefinition(value: unknown): value is EntityWritesDefinition;
1891
- /**
1892
- * The recorder handed to the {@link entityWrites} builder. `w.lifecycle({...})`
1893
- * accepts the §2 effect arrays and brands them into a {@link LifecycleContract}.
1894
- * #83 stores the effects opaquely; #84–#87 read them.
1895
- */
1896
- interface WriteRecorder {
1897
- /**
1898
- * Build one lifecycle's save contract from its §2 effect arrays. Every effect
1899
- * field is optional; an omitted field is an empty effect set. The returned
1900
- * contract is branded so the compiler can resolve it from the model's `writes`
1901
- * map by lifecycle phase.
1902
- */
1903
- lifecycle(effects?: LifecycleEffects): LifecycleContract;
1904
- /** Declare a referential-integrity requirement (§2 `requires`; derived #84). */
1905
- exists(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>): RequiresEffect;
1906
- /** Declare a uniqueness guard (§2 `unique`; derived #86). */
1907
- unique(spec: {
1908
- readonly name: string;
1909
- readonly scope: readonly EffectPath[];
1910
- readonly fields: readonly EffectPath[];
1911
- }): UniqueEffect;
1912
- /** Declare an edge create effect (§2 `edges`; derived #82/#85). */
1913
- putEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
1914
- /** Declare an edge delete effect (§2 `edges`; derived #82/#85). */
1915
- deleteEdge(target: () => new (...args: unknown[]) => unknown, relationProperty: string): EdgeEffect;
1916
- /** Declare a derived / cascading update (§2 `derive`; derived #85). */
1917
- increment(target: () => new (...args: unknown[]) => unknown, keys: Readonly<Record<string, EffectPath>>, attribute: string, amount: number): DeriveEffect;
1918
- /** Declare a domain event (§2 `emits`; derived #87). */
1919
- event(name: string, payload: Readonly<Record<string, EffectPath>>): EmitEffect;
1920
- /** Declare an idempotency guard (§2 `idempotency`; derived #87). */
1921
- idempotentBy(token: EffectPath): IdempotencyEffect;
1922
- }
1923
- /**
1924
- * The lifecycle map a {@link entityWrites} builder returns: each phase is either
1925
- * a built {@link LifecycleContract} (`w.lifecycle({...})`) or omitted.
1926
- */
1927
- interface EntityWritesShape {
1928
- readonly create?: LifecycleContract;
1929
- readonly update?: LifecycleContract;
1930
- readonly remove?: LifecycleContract;
1931
- }
1932
- /**
1933
- * Declare a model's reusable **save contract** (issue #83, proposal §2). The
1934
- * builder receives a {@link WriteRecorder} and returns the per-lifecycle map
1935
- * (`{ create?, update?, remove? }`), each value a `w.lifecycle({...})` carrying
1936
- * the §2 effect arrays. The result is stored on the model as
1937
- * `static readonly writes = entityWrites<Model>((w) => ({ … }))`.
1938
- *
1939
- * #83 reads only **which** lifecycles are declared (to resolve a fragment's base
1940
- * write op by intent) and stores the effect arrays opaquely; the effect
1941
- * derivation is #84–#87.
1942
- *
1943
- * @typeParam M The model type the save contract belongs to (documentary; the
1944
- * recorder is structural).
1945
- * @example
1946
- * ```ts
1947
- * static readonly writes = entityWrites<PostModel>((w) => ({
1948
- * create: w.lifecycle({
1949
- * requires: [w.exists(() => UserModel, { userId: '$.input.userId' })],
1950
- * edges: [w.putEdge(() => UserModel, 'posts')],
1951
- * }),
1952
- * remove: w.lifecycle({ edges: [w.deleteEdge(() => UserModel, 'posts')] }),
1953
- * }));
1954
- * ```
1955
- */
1956
- declare function entityWrites<M = unknown>(builder: (w: WriteRecorder) => EntityWritesShape): EntityWritesDefinition;
1957
- /**
1958
- * Read the {@link EntityWritesDefinition} declared on a model class as its static
1959
- * `writes` member, or `undefined` when the model declares no `entityWrites` save
1960
- * contract. Scans the class's own static members for the {@link
1961
- * ENTITY_WRITES_MARKER}, exactly as {@link getEdgeWrites} (#82) scans for the
1962
- * edge marker — so the two `writes` forms coexist without a fixed property name
1963
- * collision (a model carries one or the other; the marker disambiguates).
1964
- */
1965
- declare function getEntityWrites(modelClass: new (...args: unknown[]) => unknown): EntityWritesDefinition | undefined;
1966
- /** The lifecycle phase a mutation fragment intent selects. */
1967
- declare function lifecyclePhaseForIntent(intent: 'create' | 'update' | 'remove'): WriteLifecyclePhase;
1968
-
1969
- /**
1970
- * Internal write-plan composition DSL — `mutation` / `definePlan` (issue #83,
1971
- * Epic #80; spec `docs/mutation-command-derivation.md` §1, §3).
1972
- *
1973
- * A **mutation is INTERNAL** — it is *not* a public API. It is a *write-plan
1974
- * composition language* used as the **implementation** of a Command: it declares
1975
- * a set of write **fragments** that must execute atomically. The only externally
1976
- * exposed surface is the fixed Command IF (`publicCommandModel(...).plan(...)`,
1977
- * `src/define/contract.ts`); the mutation document is never exposed.
1978
- *
1979
- * ```ts
1980
- * const CreatePost = mutation('CreatePost', (m, $) => [
1981
- * m.create(() => PostModel, { input: { userId: $.userId, title: $.title } }),
1982
- * ]);
1983
- * ```
1984
- *
1985
- * ## Fragments are DECLARATIONS, not executed calls
1986
- *
1987
- * A mutation's top-level fields are write **fragments** — *declarations*, NOT
1988
- * executed function calls. `m.create(...)` does **not** run a write; it records a
1989
- * {@link MutationFragment} (intent + target + input + optional `use:`). The
1990
- * compiler (`src/spec/mutation-command.ts`) expands the fragment list at compile
1991
- * time into a {@link CommandSpec} / `TransactionSpec`. #83 compiles the
1992
- * **single-fragment** case end-to-end; the IR is already a **list** so the
1993
- * N-fragment atomic merge (#90) drops in without reshaping it.
1994
- *
1995
- * ## Intent selects the lifecycle; `use:` is optional
1996
- *
1997
- * Each fragment's **intent** (`m.create` / `m.update` / `m.remove`) selects the
1998
- * lifecycle from the target's `writes` save contract (`entityWrites`, §2):
1999
- * `m.create` → `writes.create`, etc. `use:` is **optional** — it defaults to the
2000
- * target model's own `writes`; supply it only to adopt a *custom* write contract,
2001
- * and it takes the **whole** `writes` set (an {@link EntityWritesDefinition}),
2002
- * never `writes.create` (the intent already picks the lifecycle, so naming the
2003
- * lifecycle would double-specify it). When the target declares **no** `writes`,
2004
- * the fragment compiles the **trivial base op** for its intent (create → `Put`
2005
- * `attribute_not_exists(PK)`, update → `Update`, remove → `Delete`).
2006
- *
2007
- * The `$` argument is the **input proxy**: `$.userId` mints a faithful
2008
- * {@link MutationInputRef} naming an input field, so a fragment's `input` map
2009
- * binds model fields to input fields declaratively (no arbitrary JS), exactly as
2010
- * the contract `params` sentinel does.
2011
- */
2012
-
2013
- /**
2014
- * The intent of a write fragment (proposal §3): `create` / `update` / `remove`
2015
- * (`remove`, **not** `del`). The intent selects the lifecycle from the target's
2016
- * save contract, and the base write op (create → `PutItem`, update →
2017
- * `UpdateItem`, remove → `DeleteItem`).
2018
- */
2019
- type MutationIntent = 'create' | 'update' | 'remove';
2020
- declare const INPUT_REF_BRAND: unique symbol;
2021
- /**
2022
- * A faithful reference to a mutation **input** field, minted by reading `$.<field>`
2023
- * in a mutation body (e.g. `$.title`). Branded so the compiler tells a faithful
2024
- * input reference from a literal and renders its `{token}` (`{title}`) into the
2025
- * derived write template — the declarative binding the proposal's `$.input.*`
2026
- * path root names (here the root is implied: a fragment `input` value is always
2027
- * an input field reference or a concrete literal).
2028
- */
2029
- interface MutationInputRef {
2030
- readonly [INPUT_REF_BRAND]: true;
2031
- /** The input field name, e.g. `title`. */
2032
- readonly field: string;
2033
- /** The template token this reference renders to, e.g. `{title}`. */
2034
- readonly token: string;
2035
- }
2036
- /** Runtime guard: is `value` a {@link MutationInputRef} (a `$.<field>` read)? */
2037
- declare function isMutationInputRef(value: unknown): value is MutationInputRef;
2038
- declare const ENTITY_REF_BRAND: unique symbol;
2039
- /**
2040
- * A faithful **cross-fragment reference** (`$.entity[i].field`): the value a later
2041
- * fragment binds from an **earlier** fragment's written field (proposal §3). The
2042
- * compiler ({@link compileMutationPlan}) resolves it at **compile time** to the
2043
- * earlier fragment's binding for that field — a `TransactWriteItems` cannot read
2044
- * one item's value mid-transaction, so the dependency is resolved declaratively
2045
- * from the shared input (the referenced field must itself trace to an input
2046
- * reference or a literal), keeping the composed transaction render-time-resolvable
2047
- * and atomic. A **forward / self** reference (to a same-or-later fragment) is a
2048
- * circular dependency, rejected at build time.
2049
- */
2050
- interface MutationEntityRef {
2051
- readonly [ENTITY_REF_BRAND]: true;
2052
- /** The 0-based index of the producing fragment (declaration order). */
2053
- readonly fragmentIndex: number;
2054
- /** The produced field name on that fragment's written entity. */
2055
- readonly field: string;
2056
- /** The source path, e.g. `$.entity[0].postId` (documentary; surfaces in errors). */
2057
- readonly path: string;
2058
- }
2059
- /**
2060
- * The input proxy handed to a mutation body as `$`. Any field read mints a
2061
- * faithful {@link MutationInputRef}; symbol probes are inert. A field is never
2062
- * coerced or transformed — a fragment `input` value is either a `$.<field>`
2063
- * reference or a concrete literal, matching the declarative-only line (§2). The
2064
- * type is `Record<string, MutationInputRef>` so a body reads `$.anyField`.
2065
- */
2066
- type MutationInputProxy = Record<string, MutationInputRef>;
2067
- declare const FRAGMENT_BRAND: unique symbol;
2068
- /**
2069
- * The per-field input binding of a fragment: model field name → a faithful
2070
- * {@link MutationInputRef} (a `$.<field>` reference) or a concrete scalar literal
2071
- * (e.g. a fixed discriminator). One declarative level — the values bind the model
2072
- * field they key, never a transform.
2073
- */
2074
- type FragmentInput = Readonly<Record<string, MutationInputRef | string | number | boolean | Date | null>>;
2075
- /**
2076
- * One recorded write fragment (proposal §3): an **intent** + a target entity + an
2077
- * optional adopted save contract (`use:`) + the input binding. It is a
2078
- * *declaration* the compiler expands — never an executed write.
2079
- *
2080
- * - `intent` — `create` / `update` / `remove`; selects the lifecycle + base op.
2081
- * - `entity` — the target entity (resolved at definition time).
2082
- * - `input` — the model-field → input-field / literal binding (declarative).
2083
- * - `use` — the adopted {@link EntityWritesDefinition} (whole set), or `undefined`
2084
- * to default to the target model's own `writes` (and, absent that, the trivial
2085
- * base op). Per the `use:` rule it is **never** a single lifecycle.
2086
- */
2087
- interface MutationFragment {
2088
- readonly [FRAGMENT_BRAND]: true;
2089
- readonly intent: MutationIntent;
2090
- readonly entity: EntityRef;
2091
- readonly input: FragmentInput;
2092
- readonly use?: EntityWritesDefinition;
2093
- }
2094
- /** Runtime guard: is `value` a {@link MutationFragment}? */
2095
- declare function isMutationFragment(value: unknown): value is MutationFragment;
2096
- /**
2097
- * Options for a write fragment: the declarative `input` binding and the optional
2098
- * `use:` save-contract override (the **whole** `writes` set, never a lifecycle).
2099
- */
2100
- interface FragmentOptions {
2101
- /** The model-field → input-field / literal binding. */
2102
- readonly input: FragmentInput;
2103
- /**
2104
- * An optional custom save contract to adopt (the **whole** {@link
2105
- * EntityWritesDefinition}). Omit to default to the target model's own `writes`.
2106
- * Per the proposal's `use:` rule this is never a single lifecycle (`writes.create`)
2107
- * — the fragment's intent already selects the lifecycle.
2108
- */
2109
- readonly use?: EntityWritesDefinition;
2110
- }
2111
- /**
2112
- * The intent recorder handed to a mutation body as `m`. `m.create` / `m.update` /
2113
- * `m.remove` each record a {@link MutationFragment} of the corresponding intent;
2114
- * `remove` (not `del`) is the deletion intent per the proposal.
2115
- */
2116
- interface MutationRecorder {
2117
- /** Record a `create` fragment (→ lifecycle `create`, base op `PutItem`). */
2118
- create(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
2119
- /** Record an `update` fragment (→ lifecycle `update`, base op `UpdateItem`). */
2120
- update(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
2121
- /** Record a `remove` fragment (→ lifecycle `remove`, base op `DeleteItem`). */
2122
- remove(target: () => ModelStatic<DDBModel> | (new (...args: unknown[]) => unknown), options: FragmentOptions): MutationFragment;
2123
- }
2124
- declare const COMMAND_PLAN_BRAND: unique symbol;
2125
- /**
2126
- * The recorded **CommandPlan** — a mutation's IR (proposal §3). It carries the
2127
- * mutation `name` and the **list** of {@link MutationFragment}s the body declared
2128
- * (1..N; #83 compiles the 1-fragment case end-to-end, the list shape is ready for
2129
- * the #90 N-fragment merge). The body closure is discarded — only the declarative
2130
- * fragment list is kept, exactly as the contract IR discards its method closures.
2131
- */
2132
- interface CommandPlan {
2133
- readonly [COMMAND_PLAN_BRAND]: true;
2134
- /** The mutation name (documentary; surfaces in compiler error messages). */
2135
- readonly name: string;
2136
- /** The declared write fragments, in declaration order (the mutation IR). */
2137
- readonly fragments: readonly MutationFragment[];
2138
- }
2139
- /** Runtime guard: is `value` a {@link CommandPlan} (a `mutation(...)` result)? */
2140
- declare function isCommandPlan(value: unknown): value is CommandPlan;
2141
- /**
2142
- * Declare an internal **mutation** — a write-plan composition (proposal §1, §3).
2143
- * The body receives the intent recorder `m` (`m.create` / `m.update` /
2144
- * `m.remove`) and the input proxy `$` (`$.field` → a {@link MutationInputRef}),
2145
- * and returns the **list** of write fragments to compose atomically. The result
2146
- * is a branded {@link CommandPlan} (the mutation IR) — *not* public; it implements
2147
- * a Command IF via `publicCommandModel(...).plan(mutation)`.
2148
- *
2149
- * @param name The mutation name (documentary; used in compiler errors).
2150
- * @param body `(m, $) => [fragment, …]` — declares the fragment list.
2151
- * @throws if the body returns a non-array, an empty list, or a non-fragment value.
2152
- */
2153
- declare function mutation(name: string, body: (m: MutationRecorder, $: MutationInputProxy) => readonly MutationFragment[]): CommandPlan;
2154
- /**
2155
- * Alias of {@link mutation} (proposal naming: the internal DSL is `mutation` /
2156
- * `definePlan`). Identical behavior; provided so a producer may name the plan
2157
- * declaration `definePlan('…', …)` when that reads more clearly at the call site.
2158
- */
2159
- declare const definePlan: typeof mutation;
2160
-
2161
- /**
2162
- * Single-fragment mutation → command-op compiler (issue #83, Epic #80; proposal
2163
- * `docs/mutation-command-derivation.md` §3).
2164
- *
2165
- * A {@link CommandPlan} (a `mutation(...)` result) is the internal IR: a **list**
2166
- * of write fragments to compose atomically. This module compiles the
2167
- * **single-fragment** case end-to-end into the **same** {@link ContractMethodOp}
2168
- * the #58 contract recorder produces, so the existing serializer
2169
- * (`opToDefinition` → `buildCommandSpec`, `src/spec/contracts.ts` /
2170
- * `src/spec/operations.ts`) and the existing runtimes consume it with no new
2171
- * write substrate. The IR is already a list, so the **N-fragment atomic merge is
2172
- * #90** — this module rejects N>1 with a clear pointer rather than half-merging.
2173
- *
2174
- * ## Intent → base op (proposal §3, "single fragment compile")
2175
- *
2176
- * | intent | base op |
2177
- * |----------|-------------------------------------------|
2178
- * | `create` | `PutItem` (`attribute_not_exists(PK)`) |
2179
- * | `update` | `UpdateItem` (key + changed fields) |
2180
- * | `remove` | `DeleteItem` (key) |
2181
- *
2182
- * ## `use:` resolution (the resolved fork — proposal §3, pinned spec correction)
2183
- *
2184
- * The fragment's **intent** selects the lifecycle; `use:` is **optional**:
2185
- *
2186
- * - `use:` present → adopt that {@link EntityWritesDefinition} (the **whole**
2187
- * `writes` set; never a single lifecycle).
2188
- * - `use:` omitted → default to the **target model's own** `entityWrites` save
2189
- * contract ({@link getEntityWrites}).
2190
- * - the resolved save contract (custom or default) has **no** entry for the
2191
- * intent's lifecycle, **or the target declares no `writes` at all** → the
2192
- * **trivial base op** for the intent (create → `Put` `attribute_not_exists(PK)`,
2193
- * update → `Update`, remove → `Delete`) against the plain model. So
2194
- * `m.create(() => PostModel, { input })` works on a bare model.
2195
- *
2196
- * Either way #83 emits the **same** base op — the resolved
2197
- * {@link LifecycleContract} only matters as the **per-fragment hook** the
2198
- * effect-derivation issues (#84–#87) read: the resolved lifecycle's
2199
- * {@link LifecycleContract.effects} are exposed on {@link CompiledFragment} and
2200
- * consumed by NONE of #83 (the empty hook).
2201
- */
2202
-
2203
- /**
2204
- * A **referential-integrity assertion** derived from a `requires`
2205
- * ({@link RequiresEffect}) effect (issue #84; proposal §2/§3). It is the
2206
- * realization of `w.exists(() => Entity, { <entityKeyField>: '$.input.<field>' })`:
2207
- * the referenced entity must already exist for the bound keys, asserted by a
2208
- * read-only `ConditionCheck` (`attribute_exists(PK)`) item composed into the
2209
- * mutation's atomic `TransactWriteItems`. A failed assertion cancels the WHOLE
2210
- * transaction, so the entity / edges / derived updates / events that the same
2211
- * transaction would write never persist (referential integrity is enforced
2212
- * atomically, not by a pre-read).
2213
- *
2214
- * The shape is **runtime-neutral** — it carries the structural binding (target
2215
- * entity + the target key fields resolved from the mutation input) that BOTH the
2216
- * spec serializer (→ a {@link import('./types.js').TransactionItemSpec} with
2217
- * templated `keyCondition`, for the Python bridge) and the in-process TS runtime
2218
- * (→ a `ConditionCheck` `TransactWriteItems` entry, keyed from `(key, params)`)
2219
- * derive their concrete form from. The compiler resolves the key binding once,
2220
- * here, so both runtimes assert the **same** keyed row from the same input.
2221
- */
2222
- interface DerivedConditionCheck {
2223
- /** The referenced entity whose existence is asserted (resolved target model). */
2224
- readonly entity: EntityRef;
2225
- /**
2226
- * The asserted row's key binding: target primary-key **input field name** → the
2227
- * **mutation input field** that supplies it (parsed from the effect's
2228
- * `$.input.<field>` value). Every target primary-key field is bound (a partial
2229
- * key cannot identify a row to assert on), so the runtimes build the referenced
2230
- * entity's full primary key from the mutation params.
2231
- */
2232
- readonly keyBinding: Readonly<Record<string, string>>;
2233
- }
2234
- /**
2235
- * A **derived edge write** — the adjacency `Put` / `Delete` {@link TransactionItemSpec}
2236
- * set that materializes a relation edge on DynamoDB (issue #85; proposal §2 `edges`),
2237
- * derived from one `w.putEdge(() => Target, 'relation')` / `w.deleteEdge(...)` effect.
2238
- *
2239
- * The effect names a **relation** (`Target.relationProperty`) whose rows form the
2240
- * edge; the **adjacency entity** — the entity whose row IS that edge — is the
2241
- * relation's *target model* ({@link RelationMetadata.targetFactory}), resolved here
2242
- * and round-trip-verified by the #82 primitive ({@link deriveEdgeWriteItemsFor}, with
2243
- * the #85-tightened PK-partition guard). The resolved item set is lifecycle-distinguished
2244
- * by the fragment's intent ({@link INTENT_EDGE_LIFECYCLE}):
2245
- *
2246
- * - `create` → `onCreate`: a single adjacency `Put` (the new edge row);
2247
- * - `remove` → `onDelete`: a single adjacency `Delete` (the edge row keyed by PK/SK);
2248
- * - `update` → `onUpdateKeyChange`: `Delete`(old edge, keyed in the `{old.*}`
2249
- * namespace) + `Put`(new edge) — the edge **moves** atomically (spec correction).
2250
- *
2251
- * The items carry templated key/item fields (`{field}` / `{old.field}`); the
2252
- * serializer composes them into the atomic `TransactWriteItems` and the in-process
2253
- * runtime renders the same items from the shared `(key, params)`, so both runtimes
2254
- * write the **same** adjacency row(s) from the same input.
2255
- */
2256
- interface DerivedEdgeWrite {
2257
- /** The adjacency entity (the relation's target — whose row is the edge). */
2258
- readonly entity: EntityRef;
2259
- /** The relation (`Target.relationProperty`) the edge materializes (documentary). */
2260
- readonly relation: {
2261
- readonly target: string;
2262
- readonly property: string;
2263
- };
2264
- /** The lifecycle-distinguished adjacency item set (`Put` / `Delete`). */
2265
- readonly items: readonly TransactionItemSpec[];
2266
- }
2267
- /**
2268
- * A **derived update** — an atomic `ADD` (`UpdateItem`) on a target row, derived
2269
- * from one `w.increment(() => Entity, { key }, 'field', delta)` effect (issue #85;
2270
- * proposal §2 `derive`). It realizes a cascading counter such as
2271
- * `User.postCount += 1`: an atomic `ADD #field :delta` that needs no prior read and
2272
- * is safe under concurrency (a `SET` would clobber a concurrent increment).
2273
- *
2274
- * The lifecycle decides the sign convention the *declaration* uses (the author
2275
- * declares `+1` on `create`, `-1` on `remove`, and a `-1`-on-old + `+1`-on-new pair
2276
- * on `update` / onUpdateKeyChange); the compiler stores the declared delta verbatim.
2277
- * The target row's key binds from the mutation input — `$.input.<field>` (the new /
2278
- * current value) or `$.old.<field>` (the pre-mutation value, for decrementing the
2279
- * **old** parent on a key change; the {@link OLD_VALUE_NAMESPACE} convention).
2280
- */
2281
- interface DerivedUpdate {
2282
- /** The entity whose attribute is derive-updated (resolved target model). */
2283
- readonly entity: EntityRef;
2284
- /**
2285
- * The target row's key binding: target primary-key **input field name** → the
2286
- * source param that supplies it. A `$.input.<field>` source binds the param
2287
- * `<field>`; a `$.old.<field>` source binds the param `old.<field>` (the
2288
- * pre-mutation value). Every target primary-key field is bound.
2289
- */
2290
- readonly keyBinding: Readonly<Record<string, string>>;
2291
- /** The attribute to atomically `ADD` to. */
2292
- readonly attribute: string;
2293
- /** The signed delta to add (e.g. `+1` create, `-1` remove). */
2294
- readonly amount: number;
2295
- }
2296
- /**
2297
- * A **derived uniqueness guard** — the marker-row `Put` (and, on a unique-field
2298
- * change, the old-guard `Delete` + new-guard `Put` swap) that enforces an
2299
- * application-level uniqueness invariant declared by `w.unique({ name, scope,
2300
- * fields })` (issue #86; proposal §2 `unique`). It is the realization of a named,
2301
- * composite-scope uniqueness constraint that a unique GSI cannot express
2302
- * (e.g. *unique title per user*).
2303
- *
2304
- * ## The guard-row key (deterministic, built from `name` / `scope` / `fields`)
2305
- *
2306
- * The guard occupies a synthetic marker row whose primary key is built **purely**
2307
- * from the constraint:
2308
- *
2309
- * - `PK` = `UNIQUE#<name>` followed by one `#{<scopeField>}` segment per `scope`
2310
- * path (a composite scope yields several segments) — the **partition** the
2311
- * uniqueness applies within, e.g. `UNIQUE#postTitlePerUser#{userId}`;
2312
- * - `SK` = `VALUE#` followed by one `#{<field>}` segment per `fields` path (the
2313
- * first carries no leading separator) — the **unique value** within that scope,
2314
- * e.g. `VALUE#{title}`.
2315
- *
2316
- * Each `$.input.<field>` path contributes a `{<field>}` template token (rendered
2317
- * from the mutation input). The shape is **deterministic** (same constraint →
2318
- * same key), so a second write of the same scoped value targets the SAME row, and
2319
- * the `attribute_not_exists` guard on the `Put` then fails — rolling back the
2320
- * WHOLE transaction so the entity is never created.
2321
- *
2322
- * ## create vs. update-swap
2323
- *
2324
- * - **create / remove** — a single guard item: `create` → `Put` (claim the value)
2325
- * guarded by `attribute_not_exists(PK)`; `remove` → `Delete` (release the value);
2326
- * - **update changing a unique field** — `Delete`(OLD guard) + `Put`(NEW guard):
2327
- * the old marker row (keyed in the `{old.*}` namespace, the {@link
2328
- * OLD_VALUE_NAMESPACE} convention shared with the edge `onUpdateKeyChange` and
2329
- * the derived-counter old-parent decrement) is released and the new value claimed
2330
- * atomically, so the uniqueness moves with the field. The new `Put` keeps the
2331
- * `attribute_not_exists` guard so the moved-to value cannot collide.
2332
- *
2333
- * The items are **runtime-neutral** {@link TransactionItemSpec}s carrying the
2334
- * literal-key marker ({@link TransactionItemSpec.literalKey}) and the
2335
- * {@link MARKER_ROW_ENTITY} sentinel (there is no manifest entity), so BOTH runtimes
2336
- * write the SAME marker row(s) from the same input and the guard composes into the
2337
- * entity's atomic `TransactWriteItems` (#90).
2338
- */
2339
- interface DerivedUniqueGuard {
2340
- /** The constraint name (its key-shape discriminator on DynamoDB). */
2341
- readonly name: string;
2342
- /** The guard's marker-row item set (`Put` / `Delete`), lifecycle-distinguished. */
2343
- readonly items: readonly TransactionItemSpec[];
2344
- }
2345
- /**
2346
- * A **derived outbox event** — the transactional-outbox `Put` ({@link
2347
- * TransactionItemSpec}) that records a domain event ATOMICALLY with the entity
2348
- * write, derived from one `w.event(name, payload)` effect (issue #87; proposal §2
2349
- * `emits`, §4 "delivery is outside"). The event row is written in the **same**
2350
- * `TransactWriteItems` as the entity, so the event and the state it announces can
2351
- * never diverge (no "wrote the row, lost the event" gap).
2352
- *
2353
- * ## The outbox-row key + shape (deterministic; drainable via `src/cdc/`)
2354
- *
2355
- * The row occupies a synthetic marker row in its own `OUTBOX#…` key-space:
2356
- *
2357
- * - `PK` = `OUTBOX#<eventName>` followed by one `#{<keyField>}` segment per
2358
- * primary-key input field of the **writing entity** — so each entity emission has
2359
- * its own deterministic outbox row, e.g. `OUTBOX#PostCreated#{postId}`;
2360
- * - `SK` = `EVENT#` followed by the same per-key-field segments — a stable,
2361
- * single-row-per-emission sort key;
2362
- * - the item also stores `eventName` plus every declared `payload` field (each a
2363
- * `{field}` template rendered from the shared method params), so the **`newImage`**
2364
- * of the row carries the complete, typed event body.
2365
- *
2366
- * Because the row is a normal table item, the `src/cdc/` Streams substrate captures
2367
- * its write as a {@link import('../cdc/types.js').ChangeEvent} (`eventName:
2368
- * 'INSERT'`, `newImage` = this item). A consumer drains the outbox by filtering the
2369
- * stream on `PK begins_with 'OUTBOX#'` and reading the event body off `newImage` —
2370
- * **delivery / handlers / business reactions live OUTSIDE the mutation** (proposal
2371
- * §4); #87 only writes the drainable RECORD.
2372
- */
2373
- interface DerivedOutboxEvent {
2374
- /** The event name (e.g. `PostCreated`). */
2375
- readonly name: string;
2376
- /** The outbox marker-row `Put` (one item; `literalKey`, `MARKER_ROW_ENTITY`). */
2377
- readonly items: readonly TransactionItemSpec[];
2378
- }
2379
- /**
2380
- * A **derived idempotency guard** — the client-token guard `Put` ({@link
2381
- * TransactionItemSpec}) that makes a same-token re-execution a NO-OP, derived from
2382
- * the lifecycle's `w.idempotentBy(token)` effect (issue #87; proposal §2
2383
- * `idempotency`). It is the transactional realization of "process this command at
2384
- * most once for this client token".
2385
- *
2386
- * ## The guard-row key (deterministic, built from the token)
2387
- *
2388
- * The guard occupies a synthetic marker row keyed **purely** by the client token:
2389
- *
2390
- * - `PK` = `IDEMP#{<tokenField>}` — the `$.input.<field>` token (e.g.
2391
- * `IDEMP#{requestId}`), rendered from the shared method params;
2392
- * - `SK` = `TOKEN` (a fixed discriminator — one guard row per token).
2393
- *
2394
- * The `Put` is guarded by `attribute_not_exists(PK)`: the FIRST execution claims the
2395
- * token row; a SECOND execution with the same token targets the SAME row and the
2396
- * guard fails, cancelling the WHOLE `TransactWriteItems` — so the entity write, the
2397
- * outbox event, and every other effect roll back and **no effect is double-applied**.
2398
- * The shape is identical to the uniqueness guard's claim `Put` (the same `literalKey`
2399
- * marker mechanism), so BOTH runtimes write / assert the SAME row from the same input.
2400
- */
2401
- interface DerivedIdempotencyGuard {
2402
- /** The mutation-input field the token binds from (documentary; surfaces in errors). */
2403
- readonly tokenField: string;
2404
- /** The guard marker-row `Put` (one item; `attribute_not_exists`, `literalKey`). */
2405
- readonly items: readonly TransactionItemSpec[];
2406
- }
2407
- /**
2408
- * The result of compiling one fragment: the {@link ContractMethodOp} the existing
2409
- * serializer / runtime consume, the resolved contract Key fields (the model's
2410
- * primary-key input fields, also the consistent read-back key), and the **opaque
2411
- * per-fragment hook** — the resolved lifecycle's §2 effect set (empty-consumed by
2412
- * #83; read by #84–#87).
2413
- */
2414
- interface CompiledFragment {
2415
- /** The resolved write op (`put` / `update` / `delete`), serializer-ready. */
2416
- readonly op: ContractMethodOp;
2417
- /** The contract Key fields (the model primary-key input fields). */
2418
- readonly keyFields: readonly string[];
2419
- /**
2420
- * The resolved lifecycle's §2 effect set, or `undefined` when the fragment
2421
- * resolved to the trivial base op (no `writes` entry). **The per-fragment hook
2422
- * for #84–#87** — #83 derives NO items from it. #84 reads the `requires` arm to
2423
- * derive {@link conditionChecks}; the remaining arms (unique / edges / derive /
2424
- * emits / idempotency) are still stored opaquely for #85–#87.
2425
- */
2426
- readonly effects?: LifecycleEffects;
2427
- /**
2428
- * The referential-integrity assertions derived from the resolved lifecycle's
2429
- * `requires` effects (issue #84): one {@link DerivedConditionCheck} per
2430
- * `w.exists(...)`. Present (non-empty) only when the fragment's lifecycle
2431
- * declares `requires`; absent otherwise (no regression — a fragment with no
2432
- * `requires` compiles to exactly the #83/#90 base op). Each becomes a read-only
2433
- * `ConditionCheck` (`attribute_exists`) item in the mutation's atomic
2434
- * `TransactWriteItems`, and their presence **promotes** an otherwise single-op
2435
- * mutation to a transaction (a `ConditionCheck` only exists inside a
2436
- * `TransactWriteItems`).
2437
- */
2438
- readonly conditionChecks?: readonly DerivedConditionCheck[];
2439
- /**
2440
- * The adjacency edge writes derived from the resolved lifecycle's `edges` effects
2441
- * (issue #85): one {@link DerivedEdgeWrite} per `w.putEdge(...)` / `w.deleteEdge(...)`,
2442
- * lifecycle-distinguished by the fragment intent (`create` → `Put`, `remove` →
2443
- * `Delete`, `update` → `Delete(old) + Put(new)`). Present (non-empty) only when the
2444
- * lifecycle declares `edges`; absent otherwise (no regression). Each derived item is
2445
- * composed into the method's atomic `TransactWriteItems`.
2446
- */
2447
- readonly edgeWrites?: readonly DerivedEdgeWrite[];
2448
- /**
2449
- * The derived (cascading) updates from the resolved lifecycle's `derive` effects
2450
- * (issue #85): one {@link DerivedUpdate} per `w.increment(...)`, each an atomic
2451
- * `ADD` on a target counter. Present (non-empty) only when the lifecycle declares
2452
- * `derive`; absent otherwise (no regression). Each becomes an `UpdateItem` (`ADD`)
2453
- * in the method's atomic `TransactWriteItems`.
2454
- */
2455
- readonly derivedUpdates?: readonly DerivedUpdate[];
2456
- /**
2457
- * The uniqueness guards derived from the resolved lifecycle's `unique` effects
2458
- * (issue #86): one {@link DerivedUniqueGuard} per `w.unique({ name, scope,
2459
- * fields })`, lifecycle-distinguished by the fragment intent (`create` → a guarded
2460
- * marker `Put`, `remove` → a marker `Delete`, `update` → `Delete(old) + Put(new)`
2461
- * — the value swap). Present (non-empty) only when the lifecycle declares
2462
- * `unique`; absent otherwise (no regression — a fragment with no `unique` compiles
2463
- * to exactly the #83/#90 base op). Each guard's items compose into the method's
2464
- * atomic `TransactWriteItems`; the guard `Put`'s `attribute_not_exists` makes a
2465
- * duplicate roll the WHOLE transaction back, so the entity is never created.
2466
- */
2467
- readonly uniqueGuards?: readonly DerivedUniqueGuard[];
2468
- /**
2469
- * The outbox events derived from the resolved lifecycle's `emits` effects (issue
2470
- * #87): one {@link DerivedOutboxEvent} per `w.event(name, payload)`, each a
2471
- * transactional-outbox `Put` recording the event ATOMICALLY with the entity write
2472
- * (so event and state cannot diverge). Present (non-empty) only when the lifecycle
2473
- * declares `emits`; absent otherwise (no regression — a fragment with no `emits`
2474
- * compiles to exactly the #83/#90 base op). Each event's `Put` composes into the
2475
- * method's atomic `TransactWriteItems`; its row is drainable via `src/cdc/`
2476
- * (`PK begins_with 'OUTBOX#'`). Delivery / handlers are OUT of scope (proposal §4).
2477
- */
2478
- readonly outboxEvents?: readonly DerivedOutboxEvent[];
2479
- /**
2480
- * The idempotency guard derived from the resolved lifecycle's `idempotency` effect
2481
- * (issue #87): a single {@link DerivedIdempotencyGuard} (the `w.idempotentBy(token)`
2482
- * client-token guard `Put`, `attribute_not_exists`). Present only when the lifecycle
2483
- * declares `idempotency`; absent otherwise (no regression). Its `Put` composes into
2484
- * the method's atomic `TransactWriteItems`; a same-token re-execution fails the
2485
- * guard and rolls the WHOLE transaction back — so no effect is double-applied.
2486
- */
2487
- readonly idempotencyGuard?: DerivedIdempotencyGuard;
2488
- }
2489
- /**
2490
- * Resolve the {@link LifecycleContract} a fragment adopts for its intent:
2491
- * `use:` (the whole save contract) when present, else the target model's own
2492
- * `entityWrites`, then the lifecycle entry for the intent's phase. Returns
2493
- * `undefined` (→ the trivial base op) when no save contract declares the phase or
2494
- * the target declares no `writes`.
2495
- */
2496
- declare function resolveLifecycle(fragment: MutationFragment): LifecycleContract | undefined;
2497
- /**
2498
- * A resolver for a {@link MutationEntityRef} (`$.entity[i].field`) — supplied by
2499
- * the multi-fragment compiler ({@link compileMutationPlan}) so a later fragment's
2500
- * leaf can be resolved to the **earlier** fragment's binding for that field. It
2501
- * returns the already-rendered value the producer fragment stores at that field
2502
- * (a {@link ContractParamRef} / {@link ContractKeyFieldRef} / literal), so the
2503
- * cross-fragment value renders from the **same** shared input at execution time
2504
- * (a `TransactWriteItems` cannot read another item mid-transaction). `null` for a
2505
- * single-fragment compile, where a cross-fragment reference is meaningless.
2506
- */
2507
- type EntityRefResolver = (ref: MutationEntityRef, consumerIndex: number, consumerField: string) => unknown;
2508
- /**
2509
- * Compile one fragment into a {@link CompiledFragment}. Builds the intent's base
2510
- * op as a {@link ContractMethodOp}:
2511
- *
2512
- * - `create` → a `put` whose `item` is the fragment input (primary-key fields as
2513
- * {@link ContractKeyFieldRef}, the rest as {@link ContractParamRef}), guarded by
2514
- * `attribute_not_exists(PK)` (`{ notExists: true }`);
2515
- * - `update` → an `update` whose `keys` is the whole-keys sentinel with
2516
- * `keyFields` = the primary-key fields (bound from `$.input.*`), and whose
2517
- * `changes` is the **non-key** fragment input;
2518
- * - `remove` → a `delete` whose `keys` is the whole-keys sentinel + `keyFields`.
2519
- *
2520
- * The resolved lifecycle's effect set is attached as the empty per-fragment hook.
2521
- *
2522
- * @param fragment The fragment to compile.
2523
- * @param index Its 0-based position in the mutation's fragment list (the
2524
- * cross-fragment reference index). `0` for a single-fragment compile.
2525
- * @param resolveEntityRef Resolver for a `$.entity[i].field` cross-fragment
2526
- * reference (multi-fragment compile), or `null` for a single-fragment compile
2527
- * (where such a reference is a hard error).
2528
- */
2529
- declare function compileFragment(fragment: MutationFragment, index?: number, resolveEntityRef?: EntityRefResolver | null): CompiledFragment;
2530
- /**
2531
- * Compile a single-fragment {@link CommandPlan} into its {@link CompiledFragment}.
2532
- * The N-fragment atomic merge is **#90** ({@link compileMutationPlan}); this entry
2533
- * point is the single-fragment fast path used by #83 and by the multi-fragment
2534
- * compiler for `N === 1` (no behavioral change). A cross-fragment reference is a
2535
- * hard error here — there is no earlier fragment to read from.
2536
- *
2537
- * @throws if `plan` is not a {@link CommandPlan}, or declares more than one fragment.
2538
- */
2539
- declare function compileSingleFragmentPlan(plan: CommandPlan): CompiledFragment;
2540
- /**
2541
- * The result of compiling a (1..N)-fragment {@link CommandPlan} (#90): the ordered
2542
- * per-fragment {@link CompiledFragment}s, merged so they form **one atomic
2543
- * `TransactWriteItems`**. For a single fragment this is exactly the #83 result with
2544
- * `fragments.length === 1` (no regression); for N≥2 the fragments are validated
2545
- * together — same-item conflicts rejected, cross-fragment `$.entity[i].field`
2546
- * dependencies resolved (circular deps rejected), and the composed item count
2547
- * capped at {@link MAX_TRANSACT_COMPOSE_ITEMS}.
2548
- *
2549
- * The **primary fragment** is fragment 0 — the entity the public Command IF is
2550
- * keyed by and read back from (`returnSelection`); the proposal's `CreatePost`
2551
- * keys on `Post` (fragment 0), with `AuditLog` (fragment 1) composed atomically.
2552
- */
2553
- interface CompiledMutationPlan {
2554
- /** The mutation name (documentary; surfaces in compiler errors). */
2555
- readonly name: string;
2556
- /** Each fragment compiled to its base op, in declaration order. */
2557
- readonly fragments: readonly CompiledFragment[];
2558
- }
2559
- /**
2560
- * Compile a (1..N)-fragment {@link CommandPlan} into one **atomically composable**
2561
- * {@link CompiledMutationPlan} (issue #90; proposal §3, "CROSS-FRAGMENT composition
2562
- * + validation"). Each fragment is compiled with #83's per-fragment compiler; the
2563
- * resulting items are **merged into one `TransactWriteItems`**, with every MUST AC
2564
- * enforced at build time:
2565
- *
2566
- * 1. **Cross-fragment data dependencies** — a fragment's `$.entity[i].field` leaf
2567
- * is resolved to fragment `i`'s binding for `field` (a build-time alias, so the
2568
- * value renders from the same shared input at execution; a `TransactWriteItems`
2569
- * cannot read another item mid-transaction). A reference to a **same-or-later**
2570
- * fragment (`i ≥ consumer`) is a **circular** dependency and is rejected; an
2571
- * out-of-range index, or a field the producer does not write, is rejected.
2572
- * 2. **Same-item conflict** — two fragments that write the **same primary key**
2573
- * ({@link keySignature}) in one transaction are rejected (DynamoDB rejects a
2574
- * transaction touching one item twice), naming the conflicting fragments.
2575
- * 3. **≤25 items** — a composed plan over {@link MAX_TRANSACT_COMPOSE_ITEMS} base
2576
- * items is a hard error (an atomic transaction is NEVER split — atomicity, #64).
2577
- *
2578
- * Because fragment `i` is fully compiled before fragment `i+1`, and a cross-fragment
2579
- * reference may only point **backward** (to an already-compiled fragment), resolving
2580
- * dependencies in declaration order is itself the topological order — a forward /
2581
- * self reference is the only cycle shape and is rejected directly, so no separate
2582
- * cycle search is needed.
2583
- *
2584
- * @throws if `plan` is not a {@link CommandPlan}; on a malformed / circular / dangling
2585
- * cross-fragment reference; on a same-item conflict; or on >25 composed items.
2586
- */
2587
- declare function compileMutationPlan(plan: CommandPlan): CompiledMutationPlan;
2588
-
2589
- /**
2590
- * Contract DSL — `publicQueryModel` / `publicCommandModel` (issue #58, CQRS
2591
- * Contract layer, Epic #57; spec `docs/cqrs-contract.md`).
2592
- *
2593
- * A **QueryModel / CommandModel** is the public, storage-independent interface
2594
- * of a CQRS (logical) service. It is **keyed** — its Key *is* the access pattern
2595
- * — and holds **one or more named Methods**, each a distinct **use case** (e.g.
2596
- * `get` + `summary` over the same key, differing only in projection). The Key is
2597
- * the access pattern; the Method is the use case.
2598
- *
2599
- * This module sits **on top of** the existing definition DSL
2600
- * (`defineQuery` / `defineList` / `definePut` / `defineUpdate` / `defineDelete`,
2601
- * `src/define/define.ts`) and shares its internal representation
2602
- * ({@link OperationDefinition}). A contract method body resolves to a single
2603
- * declarative operation on the underlying model; the closure is **never**
2604
- * serialized — only the resolved declaration is captured into the IR.
2605
- *
2606
- * ```ts
2607
- * const ArticleById = publicQueryModel<ArticleIdKey>()({
2608
- * get: (keys, params) =>
2609
- * Article.query(keys, { articleId: true, title: true, body: true }, params),
2610
- * });
2611
- * ```
2612
- *
2613
- * ## Method signatures (the generation source)
2614
- *
2615
- * Every method has one uniform signature, accepting **either one key or an array
2616
- * of keys** (the array form is what makes a contract uniformly batch-resolvable):
2617
- *
2618
- * ```ts
2619
- * type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
2620
- * type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
2621
- * ```
2622
- *
2623
- * These two types are the **generation source** for every binding target
2624
- * (OpenAPI / Python / PHP / Go / Rust) — see the proposal.
2625
- *
2626
- * ## Method names are use cases, not operations
2627
- *
2628
- * A method is named for its **use case** (`get` / `summary` / `recent` /
2629
- * `disable`), never for the storage op it runs. Whether it resolves internally
2630
- * to a `query` (point lookup) or a `list` (partition read) is the **Runtime's**
2631
- * concern and is invisible to callers and to generated bindings. The internal op
2632
- * still determines the **derived** `resolution` / `inputArity` facts (below), but
2633
- * those are bookkeeping, not part of the method's public name.
2634
- *
2635
- * ## Derived facts: `resolution` and `inputArity`
2636
- *
2637
- * Per the proposal's N+1 Safety section these are **derived** from the internal
2638
- * op kind, never hand-written:
2639
- *
2640
- * - a unique-key `query` / `GetItem` → `resolution: 'point'`, `inputArity:
2641
- * 'either'` (a key array becomes one `BatchGetItem`);
2642
- * - a partition `list` / `Query` → `resolution: 'range'`, `inputArity: 'single'`
2643
- * (DynamoDB cannot coalesce N partition `Query`s — feeding an array would be an
2644
- * N+1 fan-out, so the range method only accepts a single key).
2645
- *
2646
- * Command methods additionally carry an explicit **result type** (`void` | a
2647
- * `Result` | the updated entity) as part of the contract; see
2648
- * {@link CommandResultKind}.
2649
- *
2650
- * ## Build-time hardening (reused from `defineTransaction`)
2651
- *
2652
- * The method body is evaluated at definition time with **throwing sentinels** in
2653
- * place of `keys` / `params`, recorded through a model recorder, and subjected to
2654
- * the **same** hardening `defineTransaction` (`src/define/transaction.ts`) applies
2655
- * to value positions:
2656
- *
2657
- * 1. **Throwing sentinel Proxy** — every property / method access other than the
2658
- * internal brand reads and primitive coercion throws at the access site.
2659
- * 2. **Per-access coercion ledger (primary boundary)** — a faithful reference is
2660
- * stored as the Proxy object and is *never* coerced, so any coercion of a
2661
- * `params` field is positive proof the body consumed it into a transform /
2662
- * branch / interpolation and the build is rejected.
2663
- * 3. **Differential evaluation (secondary)** — the body runs twice with disjoint
2664
- * per-field markers; every recorded leaf must be a faithful reference or a
2665
- * byte-identical literal across passes.
2666
- * 4. **Consumed-field check** — a `params` field the body accessed but that never
2667
- * reached the recorded operation (a silent value branch) is rejected.
2668
- *
2669
- * So any non-declarative body (a transform, a value branch, a coercion, arbitrary
2670
- * JS over `keys` / `params`) is rejected at build time — never silently emitted
2671
- * as a wrong spec. The #58 issue was the **DSL + IR + type-level surface only**:
2672
- * serialization to the JSON SSoT (#59) and execution against DynamoDB (#62) were
2673
- * out of scope there.
2674
- *
2675
- * ## External Query composition (`query` / `from`, issue #63)
2676
- *
2677
- * The {@link query} / {@link from} primitive (added by #63) extends a read method
2678
- * body with a **build-time-resolved, in-process** reference to **another query
2679
- * contract's** method — relation chaining where the link is a contract reference
2680
- * instead of a model relation (there is no protocol / transport). Placed at a
2681
- * `select` property (`as: query(OtherContract.method, { childKey: from("$.field") })`),
2682
- * it is recorded as a declarative {@link RecordedCompose} edge on the method op
2683
- * (the property name becomes `as`, and it is removed from the read projection).
2684
- * The binding stays declarative — `from` admits only a literal `$`-rooted path —
2685
- * so no arbitrary JS reaches a key binding. The serializer (#59) recovers the
2686
- * referenced contract name by identity and emits the proposal's `compose` shape;
2687
- * the N+1 checker (#60) enforces the `point`-child rule; the runtimes (#62 infra
2688
- * reused) resolve the child once, batched, across all parent records.
2689
- */
2690
-
2691
- /**
2692
- * The **resolution kind** of a method, derived from its internal op:
2693
- *
2694
- * - `'point'` — target keys are known (unique-key `query` / `GetItem`, fixed key
2695
- * sets); coalesces to a `BatchGetItem` for a key array.
2696
- * - `'range'` — target key set is unknown (partition `list` / `Query`); one
2697
- * request per partition key, so it is only ever safe for a single key.
2698
- */
2699
- type Resolution = 'point' | 'range';
2700
- /**
2701
- * What input arity a method accepts, derived from its `resolution`:
2702
- *
2703
- * - `'either'` — a single key **or** an array (a `point` read; the array form is
2704
- * one `BatchGetItem`).
2705
- * - `'single'` — a single key only (a `range` read; an array would be an N+1
2706
- * fan-out and is rejected by construction).
2707
- * - `'array'` — an array only (reserved; not produced by the current resolvers).
2708
- */
2709
- type InputArity = 'single' | 'array' | 'either';
2710
- /**
2711
- * The category of a {@link CommandMethod}'s declared result, part of the
2712
- * contract (it surfaces in OpenAPI and every binding):
2713
- *
2714
- * - `'void'` — fire-and-forget write (no body);
2715
- * - `'result'` — an outcome/status object (e.g. `{ ok, version }`);
2716
- * - `'entity'` — the updated entity (the post-write projection).
2717
- *
2718
- * The concrete `TResult` type is preserved at the type level on
2719
- * {@link CommandMethodSpec}; this discriminant is the runtime/SSoT-facing label.
2720
- */
2721
- type CommandResultKind = 'void' | 'result' | 'entity';
2722
- /**
2723
- * How a command method resolves an **array of keys** to the underlying write
2724
- * surface (proposal "Consistency with the existing write surface"; issue #64).
2725
- * A single key always maps to one write op; an array maps to a batched write,
2726
- * and a contract **must declare which** form it resolves to:
2727
- *
2728
- * - `'transact'` — a `TransactWriteItems`: the per-key write is applied
2729
- * **atomically** (all-or-nothing) and **may carry the write condition subset**
2730
- * ({@link ConditionInput}, `{ notExists }` / equality). DynamoDB caps a
2731
- * `TransactWriteItems` at **25 items** and the call is atomic, so an array of
2732
- * >25 keys **cannot** be split (that would break atomicity); the runtime
2733
- * rejects it with a clear error rather than silently truncating or splitting.
2734
- * - `'batchWrite'` — a `BatchWriteItem`: the per-key writes are applied
2735
- * **non-atomically** and **carry no conditions** (DynamoDB's `BatchWriteItem`
2736
- * cannot express a `ConditionExpression`). An array of any size is permitted;
2737
- * the runtime chunks it per DynamoDB's 25-items-per-request limit and retries
2738
- * `UnprocessedItems`, reusing the existing batch machinery.
2739
- *
2740
- * The single-key form is always one write op regardless of this declaration;
2741
- * `batchMode` only decides how a key **array** is executed.
2742
- */
2743
- type CommandBatchMode = 'transact' | 'batchWrite';
2744
- /**
2745
- * Runtime guard: is `value` a {@link CommandBatchMode} literal (`'transact'` /
2746
- * `'batchWrite'`)? Used by {@link recordContractOp} to validate a declared batch
2747
- * mode at the recording boundary.
2748
- */
2749
- declare function isCommandBatchMode(value: unknown): value is CommandBatchMode;
2750
- /**
2751
- * The formal **query** method signature (proposal: "Query Method — Common
2752
- * Interface"). Accepts a single key **or** an array of keys and returns
2753
- * `TResult`. This one type is the generation source for every binding target.
2754
- *
2755
- * @typeParam TKey - The contract Key (the access pattern / join key).
2756
- * @typeParam TParams - The retrieval options (pagination / cursor / consistency).
2757
- * @typeParam TResult - The method's result type.
2758
- */
2759
- type QueryMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
2760
- /**
2761
- * The formal **command** method signature (proposal: "Command Definition").
2762
- * Symmetric to {@link QueryMethod}, with an explicit result type. A single key
2763
- * maps to one write op; an array maps to a batched write.
2764
- *
2765
- * @typeParam TKey - The contract Key (the target identity).
2766
- * @typeParam TParams - The call params.
2767
- * @typeParam TResult - The declared result type (`void` | a `Result` | entity).
2768
- */
2769
- type CommandMethod<TKey, TParams, TResult> = (key: TKey | readonly TKey[], params: TParams) => TResult;
2770
- /**
2771
- * The options a **command** contract method body passes to a model write
2772
- * (`Model.put` / `Model.update` / `Model.delete`) to declare its batched-write
2773
- * resolution (issue #64) and, optionally, a write condition:
2774
- *
2775
- * ```ts
2776
- * disableMany: (keys, params: { reason: string }) =>
2777
- * User.update(
2778
- * keys,
2779
- * { status: 'disabled', disableReason: params.reason },
2780
- * { batch: 'transact', condition: { notExists: true } },
2781
- * ),
2782
- * ```
2783
- *
2784
- * This is the **type-safe** declaration point: `batch` is constrained to a
2785
- * {@link CommandBatchMode} literal, so a typo'd mode is a compile error, and the
2786
- * recorder ({@link resolveBatchMode}) re-validates at build time (e.g. it rejects
2787
- * a `condition` under `'batchWrite'`, which DynamoDB cannot express). The
2788
- * single-key execution form ignores `batch`; only a key **array** consults it.
2789
- */
2790
- interface ContractWriteOptions {
2791
- /**
2792
- * How a **key array** resolves to the underlying write surface: `'transact'`
2793
- * (atomic `TransactWriteItems`, ≤25, condition-capable) or `'batchWrite'`
2794
- * (non-atomic `BatchWriteItem`, no conditions, chunked). Omit to resolve only a
2795
- * single key (an array is then rejected at execution).
2796
- */
2797
- readonly batch?: CommandBatchMode;
2798
- /**
2799
- * An optional declarative write condition — the existing subset
2800
- * ({@link ConditionInput}: `{ notExists }` or equality). Valid for a single
2801
- * write and for a `'transact'` batch; **not** for `'batchWrite'` (rejected at
2802
- * build time).
2803
- */
2804
- readonly condition?: ConditionInput;
2805
- }
2806
- declare const KEY_REF_BRAND: unique symbol;
2807
- declare const KEY_FIELD_REF_BRAND: unique symbol;
2808
- /**
2809
- * The captured `keys` argument of a contract method body. A branded sentinel that
2810
- * supports the two faithful uses the proposal's examples need:
2811
- *
2812
- * - **passed whole** into a model op's key position
2813
- * (`Model.query(keys, …)` / `Model.update(keys, …)`) — the point / write form;
2814
- * - **destructured per field** to rebuild a partition key
2815
- * (`Model.list({ categoryId: keys.categoryId }, …)`) — the range form. Each
2816
- * `keys.<field>` mints a faithful {@link ContractKeyFieldRef}.
2817
- *
2818
- * Both forms are declarative: the recorded key slot is either this sentinel
2819
- * itself (whole) or a `{ field: ContractKeyFieldRef }` record. Any non-faithful
2820
- * use — coercing `keys`, or transforming a `keys.<field>` ref — is rejected by the
2821
- * same coercion ledger / differential checks as `params`.
2822
- */
2823
- interface ContractKeyRef {
2824
- readonly [KEY_REF_BRAND]: true;
2825
- }
2826
- /**
2827
- * A captured reference to a single field of the method's `keys` argument
2828
- * (`keys.<field>`), minted when a range body rebuilds a partition key. Branded so
2829
- * the verifier can distinguish a faithful key-field reference from a literal, and
2830
- * so it cannot be silently coerced.
2831
- */
2832
- interface ContractKeyFieldRef {
2833
- readonly [KEY_FIELD_REF_BRAND]: true;
2834
- /** The key field name, e.g. `categoryId`. */
2835
- readonly field: string;
2836
- /** The template token this reference renders to, e.g. `{key.categoryId}`. */
2837
- readonly token: string;
2838
- }
2839
- /** Runtime guard: is `value` a {@link ContractKeyRef} (the whole key sentinel)? */
2840
- declare function isContractKeyRef(value: unknown): value is ContractKeyRef;
2841
- /**
2842
- * Mint a faithful {@link ContractKeyFieldRef} for a named key field — a plain
2843
- * (non-Proxy) reference rendering the `{key.field}` differential token but, at a
2844
- * **value** position (a `put` item field), serialized as `{field}` (see
2845
- * `templateLeaf`, `src/spec/operations.ts`). Used by the **mutation compiler**
2846
- * (#83) to mark which of a `create` fragment's item fields are the model's
2847
- * primary-key (contract Key) fields, so the existing serializer recovers the
2848
- * contract Key from the put item (`keyFieldsOf`) exactly as it does for a
2849
- * hand-written #64 `put` that binds `keys.<field>` into the item. Mirrors
2850
- * {@link mintContractParamRef}.
2851
- *
2852
- * @param field The key field name (rendered `{field}` at a value position).
2853
- */
2854
- declare function mintContractKeyFieldRef(field: string): ContractKeyFieldRef;
2855
- /**
2856
- * The whole-`keys` sentinel value an `update` / `delete` op records in its key
2857
- * slot when the body passed `keys` whole into the op's key position. Exposed so
2858
- * the **mutation compiler** (#83) can build an equivalent planned `update` /
2859
- * `delete` op without re-entering the hardening machinery (a mutation has no
2860
- * `keys` argument — its key fields are bound from `$.input.*`, captured via
2861
- * {@link ContractMethodOp.keyFields}).
2862
- */
2863
- declare function wholeKeysSentinel(): ContractKeyRef;
2864
- /** Runtime guard: is `value` a {@link ContractKeyFieldRef}? */
2865
- declare function isContractKeyFieldRef(value: unknown): value is ContractKeyFieldRef;
2866
- declare const PARAM_REF_BRAND: unique symbol;
2867
- /**
2868
- * A captured reference to a field of the method's `params` argument (`{<field>}`).
2869
- * Branded so the recorder can distinguish a faithful reference from a concrete
2870
- * literal, and so a stray reference cannot be silently coerced.
2871
- */
2872
- interface ContractParamRef {
2873
- readonly [PARAM_REF_BRAND]: true;
2874
- /** The template token this reference renders to, e.g. `{limit}`. */
2875
- readonly token: string;
2876
- /** The field name on `params`, e.g. `limit`. */
2877
- readonly field: string;
2878
- }
2879
- /** Runtime guard: is `value` a {@link ContractParamRef}? */
2880
- declare function isContractParamRef(value: unknown): value is ContractParamRef;
2881
- /**
2882
- * Mint a faithful {@link ContractParamRef} for a named field — a concrete
2883
- * (non-Proxy) reference rendering the `{field}` token. Unlike the hardening
2884
- * sentinels {@link makeParamFieldRef} builds during a contract method body (which
2885
- * throw on any transform to detect non-declarative use), this is a *plain* ref
2886
- * used by the **mutation compiler** (#83, `src/spec/mutation-command.ts`): a
2887
- * mutation fragment's `input` binding is captured as a {@link MutationInputRef}
2888
- * (its own declarative sentinel), and the compiler translates each one into this
2889
- * `ContractParamRef` so the **existing** contract serializer
2890
- * ({@link opToDefinition} → {@link buildCommandSpec}) and TS runtime
2891
- * ({@link renderLeaf}) consume it with **zero** new template / param machinery —
2892
- * the param name and `{token}` are the input field name, exactly as a hand-written
2893
- * `params.field` reference would render.
2894
- *
2895
- * @param field The param / input field name (the rendered token is `{field}`).
2896
- */
2897
- declare function mintContractParamRef(field: string): ContractParamRef;
2898
- declare const FROM_REF_BRAND: unique symbol;
2899
- declare const COMPOSE_NODE_BRAND: unique symbol;
2900
- /**
2901
- * A parent-result source path produced by {@link from} (proposal "External
2902
- * Query"). It names the field of the **parent** method's result that supplies a
2903
- * composed child's key, e.g. `from("$.billingAccountId")`. The path is a plain
2904
- * `"$.field"` string — the binding is **declarative** (no arbitrary JS): the
2905
- * value is read from the resolved parent record at execution time, never computed.
2906
- *
2907
- * The grammar is intentionally minimal — a single dotted field path rooted at
2908
- * `$` (the parent record). `$.a.b` reaches a nested field; anything else (a bare
2909
- * field with no `$`, an index, a wildcard, an empty path) is rejected by
2910
- * {@link from} at build time so a malformed binding can never reach the SSoT.
2911
- */
2912
- interface ContractFromRef {
2913
- readonly [FROM_REF_BRAND]: true;
2914
- /** The `$`-rooted source path on the parent result, e.g. `$.billingAccountId`. */
2915
- readonly path: string;
2916
- }
2917
- /** Runtime guard: is `value` a {@link ContractFromRef} (a `from(...)` binding)? */
2918
- declare function isContractFromRef(value: unknown): value is ContractFromRef;
2919
- /**
2920
- * A resolved External Query composition node produced by {@link query} and placed
2921
- * at a `select` property of a contract method body (proposal "External Query"):
2922
- *
2923
- * ```ts
2924
- * Account.query(keys, {
2925
- * accountId: true,
2926
- * billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
2927
- * })
2928
- * ```
2929
- *
2930
- * It carries a faithful reference to the **referenced method spec** (so the
2931
- * serializer can recover the referenced contract's *name* by identity against the
2932
- * contract map, and its derived `resolution` / `cardinality` facts) plus the
2933
- * declarative child-key binding (child key field → a {@link ContractFromRef}).
2934
- * The owning `select` key becomes the composition's `as` property at record time
2935
- * (it is not known to {@link query} itself).
2936
- */
2937
- interface ContractComposeNode {
2938
- readonly [COMPOSE_NODE_BRAND]: true;
2939
- /** The referenced query method spec (`OtherContract.method`). */
2940
- readonly method: QueryMethodSpec<unknown, unknown, unknown>;
2941
- /** The child-key binding: child key field → parent-result `from` path. */
2942
- readonly bind: Readonly<Record<string, ContractFromRef>>;
2943
- }
2944
- /** Runtime guard: is `value` a {@link ContractComposeNode} (a `query(...)` node)? */
2945
- declare function isContractComposeNode(value: unknown): value is ContractComposeNode;
2946
- /**
2947
- * Declare a parent-result source path for an External Query child key binding
2948
- * (proposal "External Query"). Used **only** inside a {@link query} binding:
2949
- *
2950
- * ```ts
2951
- * query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") })
2952
- * ```
2953
- *
2954
- * The path is `$`-rooted (the parent record) and dotted; the bound child-key
2955
- * field is read from that path of each resolved parent record at execution time.
2956
- * The binding is **declarative** — `from` accepts only a literal path string and
2957
- * rejects any other shape, so no arbitrary JS can sneak into a key binding.
2958
- *
2959
- * @param path A `$`-rooted dotted path, e.g. `"$.billingAccountId"` or `"$.a.b"`.
2960
- * @throws if `path` is not a non-empty `$`-rooted dotted field path.
2961
- */
2962
- declare function from(path: string): ContractFromRef;
2963
- /**
2964
- * Reference another query contract's method as an **External Query** composition
2965
- * child, bound to the parent result by a declarative `from` mapping (proposal
2966
- * "Query Composition" / "External Query"). Place the result at a `select`
2967
- * property of a contract method body; the property name becomes the composed
2968
- * value's `as`:
2969
- *
2970
- * ```ts
2971
- * export const AccountAccess = publicQueryModel<AccountKey>()({
2972
- * get: (keys, params) => Account.query(keys, {
2973
- * accountId: true,
2974
- * billing: query(BillingPlanQueries.get, { accountId: from("$.billingAccountId") }),
2975
- * }, params),
2976
- * });
2977
- * ```
2978
- *
2979
- * This is **build-time-resolved, in-process** contract chaining — the relation
2980
- * primitive extended to point at another contract's method instead of a model
2981
- * relation. There is no protocol / transport: the runtime collects every bound
2982
- * key produced by the parent step and resolves the referenced contract **once,
2983
- * batched** (proposal "External Query"). The child MUST be `point` (the parent
2984
- * may yield N records, so a `range` child would be an N+1 fan-out) — that rule is
2985
- * owned by the N+1 checker (#60), which rejects a `range` child at build time.
2986
- *
2987
- * @param method The referenced query method (`OtherContract.method`), a resolved
2988
- * {@link QueryMethodSpec}.
2989
- * @param bind The child-key binding: child key field → a {@link from} path on
2990
- * the parent result. Must be non-empty and every value a `from(...)` ref.
2991
- * @throws if `method` is not a query method spec, or `bind` is empty / carries a
2992
- * non-`from` value.
2993
- */
2994
- declare function query(method: QueryMethodSpec<unknown, unknown, unknown>, bind: Record<string, ContractFromRef>): ContractComposeNode;
2995
- /**
2996
- * The {@link QueryModelContract} that owns a resolved query method spec, or
2997
- * `undefined` if the spec was not produced by {@link publicQueryModel} (so it has
2998
- * no registered owner). Used by the serializer to resolve a composition's
2999
- * referenced contract by identity.
3000
- */
3001
- declare function contractOfMethodSpec(method: QueryMethodSpec<unknown, unknown, unknown>): QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>> | undefined;
3002
- /**
3003
- * The declarative internal operation a contract method body resolves to — the
3004
- * resolved declaration captured into the IR (the closure itself is discarded).
3005
- * Mirrors {@link OperationDefinition} but is produced by the model recorder
3006
- * handed to a contract method body rather than a `define*` entry point.
3007
- *
3008
- * @typeParam Op - The internal op kind (`query` | `list` | `put` | `update` |
3009
- * `delete`).
3010
- */
3011
- interface ContractMethodOp<Op extends OperationKind = OperationKind> {
3012
- /** @internal Marks this object as a recorded contract method op. */
3013
- readonly __isContractMethodOp: true;
3014
- /** The model the op targets (name + runtime class). */
3015
- readonly entity: EntityRef;
3016
- /** The internal op kind. Determines the derived `resolution` / `inputArity`. */
3017
- readonly operation: Op;
3018
- /**
3019
- * The captured key slot. Either the whole {@link ContractKeyRef} sentinel
3020
- * (passed faithfully into the op's key position — the point / write form) or a
3021
- * `{ field: ContractKeyFieldRef }` record rebuilt from `keys.<field>` (the range
3022
- * form). For `put` it is the inert {@link PUT_HAS_NO_KEY} marker. Stored as
3023
- * evidence of faithfulness; never serialized.
3024
- */
3025
- readonly keys: ContractKeyRef | Readonly<Record<string, unknown>>;
3026
- /**
3027
- * The **explicit contract Key field names** captured from the factory's
3028
- * key-field argument (`publicQueryModel<EmailKey>(['email'])(...)`, issue #71).
3029
- * Present only for a **whole-`keys`** op (the {@link keys} slot is the whole
3030
- * {@link ContractKeyRef} sentinel) when the author supplied a field list, or
3031
- * when the model-derived factory form defaulted it to the model's primary-key
3032
- * input fields. It is the SSoT for "which fields the Key is composed of" — fed
3033
- * through {@link resolveKey} so the serializer emits the correct base-table PK
3034
- * **or** GSI (`indexName` + GSI key template) key, and so the planner can tell a
3035
- * coalescible base-table point (`GetItem` / `BatchGetItem`) from a non-coalescible
3036
- * GSI point (a unique-GSI `Query`).
3037
- *
3038
- * Absent when the body rebuilds a partition key from `keys.<field>` references
3039
- * (the range form already captures field names in the {@link keys} record) or
3040
- * when no explicit list was given to a `publicQueryModel<TKey>()` whole-key form
3041
- * (the legacy backward-compatible path, which then defaults to the primary key).
3042
- */
3043
- readonly keyFields?: readonly string[];
3044
- /** The select projection for `query` / `list`; `undefined` for writes. */
3045
- readonly select?: Readonly<Record<string, unknown>>;
3046
- /** The changes structure for `update`; `undefined` otherwise. */
3047
- readonly changes?: Readonly<Record<string, unknown>>;
3048
- /** The item structure for `put`; `undefined` otherwise. */
3049
- readonly item?: Readonly<Record<string, unknown>>;
3050
- /** The optional declarative write condition for writes. */
3051
- readonly condition?: ConditionInput;
3052
- /**
3053
- * The declared batched-write resolution mode for a **write** op (issue #64):
3054
- * how a *key array* resolves to the underlying write surface — a
3055
- * `TransactWriteItems` (`'transact'`, atomic, condition-capable, ≤25) or a
3056
- * `BatchWriteItem` (`'batchWrite'`, non-atomic, no conditions, chunked). Absent
3057
- * when the author did not declare a batched form (the method then accepts a
3058
- * single key only at execution; see {@link CommandBatchMode}). Never present on
3059
- * a read op. The serializer (#59) reads this to emit the method's `batch`
3060
- * resolution target.
3061
- */
3062
- readonly batch?: CommandBatchMode;
3063
- /**
3064
- * External Query compositions (#63) extracted from the recorded `select`: each
3065
- * `query(OtherContract.method, { childKey: from("$.parentField") })` node placed
3066
- * at a `select` property becomes one {@link RecordedCompose} here (the property
3067
- * name is its `as`), and the property is removed from the read projection. Absent
3068
- * when the body declares no composition.
3069
- */
3070
- readonly compose?: readonly RecordedCompose[];
3071
- }
3072
- /**
3073
- * A recorded External Query composition edge on a contract method op (#63). The
3074
- * declarative result of a `query(OtherContract.method, { … })` call placed at a
3075
- * `select` property: it pairs the property name (`as`) with a faithful reference
3076
- * to the referenced method spec and the declarative child-key binding. The
3077
- * referenced contract's *name* and the child's derived `resolution` / `cardinality`
3078
- * are recovered by the serializer (`src/spec/contracts.ts`) from the method spec
3079
- * by identity against the contract map; they are not duplicated here.
3080
- */
3081
- interface RecordedCompose {
3082
- /** The result property the composed value attaches to (the `select` key). */
3083
- readonly as: string;
3084
- /** The referenced query method spec (`OtherContract.method`). */
3085
- readonly method: QueryMethodSpec<unknown, unknown, unknown>;
3086
- /** The child-key binding: child key field → parent-result `from` path. */
3087
- readonly bind: Readonly<Record<string, ContractFromRef>>;
3088
- }
3089
- /**
3090
- * The **call signature** a contract method exposes to a caller, narrowed by its
3091
- * decided {@link InputArity} — the type-level half of N+1 rule (a) ("array into a
3092
- * `range` method", #60). Given a method's `inputArity`:
3093
- *
3094
- * - `'single'` (a `range` method) — the key argument is a **single** `TKey` only;
3095
- * passing an array (`readonly TKey[]`) is a **type error** by construction, so a
3096
- * `range` method's array overload does not type-check.
3097
- * - `'either'` (a `point` read / known-key write) — a single key **or** an array
3098
- * (the array form coalesces to one `BatchGetItem` / batched write).
3099
- * - `'array'` — an array only (reserved; not produced by the current resolvers).
3100
- *
3101
- * This mirrors the proposal's "the generated binding's types encode `inputArity`
3102
- * (single → bare argument, array → array argument)". It is the type-level narrowing
3103
- * used by the runtime call surface (`executeQueryMethod`, `src/runtime/contract-runtime.ts`):
3104
- * the executor keys its key-argument type on the named method's **literal** arity
3105
- * `A` (carried on {@link QueryMethodSpec} when the body is tagged with
3106
- * {@link point} / {@link range}), so feeding an array into a `'single'` method is a
3107
- * `tsc` compile error — the **primary** N+1 rule (a) layer, complementing the
3108
- * build-time SSoT check ({@link assertContractN1Safe}) and the runtime backstop.
3109
- *
3110
- * @typeParam A - The decided input arity (a literal `InputArity`).
3111
- * @typeParam TKey - The contract Key.
3112
- * @typeParam TParams - The method's params type.
3113
- * @typeParam TResult - The method's result type.
3114
- */
3115
- type ContractCallSignature<A extends InputArity, TKey, TParams, TResult> = (key: A extends 'single' ? TKey : A extends 'array' ? readonly TKey[] : TKey | readonly TKey[], params: TParams) => TResult;
3116
- /**
3117
- * @internal The non-enumerable property an arity-tagged body ({@link point} /
3118
- * {@link range}) carries its declared literal {@link InputArity} on. The factory
3119
- * reads it to (1) thread the literal into the resolved {@link QueryMethodSpec}'s
3120
- * `inputArity` type and (2) **verify** the declared literal equals the
3121
- * runtime-derived arity ({@link deriveReadFacts}) at build time — a mismatch is a
3122
- * build error, so the type-level arity cannot lie.
3123
- */
3124
- declare const ARITY_TAG: "__contractArity";
3125
- /**
3126
- * A contract query-method body **tagged** with its declared literal
3127
- * {@link InputArity} (issue #71). Structurally an {@link AuthoredMethod} (single-key
3128
- * `(keys, params) => …`) carrying a phantom `A`, so `<const M>` factory inference
3129
- * preserves the literal arity into the resolved {@link QueryMethodSpec} and on into
3130
- * the `executeQueryMethod` call surface, where {@link ContractCallSignature} narrows
3131
- * the key argument by `A` — making an array fed into a `'single'` method a `tsc`
3132
- * error. The runtime value is the original body function with a non-enumerable
3133
- * `ARITY_TAG`, so it still records its op exactly as an untagged body does.
3134
- *
3135
- * @typeParam A - The declared literal arity (`'single'` | `'either'`).
3136
- * @typeParam TKey - The contract Key (single key at authoring time).
3137
- * @typeParam TParams - The method's params type.
3138
- * @typeParam TResult - The method's result type.
3139
- */
3140
- interface ArityTaggedQueryMethod<A extends InputArity, TKey, TParams, TResult> {
3141
- (keys: TKey, params: TParams): TResult;
3142
- /** @internal The declared literal arity, read by the factory and build-verified. */
3143
- readonly [ARITY_TAG]: A;
3144
- }
3145
- /**
3146
- * Declare a query method as a **`point`** read — a unique-key lookup whose per-key
3147
- * result is at most one item (proposal: `resolution: 'point'`). Wrap the method
3148
- * body to thread the read's **literal** {@link InputArity} into the type-level
3149
- * call surface so an array fed into a non-coalescible point is a `tsc` error
3150
- * (issue #71, N+1 rule (a)):
3151
- *
3152
- * ```ts
3153
- * // A base-table primary-key point — a key array coalesces to one BatchGetItem.
3154
- * const GroupById = publicQueryModel<{ groupId: string }>()({
3155
- * get: point((keys, params: { consistentRead?: boolean }) =>
3156
- * Group.query(keys, { groupId: true, name: true }, params)),
3157
- * });
3158
- *
3159
- * // A UNIQUE-GSI point — a per-key GSI `Query` (BatchGetItem cannot read a GSI),
3160
- * // so it is NOT coalescible: declare `coalesce: false`.
3161
- * const UserByEmail = publicQueryModel<{ email: string }>(['email'])({
3162
- * get: point((keys) => User.query(keys, { userId: true, email: true }),
3163
- * { coalesce: false }),
3164
- * });
3165
- * ```
3166
- *
3167
- * The default (`coalesce: true`) is `inputArity: 'either'` (a base-table PK point);
3168
- * `{ coalesce: false }` is `inputArity: 'single'` (a unique-GSI point). The declared
3169
- * arity is **verified against the build-derived arity** ({@link deriveReadFacts}):
3170
- * declaring `coalesce: true` on a GSI point — or `coalesce: false` on a base-table
3171
- * point — is a build error, so the type never disagrees with the resolved access
3172
- * pattern. Tagging is **optional**: an untagged body keeps the wide `InputArity`
3173
- * and relies on the build-time checker + runtime backstop only.
3174
- *
3175
- * @param body The single-key method body (`(keys, params) => Model.query(...)`).
3176
- * @param options `{ coalesce }` — `true` (default) for a base-table PK point
3177
- * (`'either'`), `false` for a unique-GSI point (`'single'`).
3178
- */
3179
- declare function point<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult, options: {
3180
- readonly coalesce: false;
3181
- }): ArityTaggedQueryMethod<'single', TKey, TParams, TResult>;
3182
- declare function point<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult, options?: {
3183
- readonly coalesce?: true;
3184
- }): ArityTaggedQueryMethod<'either', TKey, TParams, TResult>;
3185
- /**
3186
- * Declare a query method as a **`range`** read — a partition `Query` (`Model.list`)
3187
- * whose per-key result is a connection (`resolution: 'range'`). A range read is
3188
- * **never** coalescible (DynamoDB cannot coalesce N partition `Query`s), so its
3189
- * arity is fixed to `'single'`; wrapping the body threads that literal into the
3190
- * call surface so an array fed into a range method is a `tsc` error (issue #71,
3191
- * N+1 rule (a)):
3192
- *
3193
- * ```ts
3194
- * const MembersByGroup = publicQueryModel<{ groupId: string }>()({
3195
- * list: range((key, params: { limit?: number }) =>
3196
- * Member.list({ groupId: key.groupId }, { select: { userId: true }, ...params })),
3197
- * });
3198
- * ```
3199
- *
3200
- * The declared `'single'` arity is **verified against the build-derived arity**
3201
- * ({@link deriveReadFacts}): tagging a body that resolves to a coalescible base-table
3202
- * point with `range(...)` is a build error. Tagging is **optional** (see
3203
- * {@link point}).
3204
- *
3205
- * @param body The single-key method body (`(keys, params) => Model.list(...)`).
3206
- */
3207
- declare function range<TKey, TParams, TResult>(body: (keys: TKey, params: TParams) => TResult): ArityTaggedQueryMethod<'single', TKey, TParams, TResult>;
3208
- /**
3209
- * The resolved IR of one **query** method. Carries the formal method type at the
3210
- * type level (`QueryMethod<TKey, TParams, TResult>`) and, at runtime, the resolved
3211
- * internal op plus the **derived** facts (`resolution` / `inputArity`).
3212
- *
3213
- * @typeParam TKey - The contract Key.
3214
- * @typeParam TParams - The method's params type.
3215
- * @typeParam TResult - The method's result type.
3216
- * @typeParam A - The method's **literal** {@link InputArity} when the author
3217
- * tagged the body with {@link point} / {@link range} (the type-level half of
3218
- * N+1 rule (a), threaded into the call surface so an array fed into a `'single'`
3219
- * method is a `tsc` error — see {@link ContractCallSignature} and
3220
- * `executeQueryMethod`). Defaults to the wide `InputArity` for an untagged body,
3221
- * which then relies on the build-time checker + the runtime backstop only.
3222
- */
3223
- interface QueryMethodSpec<TKey, TParams, TResult, A extends InputArity = InputArity> {
3224
- /** @internal Marks this object as a query method spec. */
3225
- readonly __methodKind: 'query';
3226
- /** The resolved internal operation (closure discarded). */
3227
- readonly op: ContractMethodOp;
3228
- /** Derived: `'point'` (unique-key query) or `'range'` (partition list). */
3229
- readonly resolution: Resolution;
3230
- /**
3231
- * Derived: `'either'` for a base-table `point`, `'single'` for a `range` or a
3232
- * unique-GSI `point`. Typed as the method's **literal** arity `A` when the body
3233
- * was tagged ({@link point} / {@link range}); a tagged literal is verified
3234
- * against this derived value at build time (a mismatch is a build error), so the
3235
- * type cannot lie about the runtime arity.
3236
- */
3237
- readonly inputArity: A;
3238
- /**
3239
- * @internal The method's own name in its contract (`'get'`, `'summary'`, …),
3240
- * stamped by the factory. An External Query composition (#63) references this
3241
- * spec object; the serializer recovers the referenced method name from here and
3242
- * the referenced contract name by identity ({@link contractOfMethodSpec}).
3243
- */
3244
- readonly __methodName?: string;
3245
- /** @internal Phantom carrier retaining the formal `QueryMethod` type. */
3246
- readonly __signature?: QueryMethod<TKey, TParams, TResult>;
3247
- }
3248
- /**
3249
- * The resolved IR of one **command** method. Symmetric to {@link QueryMethodSpec},
3250
- * with an explicit declared result (`void` | a `Result` | the updated entity).
3251
- *
3252
- * @typeParam TKey - The contract Key.
3253
- * @typeParam TParams - The method's params type.
3254
- * @typeParam TResult - The method's declared result type.
3255
- */
3256
- interface CommandMethodSpec<TKey, TParams, TResult> {
3257
- /** @internal Marks this object as a command method spec. */
3258
- readonly __methodKind: 'command';
3259
- /** The resolved internal write operation (closure discarded). */
3260
- readonly op: ContractMethodOp;
3261
- /**
3262
- * Derived input arity: a single key maps to one write op; an array maps to a
3263
- * batched write (a transaction / `BatchWriteItem`). Writes accept `'either'`.
3264
- */
3265
- readonly inputArity: InputArity;
3266
- /**
3267
- * The category of the declared result type (`void` | `result` | `entity`),
3268
- * part of the contract (it surfaces in OpenAPI and every binding). Derived from
3269
- * the internal op when not explicitly annotated: an `update` returning the
3270
- * entity → `'entity'`; a `delete` → `'void'`.
3271
- */
3272
- readonly result: CommandResultKind;
3273
- /**
3274
- * The declared batched-write mode (issue #64): how a **key array** resolves —
3275
- * `'transact'` (atomic `TransactWriteItems`, ≤25, condition-capable) or
3276
- * `'batchWrite'` (non-atomic `BatchWriteItem`, no conditions, chunked). Absent
3277
- * when the author declared no batched form — the method then accepts only a
3278
- * single key at execution; an array is rejected with a clear error. The
3279
- * single-key form is unaffected by this declaration.
3280
- */
3281
- readonly batch?: CommandBatchMode;
3282
- /**
3283
- * The return projection of a {@link mutation}-derived command method (issue
3284
- * #83): a boolean field map applied to the written entity as a **consistent
3285
- * read-back** projection after the write commits (the proposal's "return =
3286
- * read projection"). Present only on a `.plan(mutation)` method; a hand-written
3287
- * #64 method (the closure form) omits it. JSON-safe (only boolean leaves), so
3288
- * it serializes verbatim into {@link CommandContractMethodSpec.returnSelection}
3289
- * and both runtimes apply the **same** projection.
3290
- */
3291
- readonly returnSelection?: Readonly<Record<string, boolean>>;
3292
- /**
3293
- * The composed write ops of a **multi-fragment** `mutation`-derived method
3294
- * (issue #90): when a mutation declares **2+ fragments**, each fragment compiles
3295
- * to one write {@link ContractMethodOp} and the whole set executes as **one
3296
- * atomic `TransactWriteItems`** (NOT sequential writes). Present only for N≥2; a
3297
- * single-fragment method carries only {@link op} (no regression — #83 behavior).
3298
- * The merge (same-item conflict / cross-fragment dep / ≤25 enforcement) is done
3299
- * at build time by `compileMutationPlan`; `op` is set to the **primary** fragment
3300
- * (fragment 0 — the entity the method is keyed by and read back from) so the
3301
- * Key / read-back machinery is unchanged.
3302
- */
3303
- readonly ops?: readonly ContractMethodOp[];
3304
- /**
3305
- * The referential-integrity assertions derived from the mutation's `requires`
3306
- * effects (issue #84): one {@link DerivedConditionCheck} per
3307
- * `w.exists(...)`, flattened across every fragment. Present (non-empty) only when
3308
- * a fragment's lifecycle declares `requires`; absent otherwise (no regression —
3309
- * the method then carries only its base op(s)). Each becomes a read-only
3310
- * `ConditionCheck` (`attribute_exists`) item in the method's atomic
3311
- * `TransactWriteItems`, and their presence **promotes** an otherwise single-op
3312
- * method to a transaction: the serializer emits a {@link
3313
- * import('../spec/types.js').TransactionItemSpec}-based `TransactionSpec` and
3314
- * `single: { mode: 'transaction' }`, and the in-process runtime composes the base
3315
- * write(s) + the ConditionCheck(s) into one transaction so an absent referent
3316
- * rolls the WHOLE write back.
3317
- */
3318
- readonly conditionChecks?: readonly DerivedConditionCheck[];
3319
- /**
3320
- * The adjacency edge writes derived from the mutation's `edges` effects (issue
3321
- * #85), flattened across every fragment in declaration order. Present (non-empty)
3322
- * only when a fragment's lifecycle declares `edges`; absent otherwise (no
3323
- * regression). Each {@link DerivedEdgeWrite} contributes its adjacency `Put` /
3324
- * `Delete` item(s) to the method's atomic `TransactWriteItems`, and their presence
3325
- * (like {@link conditionChecks}) **promotes** an otherwise single-op method to a
3326
- * transaction so the edge moves / removes atomically with the entity write.
3327
- */
3328
- readonly edgeWrites?: readonly DerivedEdgeWrite[];
3329
- /**
3330
- * The derived (cascading) updates from the mutation's `derive` effects (issue
3331
- * #85), flattened across every fragment in declaration order: each an atomic `ADD`
3332
- * (`UpdateItem`) on a target counter (e.g. `User.postCount += 1`). Present
3333
- * (non-empty) only when a fragment's lifecycle declares `derive`; absent otherwise
3334
- * (no regression). Each contributes an `UpdateItem` (`ADD`) to the method's atomic
3335
- * `TransactWriteItems` and **promotes** the method to a transaction.
3336
- */
3337
- readonly derivedUpdates?: readonly DerivedUpdate[];
3338
- /**
3339
- * The uniqueness guards derived from the mutation's `unique` effects (issue #86),
3340
- * flattened across every fragment in declaration order. Present (non-empty) only
3341
- * when a fragment's lifecycle declares `unique`; absent otherwise (no regression).
3342
- * Each {@link DerivedUniqueGuard} contributes its marker-row `Put` / `Delete`
3343
- * item(s) to the method's atomic `TransactWriteItems`, and their presence (like
3344
- * {@link conditionChecks}) **promotes** an otherwise single-op method to a
3345
- * transaction so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
3346
- * failing) rolls the WHOLE write back — the entity is never created.
3347
- */
3348
- readonly uniqueGuards?: readonly DerivedUniqueGuard[];
3349
- /**
3350
- * The outbox events derived from the mutation's `emits` effects (issue #87),
3351
- * flattened across every fragment in declaration order. Present (non-empty) only
3352
- * when a fragment's lifecycle declares `emits`; absent otherwise (no regression).
3353
- * Each {@link DerivedOutboxEvent} contributes its transactional-outbox `Put` to the
3354
- * method's atomic `TransactWriteItems` — recording the event ATOMICALLY with the
3355
- * entity write (event and state cannot diverge) — and their presence (like
3356
- * {@link conditionChecks}) **promotes** an otherwise single-op method to a
3357
- * transaction. The outbox row is drainable via `src/cdc/`; delivery is out of scope.
3358
- */
3359
- readonly outboxEvents?: readonly DerivedOutboxEvent[];
3360
- /**
3361
- * The client-token idempotency guard derived from the mutation's `idempotency`
3362
- * effect (issue #87). Present only when a fragment's lifecycle declares
3363
- * `idempotency`; absent otherwise (no regression). The {@link DerivedIdempotencyGuard}
3364
- * contributes its `attribute_not_exists` guard `Put` to the method's atomic
3365
- * `TransactWriteItems`, and its presence (like {@link conditionChecks}) **promotes**
3366
- * an otherwise single-op method to a transaction so a same-token re-execution fails
3367
- * the guard and rolls the WHOLE write back — no effect is double-applied.
3368
- */
3369
- readonly idempotencyGuard?: DerivedIdempotencyGuard;
3370
- /** @internal Phantom carrier retaining the formal `CommandMethod` type. */
3371
- readonly __signature?: CommandMethod<TKey, TParams, TResult>;
3372
- }
3373
- /**
3374
- * A resolved **QueryModel** — a keyed public read contract holding one or more
3375
- * named query methods. The Key (`TKey`) is the access pattern; each entry of
3376
- * `methods` is a use case. Carries the contract `kind` and the resolved per-method
3377
- * IR; the source method closures are discarded.
3378
- *
3379
- * @typeParam TKey - The contract Key (the access pattern / join key).
3380
- * @typeParam M - The method map (name → resolved {@link QueryMethodSpec}).
3381
- */
3382
- interface QueryModelContract<TKey, M extends Record<string, QueryMethodSpec<TKey, unknown, unknown>>> {
3383
- /** @internal Marks this object as a contract IR node. */
3384
- readonly __isContract: true;
3385
- /** Discriminant: a read contract. */
3386
- readonly kind: 'query';
3387
- /** The resolved, named query methods (use cases over the same Key). */
3388
- readonly methods: M;
3389
- /** @internal Phantom carrier retaining the contract Key type `TKey`. */
3390
- readonly __keyType?: (value: TKey) => void;
3391
- }
3392
- /**
3393
- * A resolved **CommandModel** — the write counterpart of {@link QueryModelContract},
3394
- * completely symmetric: keyed, holding one or more named write-use-case methods.
3395
- *
3396
- * @typeParam TKey - The contract Key (the target identity).
3397
- * @typeParam M - The method map (name → resolved {@link CommandMethodSpec}).
3398
- */
3399
- interface CommandModelContract<TKey, M extends Record<string, CommandMethodSpec<TKey, any, any>>> {
3400
- /** @internal Marks this object as a contract IR node. */
3401
- readonly __isContract: true;
3402
- /** Discriminant: a write contract. */
3403
- readonly kind: 'command';
3404
- /** The resolved, named command methods (write use cases over the same Key). */
3405
- readonly methods: M;
3406
- /** @internal Phantom carrier retaining the contract Key type `TKey`. */
3407
- readonly __keyType?: (value: TKey) => void;
3408
- }
3409
- /** Runtime guard: is `value` a {@link QueryModelContract}? */
3410
- declare function isQueryModelContract(value: unknown): value is QueryModelContract<unknown, Record<string, QueryMethodSpec<unknown, unknown, unknown>>>;
3411
- /** Runtime guard: is `value` a {@link CommandModelContract}? */
3412
- declare function isCommandModelContract(value: unknown): value is CommandModelContract<unknown, Record<string, CommandMethodSpec<unknown, unknown, unknown>>>;
3413
- /**
3414
- * Recovers the model-class constructor type `C` from a `ModelStatic<T, C>`
3415
- * (Model-derived key form). Mirrors the helper used by the `define*` entry
3416
- * points; the contract Key is then `PrimaryKeyOf<ClassOf<M>>`.
3417
- */
3418
- type ClassOf$1<M> = M extends ModelStatic<infer _T, infer C> ? C : unknown;
3419
- /**
3420
- * The **authoring** signature of a contract method body. Deliberately distinct
3421
- * from the formal, array-capable {@link QueryMethod} / {@link CommandMethod}: the
3422
- * author writes a **single-key** body (`(keys: TKey, params) => …`) so that
3423
- * `keys` (and `keys.<field>`) is assignable into the underlying model op's key
3424
- * position, while the array capability (`get(keys[])`) is **derived** by the
3425
- * contract and exposed on the resolved spec's `__signature`. This matches the
3426
- * proposal: a contract method body "resolves to a declarative operation"; the
3427
- * author does not hand-write the batched overload.
3428
- *
3429
- * @typeParam TKey - The contract Key (single key at authoring time).
3430
- */
3431
- type AuthoredMethod<TKey> = (keys: TKey, params: never) => unknown;
3432
- /**
3433
- * The query-method-map type accepted by a {@link publicQueryModel} factory. Each
3434
- * entry is an {@link AuthoredMethod} (a bare body) **or** an
3435
- * {@link ArityTaggedQueryMethod} (a {@link point} / {@link range}-tagged body) whose
3436
- * key type is fixed to the contract `TKey`; `TParams` / `TResult` (and, for a
3437
- * tagged body, the literal arity `A`) are inferred per method.
3438
- */
3439
- type QueryMethodMap<TKey> = Record<string, AuthoredMethod<TKey>>;
3440
- /**
3441
- * The command-method-map type accepted by a {@link publicCommandModel} factory.
3442
- * Each entry is either a hand-written #64 {@link AuthoredMethod} closure
3443
- * (`(keys, params) => Model.put|update|delete(...)`) **or** a #83
3444
- * {@link PlannedCommandMethod} (`command({ input, select }).plan(mutation)`). The
3445
- * two forms coexist in one contract — the brand routes each at build time.
3446
- */
3447
- type CommandMethodMap<TKey> = Record<string, AuthoredMethod<TKey> | PlannedCommandMethod>;
3448
- /**
3449
- * The literal {@link InputArity} a (possibly {@link point} / {@link range}-tagged)
3450
- * authored body declares, or the wide `InputArity` for a bare (untagged) body. An
3451
- * `ArityTaggedQueryMethod` carries its literal `A` on the {@link ARITY_TAG} brand;
3452
- * a bare body has none, so the resolved spec keeps the wide arity (build-checker +
3453
- * runtime backstop only).
3454
- */
3455
- type DeclaredArity<F> = F extends {
3456
- readonly [ARITY_TAG]: infer A extends InputArity;
3457
- } ? A : InputArity;
3458
- /**
3459
- * Maps a supplied query-method map `M` to its resolved {@link QueryMethodSpec}
3460
- * map. The resolved spec carries the method's authored `TParams` / `TResult` and
3461
- * its **literal** {@link InputArity} when the body was tagged ({@link point} /
3462
- * {@link range}) — the literal threads on into the `executeQueryMethod` call
3463
- * surface, where an array fed into a `'single'` method is a `tsc` error (issue
3464
- * #71, N+1 rule (a)).
3465
- */
3466
- type ResolvedQueryMethods<TKey, M extends QueryMethodMap<TKey>> = {
3467
- readonly [K in keyof M]: M[K] extends (keys: TKey, params: infer P) => infer R ? QueryMethodSpec<TKey, P, R, DeclaredArity<M[K]>> : never;
3468
- };
3469
- /**
3470
- * The TypeScript value type an input param shape `I` represents: each field
3471
- * carries the value its placeholder stands in for (e.g. `param.string()` →
3472
- * `string`). The public Command IF method's `TParams` for a #83 planned method.
3473
- */
3474
- type InputParamsOf<I extends CommandInputShape> = {
3475
- readonly [K in keyof I]: I[K] extends Param<infer V> ? V : never;
3476
- };
3477
- /**
3478
- * The result type a #83 planned method returns: the projected read-back item when
3479
- * a `select` is declared (a partial record over the projected fields), or `void`
3480
- * for a fire-and-forget command (no `select`). The concrete field set is the
3481
- * `true`-flagged keys of the declared `select`.
3482
- */
3483
- type PlannedResultOf<S extends CommandSelectShape | undefined> = S extends CommandSelectShape ? Readonly<Record<Extract<keyof S, string>, unknown>> | null : void;
3484
- /**
3485
- * Maps a supplied command-method map to its resolved {@link CommandMethodSpec}
3486
- * map. A #83 {@link PlannedCommandMethod} resolves to a spec whose `TParams` is
3487
- * its input param shape and whose `TResult` is its read-back projection (or
3488
- * `void`); a hand-written #64 closure resolves by its `(keys, params) => R`
3489
- * signature, exactly as before.
3490
- */
3491
- type ResolvedCommandMethods<TKey, M extends CommandMethodMap<TKey>> = {
3492
- readonly [K in keyof M]: M[K] extends PlannedCommandMethod<infer I, infer S> ? CommandMethodSpec<TKey, InputParamsOf<I>, PlannedResultOf<S>> : M[K] extends (keys: TKey, params: infer P) => infer R ? CommandMethodSpec<TKey, P, R> : never;
3493
- };
3494
- /**
3495
- * The explicit Key-field list a factory accepts (issue #71): the field names that
3496
- * compose the contract Key, constrained to `keyof TKey`. Supplied to the explicit
3497
- * form (`publicQueryModel<EmailKey>(['email'])`) so a **whole-`keys`** point
3498
- * contract whose Key is a GSI (not the model's primary key) is resolved to the
3499
- * right access pattern — without it, the type-erased whole-`keys` op carries no
3500
- * field names and the serializer can only default to the primary key.
3501
- */
3502
- type KeyFieldList<TKey> = readonly (keyof TKey & string)[];
3503
- /**
3504
- * Create a **QueryModel** factory bound to an explicit **Query key** (`TKey`) —
3505
- * the form used for a GSI / alternate access pattern whose key is not the model's
3506
- * primary key:
3507
- *
3508
- * ```ts
3509
- * type CategoryKey = { categoryId: string };
3510
- * const ArticleByCategory = publicQueryModel<CategoryKey>()({
3511
- * list: (key, params) => Article.list({ categoryId: key.categoryId }, { select: { … } }),
3512
- * });
3513
- * ```
3514
- *
3515
- * **Explicit Key-field capture (issue #71).** A **whole-`keys`** point contract
3516
- * whose Key is a **GSI** (e.g. `EmailKey = { email: string }` resolved through a
3517
- * unique `email` index) must declare its Key fields so the build can resolve the
3518
- * right access pattern — passing `keys` whole into `Model.query(keys, …)` erases
3519
- * the field names (only the phantom `TKey` knows them). Supply the field list:
3520
- *
3521
- * ```ts
3522
- * type EmailKey = { email: string };
3523
- * const UserByEmail = publicQueryModel<EmailKey>(['email'])({
3524
- * get: (keys, params) => User.query(keys, { userId: true, email: true }),
3525
- * });
3526
- * ```
3527
- *
3528
- * The captured fields are fed through {@link resolveKey}: a Key that resolves to a
3529
- * **unique GSI** emits the GSI `indexName` + GSI key template and is a per-key
3530
- * `Query` (`inputArity: 'single'` — not batch-coalescible), while a Key that
3531
- * resolves to the **base-table primary key** keeps `inputArity: 'either'` (a key
3532
- * array → one `BatchGetItem`). The list is **optional** for backward
3533
- * compatibility: a whole-`keys` contract with no list defaults to the model's
3534
- * primary key, exactly as before #71.
3535
- *
3536
- * The returned factory takes a record of named query methods (the use cases) and
3537
- * resolves each body to its declarative internal op, deriving `resolution` /
3538
- * `inputArity`. The method closures are discarded; only the resolved IR is kept.
3539
- *
3540
- * @typeParam TKey - The explicit contract Key (the access pattern / join key).
3541
- * @param keyFields - Optional explicit Key-field names (issue #71); when present
3542
- * they are captured into the IR and resolved to the access pattern.
3543
- */
3544
- declare function publicQueryModel<TKey>(keyFields?: KeyFieldList<TKey>): <const M extends QueryMethodMap<TKey>>(methods: M) => QueryModelContract<TKey, ResolvedQueryMethods<TKey, M>>;
3545
- /**
3546
- * Create a **QueryModel** factory whose Key is **derived** from a Model's primary
3547
- * key (no explicit key type needed):
3548
- *
3549
- * ```ts
3550
- * const ArticleById = publicQueryModel(ArticleModel)({
3551
- * get: (keys, params) => Article.query(keys, { articleId: true, title: true }, params),
3552
- * });
3553
- * ```
3554
- *
3555
- * An explicit Key-field list (issue #71) is **optional** here: the Key continues
3556
- * to derive from the model's primary key when omitted.
3557
- *
3558
- * @param model - The model whose primary key becomes the contract Key.
3559
- * @param keyFields - Optional explicit Key-field names; defaults to the model's
3560
- * primary-key input fields.
3561
- */
3562
- declare function publicQueryModel<M extends ModelStatic<DDBModel>>(model: M, keyFields?: KeyFieldList<PrimaryKeyOf<ClassOf$1<M>>>): <const Methods extends QueryMethodMap<PrimaryKeyOf<ClassOf$1<M>>>>(methods: Methods) => QueryModelContract<PrimaryKeyOf<ClassOf$1<M>>, ResolvedQueryMethods<PrimaryKeyOf<ClassOf$1<M>>, Methods>>;
3563
- /**
3564
- * Create a **CommandModel** factory bound to an explicit **Command key** (`TKey`).
3565
- * Symmetric to {@link publicQueryModel}; each method resolves to a write op
3566
- * (`put` / `update` / `delete`) and carries an explicit, contract-level result
3567
- * type (`void` | a `Result` | the updated entity):
3568
- *
3569
- * ```ts
3570
- * type UserKey = { userId: string };
3571
- * const UserCommands = publicCommandModel<UserKey>()({
3572
- * disable: (keys, params: { reason: string }): UpdatedUser =>
3573
- * User.update(keys, { status: 'disabled', disableReason: params.reason }),
3574
- * changeRole: (keys, params: { role: Role }): void =>
3575
- * User.update(keys, { role: params.role }),
3576
- * });
3577
- * ```
3578
- *
3579
- * An explicit Key-field list (issue #71) is **optional**; symmetric to
3580
- * {@link publicQueryModel}, it captures the whole-`keys` Key fields into the IR
3581
- * so the serializer emits the contract Key explicitly. A command always writes to
3582
- * the base table by its key, so the list does not change a command's
3583
- * `inputArity` (always `'either'` — a single key or a batched write).
3584
- *
3585
- * @typeParam TKey - The explicit contract Key (the target identity).
3586
- * @param keyFields - Optional explicit Key-field names (issue #71).
3587
- */
3588
- declare function publicCommandModel<TKey>(keyFields?: KeyFieldList<TKey>): <const M extends CommandMethodMap<TKey>>(methods: M) => CommandModelContract<TKey, ResolvedCommandMethods<TKey, M>>;
3589
- /**
3590
- * Create a **CommandModel** factory whose Key is **derived** from a Model's
3591
- * primary key.
3592
- *
3593
- * @param model - The model whose primary key becomes the contract Key.
3594
- * @param keyFields - Optional explicit Key-field names; defaults to the model's
3595
- * primary-key input fields.
3596
- */
3597
- declare function publicCommandModel<M extends ModelStatic<DDBModel>>(model: M, keyFields?: KeyFieldList<PrimaryKeyOf<ClassOf$1<M>>>): <const Methods extends CommandMethodMap<PrimaryKeyOf<ClassOf$1<M>>>>(methods: Methods) => CommandModelContract<PrimaryKeyOf<ClassOf$1<M>>, ResolvedCommandMethods<PrimaryKeyOf<ClassOf$1<M>>, Methods>>;
3598
- declare const PLANNED_COMMAND_BRAND: unique symbol;
3599
- /**
3600
- * The input param shape a {@link command} declares: a record of named
3601
- * {@link Param} placeholders (`param.string()` / `param.number()` /
3602
- * `param.literal(...)`). These are the **external** params of the public Command
3603
- * IF — the method's caller-supplied input. The mutation fragment binds them by
3604
- * name into the written entity (`$.<field>`), so each input field name must match
3605
- * a fragment `$.field` reference / a model field the fragment writes.
3606
- */
3607
- type CommandInputShape = Readonly<Record<string, Param<unknown>>>;
3608
- /**
3609
- * The return projection a {@link command} declares (proposal §3: "return = read
3610
- * projection"): a JSON-safe boolean field map applied to the written entity as a
3611
- * **consistent read-back** after the write commits. Omit for a fire-and-forget
3612
- * command (no projected item is returned).
3613
- */
3614
- type CommandSelectShape = Readonly<Record<string, boolean>>;
3615
- /**
3616
- * A **planned command method** (#83): the branded result of
3617
- * `command({ input, select }).plan(mutation)`. It carries the captured fragment
3618
- * IR (the {@link CommandPlan}), the public input param shape, and the return
3619
- * `select`. {@link buildCommandContract} detects the brand and routes it to the
3620
- * single-fragment mutation compiler (NOT through #64's closure path).
3621
- *
3622
- * The external surface is **params in / result out only** — the internal mutation
3623
- * document is never exposed.
3624
- *
3625
- * @typeParam TInput - The input param shape (`{ field: Param<…> }`).
3626
- * @typeParam TSelect - The return projection (`{ field: boolean }`), or `undefined`.
3627
- */
3628
- interface PlannedCommandMethod<TInput extends CommandInputShape = CommandInputShape, TSelect extends CommandSelectShape | undefined = CommandSelectShape | undefined> {
3629
- readonly [PLANNED_COMMAND_BRAND]: true;
3630
- /** The captured mutation IR (the fragment list). */
3631
- readonly plan: CommandPlan;
3632
- /** The public input param shape. */
3633
- readonly input: TInput;
3634
- /** The return projection (consistent read-back), or `undefined`. */
3635
- readonly select: TSelect;
3636
- }
3637
- /** Runtime guard: is `value` a {@link PlannedCommandMethod} (#83)? */
3638
- declare function isPlannedCommandMethod(value: unknown): value is PlannedCommandMethod;
3639
- /**
3640
- * The builder returned by {@link command}: `.plan(mutation)` finalizes the public
3641
- * Command IF method by attaching the internal {@link CommandPlan} that implements
3642
- * it, yielding a branded {@link PlannedCommandMethod}.
3643
- */
3644
- interface CommandBuilder<TInput extends CommandInputShape, TSelect extends CommandSelectShape | undefined> {
3645
- /**
3646
- * Attach the internal mutation that **implements** this Command IF method. The
3647
- * mutation is a {@link CommandPlan} (a `mutation(...)` / `definePlan(...)`
3648
- * result); the public surface stays params (the `input`) + result (the `select`
3649
- * read-back projection) only.
3650
- *
3651
- * @param plan The internal {@link CommandPlan} that implements this method.
3652
- */
3653
- plan(plan: CommandPlan): PlannedCommandMethod<TInput, TSelect>;
3654
- }
3655
- /**
3656
- * Declare a public **Command IF** method (#83; proposal §3) — the external,
3657
- * fixed surface other services depend on: **params in** (`input`) and **result
3658
- * out** (`select`, the post-write read-back projection). The method is
3659
- * *implemented by* an internal mutation, attached via {@link CommandBuilder.plan}:
3660
- *
3661
- * ```ts
3662
- * export const PostCommands = publicCommandModel<PostKey>()({
3663
- * create: command({
3664
- * input: { userId: param.string(), title: param.string(), body: param.string() },
3665
- * select: { postId: true, title: true }, // return = read-back projection
3666
- * }).plan(CreatePost), // implemented by the internal mutation
3667
- * });
3668
- * ```
3669
- *
3670
- * The mutation document is **never** exposed; only the `input` params and the
3671
- * `select` result cross the boundary.
3672
- *
3673
- * @param spec `{ input, select? }` — the public input param shape and the optional
3674
- * return projection (consistent read-back).
3675
- */
3676
- declare function command<const TInput extends CommandInputShape, const TSelect extends CommandSelectShape | undefined = undefined>(spec: {
3677
- input: TInput;
3678
- select?: TSelect;
3679
- }): CommandBuilder<TInput, TSelect>;
3680
-
3681
- /**
3682
- * Single-service contract **Runtime** — execution of CQRS query-contract methods
3683
- * (issue #62, Epic #57; spec `docs/cqrs-contract.md`, "Query Method
3684
- * — Common Interface" + "Cardinality matrix"). READS only; command/write
3685
- * execution is #64 and single-contract composition / External Query is #63 — both
3686
- * out of scope here.
3687
- *
3688
- * The Runtime is the **execution engine**: given a resolved query contract (a
3689
- * {@link QueryModelContract} from #58, carrying per-method `resolution` /
3690
- * `inputArity` and a resolved {@link ContractMethodOp}) it executes one method
3691
- * for a single key **or** an array of keys and returns the correct result shape
3692
- * from the proposal's cardinality matrix:
3693
- *
3694
- * | Input | `resolution` | Result shape | Notation |
3695
- * | -------------- | ------------ | ------------------------------------- | ---------------- |
3696
- * | `key` (single) | `point` | `Item \| null` | `key1 : result1` |
3697
- * | `key` (single) | `range` | `Connection` (`{ items, cursor }`) | `key1 : resultM` |
3698
- * | `keys[]` (N) | `point` | keyed `Map<keyId, Item \| null>` | `keyN : resultN` |
3699
- * | `keys[]` (N) | `range` | **unreachable for a single contract** | `keyN : resultN×M` |
3700
- *
3701
- * ## On `keyN : resultN×M` (range + array) — unreachable here, by design
3702
- *
3703
- * The fourth matrix cell — a keyed map of connections — is **not reachable for a
3704
- * single contract** at the direct-call surface. The proposal's N+1 Safety rule
3705
- * fixes a `range` method's `inputArity` to `'single'` ("A `range` method is
3706
- * necessarily `'single'`"; "feeding an array of keys into a range method … is an
3707
- * N+1 fan-out and is rejected at build time, with no opt-in escape hatch"). #58's
3708
- * {@link inputArityOf} derives exactly that, and #60's N+1 checker rejects any
3709
- * contract that violates it before it reaches the SSoT. So a range method only
3710
- * ever accepts one key, and this runtime **rejects** an array fed into a `range`
3711
- * method ({@link executeQueryMethod}) rather than fanning out. `keyN : resultN×M`
3712
- * arises only under cross-contract composition (a `point` parent referencing a
3713
- * child whose per-key result is a connection) — that is #63's External Query
3714
- * territory, explicitly out of scope for #62. We therefore implement the **three
3715
- * reachable shapes** and guard the fourth with a clear error, rather than
3716
- * silently fanning out a forbidden N+1.
3717
- *
3718
- * ## Reuse, not reinvention
3719
- *
3720
- * Execution delegates to the proven machinery:
3721
- *
3722
- * - `point` single → {@link executeQuery} (GetItem / unique Query).
3723
- * - `point` array → {@link executeKeyedBatchGet} below, which reuses the
3724
- * `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys retry) and
3725
- * builds the **keyed** map (BatchGet neither preserves order nor returns absent
3726
- * keys, so missing keys are filled with explicit `null`).
3727
- * - `range` single → {@link executeListInternal} for a `{ items, cursor }`
3728
- * connection, with the cursor wrapped in a per-key envelope
3729
- * ({@link encodePerKeyCursor}) so it carries the key it belongs to.
3730
- *
3731
- * The result-shape typing (single → bare, array → keyed) is expressed as call
3732
- * overloads on {@link executeQueryMethod}.
3733
- */
3734
-
3735
- /**
3736
- * The **structural** shape of a resolved query contract the runtime executes.
3737
- * It depends only on the facts the executor reads — the `query` discriminant and
3738
- * the per-method `resolution` and internal `op` — and is deliberately
3739
- * **independent of the phantom Key / params / result types** carried by
3740
- * {@link QueryModelContract}. Those phantom types are invariant /
3741
- * contravariant on the full interface, so a concrete `QueryModelContract<{…}>`
3742
- * is not assignable to `QueryModelContract<unknown, …>`; this minimal structural
3743
- * type sidesteps that variance while accepting every real contract (a
3744
- * {@link QueryModelContract} is structurally assignable to it). The runtime never
3745
- * needs the method param / result types — it returns the dynamic
3746
- * cardinality-matrix shapes ({@link ContractItem} / {@link Connection} /
3747
- * {@link KeyedResult}).
3748
- */
3749
- interface ExecutableQueryContract {
3750
- readonly kind: 'query';
3751
- readonly methods: Readonly<Record<string, {
3752
- readonly resolution: Resolution;
3753
- /**
3754
- * The method's decided input arity (issue #71): `'either'` for a
3755
- * coalescible base-table `point` (a key array → one `BatchGetItem`),
3756
- * `'single'` for a `range` (partition `Query`) **or** a unique-GSI `point`
3757
- * (a per-key GSI `Query`, not coalescible). The executor honors it to
3758
- * reject an array fed into any `'single'` method rather than fanning out.
3759
- */
3760
- readonly inputArity: InputArity;
3761
- readonly op: ContractMethodOp;
3762
- }>>;
3763
- }
3764
- /** A plain contract key (the access pattern / join key), e.g. `{ articleId }`. */
3765
- type ContractKeyInput = Record<string, unknown>;
3766
- /**
3767
- * The literal {@link InputArity} the contract `C` carries for method `K`. A
3768
- * {@link QueryMethodSpec} carries the method's literal arity in its `inputArity`
3769
- * type when the body was tagged with `point` / `range` (`src/define/contract.ts`);
3770
- * an untagged method keeps the wide `InputArity`. Falls back to `InputArity` for a
3771
- * structurally-typed contract (e.g. the bare {@link ExecutableQueryContract}),
3772
- * which keeps the call surface array-capable (the runtime backstop still rejects an
3773
- * array fed into a `'single'` method).
3774
- */
3775
- type ArityOfMethod<C extends ExecutableQueryContract, K extends keyof C['methods']> = C['methods'][K] extends {
3776
- readonly inputArity: infer A extends InputArity;
3777
- } ? A : InputArity;
3778
- /**
3779
- * The **array** key-argument type accepted by `executeQueryMethod` for method `K`
3780
- * of contract `C`. A method whose literal arity is `'single'` (a `range` read **or**
3781
- * a unique-GSI `point` — both one request per key) accepts **no** array: the type
3782
- * collapses to `never`, so passing an array is a `tsc` compile error at the
3783
- * user-facing surface (issue #71, N+1 rule (a), the **primary** enforcement layer).
3784
- * Any other arity (`'either'` / `'array'`, or the wide `InputArity` for an untagged
3785
- * / structural contract) accepts an array of keys (it coalesces to one batched
3786
- * call). This is the array-overload half of {@link ContractCallSignature}'s
3787
- * narrowing, applied at the real execution surface.
3788
- */
3789
- type ArrayKeyArg<C extends ExecutableQueryContract, K extends keyof C['methods']> = ArityOfMethod<C, K> extends 'single' ? never : readonly ContractKeyInput[];
3790
- /** A single hydrated result item (only the method's projected fields). */
3791
- type ContractItem = Record<string, unknown>;
3792
- /**
3793
- * A pagination connection: a page of items plus an opaque `cursor` for the next
3794
- * page (`null` when the page is the last). For a `range` contract method the
3795
- * cursor is a **per-key envelope** ({@link encodePerKeyCursor}) that identifies
3796
- * the key it paginates.
3797
- */
3798
- interface Connection<TItem = ContractItem> {
3799
- readonly items: TItem[];
3800
- readonly cursor: string | null;
3801
- }
3802
- /**
3803
- * The keyed result of a batched contract read: input-key identity (the canonical
3804
- * {@link serializeContractKey} string) → per-key result. Backed by a `Map` so
3805
- * the association is explicit and order-independent; missing keys are present
3806
- * with an explicit `null` value (`point`) rather than omitted.
3807
- *
3808
- * @typeParam TValue The per-key value (`Item | null` for `point`).
3809
- */
3810
- type KeyedResult<TValue> = Map<string, TValue>;
3811
- /**
3812
- * Retrieval options accepted by a contract query method (the serializable subset
3813
- * from the proposal). All optional; a `point` method ignores the pagination
3814
- * fields, a `range` method honors them.
3815
- */
3816
- interface ContractQueryParams {
3817
- /** Strongly-consistent read (point reads on the base table only). */
3818
- readonly consistentRead?: boolean;
3819
- /** Page size for a `range` (`list`) method. */
3820
- readonly limit?: number;
3821
- /** Resume token — a per-key cursor envelope from a prior `range` page. */
3822
- readonly after?: string;
3823
- /** Sort direction for a `range` (`list`) method. */
3824
- readonly order?: 'ASC' | 'DESC';
3825
- }
3826
- /**
3827
- * Maximum number of in-flight per-key queries for a batched `range` fan-out.
3828
- * Reuses the relation-traversal cap — the same rationale (bound in-flight
3829
- * requests, ride out throttling via UnprocessedKeys retry beneath the cap). Note
3830
- * that a single-contract `range` method is `inputArity: 'single'`, so the
3831
- * fan-out path is only exercised by the (out-of-scope, #63) composition runtime;
3832
- * the bound is defined here so the seam is ready and tested in isolation.
3833
- */
3834
- declare const CONTRACT_RANGE_FANOUT_CONCURRENCY = 16;
3835
- /**
3836
- * Keyed BatchGet: resolve `keys` against `modelClass` in one batched call and
3837
- * return a `Map<keyId, Item | null>` with every input key present (misses →
3838
- * `null`). Reuses the `batch-retry` primitives (chunk ≤100, dedup, UnprocessedKeys
3839
- * retry) — never reinvented.
3840
- *
3841
- * Implementation notes:
3842
- * - **Dedup**: the same key supplied twice is fetched once; both map entries
3843
- * point at the same resolved item (or `null`).
3844
- * - **Order / absence**: BatchGet returns matched items unordered and omits
3845
- * misses, so each returned raw item is indexed by its `PK::SK` and looked up
3846
- * per input key; an unmatched key gets `null`.
3847
- * - **Projection**: items are fetched whole (DynamoDB BatchGet has no reliable
3848
- * server-side projection for mixed keys here) and hydrated against the
3849
- * method's `select`, so the per-key item carries exactly the projected fields.
3850
- */
3851
- declare function executeKeyedBatchGet(modelClass: Function, keys: readonly ContractKeyInput[], select: Record<string, unknown>): Promise<KeyedResult<ContractItem | null>>;
3852
- /**
3853
- * Execute a query-contract method for a **single** key.
3854
- *
3855
- * The result shape is determined by the method's derived `resolution`:
3856
- * - `point` → `Item | null` (`key1 : result1`);
3857
- * - `range` → `Connection` (`key1 : resultM`).
3858
- *
3859
- * A single key is accepted for **every** method regardless of arity.
3860
- *
3861
- * @param contract The resolved query contract (a {@link QueryModelContract}).
3862
- * @param methodName The method to execute (a method name of `contract`).
3863
- * @param key A single contract key.
3864
- * @param params Retrieval options (pagination / consistency).
3865
- */
3866
- declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, key: ContractKeyInput, params?: ContractQueryParams): Promise<ContractItem | null | Connection>;
3867
- /**
3868
- * Execute a query-contract method for an **array** of keys (a single batched
3869
- * call). Valid **only** for a method whose literal arity is not `'single'` — a
3870
- * coalescible base-table `point` (`inputArity: 'either'`, a key array → one
3871
- * `BatchGetItem`). A `'single'` method (a `range` read **or** a unique-GSI `point`)
3872
- * makes the array key argument `never`, so passing an array is a **`tsc` compile
3873
- * error** by construction (issue #71, N+1 rule (a) — the primary enforcement
3874
- * layer; the build-time checker and runtime backstop remain). The result is a keyed
3875
- * `Map<keyId, Item | null>` (`keyN : resultN`).
3876
- *
3877
- * @param contract The resolved query contract.
3878
- * @param methodName The method to execute (a method name of `contract`).
3879
- * @param keys An array of contract keys (rejected for a `'single'` method).
3880
- * @param params Retrieval options.
3881
- */
3882
- declare function executeQueryMethod<C extends ExecutableQueryContract, K extends keyof C['methods'] & string>(contract: C, methodName: K, keys: ArrayKeyArg<C, K>, params?: ContractQueryParams): Promise<KeyedResult<ContractItem | null>>;
3883
- /**
3884
- * The runtime fan-out seam for a batched `range` (`keyN : resultN×M`). NOT
3885
- * reachable for a single contract under #62 (a `range` method's `inputArity` is
3886
- * `'single'`, so {@link executeQueryMethod} rejects an array). It is the
3887
- * cross-contract composition runtime (#63) that drives a per-key range fan-out;
3888
- * this function implements that fan-out — N bounded-parallel Queries grouped by
3889
- * input key, each connection carrying its own per-key cursor — so the seam is
3890
- * defined, reused (via {@link mapWithConcurrency}), and unit-tested in isolation
3891
- * ahead of #63, rather than left as a stub. #62 does not call it on any contract
3892
- * surface.
3893
- *
3894
- * @param op The `range` method op to run per key.
3895
- * @param keys The parent keys to fan out over.
3896
- * @param params Retrieval options applied to **every** key's connection.
3897
- */
3898
- declare function executeRangeFanout(op: ContractMethodOp, keys: readonly ContractKeyInput[], params?: ContractQueryParams): Promise<KeyedResult<Connection>>;
3899
-
3900
- /**
3901
- * Single-service contract **Runtime** — execution of CQRS command-contract
3902
- * methods (issue #64, Epic #57; spec `docs/cqrs-contract.md`,
3903
- * "Command Contract" + "Command Definition"). WRITES only; query reads are #62
3904
- * ({@link executeQueryMethod}) and cross-contract composition is #63 — both out
3905
- * of scope here.
3906
- *
3907
- * Given a resolved command contract (a {@link CommandModelContract} from #58,
3908
- * carrying per-method `result` and the declared {@link CommandBatchMode}, plus a
3909
- * resolved write {@link ContractMethodOp}) it executes one method for a **single
3910
- * key** or an **array of keys** and applies the write(s) to DynamoDB:
3911
- *
3912
- * | Input | resolution |
3913
- * | -------------- | ----------------------------------------------------------- |
3914
- * | `key` (single) | one write op (`put` / `update` / `delete`) |
3915
- * | `keys[]` (N) | a **batched** write per the method's declared `batch` mode |
3916
- *
3917
- * The batched form is **declared by the contract** (proposal "Consistency with
3918
- * the existing write surface"):
3919
- *
3920
- * - `'transact'` — one `TransactWriteItems`: the per-key write applied
3921
- * **atomically** (all-or-nothing) and **condition-capable** ({@link ConditionInput},
3922
- * `{ notExists }` / equality). DynamoDB caps `TransactWriteItems` at **25** and
3923
- * the call is atomic, so an array of **>25 keys cannot be split** without
3924
- * breaking atomicity — this runtime **rejects** it with a clear error rather
3925
- * than silently truncating or chunking ({@link MAX_TRANSACT_ITEMS}).
3926
- * - `'batchWrite'` — a `BatchWriteItem`: the per-key writes applied
3927
- * **non-atomically**, **without conditions** (DynamoDB's `BatchWriteItem` has no
3928
- * `ConditionExpression`). An array of any size is permitted; it is chunked per
3929
- * the 25-items-per-request limit with `UnprocessedItems` retry, reusing the
3930
- * existing batch machinery ({@link executeBatchWrite}).
3931
- * - **undefined** — the method declared no batched form: an array is rejected (it
3932
- * resolves only a single key).
3933
- *
3934
- * ## Reuse, not reinvention
3935
- *
3936
- * The recorded write op carries declarative sentinels ({@link ContractParamRef} /
3937
- * {@link ContractKeyFieldRef}) at its value positions; {@link renderRecord} binds
3938
- * them to the concrete `(key, params)` at execution time, producing the very same
3939
- * `item` / `changes` / `key` / `condition` objects a hand-written `Model.put` /
3940
- * `Model.update` / `Model.delete` would pass. Execution then delegates to the
3941
- * proven write machinery:
3942
- *
3943
- * - single → {@link executePut} / {@link executeUpdate} / {@link executeDelete};
3944
- * - transact → one `TransactWriteItems` built from `buildPutInput` /
3945
- * `buildUpdateInput` / `buildDeleteInput` (the same inputs the imperative
3946
- * {@link TransactionContext} uses), with the ≤25 limit enforced;
3947
- * - batchWrite → {@link executeBatchWrite} (chunk ≤25, `UnprocessedItems` retry).
3948
- *
3949
- * The TS and Python runtimes produce identical effects for the same SSoT.
3950
- */
3951
-
3952
- /**
3953
- * The **structural** shape of a resolved command contract the runtime executes.
3954
- * It depends only on the facts the executor reads — the `command` discriminant
3955
- * and the per-method `op` / `batch` / `result` — and is deliberately
3956
- * **independent of the phantom Key / params / result types** carried by
3957
- * {@link CommandModelContract} (those are invariant on the full interface, so a
3958
- * concrete `CommandModelContract<{…}>` is not assignable to
3959
- * `CommandModelContract<unknown, …>`). Every real command contract is
3960
- * structurally assignable to this minimal type.
3961
- */
3962
- interface ExecutableCommandContract {
3963
- readonly kind: 'command';
3964
- readonly methods: Readonly<Record<string, {
3965
- readonly op: ContractMethodOp;
3966
- readonly result: CommandResultKind;
3967
- readonly batch?: CommandBatchMode;
3968
- /**
3969
- * The composed write ops of a **multi-fragment** `mutation`-derived method
3970
- * (issue #90). When present (N≥2), a **single-key** call executes ALL of
3971
- * them as **one atomic `TransactWriteItems`** (not sequential writes), so a
3972
- * condition failure on any fragment rolls back the whole transaction. Absent
3973
- * for a single-fragment / #83 / hand-written #64 method (which executes its
3974
- * one {@link op}). The read-back stays keyed on the primary op ({@link op}).
3975
- */
3976
- readonly ops?: readonly ContractMethodOp[];
3977
- /**
3978
- * The referential-integrity assertions derived from the mutation's
3979
- * `requires` effects (issue #84). When present (non-empty), a
3980
- * **single-key** call composes the base write(s) **and** one read-only
3981
- * `ConditionCheck` (`attribute_exists`) per assertion into **one atomic
3982
- * `TransactWriteItems`** — so a single-op write is **promoted** to a
3983
- * transaction. If any referenced entity is absent, its assertion fails and
3984
- * the WHOLE transaction rolls back (the entity write never persists). Absent
3985
- * on a mutation with no `requires` / a #64 hand-written method.
3986
- */
3987
- readonly conditionChecks?: readonly DerivedConditionCheck[];
3988
- /**
3989
- * The adjacency edge writes derived from the mutation's `edges` effects
3990
- * (issue #85). When present, a **single-key** call composes each edge's
3991
- * adjacency `Put` / `Delete` item(s) into the **same atomic
3992
- * `TransactWriteItems`** as the entity write (and any ConditionChecks /
3993
- * derived updates), so the edge is created / removed / moved atomically with
3994
- * the entity. Absent on a mutation with no `edges` / a #64 hand-written method.
3995
- */
3996
- readonly edgeWrites?: readonly DerivedEdgeWrite[];
3997
- /**
3998
- * The derived counter updates from the mutation's `derive` effects (issue
3999
- * #85): each an atomic `ADD` (`UpdateItem`) on a target counter (e.g.
4000
- * `User.postCount += 1`). When present, a **single-key** call composes each
4001
- * `ADD` into the same atomic `TransactWriteItems` as the entity write, so the
4002
- * counter moves atomically (and never clobbers a concurrent increment — an
4003
- * `ADD` needs no prior read). Absent on a mutation with no `derive`.
4004
- */
4005
- readonly derivedUpdates?: readonly DerivedUpdate[];
4006
- /**
4007
- * The uniqueness guards derived from the mutation's `unique` effects (issue
4008
- * #86): each a marker-row `Put` (`attribute_not_exists`) / `Delete` /
4009
- * `Delete(old)+Put(new)` swap. When present, a **single-key** call composes
4010
- * the guard item(s) into the same atomic `TransactWriteItems` as the entity
4011
- * write, so a duplicate scoped value (the guard `Put`'s `attribute_not_exists`
4012
- * failing) rolls the WHOLE transaction back — the entity is never created.
4013
- * Absent on a mutation with no `unique` / a #64 hand-written method.
4014
- */
4015
- readonly uniqueGuards?: readonly DerivedUniqueGuard[];
4016
- /**
4017
- * The outbox events derived from the mutation's `emits` effects (issue #87):
4018
- * each a transactional-outbox `Put`. When present, a **single-key** call
4019
- * composes each event's `Put` into the same atomic `TransactWriteItems` as the
4020
- * entity write, so the event RECORD is written ATOMICALLY with the state it
4021
- * announces (they can never diverge). The row is drainable from real
4022
- * DynamoDB Streams (captured at the table layer regardless of code path);
4023
- * delivery / handlers are OUT of scope. Since #94 the derived-command
4024
- * transaction also feeds every committed item (including this outbox `Put`)
4025
- * into the in-process `ChangeCaptureRegistry` seam, so the dev/test
4026
- * `src/cdc/` emulator now observes this path too — production Streams and the
4027
- * emulator agree. Absent on a mutation with no `emits`.
4028
- */
4029
- readonly outboxEvents?: readonly DerivedOutboxEvent[];
4030
- /**
4031
- * The client-token idempotency guard derived from the mutation's `idempotency`
4032
- * effect (issue #87): a marker-row `Put` (`attribute_not_exists`). When present,
4033
- * a **single-key** call composes the guard `Put` into the same atomic
4034
- * `TransactWriteItems` as the entity write, so a same-token re-execution fails
4035
- * the guard and rolls the WHOLE transaction back — no effect is double-applied.
4036
- * Absent on a mutation with no `idempotency` / a #64 hand-written method.
4037
- */
4038
- readonly idempotencyGuard?: DerivedIdempotencyGuard;
4039
- /**
4040
- * The return projection of a `mutation`-derived method (issue #83). When
4041
- * present, a **single-key** call performs the write then issues a
4042
- * **consistent read-back** (`GetItem` with `ConsistentRead`) of the
4043
- * written entity's primary key and returns the projected item via the
4044
- * existing read-projection machinery. Absent on a #64 hand-written method.
4045
- */
4046
- readonly returnSelection?: Readonly<Record<string, boolean>>;
4047
- }>>;
4048
- }
4049
- /** The params a command method consumes (the body's `params` argument). */
4050
- type ContractCommandParams = Record<string, unknown>;
4051
- /**
4052
- * The projected read-back item a `mutation`-derived command method returns, or
4053
- * `null` when the written row was absent at read-back (e.g. a `remove`). A plain
4054
- * record of the projected fields.
4055
- */
4056
- type CommandReturn = Record<string, unknown> | null;
4057
- /**
4058
- * Execute a command-contract method for a **single** key — one write op
4059
- * (`put` / `update` / `delete`).
4060
- *
4061
- * @param contract The resolved command contract (a {@link CommandModelContract}).
4062
- * @param methodName The method to execute.
4063
- * @param key A single contract key (the mutation target).
4064
- * @param params The mutation params (the body's `params` argument).
4065
- */
4066
- declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, key: ContractKeyInput, params?: ContractCommandParams): Promise<void | CommandReturn>;
4067
- /**
4068
- * Execute a command-contract method for an **array** of keys — a batched write
4069
- * per the method's declared {@link CommandBatchMode}:
4070
- *
4071
- * - `'transact'` → one atomic `TransactWriteItems` (≤25, condition-capable); a
4072
- * >25-key array is **rejected** (an atomic transaction cannot be split).
4073
- * - `'batchWrite'` → a `BatchWriteItem` (no conditions; chunked ≤25 with
4074
- * `UnprocessedItems` retry).
4075
- *
4076
- * A method that declared no batched form rejects an array with a clear error.
4077
- *
4078
- * @param contract The resolved command contract.
4079
- * @param methodName The method to execute.
4080
- * @param keys An array of contract keys.
4081
- * @param params The mutation params shared across every key.
4082
- */
4083
- declare function executeCommandMethod(contract: ExecutableCommandContract, methodName: string, keys: readonly ContractKeyInput[], params?: ContractCommandParams): Promise<void>;
4084
-
4085
812
  /**
4086
813
  * Per-key cursor envelope for batched `range` (`list`) contract methods (issue
4087
814
  * #62, CQRS single-service runtime; spec `docs/cqrs-contract.md`,
@@ -4549,7 +1276,7 @@ type ParamLeavesOf<X> = X extends Param<infer V> ? V : X extends object ? {
4549
1276
  * These accept {@link Param} placeholders at scalar leaves while keeping field
4550
1277
  * names / value types / the key boundary / strict-select checked at definition
4551
1278
  * time (via {@link Parameterize} layered on the model's real key & select
4552
- * types). `Model.query` / `Model.put` / … are **untouched**.
1279
+ * types). `Model.query` / `Model.putItem` / … are **untouched**.
4553
1280
  *
4554
1281
  * `defineQueries({...})` / `defineCommands({...})` take a record of these
4555
1282
  * definition nodes and return the same record (typed), forming the IR the
@@ -5306,4 +2033,4 @@ declare function assertBundleSerializable(bundle: BridgeBundle): void;
5306
2033
  */
5307
2034
  declare function buildBridgeBundle(queries?: DefinitionMap, commands?: DefinitionMap, registry?: typeof MetadataRegistry, transactions?: Record<string, TransactionDefinition>, contractInputs?: ContractInputs): BridgeBundle;
5308
2035
 
5309
- export { type AnyModelContract, type AnyOperationDefinition, type ArityTaggedQueryMethod, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchGetExecInput, BatchWriteExecItem, type BridgeBundle, type BuiltContracts, CONTRACT_RANGE_FANOUT_CONCURRENCY, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, type CommandBatchMode, type CommandBuilder, type CommandContractMethodSpec, type CommandInputShape, type CommandMethod, type CommandMethodSpec, type CommandModelContract, type CommandPlan, type CommandResolutionTarget, type CommandResultKind, type CommandSelectShape, type CommandSpec, type CompiledFragment, type ComposeSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, type ConditionInput, type ConditionSpec, type Connection, type ContextOwnership, type ContextOwnershipMap, type ContextSpec, type ContractBoundaryViolation, type ContractCallSignature, type ContractCardinality, type ContractCommandParams, type ContractCommandResult, type ContractComposeNode, type ContractFromRef, type ContractInputArity, type ContractInputs, type ContractItem, type ContractKeyFieldRef, type ContractKeyInput, type ContractKeyRef, type ContractKeySpec, type ContractKind, type ContractMap, type ContractMethodOp, type ContractN1Violation, type ContractParamRef, type ContractQueryParams, type ContractResolution, type ContractSpec, type ContractWriteOptions, DDBModel, type DefinitionMap, DeleteInput, type DeriveEffect, type DerivedEdgeWrite, type DerivedUpdate, DynamoDBOperation, DynamoExecutor, type DynamoType, EDGE_WRITES_MARKER, ENTITY_WRITES_MARKER, type EdgeEffect, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, type EffectPath, type EmbeddedMetadata, type EmitEffect, type EntityMetadata, type EntityRef, type EntityWritesDefinition, type EntityWritesShape, EventLog, type ExecutableCommandContract, type ExecutableQueryContract, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, type FieldMetadata, type FieldOptions, type FilterExpressionResult, type FilterSpec, type FragmentInput, type FragmentOptions, type GsiDefinition, type IdempotencyEffect, type InputArity, type KeyDefinition, type KeyedResult, LIFECYCLE_CONTRACT_MARKER, type LifecycleContract, type LifecycleEffects, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, type LiteralParam, MAX_TRANSACT_ITEMS, type Manifest, type ManifestEntity, type ManifestField, type ManifestFieldType, type ManifestGsi, type ManifestKey, type ManifestRelation, type ManifestTable, MetadataRegistry, type ModelOptions, ModelStatic, type MutationFragment, type MutationInputProxy, type MutationInputRef, type MutationIntent, type MutationRecorder, type NumberParam, OLD_VALUE_NAMESPACE, type OperationDefinition, type OperationKind, type OperationSpec, type OperationsDocument, type Param, type ParamDescriptor, type ParamKind, type ParamSpec, type ParamStructure, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, type PlannedCommandMethod, PrimaryKeyOf, type ProjectionResult, PutInput, type QueryContractMethodSpec, type QueryMethod, type QueryMethodSpec, type QueryModelContract, type QueryOptions$1 as QueryOptions, type QuerySpec, type RangeConditionSpec, type ReadOperationType, type RecordedCompose, type RelationLimitOptions, type RelationMetadata, type RelationOptions, type RelationTraversalOptions, ReplayOptions, type RequiresEffect, type Resolution, ResolvedKey, SPEC_VERSION, SegmentedKey, SelectableOf, ShardId, type StringParam, TableMapping, TransactWriteExecItem, type TransactionDefinition, type TransactionItemSpec, type TransactionItemType, type TransactionParamShape, type TransactionRef, type TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, type UniqueEffect, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type WhenComparison, type WhenSpec, type WriteDefinitionOptions, WriteExecOptions, type WriteLifecyclePhase, type WriteOperationType, type WriteRecorder, WriteResult, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, command, compileFilterExpression, compileFragment, compileMutationPlan, compileSingleFragmentPlan, contractOfMethodSpec, createCdcEmulator, createDefaultLinter, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePlan, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, entityWrites, evaluateFilter, execute, executeCommandMethod, executeDeclarativeTransaction, executeExplain, executeKeyedBatchGet, executeList, executeQuery, executeQueryMethod, executeRangeFanout, expandTransaction, field, from, getEdgeWrites, getEntityWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isCommandBatchMode, isCommandModelContract, isCommandPlan, isContractComposeNode, isContractFromRef, isContractKeyFieldRef, isContractKeyRef, isContractParamRef, isEdgeWritesDefinition, isEntityWritesDefinition, isLifecycleContract, isMutationFragment, isMutationInputRef, isParam, isPlannedCommandMethod, isQueryModelContract, isSelectBuilder, isTransactionRef, lifecyclePhaseForIntent, list, literal, map, mintContractKeyFieldRef, mintContractParamRef, missingGsiRule, model, mutation, noScanRule, number, numberSet, param, plan, point, publicCommandModel, publicQueryModel, query, queryBoundaryRule, range, relationDepthRule, requireLimitRule, resolveKey, resolveLifecycle, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when, wholeKeysSentinel };
2036
+ export { type AnyModelContract, AnyOperationDefinition, BATCH_GET_MAX_KEYS, BATCH_WRITE_MAX_ITEMS, BatchGetExecInput, BatchWriteExecItem, BridgeBundle, type BuiltContracts, CdcEmulator, CdcEmulatorOptions, ChangeEvent, ChangeHandler, ClientManager, type CollectParams, CommandMethodSpec, CommandModelContract, CommandSpec, ConcurrentRecomputeRef, type ConditionExpressionResult, ConditionInput, ConditionSpec, type ContextOwnership, type ContextOwnershipMap, ContextSpec, type ContractBoundaryViolation, type ContractInputs, type ContractMap, type ContractN1Violation, ContractSpec, DDBModel, DefinitionMap, DeleteInput, DynamoDBOperation, DynamoExecutor, type DynamoType, EDGE_WRITES_MARKER, type EdgeLifecycle, type EdgeWriteDeclaration, type EdgeWriteRecorder, type EdgeWritesDefinition, type EmbeddedMetadata, type EntityMetadata, EntityRef, EventLog, ExecutionPlan, Executor, ExecutorResult, type ExplainInput, FaultSpec, type FieldMetadata, type FieldOptions, type FilterExpressionResult, type GsiDefinition, type KeyDefinition, type LintResult, type LintRule, Linter, type ListInput, type ListOptions, MAX_TRANSACT_ITEMS, Manifest, MetadataRegistry, type ModelOptions, ModelStatic, OLD_VALUE_NAMESPACE, OperationDefinition, OperationsDocument, Param, ParamDescriptor, type Parameterize, PartialQueryKeyOf, type PerKeyCursorEnvelope, type PlanInput, PrimaryKeyOf, type ProjectionResult, PutInput, QueryMethodSpec, QueryModelContract, type QueryOptions$1 as QueryOptions, QuerySpec, RawCondition, type RelationLimitOptions, type RelationMetadata, type RelationOptions, type RelationTraversalOptions, ReplayOptions, ResolvedKey, SegmentedKey, SelectableOf, ShardId, TableMapping, TransactWriteExecItem, type TransactionDefinition, TransactionItemSpec, type TransactionParamShape, type TransactionRef, TransactionSpec, type TxConditionCheckOptions, type TxForEachInstruction, type TxForEachOptions, type TxInstruction, type TxRecorder, type TxWriteInstruction, type TxWriteOptions, UniqueQueryKeyOf, type UpdateExpressionResult, UpdateInput, type WhenComparison, WriteDefinitionOptions, WriteExecOptions, WriteResult, assertBundleSerializable, assertContractBoundaries, assertContractN1Safe, assertJsonSerializable, assertSupportedCondition, belongsTo, binary, boolean, buildBridgeBundle, buildConditionExpression, buildContexts, buildContracts, buildManifest, buildOperations, buildProjection, buildQuerySpec, buildTransactionSpec, buildTransactions, buildUpdateExpression, collectContractBoundaryViolations, collectContractN1Violations, compileFilterExpression, createCdcEmulator, createDefaultLinter, datetime, decodeCursor, decodePerKeyCursor, defineCommands, defineDelete, defineList, definePut, defineQueries, defineQuery, defineTransaction, defineTransactions, defineUpdate, deriveEdgeWriteItems, deriveEdgeWriteItemsFor, deriveModelEdgeWriteItems, derivePrefix, detectRelationFields, edgeWrites, embedded, encodeCursor, encodePerKeyCursor, evaluateFilter, execute, executeDeclarativeTransaction, executeExplain, executeList, executeQuery, expandTransaction, field, getEdgeWrites, getImplicitKeyFields, gsiAmbiguityRule, hasMany, hasOne, hydrate, isEdgeWritesDefinition, isSelectBuilder, isTransactionRef, list, literal, map, missingGsiRule, model, noScanRule, number, numberSet, plan, queryBoundaryRule, relationDepthRule, requireLimitRule, resolveKey, resolveRelations, serializeContractKey, serializeFieldValue, string, stringSet, validateDepth, validateGsiAmbiguity, when };