graphddb 0.2.5 → 0.3.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.
@@ -6,10 +6,12 @@ import {
6
6
  MetadataRegistry,
7
7
  NO_MIDDLEWARE,
8
8
  TableMapping,
9
+ ViewRegistry,
9
10
  attachHiddenKey,
10
11
  attachModelClass,
11
12
  buildConditionExpression,
12
13
  buildDeleteInput,
14
+ buildMaintenanceGraph,
13
15
  buildPutInput,
14
16
  buildReadRuntime,
15
17
  buildUpdateInput,
@@ -25,9 +27,13 @@ import {
25
27
  executePut,
26
28
  executeTransaction,
27
29
  executeUpdate,
30
+ getEntityWrites,
28
31
  isColumn,
32
+ isEntityWritesDefinition,
33
+ isLifecycleContract,
29
34
  isParam,
30
35
  isRawCondition,
36
+ lifecyclePhaseForIntent,
31
37
  param,
32
38
  pkTemplate,
33
39
  resolveKey,
@@ -37,7 +43,7 @@ import {
37
43
  serializeFieldValue,
38
44
  serializeRawCondition,
39
45
  skTemplate
40
- } from "./chunk-4TYK3AV6.js";
46
+ } from "./chunk-YIXXTGZ6.js";
41
47
 
42
48
  // src/spec/types.ts
43
49
  var SPEC_VERSION = "1.0";
@@ -1756,94 +1762,6 @@ async function executeBatchWrite(requests) {
1756
1762
  }
1757
1763
  }
1758
1764
 
1759
- // src/define/entity-writes.ts
1760
- var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
1761
- "graphddb:lifecycleContract"
1762
- );
1763
- function isLifecycleContract(value) {
1764
- return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
1765
- }
1766
- var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
1767
- function isEntityWritesDefinition(value) {
1768
- return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
1769
- }
1770
- function freezeEffects(effects) {
1771
- const out = {};
1772
- if (effects.requires !== void 0) out.requires = effects.requires;
1773
- if (effects.unique !== void 0) out.unique = effects.unique;
1774
- if (effects.edges !== void 0) out.edges = effects.edges;
1775
- if (effects.derive !== void 0) out.derive = effects.derive;
1776
- if (effects.emits !== void 0) out.emits = effects.emits;
1777
- if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
1778
- return out;
1779
- }
1780
- var recorder = {
1781
- lifecycle(effects = {}) {
1782
- return {
1783
- [LIFECYCLE_CONTRACT_MARKER]: true,
1784
- effects: freezeEffects(effects)
1785
- };
1786
- },
1787
- exists(targetFactory, keys) {
1788
- return { kind: "requires", targetFactory, keys };
1789
- },
1790
- unique(spec) {
1791
- return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
1792
- },
1793
- putEdge(targetFactory, relationProperty) {
1794
- return { kind: "putEdge", targetFactory, relationProperty };
1795
- },
1796
- deleteEdge(targetFactory, relationProperty) {
1797
- return { kind: "deleteEdge", targetFactory, relationProperty };
1798
- },
1799
- increment(targetFactory, keys, attribute, amount) {
1800
- return { kind: "derive", targetFactory, keys, attribute, amount };
1801
- },
1802
- event(name, payload) {
1803
- return { kind: "event", name, payload };
1804
- },
1805
- idempotentBy(token) {
1806
- return { kind: "idempotency", token };
1807
- }
1808
- };
1809
- function entityWrites(builder) {
1810
- const shape = builder(recorder);
1811
- for (const phase of ["create", "update", "remove"]) {
1812
- const contract = shape[phase];
1813
- if (contract !== void 0 && !isLifecycleContract(contract)) {
1814
- throw new Error(
1815
- `entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
1816
- );
1817
- }
1818
- }
1819
- if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
1820
- throw new Error(
1821
- "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
1822
- );
1823
- }
1824
- return {
1825
- [ENTITY_WRITES_MARKER]: true,
1826
- ...shape.create !== void 0 ? { create: shape.create } : {},
1827
- ...shape.update !== void 0 ? { update: shape.update } : {},
1828
- ...shape.remove !== void 0 ? { remove: shape.remove } : {}
1829
- };
1830
- }
1831
- function getEntityWrites(modelClass) {
1832
- for (const name of Object.getOwnPropertyNames(modelClass)) {
1833
- let value;
1834
- try {
1835
- value = modelClass[name];
1836
- } catch {
1837
- continue;
1838
- }
1839
- if (isEntityWritesDefinition(value)) return value;
1840
- }
1841
- return void 0;
1842
- }
1843
- function lifecyclePhaseForIntent(intent) {
1844
- return intent;
1845
- }
1846
-
1847
1765
  // src/relation/edge-write.ts
1848
1766
  var OLD_VALUE_NAMESPACE = "old";
1849
1767
  var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
@@ -1853,13 +1771,22 @@ function isEdgeWritesDefinition(value) {
1853
1771
  function declaration(target, relationProperty) {
1854
1772
  return { targetFactory: target, relationProperty };
1855
1773
  }
1856
- var recorder2 = {
1774
+ var recorder = {
1857
1775
  putEdge: declaration,
1858
1776
  deleteEdge: declaration,
1859
- updateKeyChangeEdge: declaration
1777
+ updateKeyChangeEdge: declaration,
1778
+ dualEdge: (forward, inverse) => ({
1779
+ targetFactory: forward.target,
1780
+ relationProperty: forward.relationProperty,
1781
+ inverse: {
1782
+ adjacencyFactory: inverse.adjacency,
1783
+ targetFactory: inverse.target,
1784
+ relationProperty: inverse.relationProperty
1785
+ }
1786
+ })
1860
1787
  };
1861
1788
  function edgeWrites(builder) {
1862
- const edges = builder(recorder2);
1789
+ const edges = builder(recorder);
1863
1790
  if (edges.length === 0) {
1864
1791
  throw new Error(
1865
1792
  "edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
@@ -1901,6 +1828,13 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
1901
1828
  }
1902
1829
  return relation;
1903
1830
  }
1831
+ function assertDualEdge(forwardAdjacency, inverseAdjacency, decl) {
1832
+ if (inverseAdjacency === forwardAdjacency) {
1833
+ throw new Error(
1834
+ `Dual-edge declaration for forward relation '${decl.relationProperty}' on adjacency entity '${forwardAdjacency.name}' points its inverse edge at the SAME adjacency model. A dual edge is TWO physical rows (forward + inverse); both directions resolving to one model would write the same key twice in a transaction (cf. #96). Declare the inverse on a DISTINCT adjacency model, or drop \`inverse\` for the single-row (base PK + inverse GSI) bidirectional edge.`
1835
+ );
1836
+ }
1837
+ }
1904
1838
  function templateRecord(fields) {
1905
1839
  const out = {};
1906
1840
  for (const field of [...fields].sort()) {
@@ -1919,6 +1853,25 @@ function putItemTemplate(edgeFields) {
1919
1853
  return templateRecord(edgeFields);
1920
1854
  }
1921
1855
  function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
1856
+ const forward = deriveOneEdgeRow(
1857
+ adjacencyClass,
1858
+ { targetFactory: decl.targetFactory, relationProperty: decl.relationProperty },
1859
+ lifecycle
1860
+ );
1861
+ if (decl.inverse === void 0) return forward;
1862
+ const inverseAdjacency = decl.inverse.adjacencyFactory();
1863
+ assertDualEdge(adjacencyClass, inverseAdjacency, decl);
1864
+ const inverse = deriveOneEdgeRow(
1865
+ inverseAdjacency,
1866
+ {
1867
+ targetFactory: decl.inverse.targetFactory,
1868
+ relationProperty: decl.inverse.relationProperty
1869
+ },
1870
+ lifecycle
1871
+ );
1872
+ return [...forward, ...inverse];
1873
+ }
1874
+ function deriveOneEdgeRow(adjacencyClass, decl, lifecycle) {
1922
1875
  const entity = MetadataRegistry.get(adjacencyClass);
1923
1876
  if (!entity.primaryKey) {
1924
1877
  throw new Error(
@@ -2234,6 +2187,13 @@ function renderOutboxEventItems(event, key, params, contextLabel) {
2234
2187
  return { Put: put };
2235
2188
  });
2236
2189
  }
2190
+ function renderMaintainOutboxItems(outbox, key, params, contextLabel) {
2191
+ return outbox.items.map((item) => {
2192
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} maintain-outbox Put`);
2193
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2194
+ return { Put: put };
2195
+ });
2196
+ }
2237
2197
  function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2238
2198
  return guard.items.map((item) => {
2239
2199
  const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
@@ -2245,7 +2205,131 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2245
2205
  return { Put: put };
2246
2206
  });
2247
2207
  }
2248
- async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
2208
+ function applyProjectionTransform(op, args, value, contextLabel) {
2209
+ if (op === "identity") return value;
2210
+ if (op === "preview") {
2211
+ const n = args[0];
2212
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
2213
+ throw new Error(
2214
+ `Contract runtime: ${contextLabel} declares a \`preview\` projection with a non-positive-integer length bound (${JSON.stringify(n)}). A preview length must be a positive integer.`
2215
+ );
2216
+ }
2217
+ if (value === null || value === void 0) return value;
2218
+ return String(value).slice(0, n);
2219
+ }
2220
+ throw new Error(
2221
+ `Contract runtime: ${contextLabel} declares an unknown projection transform op '${String(op)}' (expected 'identity' or 'preview').`
2222
+ );
2223
+ }
2224
+ function buildMaintainProjection(maintain, key, params, contextLabel) {
2225
+ const out = {};
2226
+ for (const [attr, transform] of Object.entries(maintain.projection)) {
2227
+ const value = renderTemplate(
2228
+ `{${transform.inputField}}`,
2229
+ key,
2230
+ params,
2231
+ `${contextLabel} projection '${attr}'`
2232
+ );
2233
+ out[attr] = applyProjectionTransform(transform.op, transform.args, value, `${contextLabel} '${attr}'`);
2234
+ }
2235
+ return out;
2236
+ }
2237
+ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySignature) {
2238
+ const ownerClass = maintain.entity.modelClass;
2239
+ const metadata = MetadataRegistry.get(ownerClass);
2240
+ if (!metadata.primaryKey) {
2241
+ throw new Error(
2242
+ `Contract runtime: ${contextLabel} maintains a snapshot / collection on '${maintain.entity.name}', which has no primary key.`
2243
+ );
2244
+ }
2245
+ const keyInput = {};
2246
+ for (const [ownerField, inputField] of Object.entries(maintain.keyBinding)) {
2247
+ keyInput[ownerField] = renderTemplate(
2248
+ `{${inputField}}`,
2249
+ key,
2250
+ params,
2251
+ `${contextLabel} maintain key`
2252
+ );
2253
+ }
2254
+ const { TableName, Key } = buildDeleteInput(
2255
+ ownerClass,
2256
+ keyInput
2257
+ );
2258
+ let updateInput;
2259
+ if (maintain.kind === "counter") {
2260
+ const counter = maintain.counter;
2261
+ if (counter === void 0) {
2262
+ throw new Error(
2263
+ `Contract runtime: ${contextLabel} is a 'counter' maintenance write with no \`counter\` payload \u2014 the compiler (#141) should have set it.`
2264
+ );
2265
+ }
2266
+ if (counter.op !== "count") {
2267
+ throw new Error(
2268
+ `Contract runtime: ${contextLabel} is a 'counter' maintenance write with op '${counter.op}', which is the asynchronous stream path (#130) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
2269
+ );
2270
+ }
2271
+ updateInput = {
2272
+ TableName,
2273
+ Key,
2274
+ UpdateExpression: "ADD #a0 :a0",
2275
+ ExpressionAttributeNames: { "#a0": counter.attribute },
2276
+ ExpressionAttributeValues: { ":a0": counter.delta }
2277
+ };
2278
+ const out2 = { Update: updateInput };
2279
+ modelBySignature?.set(execItemKeySignature(out2), ownerClass.name);
2280
+ return out2;
2281
+ }
2282
+ if (maintain.kind === "membership") {
2283
+ throw new Error(
2284
+ `Contract runtime: ${contextLabel} is a 'membership' (sparse-view) maintenance write, which is stream-only (#133) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
2285
+ );
2286
+ }
2287
+ const projection = buildMaintainProjection(maintain, key, params, contextLabel);
2288
+ if (maintain.kind === "collection") {
2289
+ const field = maintain.collection?.field;
2290
+ if (field === void 0) {
2291
+ throw new Error(
2292
+ `Contract runtime: ${contextLabel} is a 'collection' maintenance write with no \`collection.field\` \u2014 the compiler (#125) should have set it.`
2293
+ );
2294
+ }
2295
+ updateInput = {
2296
+ TableName,
2297
+ Key,
2298
+ UpdateExpression: "SET #c = list_append(if_not_exists(#c, :empty), :item)",
2299
+ ExpressionAttributeNames: { "#c": field },
2300
+ ExpressionAttributeValues: { ":empty": [], ":item": [projection] }
2301
+ };
2302
+ } else {
2303
+ const setClauses = [];
2304
+ const names = {};
2305
+ const values = {};
2306
+ let i = 0;
2307
+ for (const [attr, value] of Object.entries(projection)) {
2308
+ const nameTok = `#m${i}`;
2309
+ const valTok = `:m${i}`;
2310
+ setClauses.push(`${nameTok} = ${valTok}`);
2311
+ names[nameTok] = attr;
2312
+ values[valTok] = value;
2313
+ i += 1;
2314
+ }
2315
+ if (setClauses.length === 0) {
2316
+ throw new Error(
2317
+ `Contract runtime: ${contextLabel} is a 'snapshot' maintenance write with an empty projection \u2014 nothing to mirror onto '${maintain.entity.name}'.`
2318
+ );
2319
+ }
2320
+ updateInput = {
2321
+ TableName,
2322
+ Key,
2323
+ UpdateExpression: `SET ${setClauses.join(", ")}`,
2324
+ ExpressionAttributeNames: names,
2325
+ ExpressionAttributeValues: values
2326
+ };
2327
+ }
2328
+ const out = { Update: updateInput };
2329
+ modelBySignature?.set(execItemKeySignature(out), ownerClass.name);
2330
+ return out;
2331
+ }
2332
+ async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, maintainOutboxItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
2249
2333
  const writeItems = writes.map((w) => {
2250
2334
  const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2251
2335
  let item;
@@ -2268,6 +2352,8 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
2268
2352
  ...guardItemsRaw,
2269
2353
  ...outboxItems,
2270
2354
  ...idempotencyItems,
2355
+ ...maintainItems,
2356
+ ...maintainOutboxItems,
2271
2357
  ...checkItems
2272
2358
  ],
2273
2359
  {
@@ -2275,7 +2361,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
2275
2361
  ...retry !== void 0 ? { retry } : {},
2276
2362
  ...middleware !== void 0 ? { middleware } : {},
2277
2363
  limitError: (count) => new Error(
2278
- `Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
2364
+ `Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + maintenance writes + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
2279
2365
  )
2280
2366
  }
2281
2367
  );
@@ -2335,10 +2421,10 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
2335
2421
  `Contract runtime: command method '${methodName}' is a multi-fragment mutation (it composes ${method.ops.length} fragments into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
2336
2422
  );
2337
2423
  }
2338
- const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.idempotencyGuard !== void 0;
2424
+ const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.maintainWrites !== void 0 && method.maintainWrites.length > 0 || method.maintainOutbox !== void 0 && method.maintainOutbox.length > 0 || method.idempotencyGuard !== void 0;
2339
2425
  if (hasDerivedEffects2) {
2340
2426
  throw new Error(
2341
- `Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
2427
+ `Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard / maintenance writes \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
2342
2428
  );
2343
2429
  }
2344
2430
  const writes = keys.map(
@@ -2357,7 +2443,9 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
2357
2443
  const uniqueGuards = method.uniqueGuards ?? [];
2358
2444
  const outboxEvents = method.outboxEvents ?? [];
2359
2445
  const idempotencyGuard = method.idempotencyGuard;
2360
- const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
2446
+ const maintainWrites = method.maintainWrites ?? [];
2447
+ const maintainOutbox = method.maintainOutbox ?? [];
2448
+ const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || maintainWrites.length > 0 || maintainOutbox.length > 0 || idempotencyGuard !== void 0;
2361
2449
  const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
2362
2450
  if (isMultiFragment || hasDerivedEffects) {
2363
2451
  const ops = method.ops ?? [method.op];
@@ -2396,6 +2484,18 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
2396
2484
  params,
2397
2485
  `command method '${methodName}' (idempotency)`
2398
2486
  ) : [];
2487
+ const maintainItems = maintainWrites.map(
2488
+ (m, i) => renderMaintainWriteItem(
2489
+ m,
2490
+ key,
2491
+ params,
2492
+ `command method '${methodName}' (maintain #${i})`,
2493
+ modelBySignature
2494
+ )
2495
+ );
2496
+ const maintainOutboxItems = maintainOutbox.flatMap(
2497
+ (mo, i) => renderMaintainOutboxItems(mo, key, params, `command method '${methodName}' (maintain-outbox #${i})`)
2498
+ );
2399
2499
  const checks = conditionChecks.map((check, i) => ({
2400
2500
  check: renderConditionCheck(
2401
2501
  check,
@@ -2411,6 +2511,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
2411
2511
  guardItems,
2412
2512
  outboxItems,
2413
2513
  idempotencyItems,
2514
+ maintainItems,
2515
+ maintainOutboxItems,
2414
2516
  checks,
2415
2517
  methodName,
2416
2518
  modelBySignature
@@ -2439,6 +2541,8 @@ function renderCompiledOp(op, ctx) {
2439
2541
  const guardItems = [];
2440
2542
  const outboxItems = [];
2441
2543
  const idempotencyItems = [];
2544
+ const maintainItems = [];
2545
+ const maintainOutboxItems = [];
2442
2546
  const checkItems = [];
2443
2547
  for (const edge of fragment.edgeWrites ?? []) {
2444
2548
  edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
@@ -2457,10 +2561,20 @@ function renderCompiledOp(op, ctx) {
2457
2561
  ...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
2458
2562
  );
2459
2563
  }
2564
+ for (const m of fragment.maintainWrites ?? []) {
2565
+ maintainItems.push(
2566
+ renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
2567
+ );
2568
+ }
2569
+ for (const mo of fragment.maintainOutbox ?? []) {
2570
+ maintainOutboxItems.push(
2571
+ ...renderMaintainOutboxItems(mo, op.params, op.params, `${ctx} maintain-outbox`)
2572
+ );
2573
+ }
2460
2574
  for (const c of fragment.conditionChecks ?? []) {
2461
2575
  checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
2462
2576
  }
2463
- const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
2577
+ const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || maintainOutboxItems.length > 0 || checkItems.length > 0;
2464
2578
  return {
2465
2579
  write,
2466
2580
  edgeItems,
@@ -2468,6 +2582,8 @@ function renderCompiledOp(op, ctx) {
2468
2582
  guardItems,
2469
2583
  outboxItems,
2470
2584
  idempotencyItems,
2585
+ maintainItems,
2586
+ maintainOutboxItems,
2471
2587
  checkItems,
2472
2588
  modelBySignature,
2473
2589
  hasDerivedEffects
@@ -2512,6 +2628,8 @@ async function executeCompiledWriteOp(ops, options) {
2512
2628
  const guardItems = [];
2513
2629
  const outboxItems = [];
2514
2630
  const idempotencyItems = [];
2631
+ const maintainItems = [];
2632
+ const maintainOutboxItems = [];
2515
2633
  const checkItems = [];
2516
2634
  for (const r of renderedOps) {
2517
2635
  for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
@@ -2520,6 +2638,8 @@ async function executeCompiledWriteOp(ops, options) {
2520
2638
  guardItems.push(...r.guardItems);
2521
2639
  outboxItems.push(...r.outboxItems);
2522
2640
  idempotencyItems.push(...r.idempotencyItems);
2641
+ maintainItems.push(...r.maintainItems);
2642
+ maintainOutboxItems.push(...r.maintainOutboxItems);
2523
2643
  checkItems.push(...r.checkItems);
2524
2644
  }
2525
2645
  const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
@@ -2538,6 +2658,8 @@ async function executeCompiledWriteOp(ops, options) {
2538
2658
  guardItems,
2539
2659
  outboxItems,
2540
2660
  idempotencyItems,
2661
+ maintainItems,
2662
+ maintainOutboxItems,
2541
2663
  checkItems,
2542
2664
  label,
2543
2665
  modelBySignature,
@@ -2635,6 +2757,8 @@ function rewrittenCompiledOp(write) {
2635
2757
  guardItems: [],
2636
2758
  outboxItems: [],
2637
2759
  idempotencyItems: [],
2760
+ maintainItems: [],
2761
+ maintainOutboxItems: [],
2638
2762
  checkItems: [],
2639
2763
  modelBySignature: /* @__PURE__ */ new Map(),
2640
2764
  hasDerivedEffects: false
@@ -3517,6 +3641,9 @@ function buildPlannedCommandMethod(name, planned) {
3517
3641
  );
3518
3642
  }
3519
3643
  const idempotencyGuard = idempotencyGuards[0];
3644
+ const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
3645
+ assertNoCrossFragmentMaintainCollision(name, maintainWrites);
3646
+ const maintainOutbox = plan2.fragments.flatMap((f) => f.maintainOutbox ?? []);
3520
3647
  return {
3521
3648
  __methodKind: "command",
3522
3649
  op,
@@ -3527,6 +3654,8 @@ function buildPlannedCommandMethod(name, planned) {
3527
3654
  ...uniqueGuards.length > 0 ? { uniqueGuards } : {},
3528
3655
  ...outboxEvents.length > 0 ? { outboxEvents } : {},
3529
3656
  ...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
3657
+ ...maintainWrites.length > 0 ? { maintainWrites } : {},
3658
+ ...maintainOutbox.length > 0 ? { maintainOutbox } : {},
3530
3659
  inputArity: "either",
3531
3660
  // A return projection means the method returns the read-back entity; otherwise
3532
3661
  // it is a fire-and-forget write (`void`).
@@ -3814,6 +3943,210 @@ function deriveOneUpdate(fragment, effect) {
3814
3943
  amount: effect.amount
3815
3944
  };
3816
3945
  }
3946
+ var INTENT_MAINTAIN_EVENT = {
3947
+ create: "created",
3948
+ update: "updated",
3949
+ remove: "removed"
3950
+ };
3951
+ var MAINTAIN_ENTITY_PATH_RE = /^\$\.entity\.([A-Za-z_$][\w$]*)$/;
3952
+ var cachedMaintenanceGraph;
3953
+ function globalMaintenanceGraph() {
3954
+ const generation = MetadataRegistry.generation;
3955
+ const viewGeneration = ViewRegistry.generation;
3956
+ if (cachedMaintenanceGraph?.generation !== generation || cachedMaintenanceGraph?.viewGeneration !== viewGeneration) {
3957
+ cachedMaintenanceGraph = { graph: buildMaintenanceGraph(), generation, viewGeneration };
3958
+ }
3959
+ return cachedMaintenanceGraph.graph;
3960
+ }
3961
+ function logicalEntityName(fragment) {
3962
+ const meta = MetadataRegistry.get(
3963
+ fragment.entity.modelClass
3964
+ );
3965
+ return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
3966
+ }
3967
+ function resolveMaintainers(fragment, graph) {
3968
+ const g = graph ?? globalMaintenanceGraph();
3969
+ const event = INTENT_MAINTAIN_EVENT[fragment.intent];
3970
+ const trigger = `${logicalEntityName(fragment)}.${event}`;
3971
+ return g.effectsFor(trigger);
3972
+ }
3973
+ function maintainPathToInputField(fragment, item, role, path) {
3974
+ const entityMatch = MAINTAIN_ENTITY_PATH_RE.exec(path);
3975
+ if (entityMatch !== null) return entityMatch[1];
3976
+ const inputMatch = INPUT_PATH_RE.exec(path);
3977
+ if (inputMatch !== null) return inputMatch[1];
3978
+ throw new Error(
3979
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a maintenance effect (relation '${item.relationProperty}' on '${item.ownerEntity}') whose ${role} binds to ${JSON.stringify(path)}, which is not a payload-rooted \`$.entity.<field>\` / \`$.input.<field>\` source. A maintenance write projects only from the written source row (payload \u540C\u68B1) \u2014 fix the relation's projection / key binding.`
3980
+ );
3981
+ }
3982
+ function deriveMaintainItems(fragment, items) {
3983
+ const byRow = /* @__PURE__ */ new Map();
3984
+ for (const item of items) {
3985
+ const prior = byRow.get(item.destinationRowKey);
3986
+ if (prior !== void 0) {
3987
+ const bothCounter = item.effect.kind === "counter" && prior.effect.kind === "counter";
3988
+ if (!bothCounter) {
3989
+ throw new Error(
3990
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires TWO maintenance effects that target the SAME row (relation '${prior.relationProperty}' and '${item.relationProperty}' on '${item.ownerEntity}', destination '${item.destinationRowKey}'). Phase 1 constrains a mutation to ONE non-counter maintenance effect per target row (\u8AD6\u70B92 = b) \u2014 a \`TransactWriteItems\` may not touch one key twice with a snapshot/collection write, and merging those is a separate future issue. (Counter ADDs are exempt \u2014 they merge. Consistent with the #96 same-row reject.)`
3991
+ );
3992
+ }
3993
+ } else {
3994
+ byRow.set(item.destinationRowKey, item);
3995
+ }
3996
+ }
3997
+ return items.map((item) => deriveOneMaintainWrite(fragment, item));
3998
+ }
3999
+ function maintainWriteRowKey(write) {
4000
+ const parts = Object.entries(write.keyBinding).map(([field, inputField]) => `${field}=${inputField}`).sort();
4001
+ return `${write.entity.name}#${parts.join("&")}`;
4002
+ }
4003
+ function assertNoCrossFragmentMaintainCollision(methodName, writes) {
4004
+ const byRow = /* @__PURE__ */ new Map();
4005
+ for (const write of writes) {
4006
+ const rowKey = maintainWriteRowKey(write);
4007
+ const prior = byRow.get(rowKey);
4008
+ if (prior !== void 0) {
4009
+ const bothCounter = write.kind === "counter" && prior.kind === "counter";
4010
+ if (!bothCounter) {
4011
+ throw new Error(
4012
+ `publicCommandModel: planned method '${methodName}' aggregates TWO maintenance effects that resolve to the SAME owner row (relation '${prior.relationProperty}' and '${write.relationProperty}' on '${write.entity.name}', destination '${rowKey}'), across different mutation fragments. Phase 1 constrains a mutation to ONE non-counter maintenance effect per target row (\u8AD6\u70B92 = b) \u2014 this holds for the WHOLE mutation, not just within a single fragment. Multiple snapshot/collection maintain effects resolving to one owner row is the true cause here (NOT a derived-counter merge failure); a \`TransactWriteItems\` may not touch one key twice with a snapshot/collection write, and merging those is a separate future issue. (Counter ADDs are exempt \u2014 they merge. Consistent with the intra-fragment reject and the #96 same-row reject.)`
4013
+ );
4014
+ }
4015
+ } else {
4016
+ byRow.set(rowKey, write);
4017
+ }
4018
+ }
4019
+ }
4020
+ function deriveOneMaintainWrite(fragment, item) {
4021
+ const effect = item.effect;
4022
+ const updateMode = effect.updateMode === "stream" ? "stream" : "mutation";
4023
+ const ownerClass = effect.targetFactory();
4024
+ const ownerMeta = MetadataRegistry.get(
4025
+ ownerClass
4026
+ );
4027
+ const ownerName = ownerClass.name;
4028
+ const keyBinding = {};
4029
+ for (const [ownerField, source] of Object.entries(effect.keys)) {
4030
+ keyBinding[ownerField] = maintainPathToInputField(
4031
+ fragment,
4032
+ item,
4033
+ `destination-key field '${ownerField}'`,
4034
+ source
4035
+ );
4036
+ }
4037
+ if (effect.kind === "counter") {
4038
+ return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode);
4039
+ }
4040
+ const projection = {};
4041
+ for (const [attr, transform] of Object.entries(effect.project)) {
4042
+ const inputField = maintainPathToInputField(
4043
+ fragment,
4044
+ item,
4045
+ `projection attribute '${attr}'`,
4046
+ transform.path
4047
+ );
4048
+ projection[attr] = { op: transform.op, args: transform.args, inputField };
4049
+ }
4050
+ if (!ownerMeta.primaryKey) {
4051
+ throw new Error(
4052
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a maintenance effect on '${ownerName}', which has no primary key \u2014 a maintenance write keys a single target row, so the maintained model must declare one.`
4053
+ );
4054
+ }
4055
+ const base = {
4056
+ entity: { name: ownerName, modelClass: ownerClass },
4057
+ relationProperty: item.relationProperty,
4058
+ trigger: item.trigger,
4059
+ updateMode,
4060
+ keyBinding,
4061
+ projection
4062
+ };
4063
+ if (effect.kind === "membership") {
4064
+ if (updateMode !== "stream") {
4065
+ throw new Error(
4066
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a sparse-view membership effect (view '${item.ownerEntity}', relation '${item.relationProperty}') on the SYNCHRONOUS \`mutation\` path. A membership row is PUT when its predicate holds and DELETED when it flips false; a single atomic transaction cannot branch an item between Put and Delete on a runtime predicate. Declare \`write: { updateMode: 'stream' }\` so the host-side drain applies the put/delete (#133 sparse view is the asynchronous stream path).`
4067
+ );
4068
+ }
4069
+ const predInput = maintainPathToInputField(
4070
+ fragment,
4071
+ item,
4072
+ "membership predicate",
4073
+ effect.predicate.path
4074
+ );
4075
+ return {
4076
+ ...base,
4077
+ kind: "membership",
4078
+ membership: {
4079
+ op: effect.predicate.op,
4080
+ inputField: predInput,
4081
+ ...effect.predicate.value !== void 0 ? { value: effect.predicate.value } : {}
4082
+ }
4083
+ };
4084
+ }
4085
+ if (effect.kind === "collection") {
4086
+ const orderBy = effect.collection.orderBy !== void 0 ? maintainPathToInputField(fragment, item, "collection `orderBy`", effect.collection.orderBy) : void 0;
4087
+ return {
4088
+ ...base,
4089
+ kind: "collection",
4090
+ collection: {
4091
+ field: effect.collection.field,
4092
+ ...effect.collection.maxItems !== void 0 ? { maxItems: effect.collection.maxItems } : {},
4093
+ ...orderBy !== void 0 ? { orderBy } : {},
4094
+ ...orderBy !== void 0 && effect.collection.orderDir !== void 0 ? { orderDir: effect.collection.orderDir } : {}
4095
+ }
4096
+ };
4097
+ }
4098
+ return { ...base, kind: "snapshot" };
4099
+ }
4100
+ function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode) {
4101
+ if (effect.value.op === "max") {
4102
+ if (updateMode !== "stream") {
4103
+ throw new Error(
4104
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect (\`@aggregate\` field '${item.relationProperty}' on '${item.ownerEntity}') declared \`value: max('${effect.value.field}')\` on the SYNCHRONOUS \`mutation\` path. A \`max\` needs a conditional \`SET\` whose failed guard would roll back the SOURCE write that legitimately happened, so a running max is the asynchronous stream path (#130). Use \`value: count()\` for a synchronous counter, or declare \`updateMode: 'stream'\` on the \`@aggregate\` to realize the running max.`
4105
+ );
4106
+ }
4107
+ if (!ownerMeta.primaryKey) {
4108
+ throw new Error(
4109
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a max counter maintenance effect on '${ownerName}', which has no primary key.`
4110
+ );
4111
+ }
4112
+ return {
4113
+ entity: { name: ownerName, modelClass: ownerClass },
4114
+ relationProperty: item.relationProperty,
4115
+ trigger: item.trigger,
4116
+ updateMode,
4117
+ kind: "counter",
4118
+ keyBinding,
4119
+ projection: {},
4120
+ counter: { attribute: effect.attribute, op: "max", field: effect.value.field }
4121
+ };
4122
+ }
4123
+ if (effect.delta === void 0) {
4124
+ throw new Error(
4125
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect (\`@aggregate\` field '${item.relationProperty}' on '${item.ownerEntity}') with no \`delta\` \u2014 the maintenance graph (#141) should set it from the trigger event.`
4126
+ );
4127
+ }
4128
+ if (!ownerMeta.primaryKey) {
4129
+ throw new Error(
4130
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect on '${ownerName}', which has no primary key \u2014 a counter \`UpdateItem\` keys a single row, so the maintained model must declare one.`
4131
+ );
4132
+ }
4133
+ const isField = ownerMeta.fields.some((f) => f.propertyName === effect.attribute) || ownerMeta.aggregates.some((a) => a.propertyName === effect.attribute);
4134
+ if (!isField) {
4135
+ throw new Error(
4136
+ `mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' derives a counter on '${ownerName}.${effect.attribute}', which is not a field of '${ownerName}'.`
4137
+ );
4138
+ }
4139
+ return {
4140
+ entity: { name: ownerName, modelClass: ownerClass },
4141
+ relationProperty: item.relationProperty,
4142
+ trigger: item.trigger,
4143
+ updateMode,
4144
+ kind: "counter",
4145
+ keyBinding,
4146
+ projection: {},
4147
+ counter: { attribute: effect.attribute, op: "count", delta: effect.delta }
4148
+ };
4149
+ }
3817
4150
  var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
3818
4151
  var UNIQUE_GUARD_SK_PREFIX = "VALUE#";
3819
4152
  function deriveUniqueGuards(fragment, unique) {
@@ -3993,6 +4326,67 @@ function deriveIdempotencyGuard(fragment, effect) {
3993
4326
  ]
3994
4327
  };
3995
4328
  }
4329
+ var MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
4330
+ var MAINT_OUTBOX_SK_PREFIX = "MAINT#";
4331
+ var MAINT_OUTBOX_OWNER_ATTR = "__maintOwner__";
4332
+ var MAINT_OUTBOX_RELATION_ATTR = "__maintRelation__";
4333
+ var MAINT_OUTBOX_TRIGGER_ATTR = "__maintTrigger__";
4334
+ var MAINT_OUTBOX_EVENT_VALUE = "maint";
4335
+ function deriveMaintainOutbox(fragment, write) {
4336
+ const keyFields = primaryKeyFields(fragment);
4337
+ const discriminator = `${write.entity.name}#${write.relationProperty}`;
4338
+ const keySegments = keyFields.map((f) => `#{${f}}`).join("");
4339
+ const pk = MAINT_OUTBOX_PK_PREFIX + discriminator + keySegments;
4340
+ const sk = MAINT_OUTBOX_SK_PREFIX + keyFields.map((f) => `{${f}}`).join("#");
4341
+ const item = {
4342
+ PK: pk,
4343
+ SK: sk,
4344
+ [OUTBOX_EVENT_NAME_ATTR]: MAINT_OUTBOX_EVENT_VALUE,
4345
+ [MAINT_OUTBOX_OWNER_ATTR]: write.entity.name,
4346
+ [MAINT_OUTBOX_RELATION_ATTR]: write.relationProperty,
4347
+ [MAINT_OUTBOX_TRIGGER_ATTR]: write.trigger
4348
+ };
4349
+ const sourceFields = /* @__PURE__ */ new Set();
4350
+ for (const inputField of Object.values(write.keyBinding)) sourceFields.add(inputField);
4351
+ for (const transform of Object.values(write.projection)) sourceFields.add(transform.inputField);
4352
+ if (write.collection?.orderBy !== void 0) sourceFields.add(write.collection.orderBy);
4353
+ if (write.counter !== void 0 && write.counter.op === "max") {
4354
+ sourceFields.add(write.counter.field);
4355
+ }
4356
+ if (write.membership !== void 0) sourceFields.add(write.membership.inputField);
4357
+ for (const field of sourceFields) {
4358
+ item[field] = `{${field}}`;
4359
+ }
4360
+ return {
4361
+ ownerEntity: write.entity.name,
4362
+ relationProperty: write.relationProperty,
4363
+ trigger: write.trigger,
4364
+ items: [
4365
+ {
4366
+ type: "Put",
4367
+ tableName: tableNameOf(fragment),
4368
+ entity: MARKER_ROW_ENTITY,
4369
+ item,
4370
+ // No `attribute_not_exists`: the row keys off the source entity key, which the
4371
+ // source `Put`'s own create guard already makes unique — the maintenance intent
4372
+ // is recorded atomically with the source row it derives from.
4373
+ literalKey: true
4374
+ }
4375
+ ]
4376
+ };
4377
+ }
4378
+ function splitMaintainByMode(fragment, writes) {
4379
+ const sync = [];
4380
+ const outbox = [];
4381
+ for (const write of writes) {
4382
+ if (write.updateMode === "stream") {
4383
+ outbox.push(deriveMaintainOutbox(fragment, write));
4384
+ } else {
4385
+ sync.push(write);
4386
+ }
4387
+ }
4388
+ return { sync, outbox };
4389
+ }
3996
4390
  function resolveLifecycle(fragment) {
3997
4391
  const writes = fragment.use ?? getEntityWrites(fragment.entity.modelClass);
3998
4392
  if (writes === void 0) return void 0;
@@ -4032,7 +4426,7 @@ function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, re
4032
4426
  }
4033
4427
  return leaf;
4034
4428
  }
4035
- function compileFragment(fragment, index = 0, resolveEntityRef = null) {
4429
+ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
4036
4430
  const keyFields = primaryKeyFields(fragment);
4037
4431
  const keyFieldSet = new Set(keyFields);
4038
4432
  const operation = INTENT_OPERATION[fragment.intent];
@@ -4088,6 +4482,11 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
4088
4482
  const uniqueGuards = lifecycle?.effects.unique !== void 0 && lifecycle.effects.unique.length > 0 ? deriveUniqueGuards(fragment, lifecycle.effects.unique) : void 0;
4089
4483
  const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
4090
4484
  const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
4485
+ const maintainers = resolveMaintainers(fragment, maintenanceGraph);
4486
+ const allMaintainWrites = maintainers.length > 0 ? deriveMaintainItems(fragment, maintainers) : void 0;
4487
+ const { sync: syncMaintainWrites, outbox: maintainOutboxAll } = allMaintainWrites !== void 0 ? splitMaintainByMode(fragment, allMaintainWrites) : { sync: [], outbox: [] };
4488
+ const maintainWrites = syncMaintainWrites.length > 0 ? syncMaintainWrites : void 0;
4489
+ const maintainOutbox = maintainOutboxAll.length > 0 ? maintainOutboxAll : void 0;
4091
4490
  return {
4092
4491
  op,
4093
4492
  keyFields,
@@ -4097,7 +4496,9 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
4097
4496
  ...derivedUpdates !== void 0 ? { derivedUpdates } : {},
4098
4497
  ...uniqueGuards !== void 0 ? { uniqueGuards } : {},
4099
4498
  ...outboxEvents !== void 0 ? { outboxEvents } : {},
4100
- ...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
4499
+ ...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
4500
+ ...maintainWrites !== void 0 ? { maintainWrites } : {},
4501
+ ...maintainOutbox !== void 0 ? { maintainOutbox } : {}
4101
4502
  };
4102
4503
  }
4103
4504
  function compileSingleFragmentPlan(plan2) {
@@ -4624,7 +5025,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
4624
5025
  const recordWrite = (instr) => {
4625
5026
  currentBody.push(instr);
4626
5027
  };
4627
- const recorder3 = {
5028
+ const recorder2 = {
4628
5029
  put(model, item, options) {
4629
5030
  recordWrite({
4630
5031
  kind: "write",
@@ -4717,7 +5118,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
4717
5118
  currentBody.push(block);
4718
5119
  }
4719
5120
  };
4720
- build(recorder3, pProxy);
5121
+ build(recorder2, pProxy);
4721
5122
  return instructions;
4722
5123
  }
4723
5124
  function defineTransaction(params, build) {
@@ -6143,6 +6544,75 @@ function buildDerivedUpdateItems(derivedUpdates) {
6143
6544
  }
6144
6545
  return { items, params };
6145
6546
  }
6547
+ function buildMaintainWriteItems(maintainWrites) {
6548
+ const items = [];
6549
+ const params = {};
6550
+ for (const maintain of maintainWrites) {
6551
+ const metadata = MetadataRegistry.get(
6552
+ maintain.entity.modelClass
6553
+ );
6554
+ if (!metadata.primaryKey) {
6555
+ throw new Error(
6556
+ `maintenance write: the maintained entity '${maintain.entity.name}' has no primary key, so a maintenance \`UpdateItem\` cannot be keyed.`
6557
+ );
6558
+ }
6559
+ const tableName = TableMapping.resolve(metadata.tableName);
6560
+ const present = new Set(Object.keys(maintain.keyBinding));
6561
+ const { pk, sk } = evaluateKey(
6562
+ metadata.primaryKey.segmented,
6563
+ "param",
6564
+ present,
6565
+ (field) => maintain.keyBinding[field] ?? field
6566
+ );
6567
+ const keyCondition = { PK: pk };
6568
+ if (sk !== void 0) keyCondition.SK = sk;
6569
+ if (maintain.kind === "membership") {
6570
+ throw new Error(
6571
+ `maintenance write: a sparse-view membership maintainer ('${maintain.relationProperty}' on '${maintain.entity.name}') reached the synchronous transaction spec builder. Membership is stream-only (#133) \u2014 it should have been diverted to a maintenance-outbox row. This is a compiler defect.`
6572
+ );
6573
+ }
6574
+ const projection = {};
6575
+ for (const [attr, transform] of Object.entries(maintain.projection)) {
6576
+ projection[attr] = {
6577
+ op: transform.op,
6578
+ args: [...transform.args],
6579
+ inputField: transform.inputField
6580
+ };
6581
+ }
6582
+ const maintainSpec = {
6583
+ kind: maintain.kind,
6584
+ relationProperty: maintain.relationProperty,
6585
+ trigger: maintain.trigger,
6586
+ projection,
6587
+ ...maintain.kind === "collection" && maintain.collection !== void 0 ? {
6588
+ collection: {
6589
+ field: maintain.collection.field,
6590
+ ...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
6591
+ ...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {},
6592
+ ...maintain.collection.orderBy !== void 0 && maintain.collection.orderDir !== void 0 ? { orderDir: maintain.collection.orderDir } : {}
6593
+ }
6594
+ } : {},
6595
+ // #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
6596
+ // the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
6597
+ // counter (#85) does, so a same-row counter merge stays consistent across paths.
6598
+ ...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
6599
+ counter: {
6600
+ attribute: maintain.counter.attribute,
6601
+ delta: String(maintain.counter.delta)
6602
+ }
6603
+ } : {}
6604
+ };
6605
+ items.push({
6606
+ type: "Update",
6607
+ tableName,
6608
+ entity: maintain.entity.name,
6609
+ keyCondition,
6610
+ maintain: maintainSpec
6611
+ });
6612
+ collectTemplateParams(keyCondition, metadata, params);
6613
+ }
6614
+ return { items, params };
6615
+ }
6146
6616
  function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
6147
6617
  const items = [];
6148
6618
  const params = {};
@@ -6166,6 +6636,17 @@ function buildOutboxEventItems(outboxEvents, entityMetadata) {
6166
6636
  }
6167
6637
  return { items, params };
6168
6638
  }
6639
+ function buildMaintainOutboxItems(maintainOutbox, entityMetadata) {
6640
+ const items = [];
6641
+ const params = {};
6642
+ for (const outbox of maintainOutbox) {
6643
+ for (const item of outbox.items) {
6644
+ items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
6645
+ collectTemplateParams(item.item, entityMetadata, params);
6646
+ }
6647
+ }
6648
+ return { items, params };
6649
+ }
6169
6650
  function buildIdempotencyGuardItems(guard, entityMetadata) {
6170
6651
  const items = [];
6171
6652
  const params = {};
@@ -6175,7 +6656,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
6175
6656
  }
6176
6657
  return { items, params };
6177
6658
  }
6178
- function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard) {
6659
+ function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites, maintainOutbox) {
6179
6660
  const label = opRefName(contractName, methodName);
6180
6661
  const base = composeFragmentTransaction(contractName, methodName, ops);
6181
6662
  const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
@@ -6193,6 +6674,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
6193
6674
  primaryMetadata
6194
6675
  );
6195
6676
  const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
6677
+ const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
6678
+ const { items: maintainOutboxItems, params: maintainOutboxParams } = buildMaintainOutboxItems(maintainOutbox, primaryMetadata);
6196
6679
  const baseKeys = new Set(base.items.map(itemKeySignature));
6197
6680
  for (const update of updateItems) {
6198
6681
  if (baseKeys.has(itemKeySignature(update))) {
@@ -6215,7 +6698,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
6215
6698
  checkParams,
6216
6699
  guardParams,
6217
6700
  outboxParams,
6218
- idempotencyParams
6701
+ idempotencyParams,
6702
+ maintainParams,
6703
+ maintainOutboxParams
6219
6704
  ]) {
6220
6705
  for (const [name, spec] of Object.entries(source)) params[name] = spec;
6221
6706
  }
@@ -6226,6 +6711,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
6226
6711
  ...guardItems,
6227
6712
  ...outboxItems,
6228
6713
  ...idempotencyItems,
6714
+ ...maintainItems,
6715
+ ...maintainOutboxItems,
6229
6716
  ...checkItems
6230
6717
  ];
6231
6718
  if (items.length > MAX_TRANSACT_ITEMS) {
@@ -6331,7 +6818,9 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
6331
6818
  const uniqueGuards = method.uniqueGuards ?? [];
6332
6819
  const outboxEvents = method.outboxEvents ?? [];
6333
6820
  const idempotencyGuard = method.idempotencyGuard;
6334
- const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
6821
+ const maintainWrites = method.maintainWrites ?? [];
6822
+ const maintainOutbox = method.maintainOutbox ?? [];
6823
+ const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0 || maintainWrites.length > 0 || maintainOutbox.length > 0;
6335
6824
  const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
6336
6825
  const promotedToTransaction = isMultiFragment || hasDerivedEffects;
6337
6826
  if (promotedToTransaction) {
@@ -6349,7 +6838,9 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
6349
6838
  derivedUpdates,
6350
6839
  uniqueGuards,
6351
6840
  outboxEvents,
6352
- idempotencyGuard
6841
+ idempotencyGuard,
6842
+ maintainWrites,
6843
+ maintainOutbox
6353
6844
  )
6354
6845
  ) : (
6355
6846
  // #90: writes only.
@@ -6515,13 +7006,6 @@ export {
6515
7006
  BatchGetResult,
6516
7007
  executeBatchGet,
6517
7008
  executeBatchWrite,
6518
- LIFECYCLE_CONTRACT_MARKER,
6519
- isLifecycleContract,
6520
- ENTITY_WRITES_MARKER,
6521
- isEntityWritesDefinition,
6522
- entityWrites,
6523
- getEntityWrites,
6524
- lifecyclePhaseForIntent,
6525
7009
  OLD_VALUE_NAMESPACE,
6526
7010
  EDGE_WRITES_MARKER,
6527
7011
  isEdgeWritesDefinition,