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,7 +2,7 @@ import {
2
2
  PREPARED_FORMAT_VERSION,
3
3
  buildManifest,
4
4
  entityFingerprint
5
- } from "./chunk-LGHSZIEE.js";
5
+ } from "./chunk-NWTEUWJD.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-I4LEJ4TF.js";
24
+ } from "./chunk-XZTA4VJH.js";
25
25
  import {
26
26
  detectRelationFields,
27
27
  isInlineSnapshotSpec,
28
28
  normalizeSelectSpec
29
- } from "./chunk-HNY2EJPV.js";
29
+ } from "./chunk-KOIJ4SNO.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-ZNU7OI5I.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);
@@ -185,6 +250,11 @@ function opToDefinition(contractName, methodName, op) {
185
250
  };
186
251
  }
187
252
  function paramOfKind(recovered) {
253
+ if (recovered.kind === "map") {
254
+ throw new Error(
255
+ `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.`
256
+ );
257
+ }
188
258
  if (recovered.kind === "number") return param.number();
189
259
  if (recovered.kind === "literal" && recovered.literals !== void 0 && recovered.literals.length > 0) {
190
260
  return param.literal(
@@ -402,6 +472,13 @@ function composeSpecOf(node, contractName, methodName, contracts) {
402
472
  `Contract '${contractName}.${methodName}': composed child '${c.as}' must declare a \`resolution\` of 'point' or 'range'.`
403
473
  );
404
474
  }
475
+ const target = contracts[c.contract];
476
+ if (target !== void 0 && isQueryModelContract(target)) {
477
+ const targetMethod = target.methods[c.method];
478
+ if (targetMethod !== void 0) {
479
+ assertComposeTargetNotNested(contractName, methodName, c.as, c.contract, c.method, targetMethod.op);
480
+ }
481
+ }
405
482
  const bindIn = c.bind ?? {};
406
483
  const bind = {};
407
484
  for (const field of Object.keys(bindIn).sort()) {
@@ -435,6 +512,7 @@ function recordedComposeSpec(node, contractName, methodName, contracts) {
435
512
  contracts
436
513
  );
437
514
  const refMethod = node.method;
515
+ assertComposeTargetNotNested(contractName, methodName, node.as, refName, refMethodName, refMethod.op);
438
516
  const bind = {};
439
517
  for (const field of Object.keys(node.bind).sort()) {
440
518
  bind[field] = node.bind[field].path;
@@ -470,6 +548,12 @@ function composeOf(op) {
470
548
  const raw = op.compose;
471
549
  return Array.isArray(raw) ? raw : [];
472
550
  }
551
+ function assertComposeTargetNotNested(contractName, methodName, as, targetContract, targetMethod, targetOp) {
552
+ if (targetOp === void 0 || composeOf(targetOp).length === 0) return;
553
+ throw new Error(
554
+ `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).`
555
+ );
556
+ }
473
557
  function compositionPlanOf(compose) {
474
558
  if (compose.length === 0) return void 0;
475
559
  const { stages, concurrency } = deriveCompositionPlan(compose.length);
@@ -482,6 +566,9 @@ function compositionPlanOf(compose) {
482
566
  function batchTxName(contractName, methodName) {
483
567
  return `${opRefName(contractName, methodName)}__batch`;
484
568
  }
569
+ function batchWriteTxName(contractName, methodName) {
570
+ return `${opRefName(contractName, methodName)}__batchWrite`;
571
+ }
485
572
  function keyParamNames(op) {
486
573
  return keyFieldsOf(op);
487
574
  }
@@ -556,6 +643,44 @@ function synthesizeBatchTransaction(commandSpec, op) {
556
643
  };
557
644
  return { params, items: [item] };
558
645
  }
646
+ function synthesizeBatchWriteTransaction(contractName, methodName, commandSpec) {
647
+ const at = `Contract '${contractName}.${methodName}'`;
648
+ if (commandSpec.type === "UpdateItem") {
649
+ throw new Error(
650
+ `${at}: mode 'batchWrite' cannot express an update \u2014 DynamoDB BatchWriteItem carries only PutRequest / DeleteRequest (internal invariant; the authoring layer should have rejected this).`
651
+ );
652
+ }
653
+ if (commandSpec.condition !== void 0) {
654
+ throw new Error(
655
+ `${at}: mode 'batchWrite' cannot carry a write condition \u2014 DynamoDB BatchWriteItem has no ConditionExpression (internal invariant; the authoring layer should have rejected this).`
656
+ );
657
+ }
658
+ const elementFields = {};
659
+ for (const name of Object.keys(commandSpec.params).sort()) {
660
+ elementFields[name] = commandSpec.params[name];
661
+ }
662
+ const params = {
663
+ items: { type: "array", required: true, element: elementFields }
664
+ };
665
+ const allParams = new Set(Object.keys(commandSpec.params));
666
+ const toElement = (record) => {
667
+ const out = {};
668
+ for (const field of Object.keys(record)) {
669
+ const v = record[field];
670
+ out[field] = typeof v === "string" ? toElementTemplate(v, allParams) : v;
671
+ }
672
+ return out;
673
+ };
674
+ const item = {
675
+ type: TX_ITEM_TYPE[commandSpec.type],
676
+ tableName: commandSpec.tableName,
677
+ entity: commandSpec.entity,
678
+ ...commandSpec.item !== void 0 ? { item: toElement(commandSpec.item) } : {},
679
+ ...commandSpec.keyCondition !== void 0 ? { keyCondition: toElement(commandSpec.keyCondition) } : {},
680
+ forEach: { source: "items" }
681
+ };
682
+ return { params, items: [item], batchWrite: true };
683
+ }
559
684
  function composeFragmentTransaction(contractName, methodName, ops) {
560
685
  const items = [];
561
686
  const params = {};
@@ -923,6 +1048,15 @@ function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName,
923
1048
  if (mode === "parallel") {
924
1049
  return { mode: "parallel", operation: opName };
925
1050
  }
1051
+ if (mode === "batchWrite") {
1052
+ const txName2 = batchWriteTxName(contractName, methodName);
1053
+ transactionSpecs[txName2] = synthesizeBatchWriteTransaction(
1054
+ contractName,
1055
+ methodName,
1056
+ commandSpec
1057
+ );
1058
+ return { mode: "batchWrite", transaction: txName2 };
1059
+ }
926
1060
  const txName = batchTxName(contractName, methodName);
927
1061
  transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
928
1062
  return { mode: "transaction", transaction: txName };
@@ -1138,6 +1272,11 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
1138
1272
  returnSelection
1139
1273
  );
1140
1274
  }
1275
+ if (method.mode === "batchWrite" && promotedToTransaction) {
1276
+ throw new Error(
1277
+ `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).`
1278
+ );
1279
+ }
1141
1280
  const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
1142
1281
  method.mode,
1143
1282
  op,
@@ -1314,6 +1453,417 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
1314
1453
  throw new Error(first.message + suffix);
1315
1454
  }
1316
1455
 
1456
+ // src/spec/components.ts
1457
+ import {
1458
+ buildComponentDefinition
1459
+ } from "behavior-contracts";
1460
+ function registerComponent(name, inputPorts, body, output, plan) {
1461
+ return buildComponentDefinition(
1462
+ { name, inputPorts, body, output, plan },
1463
+ { catalog: GRAPHDDB_CATALOG }
1464
+ );
1465
+ }
1466
+ function remapRefRoots(node, resultRoot, itemRoot) {
1467
+ if (node === null || typeof node !== "object") return node;
1468
+ if (Array.isArray(node)) {
1469
+ return node.map((e) => remapRefRoots(e, resultRoot, itemRoot));
1470
+ }
1471
+ const obj = node;
1472
+ const keys = Object.keys(obj);
1473
+ if (keys.length === 1) {
1474
+ const key = keys[0];
1475
+ const arg = obj[key];
1476
+ if (key === "ref" || key === "refOpt") {
1477
+ const path = arg;
1478
+ const [root, ...rest] = path;
1479
+ const remapped = root === "input" ? rest : root === "result" ? [...resultRoot, ...rest] : root === "item" && itemRoot !== void 0 ? [...itemRoot, ...rest] : path;
1480
+ return { [key]: remapped };
1481
+ }
1482
+ if (key === "int" || key === "float") return node;
1483
+ if (key === "obj") {
1484
+ const out = {};
1485
+ for (const [k, v] of Object.entries(arg)) {
1486
+ out[k] = remapRefRoots(v, resultRoot, itemRoot);
1487
+ }
1488
+ return { obj: out };
1489
+ }
1490
+ if (Array.isArray(arg)) {
1491
+ return {
1492
+ [key]: arg.map((e) => remapRefRoots(e, resultRoot, itemRoot))
1493
+ };
1494
+ }
1495
+ return {
1496
+ [key]: remapRefRoots(arg, resultRoot, itemRoot)
1497
+ };
1498
+ }
1499
+ return node;
1500
+ }
1501
+ function stableJson(value) {
1502
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
1503
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1504
+ const keys = Object.keys(value).sort();
1505
+ const body = keys.map((k) => `${JSON.stringify(k)}:${stableJson(value[k])}`).join(",");
1506
+ return `{${body}}`;
1507
+ }
1508
+ function collectParamLeaves(tree, out) {
1509
+ if (tree === null || typeof tree !== "object") return;
1510
+ if (Array.isArray(tree)) {
1511
+ for (const e of tree) collectParamLeaves(e, out);
1512
+ return;
1513
+ }
1514
+ const rec = tree;
1515
+ if (typeof rec.$param === "string" && Object.keys(rec).length === 1) {
1516
+ out.add(rec.$param);
1517
+ return;
1518
+ }
1519
+ for (const v of Object.values(rec)) collectParamLeaves(v, out);
1520
+ }
1521
+ function pathToId(resultPath) {
1522
+ if (resultPath === "$") return "root";
1523
+ return resultPath.startsWith("$.") ? resultPath.slice(2) : resultPath;
1524
+ }
1525
+ function parentPath(op) {
1526
+ const tokens = op.resultPath.split(".");
1527
+ const drop = op.resultPath.endsWith(".items") ? 2 : 1;
1528
+ const p = tokens.slice(0, tokens.length - drop).join(".");
1529
+ return p === "" ? "$" : p;
1530
+ }
1531
+ function relationProp(op) {
1532
+ const tokens = op.resultPath.split(".");
1533
+ const idx = op.resultPath.endsWith(".items") ? tokens.length - 2 : tokens.length - 1;
1534
+ return tokens[idx];
1535
+ }
1536
+ function relationKindOf(op) {
1537
+ return op.resultPath.endsWith(".items") ? "connection" : "single";
1538
+ }
1539
+ function readPorts(op, resultRoot) {
1540
+ const ports = { table: op.tableName };
1541
+ if (op.indexName !== void 0) ports.indexName = op.indexName;
1542
+ for (const [attr, node] of Object.entries(op.keyExpr ?? {})) {
1543
+ ports[attr] = remapRefRoots(node, resultRoot);
1544
+ }
1545
+ if (op.rangeCondition !== void 0) {
1546
+ ports.rangeKey = op.rangeCondition.key;
1547
+ ports.rangeOperator = op.rangeCondition.operator;
1548
+ ports.rangeValue = remapRefRoots(
1549
+ op.rangeCondition.valueExpr ?? "",
1550
+ resultRoot
1551
+ );
1552
+ }
1553
+ if (op.limit !== void 0) ports.limit = op.limit;
1554
+ if (op.filter !== void 0) ports.filter = stableJson(op.filter.declarative);
1555
+ ports.projection = { arr: [...op.projection] };
1556
+ return ports;
1557
+ }
1558
+ function buildQueryComponent(name, query) {
1559
+ const ops = query.operations;
1560
+ const idByPath = /* @__PURE__ */ new Map();
1561
+ const opByPath = /* @__PURE__ */ new Map();
1562
+ for (const op of ops) {
1563
+ idByPath.set(op.resultPath, pathToId(op.resultPath));
1564
+ opByPath.set(op.resultPath, op);
1565
+ }
1566
+ const lineages = /* @__PURE__ */ new Map();
1567
+ const elementInfo = /* @__PURE__ */ new Map();
1568
+ const body = ops.map((op) => {
1569
+ const id = pathToId(op.resultPath);
1570
+ if (op.resultPath === "$") {
1571
+ const ports = readPorts(op, []);
1572
+ if (query.cardinality === "one" && op.type === "Query") ports.cardinality = "one";
1573
+ const node2 = { id, component: op.type, ports };
1574
+ return node2;
1575
+ }
1576
+ const reject = (kind) => {
1577
+ throw new Error(`buildQueryComponent('${name}'): op '${op.resultPath}': ${kind}`);
1578
+ };
1579
+ const pp = parentPath(op);
1580
+ const producerId = idByPath.get(pp) ?? reject(`no producer op at '${pp}'`);
1581
+ if (op.sourceList !== void 0) {
1582
+ if (pp !== "$") reject("nested sourceList not expressible (v2 map)");
1583
+ const as = op.sourceList.as ?? `$${op.sourceList.key}`;
1584
+ const over = remapRefRoots(
1585
+ op.sourceList.over ?? { ref: ["result", op.sourceList.from] },
1586
+ [producerId]
1587
+ );
1588
+ const ports = readPorts(op, [as]);
1589
+ if (op.sourceList.implicit === true) ports.implicitSourceField = op.sourceList.from;
1590
+ const node2 = {
1591
+ id,
1592
+ map: {
1593
+ over,
1594
+ as,
1595
+ component: op.type,
1596
+ ports,
1597
+ parent: producerId,
1598
+ relationKind: relationKindOf(op),
1599
+ batched: true
1600
+ }
1601
+ };
1602
+ lineages.set(op.resultPath, { tailExpr: { ref: [id] }, tailId: id });
1603
+ return node2;
1604
+ }
1605
+ const producerOp = opByPath.get(pp);
1606
+ const producerIsConnection = producerOp !== void 0 && pp.endsWith(".items");
1607
+ const producerElement = elementInfo.get(pp);
1608
+ if (producerIsConnection || producerElement !== void 0) {
1609
+ const lineageKey = producerElement?.lineageKey ?? pp;
1610
+ 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}'`);
1611
+ const as = "$el";
1612
+ const attach = producerElement?.attachSeg ?? [];
1613
+ const into = relationProp(op);
1614
+ const node2 = {
1615
+ id,
1616
+ map: {
1617
+ over: lineage.tailExpr,
1618
+ as,
1619
+ component: op.type,
1620
+ ports: readPorts(op, [as, ...attach]),
1621
+ parent: lineage.tailId,
1622
+ relationKind: relationKindOf(op),
1623
+ into,
1624
+ // A belongsTo child the old path resolves via ONE deduped BatchGetItem
1625
+ // (gap 6) is batched; a per-element Query stays per-element.
1626
+ ...op.type === "BatchGetItem" ? { batched: true } : {}
1627
+ }
1628
+ };
1629
+ lineages.set(lineageKey, { tailExpr: { ref: [id] }, tailId: id });
1630
+ elementInfo.set(op.resultPath, { lineageKey, attachSeg: [into] });
1631
+ return node2;
1632
+ }
1633
+ const node = {
1634
+ id,
1635
+ component: op.type,
1636
+ ports: readPorts(op, [producerId]),
1637
+ parent: producerId,
1638
+ ...op.sourceField !== void 0 ? { bindField: op.sourceField } : {},
1639
+ relationKind: relationKindOf(op)
1640
+ };
1641
+ if (relationKindOf(op) === "connection") {
1642
+ lineages.set(op.resultPath, { tailExpr: { ref: [id, "items"] }, tailId: id });
1643
+ }
1644
+ return node;
1645
+ });
1646
+ const relOps = ops.filter((op) => op.resultPath !== "$");
1647
+ let output;
1648
+ if (relOps.length === 0) {
1649
+ output = { ref: ["root"] };
1650
+ } else {
1651
+ const outputObj = { root: { ref: ["root"] } };
1652
+ for (const op of relOps) {
1653
+ const id = pathToId(op.resultPath);
1654
+ if (elementInfo.has(op.resultPath)) continue;
1655
+ const lineage = lineages.get(op.resultPath);
1656
+ const key = parentPath(op) === "$" ? relationProp(op) : id;
1657
+ if (lineage !== void 0 && lineage.tailId !== id && relationKindOf(op) === "connection" && op.sourceList === void 0) {
1658
+ outputObj[key] = {
1659
+ obj: { cursor: { ref: [id, "cursor"] }, items: lineage.tailExpr }
1660
+ };
1661
+ } else {
1662
+ outputObj[key] = { ref: [id] };
1663
+ }
1664
+ }
1665
+ output = { obj: outputObj };
1666
+ }
1667
+ let plan;
1668
+ if (query.executionPlan) {
1669
+ const idToIndex = new Map(body.map((n, i) => [n.id, i]));
1670
+ const stageOf = [];
1671
+ const groups = [];
1672
+ body.forEach((n, i) => {
1673
+ const pid = "map" in n ? n.map.parent : "cond" in n ? n.cond.parent : n.parent;
1674
+ const stage = pid === void 0 ? 0 : stageOf[idToIndex.get(pid)] + 1;
1675
+ stageOf[i] = stage;
1676
+ (groups[stage] ??= []).push(i);
1677
+ });
1678
+ plan = { groups, concurrency: query.executionPlan.concurrency };
1679
+ }
1680
+ return registerComponent(name, portSchemas(query.params), body, output, plan ?? null);
1681
+ }
1682
+ function lowerTransactionTemplate(template) {
1683
+ const PLACEHOLDER2 = /\{([^{}]+)\}/g;
1684
+ const parts = [];
1685
+ let last = 0;
1686
+ PLACEHOLDER2.lastIndex = 0;
1687
+ for (let m = PLACEHOLDER2.exec(template); m !== null; m = PLACEHOLDER2.exec(template)) {
1688
+ const literal = template.slice(last, m.index);
1689
+ if (literal !== "") parts.push(literal);
1690
+ const token = m[1];
1691
+ parts.push(
1692
+ token.startsWith("item.") ? { ref: ["$item", token.slice("item.".length)] } : { ref: [token] }
1693
+ );
1694
+ last = m.index + m[0].length;
1695
+ }
1696
+ const tail = template.slice(last);
1697
+ if (tail !== "") parts.push(tail);
1698
+ if (parts.length === 0) return "";
1699
+ if (parts.length === 1) return parts[0];
1700
+ return { concat: parts };
1701
+ }
1702
+ function lowerTransactionValue(value) {
1703
+ return typeof value === "string" ? lowerTransactionTemplate(value) : value;
1704
+ }
1705
+ function conditionPorts(condition, ports) {
1706
+ ports["condition.kind"] = condition.kind;
1707
+ switch (condition.kind) {
1708
+ case "notExists":
1709
+ return;
1710
+ case "attributeExists":
1711
+ case "attributeNotExists":
1712
+ ports["condition.field"] = condition.field;
1713
+ return;
1714
+ case "equals": {
1715
+ for (const field of Object.keys(condition.fields).sort()) {
1716
+ ports[`condition.equals.${field}`] = lowerTransactionTemplate(condition.fields[field]);
1717
+ }
1718
+ return;
1719
+ }
1720
+ case "expr": {
1721
+ ports["condition.declarative"] = stableJson(condition.declarative);
1722
+ const params = /* @__PURE__ */ new Set();
1723
+ collectParamLeaves(condition.declarative, params);
1724
+ for (const p of [...params].sort()) {
1725
+ ports[`condition.params.${p}`] = { ref: [p] };
1726
+ }
1727
+ return;
1728
+ }
1729
+ case "raw": {
1730
+ ports["condition.expression"] = condition.expression;
1731
+ ports["condition.names"] = stableJson(condition.names);
1732
+ for (const slot of Object.keys(condition.values).sort()) {
1733
+ const v = condition.values[slot];
1734
+ const leaf = v !== null && typeof v === "object" && typeof v.$param === "string" ? { ref: [v.$param] } : v;
1735
+ ports[`condition.values.${slot}`] = leaf;
1736
+ }
1737
+ return;
1738
+ }
1739
+ case "scpExpr":
1740
+ ports["condition.expression"] = stableJson(condition.expression);
1741
+ return;
1742
+ }
1743
+ }
1744
+ function transactionItemPorts(item) {
1745
+ const ports = { table: item.tableName, entity: item.entity };
1746
+ const record = (src, group) => {
1747
+ if (src === void 0) return;
1748
+ for (const [field, value] of Object.entries(src)) {
1749
+ ports[`${group}.${field}`] = lowerTransactionValue(value);
1750
+ }
1751
+ };
1752
+ record(item.item, "item");
1753
+ record(item.keyCondition, "key");
1754
+ record(item.changes, "changes");
1755
+ record(item.add, "add");
1756
+ if (item.literalKey === true) ports.literalKey = true;
1757
+ if (item.condition !== void 0) conditionPorts(item.condition, ports);
1758
+ if (item.maintain !== void 0) ports.maintain = stableJson(item.maintain);
1759
+ return ports;
1760
+ }
1761
+ function lowerItemGuard(item) {
1762
+ if (item.guard !== void 0) {
1763
+ return remapRefRoots(item.guard.expr, [], ["$item"]);
1764
+ }
1765
+ const when = item.when;
1766
+ if (when === void 0) return void 0;
1767
+ const args = [lowerTransactionTemplate(when.left), lowerTransactionTemplate(when.right)];
1768
+ return when.op === "eq" ? { eq: args } : { ne: args };
1769
+ }
1770
+ function buildTransactionComponent(name, tx) {
1771
+ const catalog = {
1772
+ Put: "PutItem",
1773
+ Update: "UpdateItem",
1774
+ Delete: "DeleteItem",
1775
+ ConditionCheck: "ConditionCheck"
1776
+ };
1777
+ const body = tx.items.map((item, i) => {
1778
+ const id = `item${i}`;
1779
+ const component = catalog[item.type];
1780
+ const when = lowerItemGuard(item);
1781
+ if (tx.batchWrite === true) {
1782
+ const reject = (kind) => {
1783
+ throw new Error(
1784
+ `buildTransactionComponent('${name}'): batchWrite item${i}: ${kind} \u2014 DynamoDB BatchWriteItem carries only unconditional PutRequest / DeleteRequest (internal invariant; the emitter synthesizes this form).`
1785
+ );
1786
+ };
1787
+ if (component !== "PutItem" && component !== "DeleteItem") reject(`'${component}' item`);
1788
+ if (item.condition !== void 0) reject("a write condition");
1789
+ if (when !== void 0) reject("a when/guard");
1790
+ if (item.forEach === void 0) reject("no forEach (the bulk form is forEach-over-items)");
1791
+ const node2 = {
1792
+ id,
1793
+ map: {
1794
+ over: { ref: [item.forEach.source] },
1795
+ as: "$item",
1796
+ component,
1797
+ ports: transactionItemPorts(item),
1798
+ batched: true
1799
+ }
1800
+ };
1801
+ return node2;
1802
+ }
1803
+ if (item.forEach !== void 0) {
1804
+ const node2 = {
1805
+ id,
1806
+ map: {
1807
+ over: { ref: [item.forEach.source] },
1808
+ as: "$item",
1809
+ component,
1810
+ ports: transactionItemPorts(item),
1811
+ ...when !== void 0 ? { when } : {}
1812
+ }
1813
+ };
1814
+ return node2;
1815
+ }
1816
+ if (when !== void 0) {
1817
+ const node2 = {
1818
+ id,
1819
+ map: {
1820
+ over: { arr: [true] },
1821
+ as: "$item",
1822
+ component,
1823
+ ports: transactionItemPorts(item),
1824
+ when
1825
+ }
1826
+ };
1827
+ return node2;
1828
+ }
1829
+ const node = { id, component, ports: transactionItemPorts(item) };
1830
+ return node;
1831
+ });
1832
+ return registerComponent(
1833
+ name,
1834
+ portSchemas(tx.params),
1835
+ body,
1836
+ { arr: body.map((n) => ({ ref: [n.id] })) },
1837
+ null
1838
+ );
1839
+ }
1840
+ function portSchemas(params) {
1841
+ const out = {};
1842
+ for (const name of Object.keys(params).sort()) {
1843
+ const p = params[name];
1844
+ out[name] = { type: p.type, required: p.required };
1845
+ }
1846
+ return out;
1847
+ }
1848
+ function buildComponents(queries, transactions) {
1849
+ const components = [];
1850
+ for (const name of Object.keys(queries).sort()) {
1851
+ components.push(buildQueryComponent(name, queries[name]));
1852
+ }
1853
+ for (const name of Object.keys(transactions).sort()) {
1854
+ components.push(buildTransactionComponent(name, transactions[name]));
1855
+ }
1856
+ assertComponentsInCatalog(components);
1857
+ return components;
1858
+ }
1859
+ function portableIrDocument(components) {
1860
+ return {
1861
+ irVersion: 1,
1862
+ exprVersion: EXPR_VERSION,
1863
+ components: [...components]
1864
+ };
1865
+ }
1866
+
1317
1867
  // src/spec/operations.ts
1318
1868
  function paramSpecs(params) {
1319
1869
  const out = {};
@@ -1415,7 +1965,20 @@ function buildReadOperations(def) {
1415
1965
  };
1416
1966
  const ops = [root];
1417
1967
  ops.push(...buildRelationOperations(metadata, select, "$"));
1418
- return injectRefsSourceProjections(ops);
1968
+ return injectRefsSourceProjections(ops).map(lowerOperationKeyExprs);
1969
+ }
1970
+ function lowerOperationKeyExprs(op) {
1971
+ const keyExpr = op.keyCondition ? lowerKeyCondition(op.keyCondition) : {};
1972
+ const { keyCondition: _dropKeyCondition, rangeCondition, ...rest } = op;
1973
+ const nextRange = rangeCondition ? (() => {
1974
+ const { value: _dropValue, ...rc } = rangeCondition;
1975
+ return { ...rc, valueExpr: lowerKeyTemplate(rangeCondition.value ?? "") };
1976
+ })() : void 0;
1977
+ return {
1978
+ ...rest,
1979
+ keyExpr,
1980
+ ...nextRange ? { rangeCondition: nextRange } : {}
1981
+ };
1419
1982
  }
1420
1983
  function projectionFields(select) {
1421
1984
  const out = [];
@@ -1424,7 +1987,7 @@ function projectionFields(select) {
1424
1987
  }
1425
1988
  return out.sort();
1426
1989
  }
1427
- function buildRelationOperations(metadata, select, parentPath) {
1990
+ function buildRelationOperations(metadata, select, parentPath2) {
1428
1991
  const ops = [];
1429
1992
  const relations = detectRelationFields(select, metadata);
1430
1993
  for (const rel of [...relations].sort(
@@ -1434,7 +1997,7 @@ function buildRelationOperations(metadata, select, parentPath) {
1434
1997
  const childSelect = spec.select ?? {};
1435
1998
  const targetClass = rel.targetFactory();
1436
1999
  const targetMeta = MetadataRegistry.get(targetClass);
1437
- const childPath = `${parentPath}.${rel.propertyName}`;
2000
+ const childPath = `${parentPath2}.${rel.propertyName}`;
1438
2001
  if (rel.type === "refs") {
1439
2002
  if (spec.filter !== void 0) {
1440
2003
  throw new Error(
@@ -1587,7 +2150,16 @@ function buildRefsOperation(rel, targetMeta, select, resultPath) {
1587
2150
  projection: projectionFields(select),
1588
2151
  resultPath,
1589
2152
  sourceField,
1590
- sourceList: { from: refs.from, key: refs.key }
2153
+ // #289 P2-5: the sourceList carries its Phase-3 Map shape prep — `over` is
2154
+ // the Expression-IR ref to the parent list attribute the Map iterates, `as`
2155
+ // is the per-element binding name. Descriptive only in Phase 2 (behavior is
2156
+ // still driven by `from`/`key`); Phase 3 lowers the fan-out body onto them.
2157
+ sourceList: {
2158
+ from: refs.from,
2159
+ key: refs.key,
2160
+ over: { ref: ["result", refs.from] },
2161
+ as: `$${refs.key}`
2162
+ }
1591
2163
  };
1592
2164
  }
1593
2165
  function injectRefsSourceProjections(ops) {
@@ -1595,11 +2167,11 @@ function injectRefsSourceProjections(ops) {
1595
2167
  const op = ops[i];
1596
2168
  if (op.sourceList === void 0) continue;
1597
2169
  const tokens = op.resultPath.split(".");
1598
- const parentPath = tokens.slice(0, -2).join(".") || "$";
1599
- const parentIndex = ops.findIndex((p) => p.resultPath === parentPath);
2170
+ const parentPath2 = tokens.slice(0, -2).join(".") || "$";
2171
+ const parentIndex = ops.findIndex((p) => p.resultPath === parentPath2);
1600
2172
  if (parentIndex === -1) {
1601
2173
  throw new Error(
1602
- `refs fan-out at '${op.resultPath}': no parent operation at '${parentPath}'.`
2174
+ `refs fan-out at '${op.resultPath}': no parent operation at '${parentPath2}'.`
1603
2175
  );
1604
2176
  }
1605
2177
  const parent = ops[parentIndex];
@@ -1728,6 +2300,27 @@ function buildQuerySpec(def) {
1728
2300
  ...def.description !== void 0 ? { description: def.description } : {}
1729
2301
  };
1730
2302
  }
2303
+ function toPortableQuery(name, spec) {
2304
+ const root = spec.operations[0];
2305
+ if (root === void 0 || root.resultPath !== "$") {
2306
+ throw new Error(
2307
+ `toPortableQuery('${name}'): expected the root op at operations[0] (resultPath '$'); got ${root === void 0 ? "no operations" : `'${root.resultPath}'`}.`
2308
+ );
2309
+ }
2310
+ const { resultPath: _rp, sourceField: _sf, sourceList: _sl, ...portableRoot } = root;
2311
+ return {
2312
+ params: spec.params,
2313
+ operations: [portableRoot],
2314
+ ...spec.cardinality !== void 0 ? { cardinality: spec.cardinality } : {},
2315
+ ...spec.description !== void 0 ? { description: spec.description } : {}
2316
+ };
2317
+ }
2318
+ function toPortableTransaction(spec) {
2319
+ return {
2320
+ params: spec.params,
2321
+ ...spec.batchWrite === true ? { batchWrite: true } : {}
2322
+ };
2323
+ }
1731
2324
  function mergeSpecs(base, synthesized, kind) {
1732
2325
  for (const name of Object.keys(synthesized)) {
1733
2326
  if (Object.prototype.hasOwnProperty.call(base, name)) {
@@ -1759,16 +2352,55 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
1759
2352
  mergeSpecs(transactionSpecs, built.transactions, "transaction");
1760
2353
  const contractSpecs = built.contracts;
1761
2354
  const contextSpecs = buildContexts(contextInputMap);
2355
+ const components = buildComponents(querySpecs, transactionSpecs);
2356
+ const behaviorNames = /* @__PURE__ */ new Set();
2357
+ {
2358
+ const behaviorInputs = contractInputs.behaviors ?? {};
2359
+ const taken = new Set(components.map((c) => c.name));
2360
+ for (const key of Object.keys(behaviorInputs).sort()) {
2361
+ for (const c of behaviorInputs[key].components) {
2362
+ const name = `${key}__${c.name}`;
2363
+ if (taken.has(name)) {
2364
+ throw new Error(
2365
+ `publishBehaviors registration '${key}' produced component '${name}', which collides with another emitted component. Rename the behavior registration or the method.`
2366
+ );
2367
+ }
2368
+ taken.add(name);
2369
+ behaviorNames.add(name);
2370
+ components.push({ ...c, name });
2371
+ }
2372
+ }
2373
+ if (behaviorNames.size > 0) assertComponentsInCatalog(components);
2374
+ }
2375
+ assertPublishSplitMatchesDerived(
2376
+ deriveComponentEffects(components),
2377
+ {
2378
+ queries: new Set(Object.keys(querySpecs)),
2379
+ transactions: new Set(Object.keys(transactionSpecs)),
2380
+ commands: new Set(Object.keys(commandSpecs)),
2381
+ behaviors: behaviorNames
2382
+ },
2383
+ contractSpecs
2384
+ );
2385
+ const portableQueries = {};
2386
+ for (const [name, spec] of Object.entries(querySpecs)) {
2387
+ portableQueries[name] = toPortableQuery(name, spec);
2388
+ }
2389
+ const portableTransactions = {};
2390
+ for (const [name, spec] of Object.entries(transactionSpecs)) {
2391
+ portableTransactions[name] = toPortableTransaction(spec);
2392
+ }
1762
2393
  const content = {
1763
- queries: querySpecs,
2394
+ queries: portableQueries,
1764
2395
  commands: commandSpecs,
1765
- ...Object.keys(transactionSpecs).length > 0 ? { transactions: transactionSpecs } : {},
2396
+ ...Object.keys(portableTransactions).length > 0 ? { transactions: portableTransactions } : {},
1766
2397
  ...Object.keys(contractSpecs).length > 0 ? { contracts: contractSpecs } : {},
1767
2398
  ...Object.keys(contextSpecs).length > 0 ? { contexts: contextSpecs } : {}
1768
2399
  };
1769
2400
  return {
1770
2401
  version: operationsSpecVersion(content),
1771
- ...content
2402
+ ...content,
2403
+ ...components.length > 0 ? { components } : {}
1772
2404
  };
1773
2405
  }
1774
2406
 
@@ -1957,7 +2589,9 @@ function buildPreparedPlanDocument(plans) {
1957
2589
  for (const id of Object.keys(plans).sort()) sorted[id] = plans[id];
1958
2590
  return {
1959
2591
  formatVersion: PREPARED_FORMAT_VERSION,
1960
- specVersion: SPEC_VERSION,
2592
+ // #289 Phase 2: the embedded operations IR is 2.0 (major bump); the prepared
2593
+ // artifact's specVersion tracks it so a version-skewed plan fails closed.
2594
+ specVersion: SPEC_VERSION_KEY_EXPR,
1961
2595
  plans: sorted
1962
2596
  };
1963
2597
  }
@@ -1973,12 +2607,17 @@ function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegis
1973
2607
  }
1974
2608
 
1975
2609
  export {
2610
+ renderKeyExprToTemplate,
1976
2611
  collectContractN1Violations,
1977
2612
  assertContractN1Safe,
1978
2613
  buildContracts,
1979
2614
  buildContexts,
1980
2615
  collectContractBoundaryViolations,
1981
2616
  assertContractBoundaries,
2617
+ buildQueryComponent,
2618
+ buildTransactionComponent,
2619
+ buildComponents,
2620
+ portableIrDocument,
1982
2621
  buildQuerySpec,
1983
2622
  buildOperations,
1984
2623
  compilePreparedPlan,