graphddb 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,22 +2,26 @@ import {
2
2
  assertContractBoundaries,
3
3
  assertContractN1Safe,
4
4
  buildBridgeBundle,
5
+ buildComponents,
5
6
  buildContexts,
6
7
  buildContracts,
7
8
  buildOperations,
8
9
  buildPreparedPlanDocument,
10
+ buildQueryComponent,
9
11
  buildQuerySpec,
12
+ buildTransactionComponent,
10
13
  collectContractBoundaryViolations,
11
14
  collectContractN1Violations,
12
- compilePreparedPlan
13
- } from "../chunk-N4NWYNGZ.js";
15
+ compilePreparedPlan,
16
+ portableIrDocument
17
+ } from "../chunk-SSVG6NQK.js";
14
18
  import {
15
19
  PREPARED_FORMAT_VERSION,
16
20
  buildManifest,
17
21
  canonicalJson,
18
22
  entityFingerprint,
19
23
  planFingerprint
20
- } from "../chunk-LGHSZIEE.js";
24
+ } from "../chunk-NWTEUWJD.js";
21
25
  import {
22
26
  MAX_TRANSACT_COMPOSE_ITEMS,
23
27
  assertBundleSerializable,
@@ -32,24 +36,25 @@ import {
32
36
  resetMaintenanceGraphCache,
33
37
  resolveLifecycle,
34
38
  resolveMaintainers
35
- } from "../chunk-I4LEJ4TF.js";
36
- import "../chunk-HNY2EJPV.js";
39
+ } from "../chunk-XZTA4VJH.js";
40
+ import "../chunk-KOIJ4SNO.js";
37
41
  import {
38
42
  EXPR_VERSION,
39
43
  SPEC_VERSION,
44
+ SPEC_VERSION_KEY_EXPR,
40
45
  SPEC_VERSION_SCP,
41
46
  SPEC_VERSION_SUPPORTED,
42
47
  canonicalizeExpressionSpec,
43
- operationsContainScpNodes,
44
48
  operationsSpecVersion
45
- } from "../chunk-L4QRCHRQ.js";
46
- import "../chunk-L2NEDS7U.js";
49
+ } from "../chunk-WOFRHRXY.js";
50
+ import "../chunk-ZNU7OI5I.js";
47
51
  import "../chunk-XTWXMOHD.js";
48
52
  export {
49
53
  EXPR_VERSION,
50
54
  MAX_TRANSACT_COMPOSE_ITEMS,
51
55
  PREPARED_FORMAT_VERSION,
52
56
  SPEC_VERSION,
57
+ SPEC_VERSION_KEY_EXPR,
53
58
  SPEC_VERSION_SCP,
54
59
  SPEC_VERSION_SUPPORTED,
55
60
  assertBundleSerializable,
@@ -59,12 +64,15 @@ export {
59
64
  assertNoCrossFragmentMaintainCollision,
60
65
  assertSupportedCondition,
61
66
  buildBridgeBundle,
67
+ buildComponents,
62
68
  buildContexts,
63
69
  buildContracts,
64
70
  buildManifest,
65
71
  buildOperations,
66
72
  buildPreparedPlanDocument,
73
+ buildQueryComponent,
67
74
  buildQuerySpec,
75
+ buildTransactionComponent,
68
76
  buildTransactionSpec,
69
77
  buildTransactions,
70
78
  canonicalJson,
@@ -76,9 +84,9 @@ export {
76
84
  compilePreparedPlan,
77
85
  compileSingleFragmentPlan,
78
86
  entityFingerprint,
79
- operationsContainScpNodes,
80
87
  operationsSpecVersion,
81
88
  planFingerprint,
89
+ portableIrDocument,
82
90
  resetMaintenanceGraphCache,
83
91
  resolveLifecycle,
84
92
  resolveMaintainers
@@ -1,5 +1,6 @@
1
- import { n as Executor, o as DynamoDBOperation, R as ReadExecOptions, p as ExecutorResult, B as BatchGetExecInput, q as PutInput, W as WriteExecOptions, r as WriteResult, U as UpdateInput, s as DeleteInput, t as BatchWriteExecItem, u as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, D as DDBModel, v as ChangeEvent } from '../key-DR7_lpyk.js';
2
- import '../types-2PMXEn5x.js';
1
+ import { n as Executor, o as DynamoDBOperation, R as ReadExecOptions, p as ExecutorResult, B as BatchGetExecInput, q as PutInput, W as WriteExecOptions, r as WriteResult, U as UpdateInput, s as DeleteInput, t as BatchWriteExecItem, u as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, D as DDBModel, v as ChangeEvent } from '../key-Dne-a20Q.js';
2
+ import '../types-CfxzTEFL.js';
3
+ import 'behavior-contracts';
3
4
 
4
5
  /**
5
6
  * In-process physical store backing the GraphDDB Memory Test Adapter
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createCdcEmulator
3
- } from "../chunk-GS4C5VGO.js";
4
- import "../chunk-HNY2EJPV.js";
3
+ } from "../chunk-YGQ2NO6J.js";
4
+ import "../chunk-KOIJ4SNO.js";
5
5
  import {
6
6
  ClientManager,
7
7
  MetadataRegistry,
8
8
  resolveModelClass
9
- } from "../chunk-L2NEDS7U.js";
9
+ } from "../chunk-ZNU7OI5I.js";
10
10
  import {
11
11
  TableMapping
12
12
  } from "../chunk-XTWXMOHD.js";
@@ -1,5 +1,6 @@
1
1
  import ts from 'typescript';
2
- import { E as ExpressionSpec } from '../types-2PMXEn5x.js';
2
+ import { E as ExpressionSpec } from '../types-CfxzTEFL.js';
3
+ import 'behavior-contracts';
3
4
 
4
5
  /**
5
6
  * Compile-time hoist transform + no-runtime-capture build lint for
@@ -2,8 +2,8 @@ import {
2
2
  EXPR_VERSION,
3
3
  SCP_LOWERED_MARKER,
4
4
  canonicalizeExpressionSpec
5
- } from "../chunk-L4QRCHRQ.js";
6
- import "../chunk-L2NEDS7U.js";
5
+ } from "../chunk-WOFRHRXY.js";
6
+ import "../chunk-ZNU7OI5I.js";
7
7
  import "../chunk-XTWXMOHD.js";
8
8
 
9
9
  // src/transform/prepared-transform.ts
@@ -1,4 +1,4 @@
1
- import { d as SegmentedKey, e as ProjectionTransform, f as MaintainEvent, g as ProjectionMap, h as MaintainConsistency, i as MaintainUpdateMode, E as EffectPath, j as MembershipPredicate } from './key-DR7_lpyk.js';
1
+ import { d as SegmentedKey, e as ProjectionTransform, f as MaintainEvent, g as ProjectionMap, h as MaintainConsistency, i as MaintainUpdateMode, E as EffectPath, j as MembershipPredicate } from './key-Dne-a20Q.js';
2
2
 
3
3
  type DynamoType = 'S' | 'N' | 'BOOL' | 'B' | 'SS' | 'NS' | 'L' | 'M';
4
4
  interface FieldOptions {
@@ -1,3 +1,5 @@
1
+ import { Component } from 'behavior-contracts';
2
+
1
3
  /**
2
4
  * Parameter placeholders for the parameterized query / command definition DSL
3
5
  * (issue #41, Python-bridge Phase 0a).
@@ -21,7 +23,7 @@
21
23
  /** Brand tag preventing a plain object from structurally matching a {@link Param}. */
22
24
  declare const PARAM_BRAND: unique symbol;
23
25
  /** Runtime discriminant for a parameter placeholder. */
24
- type ParamKind = 'string' | 'number' | 'literal' | 'array';
26
+ type ParamKind = 'string' | 'number' | 'literal' | 'array' | 'map';
25
27
  /**
26
28
  * A branded parameter placeholder representing a value of TypeScript type `T`
27
29
  * that is bound at execution time rather than definition time.
@@ -64,6 +66,28 @@ type StringParam = Param<string>;
64
66
  type NumberParam = Param<number>;
65
67
  /** A `Param` whose represented value type is the literal union `L`. */
66
68
  type LiteralParam<L extends string | number> = Param<L>;
69
+ /**
70
+ * A `Param` standing in for a **map / embedded object** value (issue #295): a
71
+ * JSON-serializable object written to a single DynamoDB `M` attribute. The
72
+ * `kind` is narrowed to the `'map'` literal so type-level dispatch works, and
73
+ * the represented value type `T` (default `Record<string, unknown>`) lets a
74
+ * typed domain value class ride the placeholder:
75
+ *
76
+ * ```ts
77
+ * type AvgValue = Record<string, Record<string, { sum: number; count: number }>>;
78
+ * param.map<AvgValue>(); // Param<AvgValue> bound to a `@map value!: AvgValue` field
79
+ * ```
80
+ *
81
+ * The value crosses every boundary as a **native DynamoDB `M` attribute**
82
+ * (never a JSON-string blob): the placeholder renders to a `{field}` token /
83
+ * `{ref:["field"]}` Expression-IR reference in the portable IR, and each
84
+ * runtime's (already recursive) attribute marshaller serializes the supplied
85
+ * object to `M`. Generated bindings type it as the language's native free-form
86
+ * map (`dict` / `map[string]any` / `serde_json::Value` / `array`).
87
+ */
88
+ type MapParam<T extends Record<string, unknown> = Record<string, unknown>> = Param<T> & {
89
+ readonly kind: 'map';
90
+ };
67
91
  /** The element-field descriptor shape accepted by {@link param.array}. */
68
92
  type ArrayElementShape = Record<string, Param<unknown>>;
69
93
  /**
@@ -131,6 +155,29 @@ declare const param: {
131
155
  * ```
132
156
  */
133
157
  readonly array: <const E extends ArrayElementShape>(element: E, options?: ParamOptions) => ArrayParam<E>;
158
+ /**
159
+ * A placeholder for a **map / embedded object** value written to a single
160
+ * DynamoDB `M` attribute (issue #295). Free-form by design: the model-side
161
+ * vocabulary (`@map` / `@embedded`) records no nested schema, so the param is
162
+ * typed as the language-native map in generated bindings (`dict` /
163
+ * `map[string]any` / `serde_json::Value` / `array`) while the optional type
164
+ * parameter `T` keeps the TS authoring surface precise.
165
+ *
166
+ * Legal at **value positions only** (a write descriptor's `input`, a
167
+ * transaction fragment value, a batch-write per-item payload field) — a map
168
+ * cannot compose a key, so a `param.map()` in a key position is rejected
169
+ * loudly at build time.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * putLeaf: {
174
+ * upsert: TreeNode,
175
+ * key: { part: param.string(), child: param.string() },
176
+ * input: { value: param.map<AvgValue>(), computedAt: param.number() },
177
+ * }
178
+ * ```
179
+ */
180
+ readonly map: <T extends Record<string, unknown> = Record<string, unknown>>(options?: ParamOptions) => MapParam<T>;
134
181
  };
135
182
 
136
183
  /**
@@ -181,6 +228,27 @@ declare const SPEC_VERSION: "1.1";
181
228
  * (#265/#266/#267) land and bump the supported constant with the wiring.
182
229
  */
183
230
  declare const SPEC_VERSION_SCP: "1.2";
231
+ /**
232
+ * The schema version stamped on an operations document whose **read operations**
233
+ * carry the SCP IR Phase 2 Expression-IR key wiring (`keyExpr` / `valueExpr`,
234
+ * issue #289). This is the **operations IR 2.0 milestone** (the "2.0-rc" of the
235
+ * migration plan §3 Phase 2 / M3): a MAJOR bump (`1.x` → `2.x`), because the key
236
+ * wiring's serialization changed from a `{param}` string template to the portable
237
+ * Expression IR `concat`/`ref` closed vocabulary (behavior-contracts
238
+ * `expression-ir.md`). The BEHAVIOR is identical — the change is purely in IR
239
+ * serialization — but a major bump is correct: a pre-#289 runtime (supported
240
+ * `1.2`) `validateEnvelope`-rejects a `2.0` document (its `keyExpr` reader does
241
+ * not exist), which is the intended fail-closed break. Stamped **only** when a
242
+ * document actually carries a read op (a `queries` entry); a commands/transactions
243
+ * -only document has no read-path key wiring and keeps its `1.1`/`1.2` stamp,
244
+ * serializing byte-identically.
245
+ *
246
+ * The stamp string is the integer `<major>.<minor>` form `validateEnvelope`
247
+ * requires (`2.0`); "2.0-rc" is the migration MILESTONE label (dual-emit proven,
248
+ * flip landed), not the wire version — a `-rc` suffix is not a valid envelope
249
+ * version and would fail-closed in graphddb's own runtime.
250
+ */
251
+ declare const SPEC_VERSION_KEY_EXPR: "2.0";
184
252
  /**
185
253
  * The HIGHEST spec-IR minor this **TS runtime** understands and will EXECUTE
186
254
  * (issue #265, Phase 3 S5). The shared `validateEnvelope` fail-closed rule is
@@ -194,10 +262,19 @@ declare const SPEC_VERSION_SCP: "1.2";
194
262
  * unknown `exprVersion` / operator still fail-closes (the expr evaluator rejects
195
263
  * it). The py/rust/go runtimes bump in S6, PHP in S7 — the whole release column
196
264
  * moves to `1.2` together.
265
+ *
266
+ * **SCP IR Phase 2 (#289): bumped to `2.0` (major).** The read-op key wiring is
267
+ * now portable Expression IR (`keyExpr` / `valueExpr`), a MAJOR serialization
268
+ * change. `validateEnvelope`'s rule is "major must match, minor ≤ supported", so
269
+ * a `2.0` runtime accepts `2.0` documents and **rejects every `1.x` document**
270
+ * (major mismatch) — and every `1.x` runtime rejects a `2.0` document. That is
271
+ * the intended breaking cut: the whole operations IR moves to `2.0` at once
272
+ * (migration plan §4). All 4 runtimes bump their supported constant to `2.0`
273
+ * together (one release column), mirroring the `1.2` bump discipline.
197
274
  */
198
- declare const SPEC_VERSION_SUPPORTED: "1.2";
199
- /** A spec version an operations document may carry (see {@link SPEC_VERSION_SCP}). */
200
- type SpecVersion = typeof SPEC_VERSION | typeof SPEC_VERSION_SCP;
275
+ declare const SPEC_VERSION_SUPPORTED: "2.0";
276
+ /** A spec version an operations document may carry (see {@link SPEC_VERSION_KEY_EXPR}). */
277
+ type SpecVersion = typeof SPEC_VERSION | typeof SPEC_VERSION_SCP | typeof SPEC_VERSION_KEY_EXPR;
201
278
  /** Field type as carried in the manifest (derived from the DynamoDB type). */
202
279
  type ManifestFieldType = 'string' | 'number' | 'boolean' | 'binary' | 'stringSet' | 'numberSet' | 'list' | 'map';
203
280
  interface ManifestField {
@@ -305,12 +382,17 @@ interface ManifestTable {
305
382
  }
306
383
  interface Manifest {
307
384
  /**
308
- * Always the base {@link SPEC_VERSION}: the manifest carries entity/schema
309
- * metadata only the SCP Expression IR vocabulary (issue #261) lives in the
310
- * operations document, so the conditional `1.2` stamp never applies here and
311
- * every manifest stays byte-identical.
385
+ * The IR SSoT version. Historically the base {@link SPEC_VERSION} (`1.1`): the
386
+ * manifest carries entity/schema metadata only, so the conditional `1.2`
387
+ * Expression-IR stamp never applied here. **SCP IR Phase 2 (#289): moved to
388
+ * {@link SPEC_VERSION_KEY_EXPR} (`2.0`).** The operations IR took a MAJOR bump
389
+ * (portable Expression IR key wiring), and `validateEnvelope`'s "major must
390
+ * match" rule is applied to the manifest AND the operations document against a
391
+ * single supported constant — so the manifest version must track the same major
392
+ * or every load would fail major-mismatch. The manifest CONTENT is unchanged;
393
+ * only its version stamp moves to `2.0` with the rest of the IR SSoT.
312
394
  */
313
- readonly version: typeof SPEC_VERSION;
395
+ readonly version: typeof SPEC_VERSION_KEY_EXPR;
314
396
  readonly tables: Readonly<Record<string, ManifestTable>>;
315
397
  readonly entities: Readonly<Record<string, ManifestEntity>>;
316
398
  }
@@ -332,12 +414,31 @@ interface ParamSpec {
332
414
  */
333
415
  readonly description?: string;
334
416
  }
335
- /** A `begins_with` range condition on a sort key (templated value). */
417
+ /** A `begins_with` range condition on a sort key (Expression-IR value). */
336
418
  interface RangeConditionSpec {
337
419
  readonly operator: 'begins_with';
338
420
  readonly key: string;
339
- /** Template value, e.g. `GROUP#` or `GROUP#{result.groupId}`. */
340
- readonly value: string;
421
+ /**
422
+ * The range value as **Expression IR** (SCP IR Phase 2, issue #289): e.g.
423
+ * `"USER#"` (a bare literal) or `{concat:["USER#",{ref:["result","groupId"]}]}`.
424
+ * The runtime evaluates it with `evaluateExpression` — the same closed
425
+ * `concat`/`ref` vocabulary the key wiring uses (`key-expr.ts`). Replaces the
426
+ * pre-#289 `{param}` string template (dual-emitted and proven byte-identical
427
+ * through conformance before the template path was removed, P2-3).
428
+ *
429
+ * Optional only because the builders construct the raw range condition with
430
+ * `value` first and the `lowerOperationKeyExprs` pass fills `valueExpr` — every
431
+ * serialized range condition carries `valueExpr`.
432
+ */
433
+ readonly valueExpr?: ExpressionNode;
434
+ /**
435
+ * @deprecated Phase 2 dual-emit (issue #289 P2-3): the pre-#289 `{param}`
436
+ * string template (`GROUP#{result.groupId}`), emitted ALONGSIDE
437
+ * {@link valueExpr} only during the migration so the runtimes' Expression-IR
438
+ * reading can be proven byte-identical to the template render before the
439
+ * template path is deleted. Removed at the flip.
440
+ */
441
+ readonly value?: string;
341
442
  }
342
443
  /** Server-side declarative filter, carried verbatim for the runtime to compile. */
343
444
  interface FilterSpec {
@@ -354,10 +455,28 @@ interface OperationSpec {
354
455
  readonly tableName: string;
355
456
  readonly indexName?: string;
356
457
  /**
357
- * Key condition: attribute name template value. For a `BatchGetItem` this is
358
- * the per-key shape (one key per resolved source item at runtime).
458
+ * Key condition as **Expression IR** (SCP IR Phase 2, issue #289): attribute
459
+ * name an Expression IR node (`concat`/`ref`) that evaluates to the key
460
+ * value, e.g. `PK: {concat:["ARTICLE#",{ref:["input","articleId"]}]}`. For a
461
+ * `BatchGetItem` this is the per-key shape (one key per resolved source item).
462
+ * The runtime evaluates each node with `evaluateExpression` against the
463
+ * `{input, result}` scope — the universal Port-wiring vocabulary
464
+ * (scp-ir-architecture.md §5), lowered from the pre-#289 `{param}` string
465
+ * template by `key-expr.ts`.
466
+ *
467
+ * Optional only because the builders construct the raw op with `keyCondition`
468
+ * first and a single `lowerOperationKeyExprs` pass fills `keyExpr` from it
469
+ * (operations.ts) — every serialized op carries `keyExpr`.
470
+ */
471
+ readonly keyExpr?: Readonly<Record<string, ExpressionNode>>;
472
+ /**
473
+ * @deprecated Phase 2 dual-emit (issue #289 P2-3): the pre-#289 `{param}`
474
+ * string-template key condition (`ARTICLE#{articleId}`), emitted ALONGSIDE
475
+ * {@link keyExpr} only during the migration so the runtimes' Expression-IR key
476
+ * reading can be proven byte-identical to the template render before the
477
+ * template path is deleted. Removed at the flip.
359
478
  */
360
- readonly keyCondition: Readonly<Record<string, string>>;
479
+ readonly keyCondition?: Readonly<Record<string, string>>;
361
480
  readonly rangeCondition?: RangeConditionSpec;
362
481
  /** Projected fields (entity field names). */
363
482
  readonly projection: readonly string[];
@@ -412,6 +531,25 @@ interface SourceListSpec {
412
531
  * the query's select projects the attribute itself (it stays in the result).
413
532
  */
414
533
  readonly implicit?: boolean;
534
+ /**
535
+ * **SCP IR Phase 2 prep toward the universal Map node (issue #289 P2-5).** The
536
+ * shared IR expresses a fan-out as an SCP `map` node
537
+ * (`{ map: { over: <ref to the list>, as: "$el", component, ports } }`,
538
+ * scp-ir-architecture.md §5) — a single static node iterating a list Port. This
539
+ * `refs`/`sourceList` binding form is the DynamoDB-specific precursor of that
540
+ * Map. Phase 2 only PREPARES the migration (the full `map`-node emission +
541
+ * runtime expansion is Phase 3): {@link over} records the Expression-IR
542
+ * reference to the parent's list attribute the Map iterates
543
+ * (`{ ref: ["result", from] }`), and {@link as} records the per-element binding
544
+ * name (`"$<key>"`) the Phase-3 `map` body will reference. Both are ADDITIVE and
545
+ * DESCRIPTIVE only — the Phase-2 runtimes still execute the fan-out via
546
+ * {@link from}/{@link key} (unchanged behavior); Phase 3 lowers the body onto
547
+ * `over`/`as`. Carrying them now freezes the intended Map shape for review and
548
+ * lets a Phase-3 runtime resolve it without re-deriving.
549
+ */
550
+ readonly over?: ExpressionNode;
551
+ /** The Phase-3 Map element binding name (`"$<key>"`); see {@link over}. */
552
+ readonly as?: string;
415
553
  }
416
554
  /**
417
555
  * The **execution plan** for a multi-operation read (issue #70a). It is the
@@ -489,8 +627,15 @@ interface QuerySpec {
489
627
  /**
490
628
  * The Expression IR vocabulary version (behavior-contracts `expression-ir.md`
491
629
  * §9). A runtime handed an unknown `exprVersion` MUST fail closed.
630
+ *
631
+ * **SCP IR Phase 2 (#289, G6): bumped `1` → `2`.** graphddb now follows the
632
+ * behavior-contracts `exprVersion: 2` vocabulary (the v1→v2 change is the `obj`
633
+ * node `__proto__` `FORBIDDEN_KEY` fail-closed hardening — additive/backward
634
+ * compatible, `expression-ir.md` §9), closing the minor-version lag the migration
635
+ * plan flagged (§2 G6). Every embedded {@link ExpressionSpec} — guards, scpExpr
636
+ * conditions, and the new `keyExpr` key wiring — stamps `exprVersion: 2`.
492
637
  */
493
- declare const EXPR_VERSION: 1;
638
+ declare const EXPR_VERSION: 2;
494
639
  /**
495
640
  * The closed operator vocabulary of Expression IR v1 (expression-ir.md §3).
496
641
  * An operator node is a **single-key** object — key = operator name, value =
@@ -892,6 +1037,25 @@ interface TransactionSpec {
892
1037
  * DynamoDB ≤25 limit after expansion).
893
1038
  */
894
1039
  readonly maxItems?: number;
1040
+ /**
1041
+ * Marks a **BatchWriteItem** bulk form (issue #298): the spec is executed
1042
+ * NON-ATOMICALLY through DynamoDB `BatchWriteItem` — chunked ≤25 per request
1043
+ * with `UnprocessedItems` exponential retry, 1×WCU, no all-or-nothing
1044
+ * guarantee — instead of one atomic `TransactWriteItems`. Synthesized only by
1045
+ * the command-contract serializer for a `mode: 'batchWrite'` method (the
1046
+ * 0.9.0 default for an unconditional put / delete): a single `forEach`-over-
1047
+ * `items` write item whose element carries the FULL per-item payload (key +
1048
+ * input fields), so each item binds its own values.
1049
+ *
1050
+ * Fail-closed on both sides: the emitter only stamps this on a spec whose
1051
+ * every item is an **unconditional** `Put` / `Delete` `forEach` (DynamoDB's
1052
+ * `BatchWriteItem` carries no `ConditionExpression` and no `Update`), and the
1053
+ * runtimes' `executeTransaction` loud-rejects a marked spec (it must go
1054
+ * through `executeBatchWrite`) while `executeBatchWrite` loud-rejects an
1055
+ * unmarked one. Absent on every atomic transaction, so those serialize
1056
+ * byte-identically to the pre-#298 spec.
1057
+ */
1058
+ readonly batchWrite?: true;
895
1059
  }
896
1060
  /**
897
1061
  * Whether a contract is a public **read** (`'query'`) or **write** (`'command'`)
@@ -1084,6 +1248,20 @@ type CommandResolutionTarget =
1084
1248
  | {
1085
1249
  readonly mode: 'parallel';
1086
1250
  readonly operation: string;
1251
+ }
1252
+ /**
1253
+ * A non-atomic **BatchWriteItem** bulk write with per-item values (issue
1254
+ * #298; the 0.9.0 DEFAULT for an unconditional put / delete). References the
1255
+ * synthesized `<Contract>__<method>__batchWrite` {@link TransactionSpec} in
1256
+ * `transactions` (stamped {@link TransactionSpec.batchWrite}): a single
1257
+ * `forEach`-over-`items` write whose array element carries the FULL per-item
1258
+ * payload (key + input fields). The runtimes execute it through their
1259
+ * `BatchWriteItem` machinery — any array size, chunked ≤25 per request with
1260
+ * `UnprocessedItems` exponential retry, 1×WCU, no conditions, non-atomic.
1261
+ */
1262
+ | {
1263
+ readonly mode: 'batchWrite';
1264
+ readonly transaction: string;
1087
1265
  };
1088
1266
  /**
1089
1267
  * The serialized spec of one **command** contract method. Symmetric to
@@ -1170,6 +1348,50 @@ interface ContextSpec {
1170
1348
  /** Contract names owned by this context (sorted; `contracts` map keys). */
1171
1349
  readonly contracts: readonly string[];
1172
1350
  }
1351
+ /**
1352
+ * The PORTABLE form of a read operation — what `operations.json` actually
1353
+ * carries since SCP IR Phase 3 (#290 P3-5): the physical **root** op minus the
1354
+ * proprietary relation wiring (`resultPath` / `sourceField` / `sourceList`).
1355
+ * That wiring now exists ONLY inside the build pipeline ({@link OperationSpec},
1356
+ * feeding the `components[]` lowering and the TS AOT prepared plan) and never
1357
+ * reaches the portable IR — relation execution is expressed exclusively by the
1358
+ * universal SCP component graph (`components[]`).
1359
+ */
1360
+ type PortableOperationSpec = Omit<OperationSpec, 'resultPath' | 'sourceField' | 'sourceList'>;
1361
+ /**
1362
+ * The PORTABLE form of a query (#290 P3-5). `operations` carries exactly ONE
1363
+ * entry — the root physical op (the still-live single-op surfaces: the contract
1364
+ * key-array point-batch reads its `keyExpr` / `projection`; `explain` renders
1365
+ * it). Relation fan-out and staging live ONLY in `components[]`, so the old
1366
+ * relation operations and the `executionPlan` are not emitted.
1367
+ */
1368
+ interface PortableQuerySpec {
1369
+ readonly params: Readonly<Record<string, ParamSpec>>;
1370
+ /** Exactly one entry: the root physical operation. */
1371
+ readonly operations: readonly PortableOperationSpec[];
1372
+ /** See {@link QuerySpec.cardinality}. */
1373
+ readonly cardinality?: 'one' | 'many';
1374
+ /** See {@link QuerySpec.description}. */
1375
+ readonly description?: string;
1376
+ }
1377
+ /**
1378
+ * The PORTABLE form of a declarative transaction (#290 P3-5): only the input
1379
+ * contract (`params`, read by every runtime's `validate_params` surface). The
1380
+ * old templated item bodies (`items[]` with `forEach` / `{item.*}` templates)
1381
+ * are not emitted — the write plan lives ONLY in the transaction's
1382
+ * `components[]` entry, which the runtimes execute via `runBehavior`.
1383
+ */
1384
+ interface PortableTransactionSpec {
1385
+ readonly params: Readonly<Record<string, ParamSpec>>;
1386
+ /**
1387
+ * Marks the `BatchWriteItem` bulk form (issue #298 — see
1388
+ * {@link TransactionSpec.batchWrite}). Portable ON PURPOSE: it is the
1389
+ * runtimes' fail-closed dispatch signal (`executeBatchWrite` requires it,
1390
+ * `executeTransaction` rejects it) and the codegen signal for the per-item
1391
+ * `*BatchWrite` generated-method shape. Absent on every atomic transaction.
1392
+ */
1393
+ readonly batchWrite?: true;
1394
+ }
1173
1395
  interface OperationsDocument {
1174
1396
  /**
1175
1397
  * `1.1` unless the document actually contains an SCP Expression IR node (a
@@ -1177,10 +1399,18 @@ interface OperationsDocument {
1177
1399
  * (issue #261, conditional stamping — see {@link SPEC_VERSION_SCP}).
1178
1400
  */
1179
1401
  readonly version: SpecVersion;
1180
- readonly queries: Readonly<Record<string, QuerySpec>>;
1402
+ /**
1403
+ * Portable query specs (#290 P3-5): the root physical op + params only; the
1404
+ * relation wiring lives in {@link components}.
1405
+ */
1406
+ readonly queries: Readonly<Record<string, PortableQuerySpec>>;
1181
1407
  readonly commands: Readonly<Record<string, CommandSpec>>;
1182
- /** Declarative transactions (issue #46). Absent in pre-#46 specs. */
1183
- readonly transactions?: Readonly<Record<string, TransactionSpec>>;
1408
+ /**
1409
+ * Declarative transactions (issue #46). Absent in pre-#46 specs. Since #290
1410
+ * P3-5 only the params contract is emitted; the write plan lives in
1411
+ * {@link components}.
1412
+ */
1413
+ readonly transactions?: Readonly<Record<string, PortableTransactionSpec>>;
1184
1414
  /**
1185
1415
  * The CQRS Contract layer (issue #59): contract name → {@link ContractSpec}.
1186
1416
  * Layered **on top of** the existing `queries` / `commands` / `transactions`
@@ -1195,6 +1425,18 @@ interface OperationsDocument {
1195
1425
  * foreign. **Absent** when no contexts are declared (backward compatibility).
1196
1426
  */
1197
1427
  readonly contexts?: Readonly<Record<string, ContextSpec>>;
1428
+ /**
1429
+ * The universal SCP **component-graph** (issue #290, Phase 3). The queries /
1430
+ * transactions lifted into behavior-contracts `Component[]` (name /
1431
+ * inputPorts / body / output / plan, `scp-ir-architecture.md` §5). Since the
1432
+ * P3-5 flip this is the ONLY carrier of relation fan-out and transaction
1433
+ * write plans in the portable IR — every spec-consumer runtime
1434
+ * (py/php/rust/go) executes reads and transactions via `runBehavior` over
1435
+ * this graph, and the typed-binding / docs generators reconstruct the result
1436
+ * shape from it. **Absent** when the input defines no queries or
1437
+ * transactions (backward compatible).
1438
+ */
1439
+ readonly components?: readonly Component[];
1198
1440
  }
1199
1441
  /** The full `{ manifest, operations }` bundle produced by the static planner. */
1200
1442
  interface BridgeBundle {
@@ -1202,4 +1444,4 @@ interface BridgeBundle {
1202
1444
  readonly operations: OperationsDocument;
1203
1445
  }
1204
1446
 
1205
- export { type LiteralParam as $, type ManifestEntity as A, type BridgeBundle as B, type ContractSpec as C, type ManifestField as D, type ExpressionSpec as E, type FilterSpec as F, type ManifestFieldType as G, type ManifestGsi as H, type ManifestKey as I, type ManifestRelation as J, type ManifestTable as K, type OperationSpec as L, type Manifest as M, type QueryContractMethodSpec as N, type OperationsDocument as O, type ParamSpec as P, type QuerySpec as Q, type RangeConditionSpec as R, SPEC_VERSION as S, type TransactionSpec as T, type ReadOperationType as U, SPEC_VERSION_SCP as V, SPEC_VERSION_SUPPORTED as W, type TransactionItemSpec as X, type TransactionItemType as Y, type WhenSpec as Z, type WriteOperationType as _, type CommandSpec as a, type NumberParam as a0, type Param as a1, type ParamKind as a2, type StringParam as a3, param as a4, type ContextSpec as b, type ConditionSpec as c, type SpecVersion as d, type CommandContractMethodSpec as e, type CommandResolutionTarget as f, type ComposeSpec as g, type CompositionPlanSpec as h, type ContractCardinality as i, type ContractCommandResult as j, type ContractInputArity as k, type ContractKeySpec as l, type ContractKind as m, type ContractResolution as n, EXPR_VERSION as o, type ExecutionPlanSpec as p, type ExpressionArrNode as q, type ExpressionFloatNode as r, type ExpressionIntNode as s, type ExpressionNode as t, type ExpressionObjNode as u, type ExpressionOpNode as v, type ExpressionOperator as w, type ExpressionRefNode as x, type ExpressionRefOptNode as y, type ExpressionScalar as z };
1447
+ export { type WriteOperationType as $, type ManifestEntity as A, type BridgeBundle as B, type ContractSpec as C, type ManifestField as D, type ExpressionSpec as E, type FilterSpec as F, type ManifestFieldType as G, type ManifestGsi as H, type ManifestKey as I, type ManifestRelation as J, type ManifestTable as K, type OperationSpec as L, type Manifest as M, type QueryContractMethodSpec as N, type OperationsDocument as O, type ParamSpec as P, type QuerySpec as Q, type RangeConditionSpec as R, SPEC_VERSION_KEY_EXPR as S, type TransactionSpec as T, type ReadOperationType as U, SPEC_VERSION as V, SPEC_VERSION_SCP as W, SPEC_VERSION_SUPPORTED as X, type TransactionItemSpec as Y, type TransactionItemType as Z, type WhenSpec as _, type CommandSpec as a, type LiteralParam as a0, type MapParam as a1, type NumberParam as a2, type Param as a3, type ParamKind as a4, type StringParam as a5, param as a6, type ContextSpec as b, type ConditionSpec as c, type SpecVersion as d, type CommandContractMethodSpec as e, type CommandResolutionTarget as f, type ComposeSpec as g, type CompositionPlanSpec as h, type ContractCardinality as i, type ContractCommandResult as j, type ContractInputArity as k, type ContractKeySpec as l, type ContractKind as m, type ContractResolution as n, EXPR_VERSION as o, type ExecutionPlanSpec as p, type ExpressionArrNode as q, type ExpressionFloatNode as r, type ExpressionIntNode as s, type ExpressionNode as t, type ExpressionObjNode as u, type ExpressionOpNode as v, type ExpressionOperator as w, type ExpressionRefNode as x, type ExpressionRefOptNode as y, type ExpressionScalar as z };
@@ -263,6 +263,52 @@ addMany: { create: GroupMembership, key: { groupId: param.string(), userId: para
263
263
  `GraphQL contrast`: `mode: 'transaction'` is atomic all-or-nothing; `mode:
264
264
  'parallel'` is non-atomic per-field partial success.
265
265
 
266
+ ### Composite (multi-entity atomic) methods — the `writes` body
267
+
268
+ A single write descriptor covers one entity. A **composite** method writes
269
+ **multiple entities atomically** — the canonical case being a cross-entity write
270
+ that has **no relation to fall back on** (e.g. marking a hash-keyed aggregate node
271
+ `dirty` alongside a chunk's terminal update). Author it with the **`writes` body**
272
+ form:
273
+
274
+ ```ts
275
+ export const ChunkCompleteCommands = graphddb.publishCommand({
276
+ complete: {
277
+ // the public input schema — names + types the params the body references
278
+ input: { chunkId: param.string(), nodeId: param.string() },
279
+ // the mutation body: `$ => ({ alias: descriptor, … })`; `$` refers to the inputs
280
+ writes: ($) => ({
281
+ chunk: { update: Chunk, key: { chunkId: $.chunkId }, input: { done: true } },
282
+ agg: { update: AggNode, key: { nodeId: $.nodeId }, input: { dirty: true } },
283
+ }),
284
+ // optional read-back projection (keyed on the primary — first — fragment)
285
+ result: { select: { chunkId: true, done: true } },
286
+ },
287
+ });
288
+ ```
289
+
290
+ - `writes` is a body `$ => ({ alias: writeDescriptor, … })`. Each alias value is a
291
+ write descriptor (`{ create | update | remove | upsert: Model, key, input?,
292
+ condition? }` — the same shape as a single-descriptor method, minus `result` /
293
+ `mode`, which are declared on the composite as a whole). The `$` proxy fields are
294
+ the input fields declared in the separate **`input`** schema; a later descriptor
295
+ may reference an earlier alias's written field via `$.alias.field`.
296
+ - **Single-alias** `writes` ⇒ one write. **Multi-alias** ⇒ an **atomic composite**:
297
+ it is promoted to a single `TransactWriteItems` by DynamoDB necessity (there is no
298
+ non-atomic multi-item form for a cross-entity write), so no `mode` ceremony is
299
+ needed — a multi-alias composite is always atomic.
300
+ - `input` (the public param schema), `result` (read-back), and `mode` are specified
301
+ **alongside** the body, never baked into it.
302
+ - **No `mutation(...)` / `defineScpTransaction(...)` import is needed** —
303
+ `publishCommand` compiles the `writes` body internally. The emitted
304
+ `operations.json` wire is byte-identical to the equivalent internal composition.
305
+ - The single-descriptor form above stays the sugar for a single-entity write; the
306
+ `writes` body is the additive form for the multi-entity atomic case.
307
+
308
+ > The full **SCP native-syntax** composite body (imperative `$`/`?:`/`&&`/`.map`
309
+ > transaction sources) is a separate surface, deferred to a later release; the
310
+ > `writes` descriptor-map body above is the supported public composite form today.
311
+
266
312
  #### The 25-item limit is a hard error, never a silent split
267
313
 
268
314
  `TransactWriteItems` is capped at **25 items** and is atomic. A
@@ -498,15 +544,20 @@ Definition DSL (`src/define/contract.ts`) — import the `graphddb` namespace
498
544
 
499
545
  ```ts
500
546
  graphddb.publishQuery({ alias: { query | list: Model, key, select, options? } })
547
+ // single-entity write
501
548
  graphddb.publishCommand({ alias: { create | update | remove: Model, key, input, condition?, result?, use?, mode? } })
549
+ // multi-entity atomic (composite) write — the `writes` body form (#293)
550
+ graphddb.publishCommand({ alias: { input, writes: $ => ({ alias: descriptor, … }), result?, mode? } })
502
551
  param.string() / param.number() / ... // descriptor key/input parameters
503
552
  ```
504
553
 
505
554
  A query method may compose another query contract's method (cross-fragment refs
506
- via `$.alias.field`); a command descriptor composes its derived write ops and
507
- maintenance writes into one plan. These are declared inside the
508
- `publishQuery` / `publishCommand` map there is no separate public `query()` /
509
- `mutation()` composite verb (removed in 0.8.0).
555
+ via `$.alias.field`); a command method composes its derived write ops and
556
+ maintenance writes into one plan, and a **composite** command method's `writes`
557
+ body composes a multi-entity atomic write Composite methods). These are all
558
+ declared inside the `publishQuery` / `publishCommand` map there is no separate
559
+ public `query()` / `mutation()` composite verb (removed in 0.8.0; the composite
560
+ `writes` body is compiled internally, needing no import).
510
561
 
511
562
  Execution is the two `graphddb.*` verbs (`graphddb.query` / `graphddb.mutate`) —
512
563
  they route contract methods through the in-process runtime.