graphddb 0.8.1 → 0.9.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.
@@ -2,7 +2,7 @@ import {
2
2
  PREPARED_FORMAT_VERSION,
3
3
  buildManifest,
4
4
  entityFingerprint
5
- } from "./chunk-S2NI4PBW.js";
5
+ } from "./chunk-PHXUFAY2.js";
6
6
  import {
7
7
  analyzeWriteRoute,
8
8
  assertBundleSerializable,
@@ -21,33 +21,96 @@ import {
21
21
  isContractKeyRef,
22
22
  isContractParamRef,
23
23
  isQueryModelContract
24
- } from "./chunk-T44OB5GU.js";
24
+ } from "./chunk-HLFNCKFV.js";
25
25
  import {
26
26
  detectRelationFields,
27
27
  isInlineSnapshotSpec,
28
28
  normalizeSelectSpec
29
- } from "./chunk-HNY2EJPV.js";
29
+ } from "./chunk-7OCXY4R6.js";
30
30
  import {
31
+ EXPR_VERSION,
31
32
  MARKER_ROW_ENTITY,
32
- SPEC_VERSION,
33
+ SPEC_VERSION_KEY_EXPR,
33
34
  operationsSpecVersion
34
- } from "./chunk-L4QRCHRQ.js";
35
+ } from "./chunk-WOFRHRXY.js";
35
36
  import {
36
37
  BATCH_GET_MAX_KEYS,
37
38
  CONDITION_OPERATOR_KEYS,
39
+ GRAPHDDB_CATALOG,
38
40
  MAX_TRANSACT_ITEMS,
39
41
  MetadataRegistry,
42
+ assertComponentsInCatalog,
43
+ assertPublishSplitMatchesDerived,
44
+ deriveComponentEffects,
40
45
  isParam,
41
46
  isRawCondition,
42
47
  param,
43
48
  resolveModelClass
44
- } from "./chunk-L2NEDS7U.js";
49
+ } from "./chunk-GWWRXIHF.js";
45
50
  import {
46
51
  TableMapping,
47
52
  isColumn,
48
53
  resolveKey
49
54
  } from "./chunk-XTWXMOHD.js";
50
55
 
56
+ // src/spec/key-expr.ts
57
+ var PLACEHOLDER = /\{([^{}]+)\}/g;
58
+ var RESULT_PREFIX = "result.";
59
+ var ITEM_PREFIX = "item.";
60
+ function tokenRef(token, template) {
61
+ if (token.startsWith(ITEM_PREFIX)) {
62
+ throw new Error(
63
+ `lowerKeyTemplate: read-path key template '${template}' references an '${token}' (item/Map-element) binding, which is a Phase 3 write-path token, not a Phase 2 read-path key wiring. Refusing to lower it.`
64
+ );
65
+ }
66
+ if (token.startsWith(RESULT_PREFIX)) {
67
+ return { ref: ["result", token.slice(RESULT_PREFIX.length)] };
68
+ }
69
+ return { ref: ["input", token] };
70
+ }
71
+ function lowerKeyTemplate(template) {
72
+ const parts = [];
73
+ let last = 0;
74
+ PLACEHOLDER.lastIndex = 0;
75
+ for (let m = PLACEHOLDER.exec(template); m !== null; m = PLACEHOLDER.exec(template)) {
76
+ const literal = template.slice(last, m.index);
77
+ if (literal !== "") parts.push(literal);
78
+ parts.push(tokenRef(m[1], template));
79
+ last = m.index + m[0].length;
80
+ }
81
+ const tail = template.slice(last);
82
+ if (tail !== "") parts.push(tail);
83
+ if (parts.length === 0) return "";
84
+ if (parts.length === 1) return parts[0];
85
+ return { concat: parts };
86
+ }
87
+ function lowerKeyCondition(keyCondition) {
88
+ const out = {};
89
+ for (const [attr, template] of Object.entries(keyCondition)) {
90
+ out[attr] = lowerKeyTemplate(template);
91
+ }
92
+ return out;
93
+ }
94
+ function renderKeyExprToTemplate(node) {
95
+ if (typeof node === "string") return node;
96
+ if (node !== null && typeof node === "object") {
97
+ const obj = node;
98
+ if (Array.isArray(obj.ref)) return refToToken(obj.ref);
99
+ if (Array.isArray(obj.concat)) {
100
+ return obj.concat.map(renderKeyExprToTemplate).join("");
101
+ }
102
+ }
103
+ throw new Error(
104
+ `renderKeyExprToTemplate: node is not a key-wiring expression (string literal / ref / concat): ${JSON.stringify(node)}`
105
+ );
106
+ }
107
+ function refToToken(path) {
108
+ const [root, ...rest] = path;
109
+ if (root === "result") return `{result.${rest.join(".")}}`;
110
+ if (root === "input") return `{${rest.join(".")}}`;
111
+ throw new Error(`renderKeyExprToTemplate: unexpected ref root '${root}' in key wiring`);
112
+ }
113
+
51
114
  // src/spec/contract-n1-check.ts
52
115
  function isQueryMethodSpec(spec) {
53
116
  return typeof spec.resolution === "string" && spec.resolution !== void 0;
@@ -133,7 +196,9 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
133
196
 
134
197
  // src/spec/contracts.ts
135
198
  function kindForDynamoType(dynamoType) {
136
- return dynamoType === "N" ? "number" : "string";
199
+ if (dynamoType === "N") return "number";
200
+ if (dynamoType === "M") return "map";
201
+ return "string";
137
202
  }
138
203
  function recoverKind(metadata, fieldName) {
139
204
  const field = metadata.fields.find((f) => f.propertyName === fieldName);
@@ -167,6 +232,7 @@ function opToDefinition(contractName, methodName, op) {
167
232
  }
168
233
  const select = op.select !== void 0 ? assertBooleanProjection(op.select, `${contractName}.${methodName} select`) : void 0;
169
234
  const changes = op.changes !== void 0 ? convertStructure(op.changes, place, `${contractName}.${methodName} changes`) : void 0;
235
+ const add = op.add !== void 0 ? convertStructure(op.add, place, `${contractName}.${methodName} add`) : void 0;
170
236
  const condition = op.condition !== void 0 ? convertCondition(op.condition, place, `${contractName}.${methodName} condition`) : void 0;
171
237
  return {
172
238
  __isOperationDefinition: true,
@@ -175,6 +241,7 @@ function opToDefinition(contractName, methodName, op) {
175
241
  key,
176
242
  select,
177
243
  changes,
244
+ ...add !== void 0 ? { add } : {},
178
245
  ...condition !== void 0 ? { condition } : {},
179
246
  // #154: the use-case description captured on the recorded op (from the
180
247
  // descriptor's `description`) flows into the flattened `queries[op]` /
@@ -185,6 +252,11 @@ function opToDefinition(contractName, methodName, op) {
185
252
  };
186
253
  }
187
254
  function paramOfKind(recovered) {
255
+ if (recovered.kind === "map") {
256
+ throw new Error(
257
+ `A \`@map\` field cannot bind a key-position param: a DynamoDB key is composed of scalar segments, but this key field's model storage type is 'M' (issue #295). Bind map values at a value position (\`input\`) instead.`
258
+ );
259
+ }
188
260
  if (recovered.kind === "number") return param.number();
189
261
  if (recovered.kind === "literal" && recovered.literals !== void 0 && recovered.literals.length > 0) {
190
262
  return param.literal(
@@ -402,6 +474,13 @@ function composeSpecOf(node, contractName, methodName, contracts) {
402
474
  `Contract '${contractName}.${methodName}': composed child '${c.as}' must declare a \`resolution\` of 'point' or 'range'.`
403
475
  );
404
476
  }
477
+ const target = contracts[c.contract];
478
+ if (target !== void 0 && isQueryModelContract(target)) {
479
+ const targetMethod = target.methods[c.method];
480
+ if (targetMethod !== void 0) {
481
+ assertComposeTargetNotNested(contractName, methodName, c.as, c.contract, c.method, targetMethod.op);
482
+ }
483
+ }
405
484
  const bindIn = c.bind ?? {};
406
485
  const bind = {};
407
486
  for (const field of Object.keys(bindIn).sort()) {
@@ -435,6 +514,7 @@ function recordedComposeSpec(node, contractName, methodName, contracts) {
435
514
  contracts
436
515
  );
437
516
  const refMethod = node.method;
517
+ assertComposeTargetNotNested(contractName, methodName, node.as, refName, refMethodName, refMethod.op);
438
518
  const bind = {};
439
519
  for (const field of Object.keys(node.bind).sort()) {
440
520
  bind[field] = node.bind[field].path;
@@ -470,6 +550,12 @@ function composeOf(op) {
470
550
  const raw = op.compose;
471
551
  return Array.isArray(raw) ? raw : [];
472
552
  }
553
+ function assertComposeTargetNotNested(contractName, methodName, as, targetContract, targetMethod, targetOp) {
554
+ if (targetOp === void 0 || composeOf(targetOp).length === 0) return;
555
+ throw new Error(
556
+ `Contract '${contractName}.${methodName}': composed child '${as}' references '${targetContract}.${targetMethod}', which itself declares a composition (nested compose). No runtime resolves a composed child's own compose edges, so '${targetContract}.${targetMethod}'s compose field(s) would never be populated through '${contractName}.${methodName}' \u2014 the generated result type would over-promise. Compose the grandchild directly from '${contractName}.${methodName}' instead (issue #256).`
557
+ );
558
+ }
473
559
  function compositionPlanOf(compose) {
474
560
  if (compose.length === 0) return void 0;
475
561
  const { stages, concurrency } = deriveCompositionPlan(compose.length);
@@ -482,6 +568,9 @@ function compositionPlanOf(compose) {
482
568
  function batchTxName(contractName, methodName) {
483
569
  return `${opRefName(contractName, methodName)}__batch`;
484
570
  }
571
+ function batchWriteTxName(contractName, methodName) {
572
+ return `${opRefName(contractName, methodName)}__batchWrite`;
573
+ }
485
574
  function keyParamNames(op) {
486
575
  return keyFieldsOf(op);
487
576
  }
@@ -497,6 +586,14 @@ function toElementTemplate(template, keyParams) {
497
586
  );
498
587
  }
499
588
  function toElementRecord(record, keyParams) {
589
+ const out = {};
590
+ for (const field of Object.keys(record)) {
591
+ const value = record[field];
592
+ out[field] = typeof value === "string" ? toElementTemplate(value, keyParams) : value;
593
+ }
594
+ return out;
595
+ }
596
+ function toElementKeyRecord(record, keyParams) {
500
597
  const out = {};
501
598
  for (const field of Object.keys(record)) {
502
599
  out[field] = toElementTemplate(record[field], keyParams);
@@ -549,13 +646,51 @@ function synthesizeBatchTransaction(commandSpec, op) {
549
646
  tableName: commandSpec.tableName,
550
647
  entity: commandSpec.entity,
551
648
  ...commandSpec.item !== void 0 ? { item: toElementRecord(commandSpec.item, keyParams) } : {},
552
- ...commandSpec.keyCondition !== void 0 ? { keyCondition: toElementRecord(commandSpec.keyCondition, keyParams) } : {},
649
+ ...commandSpec.keyCondition !== void 0 ? { keyCondition: toElementKeyRecord(commandSpec.keyCondition, keyParams) } : {},
553
650
  ...commandSpec.changes !== void 0 ? { changes: toElementRecord(commandSpec.changes, keyParams) } : {},
554
651
  ...condition !== void 0 ? { condition } : {},
555
652
  forEach: { source: "keys" }
556
653
  };
557
654
  return { params, items: [item] };
558
655
  }
656
+ function synthesizeBatchWriteTransaction(contractName, methodName, commandSpec) {
657
+ const at = `Contract '${contractName}.${methodName}'`;
658
+ if (commandSpec.type === "UpdateItem") {
659
+ throw new Error(
660
+ `${at}: mode 'batchWrite' cannot express an update \u2014 DynamoDB BatchWriteItem carries only PutRequest / DeleteRequest (internal invariant; the authoring layer should have rejected this).`
661
+ );
662
+ }
663
+ if (commandSpec.condition !== void 0) {
664
+ throw new Error(
665
+ `${at}: mode 'batchWrite' cannot carry a write condition \u2014 DynamoDB BatchWriteItem has no ConditionExpression (internal invariant; the authoring layer should have rejected this).`
666
+ );
667
+ }
668
+ const elementFields = {};
669
+ for (const name of Object.keys(commandSpec.params).sort()) {
670
+ elementFields[name] = commandSpec.params[name];
671
+ }
672
+ const params = {
673
+ items: { type: "array", required: true, element: elementFields }
674
+ };
675
+ const allParams = new Set(Object.keys(commandSpec.params));
676
+ const toElement = (record) => {
677
+ const out = {};
678
+ for (const field of Object.keys(record)) {
679
+ const v = record[field];
680
+ out[field] = typeof v === "string" ? toElementTemplate(v, allParams) : v;
681
+ }
682
+ return out;
683
+ };
684
+ const item = {
685
+ type: TX_ITEM_TYPE[commandSpec.type],
686
+ tableName: commandSpec.tableName,
687
+ entity: commandSpec.entity,
688
+ ...commandSpec.item !== void 0 ? { item: toElement(commandSpec.item) } : {},
689
+ ...commandSpec.keyCondition !== void 0 ? { keyCondition: toElement(commandSpec.keyCondition) } : {},
690
+ forEach: { source: "items" }
691
+ };
692
+ return { params, items: [item], batchWrite: true };
693
+ }
559
694
  function composeFragmentTransaction(contractName, methodName, ops) {
560
695
  const items = [];
561
696
  const params = {};
@@ -704,8 +839,9 @@ function buildDerivedUpdateItems(derivedUpdates) {
704
839
  tableName,
705
840
  entity: update.entity.name,
706
841
  keyCondition,
707
- // The delta is a compile-time constant → a literal numeric template (no param).
708
- add: { [update.attribute]: String(update.amount) }
842
+ // The delta is a compile-time constant → a TYPED numeric literal (no param),
843
+ // carried verbatim so it marshals as `N`, not a stringified `S "1"` (#299).
844
+ add: { [update.attribute]: update.amount }
709
845
  });
710
846
  collectTemplateParams(keyCondition, metadata, params);
711
847
  }
@@ -765,7 +901,8 @@ function buildMaintainWriteItems(maintainWrites) {
765
901
  ...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
766
902
  counter: {
767
903
  attribute: maintain.counter.attribute,
768
- delta: String(maintain.counter.delta)
904
+ // TYPED numeric delta (#299): carried verbatim so it marshals as `N`.
905
+ delta: maintain.counter.delta
769
906
  }
770
907
  } : {}
771
908
  };
@@ -923,6 +1060,15 @@ function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName,
923
1060
  if (mode === "parallel") {
924
1061
  return { mode: "parallel", operation: opName };
925
1062
  }
1063
+ if (mode === "batchWrite") {
1064
+ const txName2 = batchWriteTxName(contractName, methodName);
1065
+ transactionSpecs[txName2] = synthesizeBatchWriteTransaction(
1066
+ contractName,
1067
+ methodName,
1068
+ commandSpec
1069
+ );
1070
+ return { mode: "batchWrite", transaction: txName2 };
1071
+ }
926
1072
  const txName = batchTxName(contractName, methodName);
927
1073
  transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
928
1074
  return { mode: "transaction", transaction: txName };
@@ -1138,6 +1284,11 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
1138
1284
  returnSelection
1139
1285
  );
1140
1286
  }
1287
+ if (method.mode === "batchWrite" && promotedToTransaction) {
1288
+ throw new Error(
1289
+ `Contract '${contractName}.${methodName}': mode 'batchWrite' on a method promoted to an atomic TransactWriteItems (derived effects / multi-fragment) \u2014 BatchWriteItem cannot compose those items (internal invariant).`
1290
+ );
1291
+ }
1141
1292
  const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
1142
1293
  method.mode,
1143
1294
  op,
@@ -1314,6 +1465,417 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
1314
1465
  throw new Error(first.message + suffix);
1315
1466
  }
1316
1467
 
1468
+ // src/spec/components.ts
1469
+ import {
1470
+ buildComponentDefinition
1471
+ } from "behavior-contracts";
1472
+ function registerComponent(name, inputPorts, body, output, plan) {
1473
+ return buildComponentDefinition(
1474
+ { name, inputPorts, body, output, plan },
1475
+ { catalog: GRAPHDDB_CATALOG }
1476
+ );
1477
+ }
1478
+ function remapRefRoots(node, resultRoot, itemRoot) {
1479
+ if (node === null || typeof node !== "object") return node;
1480
+ if (Array.isArray(node)) {
1481
+ return node.map((e) => remapRefRoots(e, resultRoot, itemRoot));
1482
+ }
1483
+ const obj = node;
1484
+ const keys = Object.keys(obj);
1485
+ if (keys.length === 1) {
1486
+ const key = keys[0];
1487
+ const arg = obj[key];
1488
+ if (key === "ref" || key === "refOpt") {
1489
+ const path = arg;
1490
+ const [root, ...rest] = path;
1491
+ const remapped = root === "input" ? rest : root === "result" ? [...resultRoot, ...rest] : root === "item" && itemRoot !== void 0 ? [...itemRoot, ...rest] : path;
1492
+ return { [key]: remapped };
1493
+ }
1494
+ if (key === "int" || key === "float") return node;
1495
+ if (key === "obj") {
1496
+ const out = {};
1497
+ for (const [k, v] of Object.entries(arg)) {
1498
+ out[k] = remapRefRoots(v, resultRoot, itemRoot);
1499
+ }
1500
+ return { obj: out };
1501
+ }
1502
+ if (Array.isArray(arg)) {
1503
+ return {
1504
+ [key]: arg.map((e) => remapRefRoots(e, resultRoot, itemRoot))
1505
+ };
1506
+ }
1507
+ return {
1508
+ [key]: remapRefRoots(arg, resultRoot, itemRoot)
1509
+ };
1510
+ }
1511
+ return node;
1512
+ }
1513
+ function stableJson(value) {
1514
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1515
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1516
+ const keys = Object.keys(value).sort();
1517
+ const body = keys.map((k) => `${JSON.stringify(k)}:${stableJson(value[k])}`).join(",");
1518
+ return `{${body}}`;
1519
+ }
1520
+ function collectParamLeaves(tree, out) {
1521
+ if (tree === null || typeof tree !== "object") return;
1522
+ if (Array.isArray(tree)) {
1523
+ for (const e of tree) collectParamLeaves(e, out);
1524
+ return;
1525
+ }
1526
+ const rec = tree;
1527
+ if (typeof rec.$param === "string" && Object.keys(rec).length === 1) {
1528
+ out.add(rec.$param);
1529
+ return;
1530
+ }
1531
+ for (const v of Object.values(rec)) collectParamLeaves(v, out);
1532
+ }
1533
+ function pathToId(resultPath) {
1534
+ if (resultPath === "$") return "root";
1535
+ return resultPath.startsWith("$.") ? resultPath.slice(2) : resultPath;
1536
+ }
1537
+ function parentPath(op) {
1538
+ const tokens = op.resultPath.split(".");
1539
+ const drop = op.resultPath.endsWith(".items") ? 2 : 1;
1540
+ const p = tokens.slice(0, tokens.length - drop).join(".");
1541
+ return p === "" ? "$" : p;
1542
+ }
1543
+ function relationProp(op) {
1544
+ const tokens = op.resultPath.split(".");
1545
+ const idx = op.resultPath.endsWith(".items") ? tokens.length - 2 : tokens.length - 1;
1546
+ return tokens[idx];
1547
+ }
1548
+ function relationKindOf(op) {
1549
+ return op.resultPath.endsWith(".items") ? "connection" : "single";
1550
+ }
1551
+ function readPorts(op, resultRoot) {
1552
+ const ports = { table: op.tableName };
1553
+ if (op.indexName !== void 0) ports.indexName = op.indexName;
1554
+ for (const [attr, node] of Object.entries(op.keyExpr ?? {})) {
1555
+ ports[attr] = remapRefRoots(node, resultRoot);
1556
+ }
1557
+ if (op.rangeCondition !== void 0) {
1558
+ ports.rangeKey = op.rangeCondition.key;
1559
+ ports.rangeOperator = op.rangeCondition.operator;
1560
+ ports.rangeValue = remapRefRoots(
1561
+ op.rangeCondition.valueExpr ?? "",
1562
+ resultRoot
1563
+ );
1564
+ }
1565
+ if (op.limit !== void 0) ports.limit = op.limit;
1566
+ if (op.filter !== void 0) ports.filter = stableJson(op.filter.declarative);
1567
+ ports.projection = { arr: [...op.projection] };
1568
+ return ports;
1569
+ }
1570
+ function buildQueryComponent(name, query) {
1571
+ const ops = query.operations;
1572
+ const idByPath = /* @__PURE__ */ new Map();
1573
+ const opByPath = /* @__PURE__ */ new Map();
1574
+ for (const op of ops) {
1575
+ idByPath.set(op.resultPath, pathToId(op.resultPath));
1576
+ opByPath.set(op.resultPath, op);
1577
+ }
1578
+ const lineages = /* @__PURE__ */ new Map();
1579
+ const elementInfo = /* @__PURE__ */ new Map();
1580
+ const body = ops.map((op) => {
1581
+ const id = pathToId(op.resultPath);
1582
+ if (op.resultPath === "$") {
1583
+ const ports = readPorts(op, []);
1584
+ if (query.cardinality === "one" && op.type === "Query") ports.cardinality = "one";
1585
+ const node2 = { id, component: op.type, ports };
1586
+ return node2;
1587
+ }
1588
+ const reject = (kind) => {
1589
+ throw new Error(`buildQueryComponent('${name}'): op '${op.resultPath}': ${kind}`);
1590
+ };
1591
+ const pp = parentPath(op);
1592
+ const producerId = idByPath.get(pp) ?? reject(`no producer op at '${pp}'`);
1593
+ if (op.sourceList !== void 0) {
1594
+ if (pp !== "$") reject("nested sourceList not expressible (v2 map)");
1595
+ const as = op.sourceList.as ?? `$${op.sourceList.key}`;
1596
+ const over = remapRefRoots(
1597
+ op.sourceList.over ?? { ref: ["result", op.sourceList.from] },
1598
+ [producerId]
1599
+ );
1600
+ const ports = readPorts(op, [as]);
1601
+ if (op.sourceList.implicit === true) ports.implicitSourceField = op.sourceList.from;
1602
+ const node2 = {
1603
+ id,
1604
+ map: {
1605
+ over,
1606
+ as,
1607
+ component: op.type,
1608
+ ports,
1609
+ parent: producerId,
1610
+ relationKind: relationKindOf(op),
1611
+ batched: true
1612
+ }
1613
+ };
1614
+ lineages.set(op.resultPath, { tailExpr: { ref: [id] }, tailId: id });
1615
+ return node2;
1616
+ }
1617
+ const producerOp = opByPath.get(pp);
1618
+ const producerIsConnection = producerOp !== void 0 && pp.endsWith(".items");
1619
+ const producerElement = elementInfo.get(pp);
1620
+ if (producerIsConnection || producerElement !== void 0) {
1621
+ const lineageKey = producerElement?.lineageKey ?? pp;
1622
+ const lineage = producerElement !== void 0 && pp.endsWith(".items") ? reject("nested per-element connection not expressible (v2 map)") : lineages.get(lineageKey) ?? reject(`no element chain for '${lineageKey}'`);
1623
+ const as = "$el";
1624
+ const attach = producerElement?.attachSeg ?? [];
1625
+ const into = relationProp(op);
1626
+ const node2 = {
1627
+ id,
1628
+ map: {
1629
+ over: lineage.tailExpr,
1630
+ as,
1631
+ component: op.type,
1632
+ ports: readPorts(op, [as, ...attach]),
1633
+ parent: lineage.tailId,
1634
+ relationKind: relationKindOf(op),
1635
+ into,
1636
+ // A belongsTo child the old path resolves via ONE deduped BatchGetItem
1637
+ // (gap 6) is batched; a per-element Query stays per-element.
1638
+ ...op.type === "BatchGetItem" ? { batched: true } : {}
1639
+ }
1640
+ };
1641
+ lineages.set(lineageKey, { tailExpr: { ref: [id] }, tailId: id });
1642
+ elementInfo.set(op.resultPath, { lineageKey, attachSeg: [into] });
1643
+ return node2;
1644
+ }
1645
+ const node = {
1646
+ id,
1647
+ component: op.type,
1648
+ ports: readPorts(op, [producerId]),
1649
+ parent: producerId,
1650
+ ...op.sourceField !== void 0 ? { bindField: op.sourceField } : {},
1651
+ relationKind: relationKindOf(op)
1652
+ };
1653
+ if (relationKindOf(op) === "connection") {
1654
+ lineages.set(op.resultPath, { tailExpr: { ref: [id, "items"] }, tailId: id });
1655
+ }
1656
+ return node;
1657
+ });
1658
+ const relOps = ops.filter((op) => op.resultPath !== "$");
1659
+ let output;
1660
+ if (relOps.length === 0) {
1661
+ output = { ref: ["root"] };
1662
+ } else {
1663
+ const outputObj = { root: { ref: ["root"] } };
1664
+ for (const op of relOps) {
1665
+ const id = pathToId(op.resultPath);
1666
+ if (elementInfo.has(op.resultPath)) continue;
1667
+ const lineage = lineages.get(op.resultPath);
1668
+ const key = parentPath(op) === "$" ? relationProp(op) : id;
1669
+ if (lineage !== void 0 && lineage.tailId !== id && relationKindOf(op) === "connection" && op.sourceList === void 0) {
1670
+ outputObj[key] = {
1671
+ obj: { cursor: { ref: [id, "cursor"] }, items: lineage.tailExpr }
1672
+ };
1673
+ } else {
1674
+ outputObj[key] = { ref: [id] };
1675
+ }
1676
+ }
1677
+ output = { obj: outputObj };
1678
+ }
1679
+ let plan;
1680
+ if (query.executionPlan) {
1681
+ const idToIndex = new Map(body.map((n, i) => [n.id, i]));
1682
+ const stageOf = [];
1683
+ const groups = [];
1684
+ body.forEach((n, i) => {
1685
+ const pid = "map" in n ? n.map.parent : "cond" in n ? n.cond.parent : n.parent;
1686
+ const stage = pid === void 0 ? 0 : stageOf[idToIndex.get(pid)] + 1;
1687
+ stageOf[i] = stage;
1688
+ (groups[stage] ??= []).push(i);
1689
+ });
1690
+ plan = { groups, concurrency: query.executionPlan.concurrency };
1691
+ }
1692
+ return registerComponent(name, portSchemas(query.params), body, output, plan ?? null);
1693
+ }
1694
+ function lowerTransactionTemplate(template) {
1695
+ const PLACEHOLDER2 = /\{([^{}]+)\}/g;
1696
+ const parts = [];
1697
+ let last = 0;
1698
+ PLACEHOLDER2.lastIndex = 0;
1699
+ for (let m = PLACEHOLDER2.exec(template); m !== null; m = PLACEHOLDER2.exec(template)) {
1700
+ const literal = template.slice(last, m.index);
1701
+ if (literal !== "") parts.push(literal);
1702
+ const token = m[1];
1703
+ parts.push(
1704
+ token.startsWith("item.") ? { ref: ["$item", token.slice("item.".length)] } : { ref: [token] }
1705
+ );
1706
+ last = m.index + m[0].length;
1707
+ }
1708
+ const tail = template.slice(last);
1709
+ if (tail !== "") parts.push(tail);
1710
+ if (parts.length === 0) return "";
1711
+ if (parts.length === 1) return parts[0];
1712
+ return { concat: parts };
1713
+ }
1714
+ function lowerTransactionValue(value) {
1715
+ return typeof value === "string" ? lowerTransactionTemplate(value) : value;
1716
+ }
1717
+ function conditionPorts(condition, ports) {
1718
+ ports["condition.kind"] = condition.kind;
1719
+ switch (condition.kind) {
1720
+ case "notExists":
1721
+ return;
1722
+ case "attributeExists":
1723
+ case "attributeNotExists":
1724
+ ports["condition.field"] = condition.field;
1725
+ return;
1726
+ case "equals": {
1727
+ for (const field of Object.keys(condition.fields).sort()) {
1728
+ ports[`condition.equals.${field}`] = lowerTransactionValue(condition.fields[field]);
1729
+ }
1730
+ return;
1731
+ }
1732
+ case "expr": {
1733
+ ports["condition.declarative"] = stableJson(condition.declarative);
1734
+ const params = /* @__PURE__ */ new Set();
1735
+ collectParamLeaves(condition.declarative, params);
1736
+ for (const p of [...params].sort()) {
1737
+ ports[`condition.params.${p}`] = { ref: [p] };
1738
+ }
1739
+ return;
1740
+ }
1741
+ case "raw": {
1742
+ ports["condition.expression"] = condition.expression;
1743
+ ports["condition.names"] = stableJson(condition.names);
1744
+ for (const slot of Object.keys(condition.values).sort()) {
1745
+ const v = condition.values[slot];
1746
+ const leaf = v !== null && typeof v === "object" && typeof v.$param === "string" ? { ref: [v.$param] } : v;
1747
+ ports[`condition.values.${slot}`] = leaf;
1748
+ }
1749
+ return;
1750
+ }
1751
+ case "scpExpr":
1752
+ ports["condition.expression"] = stableJson(condition.expression);
1753
+ return;
1754
+ }
1755
+ }
1756
+ function transactionItemPorts(item) {
1757
+ const ports = { table: item.tableName, entity: item.entity };
1758
+ const record = (src, group) => {
1759
+ if (src === void 0) return;
1760
+ for (const [field, value] of Object.entries(src)) {
1761
+ ports[`${group}.${field}`] = lowerTransactionValue(value);
1762
+ }
1763
+ };
1764
+ record(item.item, "item");
1765
+ record(item.keyCondition, "key");
1766
+ record(item.changes, "changes");
1767
+ record(item.add, "add");
1768
+ if (item.literalKey === true) ports.literalKey = true;
1769
+ if (item.condition !== void 0) conditionPorts(item.condition, ports);
1770
+ if (item.maintain !== void 0) ports.maintain = stableJson(item.maintain);
1771
+ return ports;
1772
+ }
1773
+ function lowerItemGuard(item) {
1774
+ if (item.guard !== void 0) {
1775
+ return remapRefRoots(item.guard.expr, [], ["$item"]);
1776
+ }
1777
+ const when = item.when;
1778
+ if (when === void 0) return void 0;
1779
+ const args = [lowerTransactionTemplate(when.left), lowerTransactionTemplate(when.right)];
1780
+ return when.op === "eq" ? { eq: args } : { ne: args };
1781
+ }
1782
+ function buildTransactionComponent(name, tx) {
1783
+ const catalog = {
1784
+ Put: "PutItem",
1785
+ Update: "UpdateItem",
1786
+ Delete: "DeleteItem",
1787
+ ConditionCheck: "ConditionCheck"
1788
+ };
1789
+ const body = tx.items.map((item, i) => {
1790
+ const id = `item${i}`;
1791
+ const component = catalog[item.type];
1792
+ const when = lowerItemGuard(item);
1793
+ if (tx.batchWrite === true) {
1794
+ const reject = (kind) => {
1795
+ throw new Error(
1796
+ `buildTransactionComponent('${name}'): batchWrite item${i}: ${kind} \u2014 DynamoDB BatchWriteItem carries only unconditional PutRequest / DeleteRequest (internal invariant; the emitter synthesizes this form).`
1797
+ );
1798
+ };
1799
+ if (component !== "PutItem" && component !== "DeleteItem") reject(`'${component}' item`);
1800
+ if (item.condition !== void 0) reject("a write condition");
1801
+ if (when !== void 0) reject("a when/guard");
1802
+ if (item.forEach === void 0) reject("no forEach (the bulk form is forEach-over-items)");
1803
+ const node2 = {
1804
+ id,
1805
+ map: {
1806
+ over: { ref: [item.forEach.source] },
1807
+ as: "$item",
1808
+ component,
1809
+ ports: transactionItemPorts(item),
1810
+ batched: true
1811
+ }
1812
+ };
1813
+ return node2;
1814
+ }
1815
+ if (item.forEach !== void 0) {
1816
+ const node2 = {
1817
+ id,
1818
+ map: {
1819
+ over: { ref: [item.forEach.source] },
1820
+ as: "$item",
1821
+ component,
1822
+ ports: transactionItemPorts(item),
1823
+ ...when !== void 0 ? { when } : {}
1824
+ }
1825
+ };
1826
+ return node2;
1827
+ }
1828
+ if (when !== void 0) {
1829
+ const node2 = {
1830
+ id,
1831
+ map: {
1832
+ over: { arr: [true] },
1833
+ as: "$item",
1834
+ component,
1835
+ ports: transactionItemPorts(item),
1836
+ when
1837
+ }
1838
+ };
1839
+ return node2;
1840
+ }
1841
+ const node = { id, component, ports: transactionItemPorts(item) };
1842
+ return node;
1843
+ });
1844
+ return registerComponent(
1845
+ name,
1846
+ portSchemas(tx.params),
1847
+ body,
1848
+ { arr: body.map((n) => ({ ref: [n.id] })) },
1849
+ null
1850
+ );
1851
+ }
1852
+ function portSchemas(params) {
1853
+ const out = {};
1854
+ for (const name of Object.keys(params).sort()) {
1855
+ const p = params[name];
1856
+ out[name] = { type: p.type, required: p.required };
1857
+ }
1858
+ return out;
1859
+ }
1860
+ function buildComponents(queries, transactions) {
1861
+ const components = [];
1862
+ for (const name of Object.keys(queries).sort()) {
1863
+ components.push(buildQueryComponent(name, queries[name]));
1864
+ }
1865
+ for (const name of Object.keys(transactions).sort()) {
1866
+ components.push(buildTransactionComponent(name, transactions[name]));
1867
+ }
1868
+ assertComponentsInCatalog(components);
1869
+ return components;
1870
+ }
1871
+ function portableIrDocument(components) {
1872
+ return {
1873
+ irVersion: 1,
1874
+ exprVersion: EXPR_VERSION,
1875
+ components: [...components]
1876
+ };
1877
+ }
1878
+
1317
1879
  // src/spec/operations.ts
1318
1880
  function paramSpecs(params) {
1319
1881
  const out = {};
@@ -1332,11 +1894,12 @@ function paramSpecs(params) {
1332
1894
  function metaFor(def) {
1333
1895
  return MetadataRegistry.get(def.entity.modelClass);
1334
1896
  }
1335
- function templateLeaf(key, value) {
1897
+ function templateValueLeaf(key, value) {
1336
1898
  if (isContractParamRef(value)) return value.token;
1337
1899
  if (isContractKeyFieldRef(value)) return `{${value.field}}`;
1338
1900
  if (isParam(value)) return `{${key}}`;
1339
1901
  if (value instanceof Date) return value.toISOString();
1902
+ if (typeof value === "number" || typeof value === "boolean") return value;
1340
1903
  return String(value);
1341
1904
  }
1342
1905
  function keyFieldNames(key) {
@@ -1415,7 +1978,20 @@ function buildReadOperations(def) {
1415
1978
  };
1416
1979
  const ops = [root];
1417
1980
  ops.push(...buildRelationOperations(metadata, select, "$"));
1418
- return injectRefsSourceProjections(ops);
1981
+ return injectRefsSourceProjections(ops).map(lowerOperationKeyExprs);
1982
+ }
1983
+ function lowerOperationKeyExprs(op) {
1984
+ const keyExpr = op.keyCondition ? lowerKeyCondition(op.keyCondition) : {};
1985
+ const { keyCondition: _dropKeyCondition, rangeCondition, ...rest } = op;
1986
+ const nextRange = rangeCondition ? (() => {
1987
+ const { value: _dropValue, ...rc } = rangeCondition;
1988
+ return { ...rc, valueExpr: lowerKeyTemplate(rangeCondition.value ?? "") };
1989
+ })() : void 0;
1990
+ return {
1991
+ ...rest,
1992
+ keyExpr,
1993
+ ...nextRange ? { rangeCondition: nextRange } : {}
1994
+ };
1419
1995
  }
1420
1996
  function projectionFields(select) {
1421
1997
  const out = [];
@@ -1424,7 +2000,7 @@ function projectionFields(select) {
1424
2000
  }
1425
2001
  return out.sort();
1426
2002
  }
1427
- function buildRelationOperations(metadata, select, parentPath) {
2003
+ function buildRelationOperations(metadata, select, parentPath2) {
1428
2004
  const ops = [];
1429
2005
  const relations = detectRelationFields(select, metadata);
1430
2006
  for (const rel of [...relations].sort(
@@ -1434,7 +2010,7 @@ function buildRelationOperations(metadata, select, parentPath) {
1434
2010
  const childSelect = spec.select ?? {};
1435
2011
  const targetClass = rel.targetFactory();
1436
2012
  const targetMeta = MetadataRegistry.get(targetClass);
1437
- const childPath = `${parentPath}.${rel.propertyName}`;
2013
+ const childPath = `${parentPath2}.${rel.propertyName}`;
1438
2014
  if (rel.type === "refs") {
1439
2015
  if (spec.filter !== void 0) {
1440
2016
  throw new Error(
@@ -1587,7 +2163,16 @@ function buildRefsOperation(rel, targetMeta, select, resultPath) {
1587
2163
  projection: projectionFields(select),
1588
2164
  resultPath,
1589
2165
  sourceField,
1590
- sourceList: { from: refs.from, key: refs.key }
2166
+ // #289 P2-5: the sourceList carries its Phase-3 Map shape prep — `over` is
2167
+ // the Expression-IR ref to the parent list attribute the Map iterates, `as`
2168
+ // is the per-element binding name. Descriptive only in Phase 2 (behavior is
2169
+ // still driven by `from`/`key`); Phase 3 lowers the fan-out body onto them.
2170
+ sourceList: {
2171
+ from: refs.from,
2172
+ key: refs.key,
2173
+ over: { ref: ["result", refs.from] },
2174
+ as: `$${refs.key}`
2175
+ }
1591
2176
  };
1592
2177
  }
1593
2178
  function injectRefsSourceProjections(ops) {
@@ -1595,11 +2180,11 @@ function injectRefsSourceProjections(ops) {
1595
2180
  const op = ops[i];
1596
2181
  if (op.sourceList === void 0) continue;
1597
2182
  const tokens = op.resultPath.split(".");
1598
- const parentPath = tokens.slice(0, -2).join(".") || "$";
1599
- const parentIndex = ops.findIndex((p) => p.resultPath === parentPath);
2183
+ const parentPath2 = tokens.slice(0, -2).join(".") || "$";
2184
+ const parentIndex = ops.findIndex((p) => p.resultPath === parentPath2);
1600
2185
  if (parentIndex === -1) {
1601
2186
  throw new Error(
1602
- `refs fan-out at '${op.resultPath}': no parent operation at '${parentPath}'.`
2187
+ `refs fan-out at '${op.resultPath}': no parent operation at '${parentPath2}'.`
1603
2188
  );
1604
2189
  }
1605
2190
  const parent = ops[parentIndex];
@@ -1648,6 +2233,8 @@ function buildCommandSpec(def) {
1648
2233
  ...description
1649
2234
  };
1650
2235
  }
2236
+ const addStructure = def.add;
2237
+ const add = addStructure !== void 0 && Object.keys(addStructure).length > 0 ? { add: templateRecord(addStructure) } : {};
1651
2238
  return {
1652
2239
  type: "UpdateItem",
1653
2240
  tableName,
@@ -1655,6 +2242,7 @@ function buildCommandSpec(def) {
1655
2242
  params,
1656
2243
  keyCondition: writeKeyCondition(metadata, def.key, entity),
1657
2244
  changes: templateRecord(def.changes),
2245
+ ...add,
1658
2246
  ...condition ? { condition } : {},
1659
2247
  ...description
1660
2248
  };
@@ -1681,7 +2269,7 @@ function writeKeyCondition(metadata, key, entity) {
1681
2269
  function templateRecord(structure) {
1682
2270
  const out = {};
1683
2271
  for (const key of Object.keys(structure).sort()) {
1684
- out[key] = templateLeaf(key, structure[key]);
2272
+ out[key] = templateValueLeaf(key, structure[key]);
1685
2273
  }
1686
2274
  return out;
1687
2275
  }
@@ -1689,7 +2277,7 @@ function extractCondition(def) {
1689
2277
  return conditionInputToSpec(
1690
2278
  def.condition,
1691
2279
  `Command on '${def.entity.name}'`,
1692
- (field, value) => templateLeaf(field, value),
2280
+ (field, value) => templateValueLeaf(field, value),
1693
2281
  (value, name) => conditionTreeLeaf(value, name),
1694
2282
  // A raw `cond` value slot (issue #114-B): a contract param / key ref carries
1695
2283
  // its own name; a plain `param.*` falls through to the positional default.
@@ -1728,6 +2316,27 @@ function buildQuerySpec(def) {
1728
2316
  ...def.description !== void 0 ? { description: def.description } : {}
1729
2317
  };
1730
2318
  }
2319
+ function toPortableQuery(name, spec) {
2320
+ const root = spec.operations[0];
2321
+ if (root === void 0 || root.resultPath !== "$") {
2322
+ throw new Error(
2323
+ `toPortableQuery('${name}'): expected the root op at operations[0] (resultPath '$'); got ${root === void 0 ? "no operations" : `'${root.resultPath}'`}.`
2324
+ );
2325
+ }
2326
+ const { resultPath: _rp, sourceField: _sf, sourceList: _sl, ...portableRoot } = root;
2327
+ return {
2328
+ params: spec.params,
2329
+ operations: [portableRoot],
2330
+ ...spec.cardinality !== void 0 ? { cardinality: spec.cardinality } : {},
2331
+ ...spec.description !== void 0 ? { description: spec.description } : {}
2332
+ };
2333
+ }
2334
+ function toPortableTransaction(spec) {
2335
+ return {
2336
+ params: spec.params,
2337
+ ...spec.batchWrite === true ? { batchWrite: true } : {}
2338
+ };
2339
+ }
1731
2340
  function mergeSpecs(base, synthesized, kind) {
1732
2341
  for (const name of Object.keys(synthesized)) {
1733
2342
  if (Object.prototype.hasOwnProperty.call(base, name)) {
@@ -1759,16 +2368,55 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
1759
2368
  mergeSpecs(transactionSpecs, built.transactions, "transaction");
1760
2369
  const contractSpecs = built.contracts;
1761
2370
  const contextSpecs = buildContexts(contextInputMap);
2371
+ const components = buildComponents(querySpecs, transactionSpecs);
2372
+ const behaviorNames = /* @__PURE__ */ new Set();
2373
+ {
2374
+ const behaviorInputs = contractInputs.behaviors ?? {};
2375
+ const taken = new Set(components.map((c) => c.name));
2376
+ for (const key of Object.keys(behaviorInputs).sort()) {
2377
+ for (const c of behaviorInputs[key].components) {
2378
+ const name = `${key}__${c.name}`;
2379
+ if (taken.has(name)) {
2380
+ throw new Error(
2381
+ `publishBehaviors registration '${key}' produced component '${name}', which collides with another emitted component. Rename the behavior registration or the method.`
2382
+ );
2383
+ }
2384
+ taken.add(name);
2385
+ behaviorNames.add(name);
2386
+ components.push({ ...c, name });
2387
+ }
2388
+ }
2389
+ if (behaviorNames.size > 0) assertComponentsInCatalog(components);
2390
+ }
2391
+ assertPublishSplitMatchesDerived(
2392
+ deriveComponentEffects(components),
2393
+ {
2394
+ queries: new Set(Object.keys(querySpecs)),
2395
+ transactions: new Set(Object.keys(transactionSpecs)),
2396
+ commands: new Set(Object.keys(commandSpecs)),
2397
+ behaviors: behaviorNames
2398
+ },
2399
+ contractSpecs
2400
+ );
2401
+ const portableQueries = {};
2402
+ for (const [name, spec] of Object.entries(querySpecs)) {
2403
+ portableQueries[name] = toPortableQuery(name, spec);
2404
+ }
2405
+ const portableTransactions = {};
2406
+ for (const [name, spec] of Object.entries(transactionSpecs)) {
2407
+ portableTransactions[name] = toPortableTransaction(spec);
2408
+ }
1762
2409
  const content = {
1763
- queries: querySpecs,
2410
+ queries: portableQueries,
1764
2411
  commands: commandSpecs,
1765
- ...Object.keys(transactionSpecs).length > 0 ? { transactions: transactionSpecs } : {},
2412
+ ...Object.keys(portableTransactions).length > 0 ? { transactions: portableTransactions } : {},
1766
2413
  ...Object.keys(contractSpecs).length > 0 ? { contracts: contractSpecs } : {},
1767
2414
  ...Object.keys(contextSpecs).length > 0 ? { contexts: contextSpecs } : {}
1768
2415
  };
1769
2416
  return {
1770
2417
  version: operationsSpecVersion(content),
1771
- ...content
2418
+ ...content,
2419
+ ...components.length > 0 ? { components } : {}
1772
2420
  };
1773
2421
  }
1774
2422
 
@@ -1957,7 +2605,9 @@ function buildPreparedPlanDocument(plans) {
1957
2605
  for (const id of Object.keys(plans).sort()) sorted[id] = plans[id];
1958
2606
  return {
1959
2607
  formatVersion: PREPARED_FORMAT_VERSION,
1960
- specVersion: SPEC_VERSION,
2608
+ // #289 Phase 2: the embedded operations IR is 2.0 (major bump); the prepared
2609
+ // artifact's specVersion tracks it so a version-skewed plan fails closed.
2610
+ specVersion: SPEC_VERSION_KEY_EXPR,
1961
2611
  plans: sorted
1962
2612
  };
1963
2613
  }
@@ -1973,12 +2623,17 @@ function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegis
1973
2623
  }
1974
2624
 
1975
2625
  export {
2626
+ renderKeyExprToTemplate,
1976
2627
  collectContractN1Violations,
1977
2628
  assertContractN1Safe,
1978
2629
  buildContracts,
1979
2630
  buildContexts,
1980
2631
  collectContractBoundaryViolations,
1981
2632
  assertContractBoundaries,
2633
+ buildQueryComponent,
2634
+ buildTransactionComponent,
2635
+ buildComponents,
2636
+ portableIrDocument,
1982
2637
  buildQuerySpec,
1983
2638
  buildOperations,
1984
2639
  compilePreparedPlan,