@hyperscale0/hsx 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/lower.js CHANGED
@@ -29,14 +29,17 @@
29
29
  * The lowering never shares code with the checker that verifies its output;
30
30
  * that independence is the safety argument of the whole compiler.
31
31
  */
32
+ import { ARCHETYPE_DEFINITIONS } from "./archetypes.js";
32
33
  import { HSX_IR_VERSION } from "./version.js";
33
34
  /**
34
35
  * The most money events one program may mint. The Business Frame contract caps
35
36
  * its moneyEvents array at the same number, and a runtime spec pins the two
36
- * against each other, so neither can drift alone. Every installment anchor,
37
- * fee leg, cancellation leg, abandonment refund, and forward counts one.
37
+ * against each other, so neither can drift alone. A repeatable schedule costs
38
+ * one event declaration. Fee legs, cancellation legs, refunds, and forwards
39
+ * each count when they emit their own event declaration.
38
40
  */
39
- export const MONEY_EVENT_BUDGET = 14;
41
+ export const MONEY_EVENT_BUDGET = 20;
42
+ const PUBLIC_INTENT_BUDGET = 48;
40
43
  const TOTAL_BPS = 10000n;
41
44
  /**
42
45
  * Split a minor-unit amount across pieces by exact basis points. Floors every
@@ -54,6 +57,55 @@ export function pieceAmounts(pieces, amountMinor, remainderIndex = 0) {
54
57
  }
55
58
  return floors;
56
59
  }
60
+ /** Lower one arithmetic dependency into the Business Frame's existing keys. */
61
+ export function lowerAmountDependency(expression) {
62
+ if (!expression)
63
+ return { amountDependencies: [], amountMode: "fixed" };
64
+ switch (expression.kind) {
65
+ case "bounded_by_reference":
66
+ return {
67
+ amountDependencies: [frameKey(expression.reference)],
68
+ amountMode: "runtime_bounded",
69
+ };
70
+ case "net_of_offsets": {
71
+ if (expression.offsets.length === 0) {
72
+ throw new Error("net_of_offsets requires at least one offset event");
73
+ }
74
+ const dependencies = canonicalDependencies(expression.source, expression.offsets);
75
+ return {
76
+ amountDependencies: dependencies,
77
+ amountMode: "runtime_bounded",
78
+ };
79
+ }
80
+ case "percent_of_reference":
81
+ if (!Number.isInteger(expression.bps) ||
82
+ expression.bps <= 0 ||
83
+ expression.bps > 10_000) {
84
+ throw new Error("percent_of_reference bps must be an integer between 1 and 10000");
85
+ }
86
+ return {
87
+ amountDependencies: [frameKey(expression.reference)],
88
+ amountMode: "runtime_bounded",
89
+ };
90
+ case "remainder": {
91
+ if (expression.consumed.length === 0) {
92
+ throw new Error("remainder requires at least one consumed event");
93
+ }
94
+ const dependencies = canonicalDependencies(expression.source, expression.consumed);
95
+ return {
96
+ amountDependencies: dependencies,
97
+ amountMode: "remaining_balance",
98
+ };
99
+ }
100
+ }
101
+ }
102
+ function canonicalDependencies(source, dependents) {
103
+ const all = [source, ...dependents].map(frameKey);
104
+ if (new Set(all).size !== all.length) {
105
+ throw new Error("amount dependency event keys must be distinct");
106
+ }
107
+ return all;
108
+ }
57
109
  /**
58
110
  * Frame keys carry a 40-char snake_case budget, set by the Business Frame
59
111
  * contract's key text. Composed keys include model-authored names (ports,
@@ -114,12 +166,26 @@ function frameKey(key) {
114
166
  }
115
167
  return `${key.slice(0, 33)}_${(hash >>> 0).toString(36).slice(0, 6)}`;
116
168
  }
117
- function mintEvent(spec) {
169
+ function dependentAmountDescription(spec) {
170
+ const expression = spec.amountDependency;
171
+ if (!expression)
172
+ return spec.amount;
173
+ switch (expression.kind) {
174
+ case "bounded_by_reference":
175
+ return `Bounded by ${expression.reference}: ${spec.amount}`;
176
+ case "net_of_offsets":
177
+ return `Net of ${expression.source} after ${expression.offsets.join(", ")}: ${spec.amount}`;
178
+ case "percent_of_reference":
179
+ return `${formatBps(expression.bps)} of ${expression.reference}: ${spec.amount}`;
180
+ case "remainder":
181
+ return `Remainder of ${expression.source} after ${expression.consumed.join(", ")}: ${spec.amount}`;
182
+ }
183
+ }
184
+ export function mintEvent(spec) {
118
185
  return {
119
186
  allocationTotalBps: 0,
120
- amount: spec.amount,
121
- amountDependencies: [],
122
- amountMode: "fixed",
187
+ amount: dependentAmountDescription(spec),
188
+ ...lowerAmountDependency(spec.amountDependency),
123
189
  amountSchedule: [],
124
190
  distribution: "single",
125
191
  fromActor: spec.fromActor,
@@ -170,6 +236,38 @@ function moneyFieldSpec(desc) {
170
236
  function dateFieldSpec(desc) {
171
237
  return { desc, type: "date" };
172
238
  }
239
+ function optionalDateFieldSpec(desc) {
240
+ return { desc, type: "date?" };
241
+ }
242
+ function derivedNounPrefix(noun) {
243
+ if (typeof noun.prefix === "string")
244
+ return noun.prefix;
245
+ const id = noun.id;
246
+ const words = id.split("_");
247
+ const derived = words.length > 1 ? words.map((word) => word[0]).join("") : id.slice(0, 4);
248
+ return derived.slice(0, 8).padEnd(2, "x");
249
+ }
250
+ function allocatedGeneratedPrefixes(nouns, generatedIds) {
251
+ const used = new Set(nouns
252
+ .filter((noun) => !generatedIds.has(noun.id))
253
+ .map(derivedNounPrefix));
254
+ let ordinal = 0;
255
+ const nextPrefix = () => {
256
+ while (true) {
257
+ const high = String.fromCharCode(97 + Math.floor(ordinal / 26));
258
+ const low = String.fromCharCode(97 + (ordinal % 26));
259
+ ordinal += 1;
260
+ const candidate = `zz${high}${low}`;
261
+ if (!used.has(candidate)) {
262
+ used.add(candidate);
263
+ return candidate;
264
+ }
265
+ }
266
+ };
267
+ return nouns.map((noun) => generatedIds.has(noun.id)
268
+ ? { ...noun, prefix: nextPrefix() }
269
+ : noun);
270
+ }
173
271
  // ---------------------------------------------------------------------------
174
272
  // The whole-program lowering
175
273
  export function lowerProgram(program) {
@@ -180,6 +278,8 @@ export function lowerProgram(program) {
180
278
  const rules = [];
181
279
  const design = [];
182
280
  const feeLines = [];
281
+ const repeatableCounterparties = [];
282
+ const generatedPrefixNounIds = new Set();
183
283
  const mintedKeys = new Map();
184
284
  const portsByName = new Map(program.ports.map((port) => [port.name, port]));
185
285
  const portFor = (settlement, portName, origin) => {
@@ -199,6 +299,22 @@ export function lowerProgram(program) {
199
299
  const carveFunderByHold = new Map(program.settlements.flatMap((settlement) => settlement.archetype === "advance" && settlement.source.kind === "carve"
200
300
  ? [[settlement.source.settlement, settlement.funder]]
201
301
  : []));
302
+ const recoursesByAdvance = new Map(program.settlements.flatMap((settlement) => {
303
+ if (settlement.archetype !== "advance" ||
304
+ settlement.source.kind !== "carve") {
305
+ return [];
306
+ }
307
+ const recourses = program.settlements.filter((candidate) => candidate.archetype === "scheduled" &&
308
+ candidate.mode === "transfer" &&
309
+ candidate.payer === settlement.advanced &&
310
+ candidate.payee === settlement.funder &&
311
+ candidate.amount.name === settlement.amount.name &&
312
+ candidate.amount.currency === settlement.amount.currency);
313
+ return [[settlement.name, recourses]];
314
+ }));
315
+ const collectionByObligation = new Map(program.settlements.flatMap((settlement) => settlement.archetype === "recurring_collection"
316
+ ? [[settlement.obligation.settlement, settlement]]
317
+ : []));
202
318
  for (const settlement of program.settlements) {
203
319
  let lowered;
204
320
  switch (settlement.archetype) {
@@ -209,11 +325,57 @@ export function lowerProgram(program) {
209
325
  lowered = lowerHeldPayment(settlement, port, carveFunderByHold.get(settlement.name), issues);
210
326
  break;
211
327
  }
328
+ case "captured_payment": {
329
+ const correction = portFor(settlement, settlement.correction.port, settlement.correction.origin);
330
+ const externalReversal = portFor(settlement, settlement.externalReversal.port, settlement.externalReversal.origin);
331
+ if (!correction || !externalReversal)
332
+ continue;
333
+ lowered = lowerCaptureReservation(settlement, correction, externalReversal, issues);
334
+ break;
335
+ }
336
+ case "settlement_batch": {
337
+ const acknowledgement = portFor(settlement, settlement.payoutAcknowledgement.port, settlement.payoutAcknowledgement.origin);
338
+ if (!acknowledgement)
339
+ continue;
340
+ lowered = lowerSettlementBatch(settlement, acknowledgement, issues);
341
+ break;
342
+ }
343
+ case "funding_round":
344
+ lowered = lowerFundingRound(settlement);
345
+ break;
346
+ case "weighted_distribution": {
347
+ const snapshot = portFor(settlement, settlement.snapshot.port, settlement.snapshot.origin);
348
+ if (!snapshot)
349
+ continue;
350
+ lowered = lowerWeightedDistribution(settlement, snapshot);
351
+ break;
352
+ }
353
+ case "credit_facility":
354
+ lowered = lowerCreditFacility(settlement);
355
+ break;
356
+ case "recurring_collection":
357
+ // The referenced scheduled obligation owns the payment nouns, amount
358
+ // allocation, and delinquency. Its lowerer adds the explicit mandate
359
+ // evidence gate, so this declaration mints no second noun or event.
360
+ continue;
361
+ case "conditional_disbursement": {
362
+ const decision = portFor(settlement, settlement.decision.port, settlement.decision.origin);
363
+ if (!decision)
364
+ continue;
365
+ lowered = lowerConditionalDisbursement(settlement, decision);
366
+ break;
367
+ }
368
+ case "rotating_pool":
369
+ lowered = lowerRotatingPool(settlement);
370
+ break;
212
371
  case "premium_forward": {
213
372
  const port = portFor(settlement, settlement.bind.port, settlement.bind.origin);
214
- if (!port)
373
+ const endorsement = settlement.endorsement
374
+ ? portFor(settlement, settlement.endorsement.port, settlement.endorsement.origin)
375
+ : undefined;
376
+ if (!port || (settlement.endorsement && !endorsement))
215
377
  continue;
216
- lowered = lowerPremiumForward(settlement, port, issues);
378
+ lowered = lowerPremiumForward(settlement, port, endorsement, issues);
217
379
  break;
218
380
  }
219
381
  case "deposit": {
@@ -228,10 +390,16 @@ export function lowerProgram(program) {
228
390
  lowered = lowerInstantTransfer(settlement);
229
391
  break;
230
392
  case "scheduled":
231
- lowered = lowerScheduled(settlement);
393
+ lowered =
394
+ settlement.mode === "obligation"
395
+ ? lowerScheduledObligation(settlement, collectionByObligation.get(settlement.name), collectionByObligation.has(settlement.name)
396
+ ? portFor(collectionByObligation.get(settlement.name), collectionByObligation.get(settlement.name).mandate.port, collectionByObligation.get(settlement.name).mandate
397
+ .origin)
398
+ : undefined)
399
+ : lowerScheduled(settlement);
232
400
  break;
233
401
  case "advance":
234
- lowered = lowerAdvance(settlement);
402
+ lowered = lowerAdvance(settlement, recoursesByAdvance.get(settlement.name) ?? []);
235
403
  break;
236
404
  case "metered":
237
405
  lowered = lowerMetered(settlement);
@@ -252,6 +420,21 @@ export function lowerProgram(program) {
252
420
  }
253
421
  if (!lowered)
254
422
  continue;
423
+ const derivedAmounts = (program.derivedAmounts ?? []).filter((amount) => amount.settlement === settlement.name);
424
+ if (derivedAmounts.length > 0) {
425
+ lowered = addDerivedAmounts(lowered, settlement, derivedAmounts, issues);
426
+ if (!lowered)
427
+ continue;
428
+ }
429
+ const localCap = ARCHETYPE_DEFINITIONS[settlement.archetype].eventCap +
430
+ derivedAmounts.length;
431
+ if (lowered.moneyEvents.length > localCap) {
432
+ issues.push({
433
+ message: `settlement ${settlement.name} emits ${lowered.moneyEvents.length} money events, but ${settlement.archetype} carries a local cap of ${localCap}`,
434
+ span: settlement.origin,
435
+ });
436
+ continue;
437
+ }
255
438
  // Event and rule keys concatenate settlement names with generated stems,
256
439
  // so two settlements can mint the same key (a + b_service_fee vs a_b +
257
440
  // service_fee). The frame schema refuses duplicates wholesale, which
@@ -268,11 +451,34 @@ export function lowerProgram(program) {
268
451
  mintedKeys.set(key, settlement.name);
269
452
  }
270
453
  settlements.push(lowered.settlement);
271
- nouns.push(lowered.noun);
454
+ const loweredNouns = [lowered.noun, ...(lowered.extraNouns ?? [])];
455
+ for (const noun of loweredNouns) {
456
+ const verbs = noun.verbs;
457
+ for (const [verbName, verb] of Object.entries(verbs)) {
458
+ if (Object.hasOwn(verb, "due") ||
459
+ Object.hasOwn(verb, "requiresSettlement")) {
460
+ continue;
461
+ }
462
+ const publicIntent = callerDrivenPublicIntent(noun.id, verbName);
463
+ if (publicIntent.length <= PUBLIC_INTENT_BUDGET)
464
+ continue;
465
+ issues.push({
466
+ message: `settlement ${settlement.name} generates public intent "${publicIntent}" with ${publicIntent.length} characters; rename the settlement so each public intent fits the ${PUBLIC_INTENT_BUDGET}-character camelName limit`,
467
+ span: settlement.origin,
468
+ });
469
+ }
470
+ }
471
+ nouns.push(...loweredNouns);
472
+ for (const nounId of lowered.generatedPrefixNounIds ?? []) {
473
+ generatedPrefixNounIds.add(nounId);
474
+ }
272
475
  moneyEvents.push(...lowered.moneyEvents);
273
476
  rules.push(...lowered.rules);
274
477
  design.push(...lowered.design);
275
478
  feeLines.push(...lowered.feeLines);
479
+ if (lowered.repeatableCounterparty) {
480
+ repeatableCounterparties.push(lowered.repeatableCounterparty);
481
+ }
276
482
  }
277
483
  if (moneyEvents.length > MONEY_EVENT_BUDGET) {
278
484
  issues.push({
@@ -280,8 +486,12 @@ export function lowerProgram(program) {
280
486
  span: program.settlements[0]?.origin ?? { end: 0, start: 0 },
281
487
  });
282
488
  }
489
+ issues.push(...validateAmountDependencyGraph(moneyEvents, program.settlements[0]?.origin ?? { end: 0, start: 0 }));
490
+ const actorLowering = lowerFrameActors(program, repeatableCounterparties);
491
+ issues.push(...actorLowering.issues);
283
492
  if (issues.length > 0)
284
493
  return { issues, ok: false };
494
+ const publishedNouns = publishCallerDrivenVerbs(allocatedGeneratedPrefixes(nouns, generatedPrefixNounIds));
285
495
  const subjects = program.assets.map((asset) => ({
286
496
  kind: asset.name,
287
497
  title: titleize(asset.name),
@@ -289,31 +499,13 @@ export function lowerProgram(program) {
289
499
  }));
290
500
  const document = {
291
501
  hsx: HSX_IR_VERSION,
292
- nouns,
502
+ nouns: publishedNouns,
293
503
  product: program.name,
294
504
  ...(subjects.length > 0 ? { subjects } : {}),
295
505
  title: program.title,
296
506
  };
297
- const roles = partyRoles(program.settlements);
298
507
  const frame = {
299
- actors: [
300
- ...program.parties
301
- .filter((party) => roles.has(party.name))
302
- .map((party) => ({
303
- key: party.name,
304
- label: titleize(party.name),
305
- maxCount: 1,
306
- minCount: 1,
307
- role: roles.get(party.name),
308
- })),
309
- {
310
- key: "platform",
311
- label: "Platform",
312
- maxCount: 1,
313
- minCount: 1,
314
- role: "platform",
315
- },
316
- ],
508
+ actors: actorLowering.actors,
317
509
  confidence: "high",
318
510
  conservationGroups: [],
319
511
  design,
@@ -341,6 +533,261 @@ export function lowerProgram(program) {
341
533
  value: { document, frame: clampFrameProse(frame), settlements },
342
534
  };
343
535
  }
536
+ function publishCallerDrivenVerbs(nouns) {
537
+ return nouns.map((noun) => {
538
+ const verbs = noun.verbs;
539
+ return {
540
+ ...noun,
541
+ verbs: Object.fromEntries(Object.entries(verbs).map(([verbName, verb]) => [
542
+ verbName,
543
+ Object.hasOwn(verb, "due") ||
544
+ Object.hasOwn(verb, "requiresSettlement")
545
+ ? verb
546
+ : {
547
+ ...verb,
548
+ publicIntent: callerDrivenPublicIntent(noun.id, verbName),
549
+ },
550
+ ])),
551
+ };
552
+ });
553
+ }
554
+ function callerDrivenPublicIntent(nounId, verbName) {
555
+ const nounName = camelize(nounId);
556
+ const domainName = nounName.charAt(0).toUpperCase() + nounName.slice(1);
557
+ return `${camelize(verbName)}${domainName}`;
558
+ }
559
+ /** Add generic on-top amounts after archetype lowering, so no brick owns fee syntax. */
560
+ function addDerivedAmounts(lowered, settlement, amounts, issues) {
561
+ const noun = lowered.noun;
562
+ const fields = { ...(noun.fields ?? {}) };
563
+ const verbs = { ...(noun.verbs ?? {}) };
564
+ const create = { ...(verbs.create ?? {}) };
565
+ const moves = [...(create.moves ?? [])];
566
+ const actors = { ...(noun.actors ?? {}) };
567
+ const derived = [];
568
+ const events = [...lowered.moneyEvents];
569
+ const lines = [...lowered.feeLines];
570
+ for (const amount of amounts) {
571
+ const sourceField = fields[amount.baseField];
572
+ if (sourceField === undefined || sourceField.type !== "money") {
573
+ issues.push({
574
+ message: `settlement ${settlement.name} derives ${amount.field} from ${sourceField === undefined ? "unknown " : "non-money "}field ${amount.baseField}; from must name a stored money field on the settlement owner`,
575
+ span: amount.origin,
576
+ });
577
+ return undefined;
578
+ }
579
+ if (fields[amount.field] !== undefined) {
580
+ issues.push({
581
+ message: `settlement ${settlement.name} derives into existing field ${amount.field}; choose a new derived amount field`,
582
+ span: amount.origin,
583
+ });
584
+ return undefined;
585
+ }
586
+ const eventKey = frameKey(`${settlement.name}_derived_amount`);
587
+ fields[amount.field] = moneyFieldSpec(`Machine-computed ${formatBps(amount.bps)} of ${amount.baseField}; callers never supply it`);
588
+ if (actors[amount.bearer] === undefined) {
589
+ actors[amount.bearer] = "payer";
590
+ }
591
+ actors.platform = "beneficiary";
592
+ derived.push({
593
+ field: amount.field,
594
+ rounding: "floor",
595
+ rule: { bps: amount.bps, kind: "percentage_of" },
596
+ sourceField: amount.baseField,
597
+ });
598
+ moves.push({
599
+ amount: amount.field,
600
+ from: amount.bearer,
601
+ key: "derived_amount",
602
+ moneyEvent: eventKey,
603
+ operation: "create",
604
+ to: "platform",
605
+ });
606
+ events.push(mintEvent({
607
+ amount: `The machine-computed ${amount.field}`,
608
+ fromActor: amount.bearer,
609
+ key: eventKey,
610
+ kind: "charge",
611
+ toActor: "platform",
612
+ trigger: `Collect ${amount.field} with settlement creation`,
613
+ }));
614
+ lines.push({
615
+ label: titleize(amount.field),
616
+ on: `each ${settlement.name.replaceAll("_", " ")}`,
617
+ structure: `${formatBps(amount.bps)} of stored ${amount.baseField}, computed by the runtime`,
618
+ });
619
+ }
620
+ create.moves = moves;
621
+ verbs.create = create;
622
+ return {
623
+ ...lowered,
624
+ design: [
625
+ ...lowered.design,
626
+ `${settlement.name}: derived amounts are machine-computed from stored source fields before create movements; fixed and tiered rules are refused`,
627
+ ],
628
+ feeLines: lines,
629
+ moneyEvents: events,
630
+ noun: {
631
+ ...noun,
632
+ actors,
633
+ derivedAmounts: derived,
634
+ fields,
635
+ verbs,
636
+ },
637
+ };
638
+ }
639
+ export function lowerFrameActors(program, repeatableCounterparties = []) {
640
+ const roles = partyRoles(program.settlements);
641
+ const parties = new Set(program.parties.map((party) => party.name));
642
+ const overrides = new Map();
643
+ const issues = [];
644
+ for (const counterparty of repeatableCounterparties) {
645
+ if (!parties.has(counterparty.key)) {
646
+ issues.push({
647
+ message: `repeatable counterparty ${counterparty.key} is not a declared party`,
648
+ span: counterparty.origin,
649
+ });
650
+ continue;
651
+ }
652
+ const fixedRole = roles.get(counterparty.key);
653
+ if (!fixedRole) {
654
+ issues.push({
655
+ message: `repeatable counterparty ${counterparty.key} is not used by any settlement`,
656
+ span: counterparty.origin,
657
+ });
658
+ continue;
659
+ }
660
+ if (fixedRole !== counterparty.role) {
661
+ issues.push({
662
+ message: `repeatable counterparty ${counterparty.key} declares role ${counterparty.role}, but its settlement uses role ${fixedRole}`,
663
+ span: counterparty.origin,
664
+ });
665
+ continue;
666
+ }
667
+ if (counterparty.label.trim().length === 0 ||
668
+ counterparty.label.length > 160) {
669
+ issues.push({
670
+ message: `repeatable counterparty ${counterparty.key} label must contain 1 through 160 characters`,
671
+ span: counterparty.origin,
672
+ });
673
+ continue;
674
+ }
675
+ if (!Number.isInteger(counterparty.minCount) ||
676
+ !Number.isInteger(counterparty.maxCount) ||
677
+ counterparty.minCount < 1 ||
678
+ counterparty.maxCount > 10_000) {
679
+ issues.push({
680
+ message: `repeatable counterparty ${counterparty.key} counts must be integers from 1 through 10000`,
681
+ span: counterparty.origin,
682
+ });
683
+ continue;
684
+ }
685
+ if (counterparty.minCount > counterparty.maxCount) {
686
+ issues.push({
687
+ message: `repeatable counterparty ${counterparty.key} has minCount ${counterparty.minCount} above maxCount ${counterparty.maxCount}`,
688
+ span: counterparty.origin,
689
+ });
690
+ continue;
691
+ }
692
+ if (overrides.has(counterparty.key)) {
693
+ issues.push({
694
+ message: `repeatable counterparty ${counterparty.key} is declared twice`,
695
+ span: counterparty.origin,
696
+ });
697
+ continue;
698
+ }
699
+ overrides.set(counterparty.key, counterparty);
700
+ }
701
+ const actors = [
702
+ ...program.parties
703
+ .filter((party) => roles.has(party.name))
704
+ .map((party) => {
705
+ const override = overrides.get(party.name);
706
+ return override
707
+ ? {
708
+ key: override.key,
709
+ label: override.label,
710
+ maxCount: override.maxCount,
711
+ minCount: override.minCount,
712
+ role: override.role,
713
+ }
714
+ : {
715
+ key: party.name,
716
+ label: titleize(party.name),
717
+ maxCount: 1,
718
+ minCount: 1,
719
+ role: roles.get(party.name),
720
+ };
721
+ }),
722
+ {
723
+ key: "platform",
724
+ label: "Platform",
725
+ maxCount: 1,
726
+ minCount: 1,
727
+ role: "platform",
728
+ },
729
+ ];
730
+ return { actors, issues };
731
+ }
732
+ /** Validate the completed event graph before HSX returns a frame. */
733
+ export function validateAmountDependencyGraph(events, origin) {
734
+ const issues = [];
735
+ const dependenciesByKey = new Map();
736
+ for (const event of events) {
737
+ if (typeof event.key !== "string")
738
+ continue;
739
+ const dependencies = Array.isArray(event.amountDependencies)
740
+ ? event.amountDependencies.filter((dependency) => typeof dependency === "string")
741
+ : [];
742
+ dependenciesByKey.set(event.key, dependencies);
743
+ }
744
+ for (const [key, dependencies] of dependenciesByKey) {
745
+ for (const dependency of dependencies) {
746
+ if (dependency === key) {
747
+ issues.push({
748
+ message: `money event ${key} cannot depend on itself`,
749
+ span: origin,
750
+ });
751
+ continue;
752
+ }
753
+ if (!dependenciesByKey.has(dependency)) {
754
+ issues.push({
755
+ message: `money event ${key} depends on missing money event ${dependency}`,
756
+ span: origin,
757
+ });
758
+ }
759
+ }
760
+ }
761
+ const visiting = new Set();
762
+ const visited = new Set();
763
+ const cyclic = new Set();
764
+ const visit = (key) => {
765
+ if (visited.has(key) || cyclic.has(key))
766
+ return;
767
+ if (visiting.has(key)) {
768
+ cyclic.add(key);
769
+ return;
770
+ }
771
+ visiting.add(key);
772
+ for (const dependency of dependenciesByKey.get(key) ?? []) {
773
+ if (dependenciesByKey.has(dependency))
774
+ visit(dependency);
775
+ if (cyclic.has(dependency))
776
+ cyclic.add(key);
777
+ }
778
+ visiting.delete(key);
779
+ visited.add(key);
780
+ };
781
+ for (const key of dependenciesByKey.keys())
782
+ visit(key);
783
+ if (cyclic.size > 0) {
784
+ issues.push({
785
+ message: `money event amount dependencies contain a cycle through ${[...cyclic].sort().join(", ")}`,
786
+ span: origin,
787
+ });
788
+ }
789
+ return issues;
790
+ }
344
791
  /** Frame actor role per party, with a fixed precedence when roles overlap. */
345
792
  function partyRoles(settlements) {
346
793
  const payers = new Set();
@@ -350,12 +797,21 @@ function partyRoles(settlements) {
350
797
  for (const settlement of settlements) {
351
798
  switch (settlement.archetype) {
352
799
  case "held_payment":
800
+ case "captured_payment":
353
801
  case "instant_transfer":
354
- case "scheduled":
355
802
  case "metered":
356
803
  payers.add(settlement.payer);
357
804
  beneficiaries.add(settlement.payee);
358
805
  break;
806
+ case "scheduled":
807
+ payers.add(settlement.payer);
808
+ beneficiaries.add(settlement.payee);
809
+ if (settlement.mode === "obligation") {
810
+ payers.add(settlement.debtor);
811
+ if (settlement.advanceTo)
812
+ beneficiaries.add(settlement.advanceTo);
813
+ }
814
+ break;
359
815
  case "premium_forward":
360
816
  payers.add(settlement.payer);
361
817
  providers.add(settlement.carrier);
@@ -373,6 +829,37 @@ function partyRoles(settlements) {
373
829
  for (const share of settlement.shares)
374
830
  beneficiaries.add(share.to);
375
831
  break;
832
+ case "settlement_batch":
833
+ payers.add(settlement.settlementAccount);
834
+ beneficiaries.add(settlement.payoutDestination);
835
+ break;
836
+ case "funding_round":
837
+ payers.add(settlement.contributor);
838
+ beneficiaries.add(settlement.beneficiary);
839
+ break;
840
+ case "weighted_distribution":
841
+ payers.add(settlement.source);
842
+ beneficiaries.add(settlement.recipient);
843
+ break;
844
+ case "credit_facility":
845
+ payers.add(settlement.lender);
846
+ beneficiaries.add(settlement.borrower);
847
+ beneficiaries.add(settlement.drawDestination);
848
+ break;
849
+ case "recurring_collection":
850
+ break;
851
+ case "conditional_disbursement":
852
+ payers.add(settlement.source);
853
+ beneficiaries.add(settlement.destination);
854
+ break;
855
+ case "rotating_pool":
856
+ for (const member of settlement.members) {
857
+ payers.add(member);
858
+ beneficiaries.add(member);
859
+ }
860
+ if (settlement.guarantor)
861
+ payers.add(settlement.guarantor);
862
+ break;
376
863
  case "swap":
377
864
  payers.add(settlement.sides[0].party);
378
865
  beneficiaries.add(settlement.sides[1].party);
@@ -393,17 +880,27 @@ function partyRoles(settlements) {
393
880
  }
394
881
  const ARCHETYPE_MECHANICS = {
395
882
  advance: "credit",
883
+ captured_payment: "escrow",
884
+ conditional_disbursement: "marketplace",
885
+ credit_facility: "credit",
396
886
  deposit: "escrow",
887
+ funding_round: "credit",
397
888
  held_payment: "escrow",
398
889
  instant_transfer: "marketplace",
399
890
  metered: "recurring_billing",
400
891
  pooled_split: "marketplace",
401
892
  premium_forward: "insurance",
893
+ recurring_collection: "recurring_billing",
894
+ rotating_pool: "recurring_billing",
402
895
  scheduled: "recurring_billing",
896
+ settlement_batch: "marketplace",
403
897
  swap: "escrow",
898
+ weighted_distribution: "marketplace",
404
899
  };
405
900
  function mechanicsOf(settlements) {
406
- const mechanics = new Set(settlements.map((settlement) => ARCHETYPE_MECHANICS[settlement.archetype]));
901
+ const mechanics = new Set(settlements.map((settlement) => settlement.archetype === "scheduled" && settlement.mode === "obligation"
902
+ ? "credit"
903
+ : ARCHETYPE_MECHANICS[settlement.archetype]));
407
904
  return mechanics.size > 0 ? [...mechanics] : ["escrow"];
408
905
  }
409
906
  // ---------------------------------------------------------------------------
@@ -746,7 +1243,7 @@ carveTo, issues) {
746
1243
  },
747
1244
  };
748
1245
  }
749
- function lowerPremiumForward(settlement, port, issues) {
1246
+ function lowerPremiumForward(settlement, port, endorsement, issues) {
750
1247
  const held = lowerHeldFamily({
751
1248
  amount: settlement.amount,
752
1249
  // A premium is the carrier's, never the payer's receivable, so there is
@@ -768,10 +1265,58 @@ function lowerPremiumForward(settlement, port, issues) {
768
1265
  }, issues);
769
1266
  if (!held)
770
1267
  return undefined;
1268
+ const baseNoun = held.noun;
1269
+ const fields = { ...(baseNoun.fields ?? {}) };
1270
+ const verbs = { ...(baseNoun.verbs ?? {}) };
1271
+ const rules = [...held.rules];
1272
+ if (settlement.policyReferenceField &&
1273
+ settlement.renewalDueField &&
1274
+ settlement.endorsement &&
1275
+ endorsement) {
1276
+ fields[settlement.policyReferenceField] = {
1277
+ desc: "Immutable external policy reference recorded with this forward",
1278
+ type: "text",
1279
+ };
1280
+ fields[settlement.renewalDueField] = dateFieldSpec("Stored renewal due condition for the forwarded policy");
1281
+ verbs[settlement.endorsement.port] = {
1282
+ captureInput: { endorsementEvidenceReference: "evidenceReference" },
1283
+ from: ["released"],
1284
+ port: {
1285
+ allowed: endorsement.allowed,
1286
+ fields: { evidenceReference: "text" },
1287
+ },
1288
+ summary: "Record one non-money endorsement from external evidence",
1289
+ to: "endorsed",
1290
+ };
1291
+ const lapseRule = frameKey(`${settlement.name}_renewal_due`);
1292
+ verbs.lapse = {
1293
+ due: { field: settlement.renewalDueField, rule: lapseRule },
1294
+ from: ["released", "endorsed"],
1295
+ requiresDrainedAccount: { path: "refs.escrowAccountId" },
1296
+ summary: "Mark the forwarded policy lapsed at its stored renewal due condition",
1297
+ to: "lapsed",
1298
+ };
1299
+ rules.push({
1300
+ allowedActors: [],
1301
+ detail: "The stored renewal due condition changes policy state without moving money",
1302
+ dueDriven: true,
1303
+ enforcement: "platform",
1304
+ gatesEvent: null,
1305
+ key: lapseRule,
1306
+ kind: "deadline",
1307
+ label: "Policy lapses at its stored renewal due condition",
1308
+ tenantTunable: false,
1309
+ });
1310
+ }
771
1311
  return {
772
1312
  ...held,
773
1313
  design: [
774
1314
  `${settlement.name}: premium forwards to the ${settlement.carrier.replaceAll("_", " ")} exactly once on ${port.name}; ${formatBps(settlement.commissionBps)} commission retained by the platform`,
1315
+ ...(settlement.policyReferenceField
1316
+ ? [
1317
+ `${settlement.name}: extends premium_forward with stored policy reference, non-money endorsement evidence, and a due-only lapse; renewal creates a new forward`,
1318
+ ]
1319
+ : []),
775
1320
  ],
776
1321
  feeLines: settlement.commissionBps > 0
777
1322
  ? [
@@ -784,9 +1329,12 @@ function lowerPremiumForward(settlement, port, issues) {
784
1329
  : [],
785
1330
  noun: {
786
1331
  ...held.noun,
1332
+ fields,
1333
+ verbs,
787
1334
  desc: `Premium forward: the ${settlement.payer.replaceAll("_", " ")} funds the ${settlement.amount.name} into this settlement's own escrow; binding through ${port.name} forwards it to the ${settlement.carrier.replaceAll("_", " ")} exactly once, minus the platform commission`,
788
1335
  summary: `Premium held for the ${settlement.carrier.replaceAll("_", " ")} until the policy binds`,
789
1336
  },
1337
+ rules,
790
1338
  };
791
1339
  }
792
1340
  function lowerHeldFamily(params, issues) {
@@ -959,6 +1507,14 @@ function lowerHeldFamily(params, issues) {
959
1507
  to: piece.releaseTo,
960
1508
  },
961
1509
  ],
1510
+ ...(index === 0
1511
+ ? {
1512
+ port: {
1513
+ allowed: [...params.port.allowed],
1514
+ fields: Object.fromEntries(params.port.fields.map((field) => [field.name, "text"])),
1515
+ },
1516
+ }
1517
+ : {}),
962
1518
  summary: index === 0
963
1519
  ? `Confirm through ${params.port.name} and start the ${params.releaseWord} payout`
964
1520
  : `${titleize(params.releaseWord)} piece ${index + 1} of the held amount`,
@@ -1064,6 +1620,7 @@ function lowerHeldFamily(params, issues) {
1064
1620
  }
1065
1621
  verbs.abandon = {
1066
1622
  from: ["created"],
1623
+ requiresDrainedAccount: { path: "refs.escrowAccountId" },
1067
1624
  summary: "Abandon the settlement before any money is held",
1068
1625
  to: "abandoned",
1069
1626
  };
@@ -1298,116 +1855,2004 @@ function lowerInstantTransfer(settlement) {
1298
1855
  }
1299
1856
  // ---------------------------------------------------------------------------
1300
1857
  // deposit: a reservation placed, then claimed or returned
1301
- function lowerDeposit(settlement, claim, giveBack, issues) {
1858
+ function lowerCaptureReservation(settlement, correctionPort, reversalPort, issues) {
1302
1859
  const noun = settlement.name;
1303
1860
  const amountName = settlement.amount.name;
1304
- if (!verbNameIssues(noun, ["place_deposit", claim.name, giveBack.name], settlement.origin, issues)) {
1861
+ const reserveRef = "authorize_reservation";
1862
+ const capturedRef = "capturedAmount";
1863
+ const reversalCutoffField = "reversalUntil";
1864
+ const captureVerbs = ["capture", "capture_more"];
1865
+ const verbNames = [
1866
+ "authorize",
1867
+ ...captureVerbs,
1868
+ "settle",
1869
+ "void",
1870
+ "expire",
1871
+ "settle_on_expiry",
1872
+ settlement.correction.port,
1873
+ settlement.externalReversal.port,
1874
+ ];
1875
+ if (!verbNameIssues(noun, verbNames, settlement.origin, issues)) {
1305
1876
  return undefined;
1306
1877
  }
1307
- const eventKey = `${noun}_hold_1`;
1308
- const events = [
1309
- mintEvent({
1310
- amount: `The full ${amountName}`,
1311
- fromActor: settlement.payer,
1312
- key: eventKey,
1313
- kind: "hold",
1314
- toActor: settlement.holder,
1315
- trigger: `Reserve the ${amountName} in the ${settlement.holder.replaceAll("_", " ")}'s favor`,
1316
- }),
1317
- ];
1878
+ const reserveEventKey = frameKey(`${noun}_reserve`);
1879
+ const captureEventKey = frameKey(`${noun}_capture`);
1880
+ const correctionEventKey = frameKey(`${noun}_correction`);
1881
+ const reversalEventKey = frameKey(`${noun}_external_reversal`);
1882
+ const expiryRuleKey = frameKey(`${noun}_reservation_expiry`);
1883
+ const captureMove = (partialOnly) => ({
1884
+ amount: "captureAmount",
1885
+ capture: { [capturedRef]: "postedAmount" },
1886
+ key: "post",
1887
+ operation: "post",
1888
+ ...(partialOnly ? { partialOnly: true } : {}),
1889
+ reservation: reserveRef,
1890
+ });
1891
+ const reverseMove = () => ({
1892
+ amount: `refs.${capturedRef}`,
1893
+ clawbackOf: reserveRef,
1894
+ from: settlement.payee,
1895
+ key: "transfer",
1896
+ operation: "create",
1897
+ to: settlement.payer,
1898
+ });
1318
1899
  const verbs = {
1319
1900
  create: {
1320
1901
  summary: `Create a ${titleize(noun).toLowerCase()}`,
1321
1902
  to: "created",
1322
1903
  },
1323
- place_deposit: {
1904
+ authorize: {
1324
1905
  from: ["created"],
1906
+ moneyEvent: reserveEventKey,
1325
1907
  moves: [
1326
1908
  {
1327
- key: "reservation",
1328
- operation: "reserve",
1329
1909
  amount: amountName,
1330
1910
  from: settlement.payer,
1331
- to: settlement.holder,
1911
+ key: "reservation",
1912
+ operation: "reserve",
1913
+ to: settlement.payee,
1332
1914
  },
1333
1915
  ],
1334
- moneyEvent: eventKey,
1335
- summary: `Reserve the ${amountName} against the ${settlement.payer.replaceAll("_", " ")}'s account`,
1336
- to: "held",
1916
+ summary: `Reserve the ${amountName} until ${settlement.reserveUntilField}`,
1917
+ to: "authorized",
1337
1918
  },
1338
- [claim.name]: {
1339
- from: ["held"],
1919
+ capture: {
1920
+ deadline: { field: settlement.reserveUntilField },
1921
+ from: ["authorized"],
1922
+ moneyEvent: captureEventKey,
1923
+ moves: [captureMove(true)],
1924
+ summary: "Post one strict partial capture slice",
1925
+ to: "partially_captured",
1926
+ },
1927
+ capture_more: {
1928
+ deadline: { field: settlement.reserveUntilField },
1929
+ from: ["partially_captured"],
1930
+ moneyEvent: captureEventKey,
1931
+ moves: [captureMove(true)],
1932
+ summary: "Post another strict partial capture slice",
1933
+ to: "partially_captured",
1934
+ },
1935
+ settle: {
1936
+ deadline: { field: settlement.reserveUntilField },
1937
+ from: ["authorized", "partially_captured"],
1938
+ moneyEvent: captureEventKey,
1340
1939
  moves: [
1341
1940
  {
1941
+ capture: { [capturedRef]: "postedAmount" },
1342
1942
  key: "post",
1343
1943
  operation: "post",
1344
- reservation: "place_deposit_reservation",
1944
+ reservation: reserveRef,
1345
1945
  },
1346
1946
  ],
1347
- summary: `Claim the deposit for the ${settlement.holder.replaceAll("_", " ")} through ${claim.name}`,
1348
- to: "claimed",
1947
+ summary: "Post the full reserved remainder and settle",
1948
+ setsAt: {
1949
+ field: reversalCutoffField,
1950
+ offset: settlement.externalReversal.window.raw,
1951
+ },
1952
+ to: "settled",
1349
1953
  },
1350
- [giveBack.name]: {
1351
- from: ["held"],
1352
- summary: `Return the deposit to the ${settlement.payer.replaceAll("_", " ")} through ${giveBack.name}`,
1353
- to: "returned",
1954
+ void: {
1955
+ from: ["authorized"],
1354
1956
  moves: [
1355
1957
  {
1356
1958
  key: "void",
1357
1959
  operation: "void",
1358
- reason: "Deposit returned in full",
1359
- reservation: "place_deposit_reservation",
1960
+ reason: "Reservation voided before any capture",
1961
+ reservation: reserveRef,
1360
1962
  },
1361
1963
  ],
1964
+ summary: "Release an entirely uncaptured reservation",
1965
+ to: "voided",
1966
+ },
1967
+ expire: {
1968
+ due: { field: settlement.reserveUntilField, rule: expiryRuleKey },
1969
+ from: ["authorized"],
1970
+ moves: [
1971
+ {
1972
+ key: "void",
1973
+ operation: "void",
1974
+ reason: "Uncaptured reservation expired",
1975
+ reservation: reserveRef,
1976
+ },
1977
+ ],
1978
+ summary: "Release an uncaptured reservation at expiry",
1979
+ to: "expired",
1980
+ },
1981
+ settle_on_expiry: {
1982
+ due: { field: settlement.reserveUntilField, rule: expiryRuleKey },
1983
+ from: ["partially_captured"],
1984
+ moves: [
1985
+ {
1986
+ key: "void",
1987
+ operation: "void",
1988
+ reason: "Uncaptured remainder released at expiry",
1989
+ reservation: reserveRef,
1990
+ },
1991
+ ],
1992
+ summary: "Release the uncaptured remainder and settle captured slices",
1993
+ setsAt: {
1994
+ field: reversalCutoffField,
1995
+ offset: settlement.externalReversal.window.raw,
1996
+ },
1997
+ to: "settled",
1998
+ },
1999
+ [settlement.correction.port]: {
2000
+ from: ["settled"],
2001
+ moneyEvent: correctionEventKey,
2002
+ moves: [reverseMove()],
2003
+ port: { allowed: correctionPort.allowed },
2004
+ summary: "Return the full captured amount on payee correction",
2005
+ to: "corrected",
2006
+ },
2007
+ [settlement.externalReversal.port]: {
2008
+ captureInput: { externalReference: "externalReference" },
2009
+ deadline: { field: reversalCutoffField },
2010
+ from: ["settled"],
2011
+ moneyEvent: reversalEventKey,
2012
+ moves: [reverseMove()],
2013
+ port: {
2014
+ allowed: reversalPort.allowed,
2015
+ fields: { externalReference: "text" },
2016
+ },
2017
+ summary: "Return the full captured amount on an external reversal",
2018
+ to: "reversed",
1362
2019
  },
1363
2020
  };
1364
- const portRule = (port, verbLabel) => ({
1365
- allowedActors: [...port.allowed],
1366
- detail: `${port.allowed.map(titleize).join(" or ")} decides through the tenant backend`,
1367
- dueDriven: false,
1368
- enforcement: "tenant_app",
1369
- gatesEvent: null,
1370
- key: frameKey(`${noun}_${port.name}_gate`),
1371
- kind: "release_condition",
1372
- label: `${verbLabel} decided through ${port.name}`,
1373
- tenantTunable: false,
1374
- });
1375
- return {
1376
- design: [
2021
+ const events = [
2022
+ mintEvent({
2023
+ amount: `The full ${amountName}`,
2024
+ fromActor: settlement.payer,
2025
+ key: reserveEventKey,
2026
+ kind: "hold",
2027
+ toActor: settlement.payee,
2028
+ trigger: `Reserve ${amountName} until ${settlement.reserveUntilField}`,
2029
+ }),
2030
+ mintEvent({
2031
+ amount: `Each posted slice, never more than the remaining ${amountName}`,
2032
+ amountDependency: {
2033
+ kind: "bounded_by_reference",
2034
+ reference: reserveEventKey,
2035
+ },
2036
+ fromActor: settlement.payer,
2037
+ key: captureEventKey,
2038
+ kind: "payout",
2039
+ occurrence: "repeatable",
2040
+ toActor: settlement.payee,
2041
+ trigger: "Post a capture slice or the final remainder",
2042
+ }),
2043
+ mintEvent({
2044
+ amount: "100% of the cumulative captured amount",
2045
+ amountDependency: {
2046
+ bps: 10_000,
2047
+ kind: "percent_of_reference",
2048
+ reference: captureEventKey,
2049
+ },
2050
+ fromActor: settlement.payee,
2051
+ key: correctionEventKey,
2052
+ kind: "refund",
2053
+ toActor: settlement.payer,
2054
+ trigger: "Apply one full payee correction",
2055
+ }),
2056
+ mintEvent({
2057
+ amount: "100% of the cumulative captured amount",
2058
+ amountDependency: {
2059
+ bps: 10_000,
2060
+ kind: "percent_of_reference",
2061
+ reference: captureEventKey,
2062
+ },
2063
+ fromActor: settlement.payee,
2064
+ key: reversalEventKey,
2065
+ kind: "refund",
2066
+ toActor: settlement.payer,
2067
+ trigger: "Apply one full externally decided reversal",
2068
+ }),
2069
+ ];
2070
+ return {
2071
+ design: [
2072
+ `${noun}: reserve ${amountName} until ${settlement.reserveUntilField}; capture in strict partial slices; post the remainder to settle; expiry releases only the uncaptured remainder`,
2073
+ `${noun}: correction and external reversal each return the full captured amount once; insufficient payee funds reject the move instead of creating a negative position`,
2074
+ ],
2075
+ feeLines: [],
2076
+ moneyEvents: events,
2077
+ noun: {
2078
+ actors: {
2079
+ [settlement.payer]: "payer",
2080
+ [settlement.payee]: "beneficiary",
2081
+ },
2082
+ desc: `Payer reservation captured by the payee in slices within a fixed window`,
2083
+ fields: {
2084
+ [amountName]: moneyFieldSpec(`Maximum captured amount in ${settlement.amount.currency} minor units`),
2085
+ [settlement.reserveUntilField]: dateFieldSpec("Reservation expiry that releases any uncaptured remainder"),
2086
+ [reversalCutoffField]: {
2087
+ desc: "Machine-owned external reversal cutoff anchored when settlement completes",
2088
+ type: "date?",
2089
+ },
2090
+ },
2091
+ id: noun,
2092
+ summary: `Capture reservation from ${settlement.payer.replaceAll("_", " ")} to ${settlement.payee.replaceAll("_", " ")}`,
2093
+ title: titleize(noun),
2094
+ verbs,
2095
+ },
2096
+ rules: [
2097
+ {
2098
+ allowedActors: [],
2099
+ detail: `At ${settlement.reserveUntilField}, the platform releases the uncaptured remainder and preserves any posted slices`,
2100
+ dueDriven: true,
2101
+ enforcement: "platform",
2102
+ gatesEvent: null,
2103
+ key: expiryRuleKey,
2104
+ kind: "deadline",
2105
+ label: `Uncaptured remainder releases on ${settlement.reserveUntilField}`,
2106
+ tenantTunable: false,
2107
+ },
2108
+ {
2109
+ allowedActors: [...correctionPort.allowed],
2110
+ detail: "The payee may return the full captured amount once",
2111
+ dueDriven: false,
2112
+ enforcement: "tenant_app",
2113
+ gatesEvent: correctionEventKey,
2114
+ key: frameKey(`${noun}_${settlement.correction.port}_gate`),
2115
+ kind: "release_condition",
2116
+ label: "Full correction confirmed through the tenant backend",
2117
+ tenantTunable: false,
2118
+ },
2119
+ {
2120
+ allowedActors: [...reversalPort.allowed],
2121
+ detail: `A confirmed external decision may reverse the full captured amount within ${settlement.externalReversal.window.raw}; timeout moves nothing`,
2122
+ dueDriven: false,
2123
+ enforcement: "tenant_app",
2124
+ gatesEvent: reversalEventKey,
2125
+ key: frameKey(`${noun}_${settlement.externalReversal.port}_gate`),
2126
+ kind: "release_condition",
2127
+ label: "External reversal confirmed through the tenant backend",
2128
+ tenantTunable: false,
2129
+ },
2130
+ ],
2131
+ settlement: { name: noun, pieces: [] },
2132
+ };
2133
+ }
2134
+ // ---------------------------------------------------------------------------
2135
+ // settlement_batch: immutable close, signed lineage sum, one payout
2136
+ function lowerSettlementBatch(settlement, acknowledgementPort, issues) {
2137
+ const noun = settlement.name;
2138
+ const captureEntry = `${noun}_capture_entry`;
2139
+ const creditAdjustment = `${noun}_credit_adjustment`;
2140
+ const debitAdjustment = `${noun}_debit_adjustment`;
2141
+ const batchIdField = `${noun.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase())}Id`;
2142
+ const payoutEventKey = frameKey(`${noun}_payout`);
2143
+ const closeRuleKey = frameKey(`${noun}_close`);
2144
+ if (!verbNameIssues(noun, [
2145
+ "close",
2146
+ "calculate",
2147
+ "approve",
2148
+ "instruct",
2149
+ "reconcile",
2150
+ settlement.payoutAcknowledgement.port,
2151
+ ], settlement.origin, issues)) {
2152
+ return undefined;
2153
+ }
2154
+ const parentRequirement = {
2155
+ [batchIdField]: {
2156
+ match: { "fields.currency": "fields.currency" },
2157
+ statuses: ["open"],
2158
+ },
2159
+ };
2160
+ const captureNoun = {
2161
+ desc: "One gross capture entry linked to an open payout batch",
2162
+ fields: {
2163
+ amount: moneyFieldSpec("Gross captured amount in minor units"),
2164
+ currency: {
2165
+ desc: "ISO 4217 currency shared with the payout batch",
2166
+ type: "currency",
2167
+ },
2168
+ [batchIdField]: {
2169
+ desc: "Open batch this capture entry accrues into",
2170
+ type: `ref:${noun}`,
2171
+ },
2172
+ [settlement.sourceCaptureReferenceField]: {
2173
+ desc: "Immutable source capture reference",
2174
+ type: "text",
2175
+ },
2176
+ },
2177
+ id: captureEntry,
2178
+ summary: "Gross capture lineage entry",
2179
+ title: `${titleize(noun)} Capture Entry`,
2180
+ verbs: {
2181
+ create: {
2182
+ requires: parentRequirement,
2183
+ summary: "Create a capture lineage entry on an open batch",
2184
+ to: "created",
2185
+ },
2186
+ accrue: {
2187
+ from: ["created"],
2188
+ requires: parentRequirement,
2189
+ summary: "Accrue the capture entry into the open batch",
2190
+ to: "accrued",
2191
+ },
2192
+ },
2193
+ };
2194
+ const adjustmentNoun = (id, direction) => ({
2195
+ desc: `One ${direction} adjustment linked to an open payout batch; closed batches stay unchanged`,
2196
+ fields: {
2197
+ amount: moneyFieldSpec(`${titleize(direction)} adjustment amount in minor units`),
2198
+ currency: {
2199
+ desc: "ISO 4217 currency shared with the payout batch",
2200
+ type: "currency",
2201
+ },
2202
+ adjustmentReference: {
2203
+ desc: "Immutable explicit adjustment reference",
2204
+ type: "text",
2205
+ },
2206
+ [batchIdField]: {
2207
+ desc: "Open batch this adjustment applies to",
2208
+ type: `ref:${noun}`,
2209
+ },
2210
+ [settlement.externalReversalReferenceField]: {
2211
+ desc: "Optional externally decided reversal reference",
2212
+ type: "text?",
2213
+ },
2214
+ [settlement.feeReferenceField]: {
2215
+ desc: "Optional fee entry reference",
2216
+ type: "text?",
2217
+ },
2218
+ [settlement.sourceCaptureReferenceField]: {
2219
+ desc: "Original capture reference that this adjustment corrects",
2220
+ type: "text",
2221
+ },
2222
+ },
2223
+ id,
2224
+ summary: `${titleize(direction)} adjustment with capture lineage`,
2225
+ title: `${titleize(noun)} ${titleize(direction)} Adjustment`,
2226
+ verbs: {
2227
+ create: {
2228
+ requires: parentRequirement,
2229
+ summary: `Create a ${direction} adjustment on an open batch`,
2230
+ to: "created",
2231
+ },
2232
+ adjust: {
2233
+ from: ["created"],
2234
+ requires: parentRequirement,
2235
+ summary: `Apply the ${direction} adjustment to the open batch`,
2236
+ to: "applied",
2237
+ },
2238
+ correct: {
2239
+ from: ["created"],
2240
+ requires: parentRequirement,
2241
+ summary: "Record a later correction on this open batch instead of changing the closed source batch",
2242
+ to: "applied",
2243
+ },
2244
+ },
2245
+ });
2246
+ const verbs = {
2247
+ create: {
2248
+ summary: `Open a ${titleize(noun).toLowerCase()}`,
2249
+ to: "open",
2250
+ },
2251
+ close: {
2252
+ due: { field: settlement.closeTriggerField, rule: closeRuleKey },
2253
+ from: ["open"],
2254
+ summary: "Freeze the batch and stop all new entries",
2255
+ to: "closed",
2256
+ },
2257
+ calculate: {
2258
+ from: ["closed"],
2259
+ signedSum: {
2260
+ amountRef: "netPayable",
2261
+ onNegative: "refuse",
2262
+ onZero: "refuse",
2263
+ sources: [
2264
+ {
2265
+ amountField: "amount",
2266
+ nounId: captureEntry,
2267
+ refField: batchIdField,
2268
+ sign: "add",
2269
+ statuses: ["accrued"],
2270
+ subtotalRef: "grossCaptureAmount",
2271
+ },
2272
+ {
2273
+ amountField: "amount",
2274
+ nounId: creditAdjustment,
2275
+ refField: batchIdField,
2276
+ sign: "add",
2277
+ statuses: ["applied"],
2278
+ subtotalRef: "creditAdjustmentAmount",
2279
+ },
2280
+ {
2281
+ amountField: "amount",
2282
+ nounId: debitAdjustment,
2283
+ refField: batchIdField,
2284
+ sign: "subtract",
2285
+ statuses: ["applied"],
2286
+ subtotalRef: "debitAdjustmentAmount",
2287
+ },
2288
+ ],
2289
+ },
2290
+ summary: "Prove and freeze the one signed net payable amount",
2291
+ to: "calculated",
2292
+ },
2293
+ approve: {
2294
+ from: ["calculated"],
2295
+ summary: "Approve the frozen payable without recomputing it",
2296
+ to: "approved",
2297
+ },
2298
+ instruct: {
2299
+ from: ["approved"],
2300
+ moneyEvent: payoutEventKey,
2301
+ payout: {
2302
+ amount: "refs.netPayable",
2303
+ beneficiaryField: settlement.payoutBeneficiaryReferenceField,
2304
+ beneficiaryPartyField: `${camelize(settlement.payoutDestination)}AccountId`,
2305
+ capture: "payoutId",
2306
+ currencyField: "currency",
2307
+ sourceAccountField: `${camelize(settlement.settlementAccount)}AccountId`,
2308
+ speed: "standard",
2309
+ },
2310
+ summary: "Create one idempotent payout from the frozen net payable",
2311
+ to: "instructed",
2312
+ },
2313
+ [settlement.payoutAcknowledgement.port]: {
2314
+ captureInput: {
2315
+ acknowledgementReference: "acknowledgementReference",
2316
+ },
2317
+ from: ["instructed"],
2318
+ port: {
2319
+ allowed: acknowledgementPort.allowed,
2320
+ fields: { acknowledgementReference: "text" },
2321
+ },
2322
+ summary: "Record the tenant's payout acknowledgement in the receipt",
2323
+ to: "acknowledged",
2324
+ },
2325
+ reconcile: {
2326
+ from: ["instructed", "acknowledged"],
2327
+ requiresSettlement: {
2328
+ capture: "settlementEvidenceId",
2329
+ payoutRef: "payoutId",
2330
+ },
2331
+ summary: "Record durable evidence that the payout settled",
2332
+ to: "reconciled",
2333
+ },
2334
+ };
2335
+ return {
2336
+ design: [
2337
+ `${noun}: capture entries plus signed adjustments freeze at ${settlement.closeTriggerField}; calculate persists gross, credit, debit, and net refs; negative or zero net refuses`,
2338
+ `${noun}: instruct creates one payout intent for the frozen refs.netPayable; only matched settlement evidence can reconcile it`,
2339
+ ],
2340
+ extraNouns: [
2341
+ captureNoun,
2342
+ adjustmentNoun(creditAdjustment, "credit"),
2343
+ adjustmentNoun(debitAdjustment, "debit"),
2344
+ ],
2345
+ feeLines: [],
2346
+ moneyEvents: [
2347
+ mintEvent({
2348
+ amount: "The frozen signed sum of gross capture entries plus credit adjustments minus debit adjustments",
2349
+ fromActor: settlement.settlementAccount,
2350
+ key: payoutEventKey,
2351
+ kind: "payout",
2352
+ toActor: settlement.payoutDestination,
2353
+ trigger: "Instruct the approved batch payout exactly once",
2354
+ }),
2355
+ ],
2356
+ noun: {
2357
+ actors: {
2358
+ [settlement.payoutDestination]: "beneficiary",
2359
+ [settlement.settlementAccount]: "payer",
2360
+ },
2361
+ desc: "Immutable batch of capture lineage and signed adjustments that creates one payout",
2362
+ fields: {
2363
+ [settlement.closeTriggerField]: dateFieldSpec("Date the open batch freezes against later entries"),
2364
+ currency: {
2365
+ desc: "ISO 4217 currency shared by the batch and payout instruction",
2366
+ type: "currency",
2367
+ },
2368
+ [settlement.payoutBeneficiaryReferenceField]: {
2369
+ desc: "Beneficiary ID for the payout instruction",
2370
+ type: "beneficiary",
2371
+ },
2372
+ },
2373
+ id: noun,
2374
+ summary: `Payout batch from ${settlement.settlementAccount.replaceAll("_", " ")} to ${settlement.payoutDestination.replaceAll("_", " ")}`,
2375
+ title: titleize(noun),
2376
+ verbs,
2377
+ },
2378
+ rules: [
2379
+ {
2380
+ allowedActors: [],
2381
+ detail: `At ${settlement.closeTriggerField}, the platform closes the batch and every child reference gate refuses later entries`,
2382
+ dueDriven: true,
2383
+ enforcement: "platform",
2384
+ gatesEvent: null,
2385
+ key: closeRuleKey,
2386
+ kind: "deadline",
2387
+ label: `Batch freezes on ${settlement.closeTriggerField}`,
2388
+ tenantTunable: false,
2389
+ },
2390
+ ],
2391
+ settlement: { name: noun, pieces: [] },
2392
+ };
2393
+ }
2394
+ function lowerDeposit(settlement, claim, giveBack, issues) {
2395
+ const noun = settlement.name;
2396
+ const amountName = settlement.amount.name;
2397
+ if (!verbNameIssues(noun, ["place_deposit", claim.name, giveBack.name], settlement.origin, issues)) {
2398
+ return undefined;
2399
+ }
2400
+ const eventKey = `${noun}_hold_1`;
2401
+ const events = [
2402
+ mintEvent({
2403
+ amount: `The full ${amountName}`,
2404
+ fromActor: settlement.payer,
2405
+ key: eventKey,
2406
+ kind: "hold",
2407
+ toActor: settlement.holder,
2408
+ trigger: `Reserve the ${amountName} in the ${settlement.holder.replaceAll("_", " ")}'s favor`,
2409
+ }),
2410
+ ];
2411
+ const verbs = {
2412
+ create: {
2413
+ summary: `Create a ${titleize(noun).toLowerCase()}`,
2414
+ to: "created",
2415
+ },
2416
+ place_deposit: {
2417
+ from: ["created"],
2418
+ moves: [
2419
+ {
2420
+ key: "reservation",
2421
+ operation: "reserve",
2422
+ amount: amountName,
2423
+ from: settlement.payer,
2424
+ to: settlement.holder,
2425
+ },
2426
+ ],
2427
+ moneyEvent: eventKey,
2428
+ summary: `Reserve the ${amountName} against the ${settlement.payer.replaceAll("_", " ")}'s account`,
2429
+ to: "held",
2430
+ },
2431
+ [claim.name]: {
2432
+ from: ["held"],
2433
+ moves: [
2434
+ {
2435
+ key: "post",
2436
+ operation: "post",
2437
+ reservation: "place_deposit_reservation",
2438
+ },
2439
+ ],
2440
+ summary: `Claim the deposit for the ${settlement.holder.replaceAll("_", " ")} through ${claim.name}`,
2441
+ to: "claimed",
2442
+ },
2443
+ [giveBack.name]: {
2444
+ from: ["held"],
2445
+ summary: `Return the deposit to the ${settlement.payer.replaceAll("_", " ")} through ${giveBack.name}`,
2446
+ to: "returned",
2447
+ moves: [
2448
+ {
2449
+ key: "void",
2450
+ operation: "void",
2451
+ reason: "Deposit returned in full",
2452
+ reservation: "place_deposit_reservation",
2453
+ },
2454
+ ],
2455
+ },
2456
+ };
2457
+ const portRule = (port, verbLabel) => ({
2458
+ allowedActors: [...port.allowed],
2459
+ detail: `${port.allowed.map(titleize).join(" or ")} decides through the tenant backend`,
2460
+ dueDriven: false,
2461
+ enforcement: "tenant_app",
2462
+ gatesEvent: null,
2463
+ key: frameKey(`${noun}_${port.name}_gate`),
2464
+ kind: "release_condition",
2465
+ label: `${verbLabel} decided through ${port.name}`,
2466
+ tenantTunable: false,
2467
+ });
2468
+ return {
2469
+ design: [
1377
2470
  `${noun}: ${amountName} held as a reservation on the ${settlement.payer.replaceAll("_", " ")}'s account; claimed whole through ${claim.name} or returned whole through ${giveBack.name}`,
1378
2471
  ],
1379
2472
  feeLines: [],
1380
- moneyEvents: events,
2473
+ moneyEvents: events,
2474
+ noun: {
2475
+ actors: {
2476
+ [settlement.payer]: "payer",
2477
+ [settlement.holder]: "beneficiary",
2478
+ },
2479
+ desc: `Deposit: the ${amountName} is reserved against the ${settlement.payer.replaceAll("_", " ")}'s account in the ${settlement.holder.replaceAll("_", " ")}'s favor, then claimed or returned in full`,
2480
+ fields: {
2481
+ [amountName]: moneyFieldSpec(`The deposit amount in ${settlement.amount.currency} minor units, reserved in full and fully accounted on claim or return`),
2482
+ },
2483
+ id: noun,
2484
+ summary: `Refundable deposit from ${settlement.payer.replaceAll("_", " ")} held for ${settlement.holder.replaceAll("_", " ")}`,
2485
+ title: titleize(noun),
2486
+ verbs,
2487
+ },
2488
+ rules: [portRule(claim, "Claim"), portRule(giveBack, "Return")],
2489
+ settlement: { name: noun, pieces: [] },
2490
+ };
2491
+ }
2492
+ // ---------------------------------------------------------------------------
2493
+ // scheduled and advance: finite due-driven anchors
2494
+ /** Equal N-way piece widths in bps; the first anchor absorbs the remainder. */
2495
+ function evenPieceBps(count) {
2496
+ const base = Math.floor(Number(TOTAL_BPS) / count);
2497
+ const widths = Array.from({ length: count }, () => base);
2498
+ widths[0] = Number(TOTAL_BPS) - base * (count - 1);
2499
+ return widths;
2500
+ }
2501
+ // ---------------------------------------------------------------------------
2502
+ // funding_round: aggregate commitments with threshold close and whole unwind
2503
+ function lowerFundingRound(settlement) {
2504
+ const noun = settlement.name;
2505
+ const child = `${noun}_commitment`;
2506
+ const parentRef = `${camelize(noun)}Id`;
2507
+ const commitEvent = frameKey(`${noun}_commit`);
2508
+ const cancelEvent = frameKey(`${noun}_cancel`);
2509
+ const collectEvent = frameKey(`${noun}_collect`);
2510
+ const refundEvent = frameKey(`${noun}_refund`);
2511
+ const closeRule = frameKey(`${noun}_close`);
2512
+ const aggregate = (kind) => [
2513
+ {
2514
+ check: {
2515
+ amountField: "amount",
2516
+ kind,
2517
+ targetField: settlement.target.name,
2518
+ },
2519
+ nounId: child,
2520
+ over: "children",
2521
+ refField: parentRef,
2522
+ statuses: ["committed"],
2523
+ },
2524
+ ];
2525
+ const parentRequirement = (statuses) => ({
2526
+ [parentRef]: {
2527
+ bind: {
2528
+ currency: "fields.currency",
2529
+ [`${camelize(settlement.beneficiary)}AccountId`]: `fields.${camelize(settlement.beneficiary)}AccountId`,
2530
+ },
2531
+ statuses,
2532
+ },
2533
+ });
2534
+ const transitionRequirement = (statuses) => ({
2535
+ [parentRef]: {
2536
+ match: {
2537
+ "fields.currency": "fields.currency",
2538
+ [`fields.${camelize(settlement.beneficiary)}AccountId`]: `fields.${camelize(settlement.beneficiary)}AccountId`,
2539
+ },
2540
+ statuses,
2541
+ },
2542
+ });
2543
+ return {
2544
+ design: [
2545
+ `${noun}: reuses the catalog funding round and commitment mechanism; the parent lock caps committed rows by target and contributor count`,
2546
+ `${noun}: the stored close anchor chooses threshold activation or failure; each commitment then moves whole from its own custody`,
2547
+ ],
2548
+ extraNouns: [
2549
+ {
2550
+ actors: {
2551
+ [settlement.beneficiary]: "beneficiary",
2552
+ [settlement.contributor]: "payer",
2553
+ },
2554
+ desc: `One whole commitment linked to ${noun}`,
2555
+ escrow: true,
2556
+ fields: {
2557
+ amount: moneyFieldSpec("One whole commitment amount"),
2558
+ currency: {
2559
+ desc: "Currency derived from the funding round",
2560
+ type: "currency",
2561
+ },
2562
+ [parentRef]: { desc: `The exact ${noun}`, type: `ref:${noun}` },
2563
+ },
2564
+ id: child,
2565
+ summary: `Whole commitment to ${noun}`,
2566
+ title: `${titleize(noun)} Commitment`,
2567
+ verbs: {
2568
+ create: {
2569
+ moneyEvent: commitEvent,
2570
+ moves: [
2571
+ {
2572
+ amount: "amount",
2573
+ from: settlement.contributor,
2574
+ key: "commit",
2575
+ operation: "create",
2576
+ to: "escrow",
2577
+ },
2578
+ ],
2579
+ requires: parentRequirement(["open"]),
2580
+ requiresExposure: [
2581
+ {
2582
+ amountField: "amount",
2583
+ anchorField: parentRef,
2584
+ capField: settlement.target.name,
2585
+ capOnAnchor: true,
2586
+ childNounId: child,
2587
+ statuses: ["committed"],
2588
+ },
2589
+ ],
2590
+ summary: "Store one whole commitment without exceeding the round target",
2591
+ to: "committed",
2592
+ },
2593
+ cancel: {
2594
+ from: ["committed"],
2595
+ moneyEvent: cancelEvent,
2596
+ moves: [
2597
+ {
2598
+ amount: "amount",
2599
+ from: "escrow",
2600
+ key: "cancel",
2601
+ operation: "create",
2602
+ to: settlement.contributor,
2603
+ },
2604
+ ],
2605
+ requires: transitionRequirement(["open"]),
2606
+ summary: "Cancel one commitment while the round is open",
2607
+ to: "cancelled",
2608
+ },
2609
+ collect: {
2610
+ from: ["committed"],
2611
+ moneyEvent: collectEvent,
2612
+ moves: [
2613
+ {
2614
+ amount: "amount",
2615
+ from: "escrow",
2616
+ key: "collect",
2617
+ operation: "create",
2618
+ to: settlement.beneficiary,
2619
+ },
2620
+ ],
2621
+ requires: transitionRequirement(["active"]),
2622
+ summary: "Collect one successful commitment whole",
2623
+ to: "collected",
2624
+ },
2625
+ refund: {
2626
+ from: ["committed"],
2627
+ moneyEvent: refundEvent,
2628
+ moves: [
2629
+ {
2630
+ amount: "amount",
2631
+ from: "escrow",
2632
+ key: "refund",
2633
+ operation: "create",
2634
+ to: settlement.contributor,
2635
+ },
2636
+ ],
2637
+ requires: transitionRequirement(["failed"]),
2638
+ summary: "Refund one failed-round commitment whole",
2639
+ to: "refunded",
2640
+ },
2641
+ },
2642
+ },
2643
+ ],
2644
+ generatedPrefixNounIds: [child],
2645
+ feeLines: [],
2646
+ moneyEvents: [
2647
+ mintEvent({
2648
+ amount: "One stored commitment",
2649
+ fromActor: settlement.contributor,
2650
+ key: commitEvent,
2651
+ kind: "charge",
2652
+ occurrence: "repeatable",
2653
+ toActor: "escrow",
2654
+ trigger: "Create one target-capped commitment",
2655
+ }),
2656
+ mintEvent({
2657
+ amount: "One stored commitment whole",
2658
+ fromActor: "escrow",
2659
+ key: cancelEvent,
2660
+ kind: "refund",
2661
+ occurrence: "repeatable",
2662
+ toActor: settlement.contributor,
2663
+ trigger: "Cancel before close",
2664
+ }),
2665
+ mintEvent({
2666
+ amount: "One stored commitment whole",
2667
+ fromActor: "escrow",
2668
+ key: collectEvent,
2669
+ kind: "payout",
2670
+ occurrence: "repeatable",
2671
+ toActor: settlement.beneficiary,
2672
+ trigger: "Collect after threshold close",
2673
+ }),
2674
+ mintEvent({
2675
+ amount: "One stored commitment whole",
2676
+ fromActor: "escrow",
2677
+ key: refundEvent,
2678
+ kind: "refund",
2679
+ occurrence: "repeatable",
2680
+ toActor: settlement.contributor,
2681
+ trigger: "Refund after failed close",
2682
+ }),
2683
+ ],
2684
+ noun: {
2685
+ actors: { [settlement.beneficiary]: "beneficiary" },
2686
+ aggregateInvariants: [
2687
+ {
2688
+ childField: "amount",
2689
+ childNounId: child,
2690
+ childRefField: parentRef,
2691
+ childStatuses: ["committed"],
2692
+ parentField: settlement.target.name,
2693
+ },
2694
+ {
2695
+ count: true,
2696
+ childNounId: child,
2697
+ childRefField: parentRef,
2698
+ childStatuses: ["committed"],
2699
+ parentField: "maxContributors",
2700
+ },
2701
+ ],
2702
+ desc: "All-or-nothing aggregate funding threshold",
2703
+ fields: {
2704
+ currency: {
2705
+ desc: `Currency fixed to ${settlement.target.currency}`,
2706
+ type: "currency",
2707
+ },
2708
+ [settlement.target.name]: moneyFieldSpec(`Funding target in ${settlement.target.currency} minor units`),
2709
+ [settlement.closeByField]: dateFieldSpec("Stored close anchor"),
2710
+ maxContributors: {
2711
+ desc: `Exactly ${settlement.maxContributors} admitted contributors`,
2712
+ type: `const:${settlement.maxContributors}`,
2713
+ },
2714
+ },
2715
+ id: noun,
2716
+ summary: `Threshold funding round for ${settlement.beneficiary.replaceAll("_", " ")}`,
2717
+ title: titleize(noun),
2718
+ verbs: {
2719
+ create: { summary: "Open the funding round", to: "open" },
2720
+ activate: {
2721
+ due: { field: settlement.closeByField, rule: closeRule },
2722
+ from: ["open"],
2723
+ requiresAggregate: aggregate("sum_at_least"),
2724
+ summary: "Activate when commitments meet the target",
2725
+ to: "active",
2726
+ },
2727
+ fail: {
2728
+ due: { field: settlement.closeByField, rule: closeRule },
2729
+ from: ["open"],
2730
+ requiresAggregate: aggregate("sum_below"),
2731
+ summary: "Fail when commitments remain below target",
2732
+ to: "failed",
2733
+ },
2734
+ close: {
2735
+ from: ["active"],
2736
+ requiresAggregate: [
2737
+ {
2738
+ check: { kind: "all_in" },
2739
+ nounId: child,
2740
+ over: "children",
2741
+ refField: parentRef,
2742
+ statuses: ["cancelled", "collected"],
2743
+ },
2744
+ ],
2745
+ summary: "Settle after every admitted row is collected or was cancelled before activation",
2746
+ to: "settled",
2747
+ },
2748
+ },
2749
+ },
2750
+ rules: [
2751
+ {
2752
+ allowedActors: [],
2753
+ detail: "The stored close anchor compares committed rows with the target",
2754
+ dueDriven: true,
2755
+ enforcement: "platform",
2756
+ gatesEvent: null,
2757
+ key: closeRule,
2758
+ kind: "deadline",
2759
+ label: "Round closes against its stored threshold",
2760
+ tenantTunable: false,
2761
+ },
2762
+ ],
2763
+ settlement: { name: noun, pieces: [] },
2764
+ };
2765
+ }
2766
+ // ---------------------------------------------------------------------------
2767
+ // weighted_distribution: frozen weights with deterministic largest remainder
2768
+ function lowerWeightedDistribution(settlement, snapshot) {
2769
+ const noun = settlement.name;
2770
+ const child = `${noun}_entitlement`;
2771
+ const parentRef = `${camelize(noun)}Id`;
2772
+ const payoutEvent = frameKey(`${noun}_payout`);
2773
+ const parentRequirement = (statuses) => ({
2774
+ [parentRef]: {
2775
+ bind: {
2776
+ currency: "fields.currency",
2777
+ [`${camelize(settlement.source)}AccountId`]: `fields.${camelize(settlement.source)}AccountId`,
2778
+ },
2779
+ statuses,
2780
+ },
2781
+ });
2782
+ return {
2783
+ design: [
2784
+ `${noun}: reuses the catalog largest-remainder distribution; the evidence port freezes the claimant set before any payout`,
2785
+ ],
2786
+ extraNouns: [
2787
+ {
2788
+ actors: {
2789
+ [settlement.recipient]: "beneficiary",
2790
+ [settlement.source]: "payer",
2791
+ },
2792
+ desc: `One frozen weighted entitlement in ${noun}`,
2793
+ fields: {
2794
+ currency: {
2795
+ desc: "Currency derived from the distribution",
2796
+ type: "currency",
2797
+ },
2798
+ [parentRef]: { desc: `The exact ${noun}`, type: `ref:${noun}` },
2799
+ [settlement.weight.name]: moneyFieldSpec("Stored non-negative entitlement weight"),
2800
+ },
2801
+ id: child,
2802
+ summary: `Frozen entitlement in ${noun}`,
2803
+ title: `${titleize(noun)} Entitlement`,
2804
+ verbs: {
2805
+ create: {
2806
+ requires: parentRequirement(["open"]),
2807
+ summary: "Record one entitlement before snapshot",
2808
+ to: "recorded",
2809
+ },
2810
+ payout: {
2811
+ distribute: {
2812
+ amountRef: "payoutShare",
2813
+ onZero: "skip_steps",
2814
+ pool: {
2815
+ from: "parent",
2816
+ path: `fields.${settlement.amount.name}`,
2817
+ },
2818
+ refField: parentRef,
2819
+ statuses: ["recorded", "paid"],
2820
+ weightField: settlement.weight.name,
2821
+ },
2822
+ from: ["recorded"],
2823
+ moneyEvent: payoutEvent,
2824
+ moves: [
2825
+ {
2826
+ amount: "refs.payoutShare",
2827
+ from: settlement.source,
2828
+ key: "payout",
2829
+ operation: "create",
2830
+ to: settlement.recipient,
2831
+ },
2832
+ ],
2833
+ requires: {
2834
+ [parentRef]: {
2835
+ match: {
2836
+ "fields.currency": "fields.currency",
2837
+ [`fields.${camelize(settlement.source)}AccountId`]: `fields.${camelize(settlement.source)}AccountId`,
2838
+ },
2839
+ statuses: ["snapshotted"],
2840
+ },
2841
+ },
2842
+ summary: "Pay the deterministic largest-remainder share once",
2843
+ to: "paid",
2844
+ },
2845
+ },
2846
+ },
2847
+ ],
2848
+ generatedPrefixNounIds: [child],
2849
+ feeLines: [],
2850
+ moneyEvents: [
2851
+ mintEvent({
2852
+ amount: "A deterministic largest-remainder share of the stored pool",
2853
+ fromActor: settlement.source,
2854
+ key: payoutEvent,
2855
+ kind: "payout",
2856
+ occurrence: "repeatable",
2857
+ toActor: settlement.recipient,
2858
+ trigger: "Pay one frozen entitlement",
2859
+ }),
2860
+ ],
2861
+ noun: {
2862
+ actors: { [settlement.source]: "payer" },
2863
+ aggregateInvariants: [
2864
+ {
2865
+ count: true,
2866
+ childNounId: child,
2867
+ childRefField: parentRef,
2868
+ childStatuses: ["recorded", "paid"],
2869
+ parentField: "maxRecipients",
2870
+ },
2871
+ ],
2872
+ desc: "Evidence-frozen weighted distribution",
2873
+ fields: {
2874
+ currency: {
2875
+ desc: `Currency fixed to ${settlement.amount.currency}`,
2876
+ type: "currency",
2877
+ },
2878
+ [settlement.amount.name]: moneyFieldSpec(`Distribution pool in ${settlement.amount.currency} minor units`),
2879
+ [settlement.recordAtField]: dateFieldSpec("Stored record date"),
2880
+ maxRecipients: {
2881
+ desc: `Exactly ${settlement.maxRecipients} frozen entitlement rows`,
2882
+ type: `const:${settlement.maxRecipients}`,
2883
+ },
2884
+ },
2885
+ id: noun,
2886
+ summary: "Frozen largest-remainder distribution",
2887
+ title: titleize(noun),
2888
+ verbs: {
2889
+ create: { summary: "Open entitlement recording", to: "open" },
2890
+ [settlement.snapshot.port]: {
2891
+ captureInput: { snapshotEvidenceReference: "evidenceReference" },
2892
+ from: ["open"],
2893
+ port: {
2894
+ allowed: snapshot.allowed,
2895
+ fields: { evidenceReference: "text" },
2896
+ },
2897
+ summary: "Freeze the entitlement set from stored evidence",
2898
+ to: "snapshotted",
2899
+ },
2900
+ },
2901
+ },
2902
+ rules: [],
2903
+ settlement: { name: noun, pieces: [] },
2904
+ };
2905
+ }
2906
+ // ---------------------------------------------------------------------------
2907
+ // credit_facility: draw capacity only, repayment remains on scheduled obligation
2908
+ function lowerCreditFacility(settlement) {
2909
+ const noun = settlement.name;
2910
+ const child = `${noun}_draw`;
2911
+ const facilityRef = `${camelize(noun)}Id`;
2912
+ const obligationRef = `${camelize(settlement.obligation.settlement)}Id`;
2913
+ const drawEvent = frameKey(`${noun}_draw`);
2914
+ const expiryRule = frameKey(`${noun}_expiry`);
2915
+ const countedStatuses = settlement.availabilityPolicy === "revolving"
2916
+ ? ["drawn"]
2917
+ : ["drawn", "resolved"];
2918
+ return {
2919
+ design: [
2920
+ `${noun}: owns reusable draw capacity only; ${settlement.obligation.settlement} remains the sole repayment and delinquency owner`,
2921
+ ],
2922
+ extraNouns: [
2923
+ {
2924
+ actors: {
2925
+ [settlement.drawDestination]: "beneficiary",
2926
+ [settlement.lender]: "payer",
2927
+ },
2928
+ desc: `One capacity-capped draw linked to ${settlement.obligation.settlement}`,
2929
+ fields: {
2930
+ amount: moneyFieldSpec("One draw amount"),
2931
+ currency: {
2932
+ desc: "Currency derived from the facility",
2933
+ type: "currency",
2934
+ },
2935
+ [facilityRef]: { desc: `The exact ${noun}`, type: `ref:${noun}` },
2936
+ [obligationRef]: {
2937
+ desc: "The sole repayment obligation",
2938
+ type: `ref:${settlement.obligation.settlement}`,
2939
+ },
2940
+ },
2941
+ id: child,
2942
+ summary: `Draw from ${noun}`,
2943
+ title: `${titleize(noun)} Draw`,
2944
+ verbs: {
2945
+ create: {
2946
+ moneyEvent: drawEvent,
2947
+ moves: [
2948
+ {
2949
+ amount: "amount",
2950
+ from: settlement.lender,
2951
+ key: "draw",
2952
+ operation: "create",
2953
+ to: settlement.drawDestination,
2954
+ },
2955
+ ],
2956
+ requires: {
2957
+ [facilityRef]: {
2958
+ bind: {
2959
+ currency: "fields.currency",
2960
+ [`${camelize(settlement.drawDestination)}AccountId`]: `fields.${camelize(settlement.drawDestination)}AccountId`,
2961
+ [`${camelize(settlement.lender)}AccountId`]: `fields.${camelize(settlement.lender)}AccountId`,
2962
+ },
2963
+ statuses: ["active"],
2964
+ },
2965
+ [obligationRef]: { statuses: ["active"], unique: true },
2966
+ },
2967
+ requiresExposure: [
2968
+ {
2969
+ amountField: "amount",
2970
+ anchorField: facilityRef,
2971
+ capField: settlement.limit.name,
2972
+ capOnAnchor: true,
2973
+ childNounId: child,
2974
+ statuses: countedStatuses,
2975
+ },
2976
+ ],
2977
+ summary: "Create one draw under the locked facility capacity",
2978
+ to: "drawn",
2979
+ },
2980
+ resolve: {
2981
+ from: ["drawn"],
2982
+ requires: {
2983
+ [obligationRef]: ["repaid", "written_off"],
2984
+ },
2985
+ summary: "Release revolving capacity only after the linked obligation resolves",
2986
+ to: "resolved",
2987
+ },
2988
+ },
2989
+ },
2990
+ ],
2991
+ generatedPrefixNounIds: [child],
2992
+ feeLines: [],
2993
+ moneyEvents: [
2994
+ mintEvent({
2995
+ amount: "One draw under the stored facility limit",
2996
+ fromActor: settlement.lender,
2997
+ key: drawEvent,
2998
+ kind: "payout",
2999
+ occurrence: "repeatable",
3000
+ toActor: settlement.drawDestination,
3001
+ trigger: "Admit one linked draw",
3002
+ }),
3003
+ ],
3004
+ noun: {
3005
+ actors: {
3006
+ [settlement.borrower]: "party",
3007
+ [settlement.drawDestination]: "beneficiary",
3008
+ [settlement.lender]: "payer",
3009
+ },
3010
+ desc: "Reusable capacity with repayment delegated to one scheduled obligation",
3011
+ fields: {
3012
+ currency: {
3013
+ desc: `Currency fixed to ${settlement.limit.currency}`,
3014
+ type: "currency",
3015
+ },
3016
+ [settlement.limit.name]: moneyFieldSpec(`Facility limit in ${settlement.limit.currency} minor units`),
3017
+ [settlement.expiresAtField]: dateFieldSpec("Stored draw expiry"),
3018
+ },
3019
+ id: noun,
3020
+ summary: `Draw capacity for ${settlement.borrower.replaceAll("_", " ")}`,
3021
+ title: titleize(noun),
3022
+ verbs: {
3023
+ create: { summary: "Open the facility", to: "active" },
3024
+ freeze: {
3025
+ due: { field: settlement.expiresAtField, rule: expiryRule },
3026
+ from: ["active"],
3027
+ summary: "Freeze new draws at expiry",
3028
+ to: "frozen",
3029
+ },
3030
+ close: {
3031
+ from: ["active", "frozen"],
3032
+ requiresAggregate: [
3033
+ {
3034
+ check: { kind: "all_in" },
3035
+ nounId: child,
3036
+ over: "children",
3037
+ refField: facilityRef,
3038
+ statuses: ["resolved"],
3039
+ },
3040
+ ],
3041
+ summary: "Close only when every admitted draw resolved",
3042
+ to: "closed",
3043
+ },
3044
+ },
3045
+ },
3046
+ rules: [
3047
+ {
3048
+ allowedActors: [],
3049
+ detail: "The stored expiry freezes new draws without changing repayment state",
3050
+ dueDriven: true,
3051
+ enforcement: "platform",
3052
+ gatesEvent: null,
3053
+ key: expiryRule,
3054
+ kind: "deadline",
3055
+ label: "Facility freezes at expiry",
3056
+ tenantTunable: false,
3057
+ },
3058
+ ],
3059
+ settlement: { name: noun, pieces: [] },
3060
+ };
3061
+ }
3062
+ // ---------------------------------------------------------------------------
3063
+ // conditional_disbursement: one evidence-gated amount under a stored cap
3064
+ function lowerConditionalDisbursement(settlement, decision) {
3065
+ const noun = settlement.name;
3066
+ const child = `${noun}_approved_amount`;
3067
+ const parentRef = `${camelize(noun)}Id`;
3068
+ const payoutEvent = frameKey(`${noun}_payout`);
3069
+ const sourceAccountField = `${camelize(settlement.source)}AccountId`;
3070
+ const destinationAccountField = `${camelize(settlement.destination)}AccountId`;
3071
+ const parentTransitionRequirement = {
3072
+ [parentRef]: {
3073
+ match: {
3074
+ "fields.currency": "fields.currency",
3075
+ [`fields.${destinationAccountField}`]: `fields.${destinationAccountField}`,
3076
+ [`fields.${sourceAccountField}`]: `fields.${sourceAccountField}`,
3077
+ },
3078
+ statuses: ["submitted"],
3079
+ },
3080
+ };
3081
+ return {
3082
+ design: [
3083
+ `${noun}: a stored external decision may approve one amount under the cap; recovery requires a separate transfer`,
3084
+ ],
3085
+ extraNouns: [
3086
+ {
3087
+ actors: {
3088
+ [settlement.destination]: "beneficiary",
3089
+ [settlement.source]: "payer",
3090
+ },
3091
+ desc: `One evidence-gated amount under ${noun}`,
3092
+ fields: {
3093
+ amount: moneyFieldSpec("Approved amount under the parent cap"),
3094
+ currency: {
3095
+ desc: "Currency derived from the parent cap",
3096
+ type: "currency",
3097
+ },
3098
+ [parentRef]: { desc: `The exact ${noun}`, type: `ref:${noun}` },
3099
+ },
3100
+ id: child,
3101
+ summary: `Approved amount under ${noun}`,
3102
+ title: `${titleize(noun)} Approved Amount`,
3103
+ verbs: {
3104
+ create: {
3105
+ requires: {
3106
+ [parentRef]: {
3107
+ bind: {
3108
+ currency: "fields.currency",
3109
+ [destinationAccountField]: `fields.${destinationAccountField}`,
3110
+ [sourceAccountField]: `fields.${sourceAccountField}`,
3111
+ },
3112
+ statuses: ["submitted"],
3113
+ unique: true,
3114
+ },
3115
+ },
3116
+ summary: "Create one candidate amount under the parent",
3117
+ to: "created",
3118
+ },
3119
+ approve: {
3120
+ captureInput: { decisionEvidenceReference: "evidenceReference" },
3121
+ from: ["created"],
3122
+ port: {
3123
+ allowed: decision.allowed,
3124
+ fields: { evidenceReference: "text" },
3125
+ },
3126
+ requires: parentTransitionRequirement,
3127
+ requiresExposure: [
3128
+ {
3129
+ amountField: "amount",
3130
+ anchorField: parentRef,
3131
+ capField: settlement.cap.name,
3132
+ capOnAnchor: true,
3133
+ childNounId: child,
3134
+ statuses: ["approved", "paid"],
3135
+ },
3136
+ ],
3137
+ summary: "Store one externally approved amount under the cap",
3138
+ to: "approved",
3139
+ },
3140
+ pay: {
3141
+ from: ["approved"],
3142
+ moneyEvent: payoutEvent,
3143
+ moves: [
3144
+ {
3145
+ amount: "amount",
3146
+ from: settlement.source,
3147
+ key: "payout",
3148
+ operation: "create",
3149
+ to: settlement.destination,
3150
+ },
3151
+ ],
3152
+ requires: parentTransitionRequirement,
3153
+ summary: "Pay the stored approved amount once",
3154
+ to: "paid",
3155
+ },
3156
+ },
3157
+ },
3158
+ ],
3159
+ generatedPrefixNounIds: [child],
3160
+ feeLines: [],
3161
+ moneyEvents: [
3162
+ mintEvent({
3163
+ amount: "The stored approved amount under the cap",
3164
+ fromActor: settlement.source,
3165
+ key: payoutEvent,
3166
+ kind: "payout",
3167
+ occurrence: "repeatable",
3168
+ toActor: settlement.destination,
3169
+ trigger: "Pay one approved amount",
3170
+ }),
3171
+ ],
1381
3172
  noun: {
1382
3173
  actors: {
1383
- [settlement.payer]: "payer",
1384
- [settlement.holder]: "beneficiary",
3174
+ [settlement.destination]: "beneficiary",
3175
+ [settlement.source]: "payer",
1385
3176
  },
1386
- desc: `Deposit: the ${amountName} is reserved against the ${settlement.payer.replaceAll("_", " ")}'s account in the ${settlement.holder.replaceAll("_", " ")}'s favor, then claimed or returned in full`,
3177
+ desc: "Capped disbursement controlled by stored external evidence",
1387
3178
  fields: {
1388
- [amountName]: moneyFieldSpec(`The deposit amount in ${settlement.amount.currency} minor units, reserved in full and fully accounted on claim or return`),
3179
+ currency: {
3180
+ desc: `Currency fixed to ${settlement.cap.currency}`,
3181
+ type: "currency",
3182
+ },
3183
+ [settlement.cap.name]: moneyFieldSpec(`Disbursement cap in ${settlement.cap.currency} minor units`),
1389
3184
  },
1390
3185
  id: noun,
1391
- summary: `Refundable deposit from ${settlement.payer.replaceAll("_", " ")} held for ${settlement.holder.replaceAll("_", " ")}`,
3186
+ summary: `Capped disbursement to ${settlement.destination.replaceAll("_", " ")}`,
1392
3187
  title: titleize(noun),
1393
- verbs,
3188
+ verbs: {
3189
+ create: { summary: "Submit the capped disbursement", to: "submitted" },
3190
+ deny: {
3191
+ captureInput: { decisionEvidenceReference: "evidenceReference" },
3192
+ from: ["submitted"],
3193
+ port: {
3194
+ allowed: decision.allowed,
3195
+ fields: { evidenceReference: "text" },
3196
+ },
3197
+ requiresAggregate: [
3198
+ {
3199
+ check: { kind: "all_in" },
3200
+ nounId: child,
3201
+ over: "children",
3202
+ refField: parentRef,
3203
+ statuses: ["created"],
3204
+ },
3205
+ ],
3206
+ summary: "Record a denial without moving money",
3207
+ to: "denied",
3208
+ },
3209
+ },
1394
3210
  },
1395
- rules: [portRule(claim, "Claim"), portRule(giveBack, "Return")],
3211
+ rules: [],
1396
3212
  settlement: { name: noun, pieces: [] },
1397
3213
  };
1398
3214
  }
1399
3215
  // ---------------------------------------------------------------------------
1400
- // scheduled and advance: finite due-driven anchors
1401
- /** Equal N-way piece widths in bps; the first anchor absorbs the remainder. */
1402
- function evenPieceBps(count) {
1403
- const base = Math.floor(Number(TOTAL_BPS) / count);
1404
- const widths = Array.from({ length: count }, () => base);
1405
- widths[0] = Number(TOTAL_BPS) - base * (count - 1);
1406
- return widths;
3216
+ // rotating_pool: fixed roster, one contribution per member and cycle
3217
+ function lowerRotatingPool(settlement) {
3218
+ const noun = settlement.name;
3219
+ const parentRef = `${camelize(noun)}Id`;
3220
+ const contributionEvent = frameKey(`${noun}_contribution`);
3221
+ const guaranteeEvent = frameKey(`${noun}_guarantee_contribution`);
3222
+ const payoutEvent = frameKey(`${noun}_payout`);
3223
+ const fixedActors = [
3224
+ ...new Set([
3225
+ ...settlement.members,
3226
+ ...settlement.payoutOrder,
3227
+ ...(settlement.guarantor ? [settlement.guarantor] : []),
3228
+ ]),
3229
+ ];
3230
+ const fixedActorBindings = Object.fromEntries(fixedActors.map((actor) => {
3231
+ const accountField = `${camelize(actor)}AccountId`;
3232
+ return [accountField, `fields.${accountField}`];
3233
+ }));
3234
+ const childIds = settlement.members.map((member) => `${noun}_${member}_contribution`);
3235
+ const childNouns = settlement.members.map((member, memberIndex) => {
3236
+ const id = childIds[memberIndex];
3237
+ const verbs = {
3238
+ create: {
3239
+ requires: {
3240
+ [parentRef]: {
3241
+ bind: {
3242
+ currency: "fields.currency",
3243
+ ...fixedActorBindings,
3244
+ [settlement.schedule.firstDueField]: `fields.${settlement.schedule.firstDueField}`,
3245
+ [settlement.contribution.name]: `fields.${settlement.contribution.name}`,
3246
+ },
3247
+ statuses: ["forming"],
3248
+ unique: true,
3249
+ },
3250
+ },
3251
+ summary: `Create the fixed contribution row for ${member.replaceAll("_", " ")}`,
3252
+ to: "cycle_1_due",
3253
+ },
3254
+ };
3255
+ for (let index = 0; index < settlement.schedule.count; index += 1) {
3256
+ const cycle = index + 1;
3257
+ const dueState = `cycle_${cycle}_due`;
3258
+ const defaultState = `cycle_${cycle}_defaulted`;
3259
+ const fundedState = `cycle_${cycle}_funded`;
3260
+ const guaranteedState = `cycle_${cycle}_guaranteed`;
3261
+ const nextState = cycle === settlement.schedule.count
3262
+ ? "final_paid"
3263
+ : `cycle_${cycle + 1}_due`;
3264
+ const dueRule = frameKey(`${noun}_cycle_${cycle}_due`);
3265
+ const cycleAmount = settlement.contribution.name;
3266
+ const guaranteeAmount = settlement.contribution.name;
3267
+ const due = {
3268
+ field: settlement.schedule.firstDueField,
3269
+ rule: dueRule,
3270
+ ...anchorOffset(settlement.schedule, index),
3271
+ };
3272
+ verbs[`contribute_cycle_${cycle}`] = {
3273
+ due,
3274
+ from: [dueState],
3275
+ moneyEvent: contributionEvent,
3276
+ moves: [
3277
+ {
3278
+ amount: cycleAmount,
3279
+ from: member,
3280
+ key: "contribution",
3281
+ operation: "create",
3282
+ to: "escrow",
3283
+ },
3284
+ ],
3285
+ requires: { [parentRef]: [`active_cycle_${cycle}`] },
3286
+ summary: `Fund ${member.replaceAll("_", " ")}'s cycle ${cycle} contribution`,
3287
+ to: fundedState,
3288
+ };
3289
+ verbs[`mark_default_cycle_${cycle}`] = {
3290
+ due,
3291
+ from: [dueState],
3292
+ requires: { [parentRef]: [`active_cycle_${cycle}`] },
3293
+ summary: `Mark the stored cycle ${cycle} due condition`,
3294
+ to: defaultState,
3295
+ };
3296
+ if (settlement.guarantor) {
3297
+ verbs[`guarantee_cycle_${cycle}`] = {
3298
+ from: [defaultState],
3299
+ moneyEvent: guaranteeEvent,
3300
+ moves: [
3301
+ {
3302
+ amount: guaranteeAmount,
3303
+ from: settlement.guarantor,
3304
+ key: "guarantee",
3305
+ operation: "create",
3306
+ to: "escrow",
3307
+ },
3308
+ ],
3309
+ requires: { [parentRef]: [`active_cycle_${cycle}`] },
3310
+ summary: `Fund the defaulted cycle ${cycle} amount before payout`,
3311
+ to: guaranteedState,
3312
+ };
3313
+ }
3314
+ verbs[`pay_cycle_${cycle}`] = {
3315
+ from: [fundedState],
3316
+ moneyEvent: payoutEvent,
3317
+ moves: [
3318
+ {
3319
+ amount: cycleAmount,
3320
+ from: "escrow",
3321
+ key: "payout",
3322
+ operation: "create",
3323
+ to: settlement.payoutOrder[index],
3324
+ },
3325
+ ],
3326
+ requires: { [parentRef]: [`cycle_${cycle}_ready`] },
3327
+ summary: `Pay this member's stored contribution into cycle ${cycle}'s shared pot recipient`,
3328
+ to: nextState,
3329
+ };
3330
+ if (settlement.guarantor) {
3331
+ verbs[`pay_guaranteed_cycle_${cycle}`] = {
3332
+ from: [guaranteedState],
3333
+ moneyEvent: payoutEvent,
3334
+ moves: [
3335
+ {
3336
+ amount: guaranteeAmount,
3337
+ from: "escrow",
3338
+ key: "payout",
3339
+ operation: "create",
3340
+ to: settlement.payoutOrder[index],
3341
+ },
3342
+ ],
3343
+ requires: { [parentRef]: [`cycle_${cycle}_ready`] },
3344
+ summary: `Pay the funded default into cycle ${cycle}'s stored recipient`,
3345
+ to: nextState,
3346
+ };
3347
+ }
3348
+ }
3349
+ verbs.close = {
3350
+ from: ["final_paid"],
3351
+ requiresDrainedAccount: { path: "refs.escrowAccountId" },
3352
+ summary: "Complete after the final payout drains this member custody",
3353
+ to: "completed",
3354
+ };
3355
+ return {
3356
+ actors: Object.fromEntries([
3357
+ [member, "payer"],
3358
+ ...(settlement.guarantor ? [[settlement.guarantor, "payer"]] : []),
3359
+ ...settlement.payoutOrder.map((recipient) => [
3360
+ recipient,
3361
+ "beneficiary",
3362
+ ]),
3363
+ ]),
3364
+ desc: `Fixed contribution row for ${member.replaceAll("_", " ")}`,
3365
+ escrow: true,
3366
+ fields: {
3367
+ [settlement.contribution.name]: moneyFieldSpec("Exact contribution amount shared by every cycle"),
3368
+ currency: { desc: "Currency derived from the pool", type: "currency" },
3369
+ [settlement.schedule.firstDueField]: dateFieldSpec("First due anchor derived from the pool"),
3370
+ [parentRef]: { desc: `The exact ${noun}`, type: `ref:${noun}` },
3371
+ },
3372
+ id,
3373
+ summary: `${member.replaceAll("_", " ")} contribution row`,
3374
+ title: `${titleize(noun)} ${titleize(member)} Contribution`,
3375
+ verbs,
3376
+ };
3377
+ });
3378
+ const parentVerbs = {
3379
+ create: {
3380
+ summary: "Create the fixed roster before activation",
3381
+ to: "forming",
3382
+ },
3383
+ cancel: {
3384
+ from: ["forming"],
3385
+ summary: "Cancel before activation without moving money",
3386
+ to: "cancelled",
3387
+ },
3388
+ activate: {
3389
+ from: ["forming"],
3390
+ requiresAggregate: childIds.map((childId) => ({
3391
+ check: { kind: "count_equals_field", field: "one" },
3392
+ nounId: childId,
3393
+ over: "children",
3394
+ refField: parentRef,
3395
+ statuses: ["cycle_1_due"],
3396
+ })),
3397
+ summary: "Activate only after every fixed member row exists once",
3398
+ to: "active_cycle_1",
3399
+ },
3400
+ };
3401
+ for (let index = 0; index < settlement.schedule.count; index += 1) {
3402
+ const cycle = index + 1;
3403
+ parentVerbs[`ready_cycle_${cycle}`] = {
3404
+ from: [`active_cycle_${cycle}`],
3405
+ requiresAggregate: childIds.map((childId) => ({
3406
+ check: { kind: "all_in" },
3407
+ nounId: childId,
3408
+ over: "children",
3409
+ refField: parentRef,
3410
+ statuses: [
3411
+ `cycle_${cycle}_funded`,
3412
+ ...(settlement.guarantor ? [`cycle_${cycle}_guaranteed`] : []),
3413
+ ],
3414
+ })),
3415
+ summary: `Lock cycle ${cycle} only after every member row is funded or guaranteed`,
3416
+ to: `cycle_${cycle}_ready`,
3417
+ };
3418
+ parentVerbs[`advance_cycle_${cycle}`] = {
3419
+ from: [`cycle_${cycle}_ready`],
3420
+ requiresAggregate: childIds.map((childId) => ({
3421
+ check: { kind: "all_in" },
3422
+ nounId: childId,
3423
+ over: "children",
3424
+ refField: parentRef,
3425
+ statuses: [
3426
+ cycle === settlement.schedule.count
3427
+ ? "completed"
3428
+ : `cycle_${cycle + 1}_due`,
3429
+ ],
3430
+ })),
3431
+ summary: cycle === settlement.schedule.count
3432
+ ? "Complete after the final shared pot pays"
3433
+ : `Advance after every cycle ${cycle} contribution pays`,
3434
+ to: cycle === settlement.schedule.count
3435
+ ? "completed"
3436
+ : `active_cycle_${cycle + 1}`,
3437
+ };
3438
+ }
3439
+ return {
3440
+ design: [
3441
+ `${noun}: fixed roster and stored payout order; one member-specific row per member avoids tuple identity and keeps each cycle idempotent`,
3442
+ ],
3443
+ extraNouns: childNouns,
3444
+ generatedPrefixNounIds: childIds,
3445
+ feeLines: [],
3446
+ moneyEvents: [
3447
+ mintEvent({
3448
+ amount: "One exact member contribution",
3449
+ fromActor: settlement.members[0],
3450
+ key: contributionEvent,
3451
+ kind: "charge",
3452
+ occurrence: "repeatable",
3453
+ toActor: "escrow",
3454
+ trigger: "Fund one member and cycle",
3455
+ }),
3456
+ ...(settlement.guarantor
3457
+ ? [
3458
+ mintEvent({
3459
+ amount: "One exact defaulted contribution",
3460
+ fromActor: settlement.guarantor,
3461
+ key: guaranteeEvent,
3462
+ kind: "charge",
3463
+ occurrence: "repeatable",
3464
+ toActor: "escrow",
3465
+ trigger: "Fund one defaulted member and cycle",
3466
+ }),
3467
+ ]
3468
+ : []),
3469
+ mintEvent({
3470
+ amount: "One exact member contribution from the cycle pot",
3471
+ fromActor: "escrow",
3472
+ key: payoutEvent,
3473
+ kind: "payout",
3474
+ occurrence: "repeatable",
3475
+ toActor: settlement.payoutOrder[0],
3476
+ trigger: "Pay the stored cycle recipient",
3477
+ }),
3478
+ ],
3479
+ noun: {
3480
+ actors: Object.fromEntries([
3481
+ ...settlement.members.map((member) => [member, "party"]),
3482
+ ...(settlement.guarantor ? [[settlement.guarantor, "payer"]] : []),
3483
+ ]),
3484
+ desc: "Fixed rotating contribution and payout order",
3485
+ fields: {
3486
+ currency: {
3487
+ desc: `Currency fixed to ${settlement.contribution.currency}`,
3488
+ type: "currency",
3489
+ },
3490
+ [settlement.contribution.name]: moneyFieldSpec(`Exact contribution in ${settlement.contribution.currency} minor units`),
3491
+ [settlement.schedule.firstDueField]: dateFieldSpec("Stored first contribution due date"),
3492
+ one: { desc: "Exact fixed member-row count", type: "const:1" },
3493
+ },
3494
+ id: noun,
3495
+ summary: `${settlement.members.length}-member rotating pool`,
3496
+ title: titleize(noun),
3497
+ verbs: parentVerbs,
3498
+ },
3499
+ rules: Array.from({ length: settlement.schedule.count }, (_, index) => ({
3500
+ allowedActors: [],
3501
+ detail: `Cycle ${index + 1} default follows its stored due condition`,
3502
+ dueDriven: true,
3503
+ enforcement: "platform",
3504
+ gatesEvent: null,
3505
+ key: frameKey(`${noun}_cycle_${index + 1}_due`),
3506
+ kind: "deadline",
3507
+ label: `Cycle ${index + 1} due condition`,
3508
+ tenantTunable: false,
3509
+ })),
3510
+ settlement: { name: noun, pieces: [] },
3511
+ };
1407
3512
  }
1408
3513
  function anchorOffset(schedule, index) {
1409
3514
  return index === 0 ? {} : { offset: `P${schedule.every.days * index}D` };
1410
3515
  }
3516
+ /**
3517
+ * Obligation mode extends the existing schedule instead of minting a second
3518
+ * repayment archetype. The parent stores every anchor amount and date. One
3519
+ * generated payment noun per anchor makes matching exact at the operation
3520
+ * boundary and lets the generic aggregate lock cap concurrent partial pays.
3521
+ */
3522
+ function lowerScheduledObligation(settlement, collection, collectionMandate) {
3523
+ const noun = settlement.name;
3524
+ const amountName = settlement.amount.name;
3525
+ const obligationIdField = `${noun.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase())}Id`;
3526
+ const widths = evenPieceBps(settlement.schedule.count);
3527
+ const installmentFields = widths.map((_, index) => `installment${index + 1}Amount`);
3528
+ const paymentNouns = widths.map((_, index) => `${noun}_installment_${index + 1}_payment`);
3529
+ const activeState = "active";
3530
+ const delinquentState = (index) => `installment_${index + 1}_delinquent`;
3531
+ const delinquentStates = widths.map((_, index) => delinquentState(index));
3532
+ const liveStates = [activeState, ...delinquentStates];
3533
+ const repaymentEvent = frameKey(`${noun}_repayment`);
3534
+ const refundEvent = frameKey(`${noun}_refund`);
3535
+ const advanceEvent = frameKey(`${noun}_advance`);
3536
+ const fields = {
3537
+ currency: {
3538
+ desc: `Currency fixed to ${settlement.amount.currency}`,
3539
+ type: "currency",
3540
+ },
3541
+ [amountName]: moneyFieldSpec(`The principal in ${settlement.amount.currency} minor units; the stored installment anchors partition it exactly`),
3542
+ [settlement.schedule.firstDueField]: dateFieldSpec(`Due date of the first installment; later anchors use fixed offsets of ${settlement.schedule.every.raw}`),
3543
+ };
3544
+ for (const [index, field] of installmentFields.entries()) {
3545
+ fields[field] = moneyFieldSpec(`Stored amount for installment ${index + 1} of ${settlement.schedule.count}${index === 0 ? " (carries the integer-division remainder)" : ""}`);
3546
+ fields[`installment${index + 1}DelinquentAfter`] = optionalDateFieldSpec(`Machine-set marker proving installment ${index + 1} reached its stored due date while unpaid`);
3547
+ }
3548
+ const aggregateInvariants = paymentNouns.map((paymentNoun, index) => ({
3549
+ childField: "amount",
3550
+ childNounId: paymentNoun,
3551
+ childRefField: obligationIdField,
3552
+ childStatuses: ["paid"],
3553
+ parentField: installmentFields[index],
3554
+ }));
3555
+ const verbs = {
3556
+ create: {
3557
+ summary: `Create a ${titleize(noun).toLowerCase()} obligation`,
3558
+ to: "draft",
3559
+ },
3560
+ approve: {
3561
+ from: ["draft"],
3562
+ summary: "Approve the immutable principal partition and stored anchors",
3563
+ to: settlement.advanceTo ? "approved" : activeState,
3564
+ },
3565
+ ...(settlement.advanceTo
3566
+ ? {
3567
+ advance: {
3568
+ from: ["approved"],
3569
+ moneyEvent: advanceEvent,
3570
+ moves: [
3571
+ {
3572
+ amount: amountName,
3573
+ from: settlement.payee,
3574
+ key: "advance",
3575
+ operation: "create",
3576
+ to: settlement.advanceTo,
3577
+ },
3578
+ ],
3579
+ summary: `Advance the principal to the ${settlement.advanceTo.replaceAll("_", " ")}; the internal ledger receipt is the confirmation`,
3580
+ to: activeState,
3581
+ },
3582
+ }
3583
+ : {}),
3584
+ write_off: {
3585
+ from: [
3586
+ "draft",
3587
+ ...(settlement.advanceTo ? ["approved"] : []),
3588
+ ...liveStates,
3589
+ ],
3590
+ summary: "Write off the remaining exposure without moving money",
3591
+ to: "written_off",
3592
+ },
3593
+ };
3594
+ const rules = [];
3595
+ for (const [index, paymentNoun] of paymentNouns.entries()) {
3596
+ const anchor = index + 1;
3597
+ const ruleKey = frameKey(`${noun}_installment_${anchor}_due`);
3598
+ const due = {
3599
+ field: settlement.schedule.firstDueField,
3600
+ rule: ruleKey,
3601
+ ...anchorOffset(settlement.schedule, index),
3602
+ };
3603
+ const aggregate = (kind) => [
3604
+ {
3605
+ check: {
3606
+ amountField: "amount",
3607
+ kind,
3608
+ targetField: installmentFields[index],
3609
+ },
3610
+ nounId: paymentNoun,
3611
+ over: "children",
3612
+ refField: obligationIdField,
3613
+ statuses: ["paid"],
3614
+ },
3615
+ ];
3616
+ verbs[`mark_installment_${anchor}_delinquent`] = {
3617
+ due,
3618
+ from: liveStates.filter((state) => state !== delinquentState(index)),
3619
+ requiresAggregate: aggregate("sum_below"),
3620
+ setsAt: {
3621
+ field: `installment${anchor}DelinquentAfter`,
3622
+ marker: true,
3623
+ offset: "PT1S",
3624
+ },
3625
+ summary: `Mark installment ${anchor} delinquent only when its due anchor is unmet`,
3626
+ to: delinquentState(index),
3627
+ };
3628
+ verbs[`collect_installment_${anchor}`] = {
3629
+ due,
3630
+ from: delinquentStates,
3631
+ requiresAggregate: aggregate("sum_exactly"),
3632
+ summary: `Close delinquent installment ${anchor} after linked payments reach its stored amount`,
3633
+ to: activeState,
3634
+ };
3635
+ rules.push({
3636
+ allowedActors: [],
3637
+ detail: `At stored anchor ${anchor}, the platform compares paid child rows with ${installmentFields[index]} and chooses paid or delinquent`,
3638
+ dueDriven: true,
3639
+ enforcement: "platform",
3640
+ gatesEvent: null,
3641
+ key: ruleKey,
3642
+ kind: "deadline",
3643
+ label: `Installment ${anchor} resolves from its stored due condition`,
3644
+ tenantTunable: false,
3645
+ });
3646
+ }
3647
+ const completionRuleKey = frameKey(`${noun}_completion_due`);
3648
+ verbs.complete = {
3649
+ due: {
3650
+ field: settlement.schedule.firstDueField,
3651
+ rule: completionRuleKey,
3652
+ ...anchorOffset(settlement.schedule, widths.length - 1),
3653
+ },
3654
+ from: liveStates,
3655
+ requiresAggregate: paymentNouns.map((paymentNoun, index) => ({
3656
+ check: {
3657
+ amountField: "amount",
3658
+ kind: "sum_exactly",
3659
+ targetField: installmentFields[index],
3660
+ },
3661
+ nounId: paymentNoun,
3662
+ over: "children",
3663
+ refField: obligationIdField,
3664
+ statuses: ["paid"],
3665
+ })),
3666
+ summary: "Close the obligation only after every stored anchor is paid exactly",
3667
+ to: "repaid",
3668
+ };
3669
+ rules.push({
3670
+ allowedActors: [],
3671
+ detail: "After the final stored anchor, the platform closes only when every anchor is paid exactly",
3672
+ dueDriven: true,
3673
+ enforcement: "platform",
3674
+ gatesEvent: null,
3675
+ key: completionRuleKey,
3676
+ kind: "deadline",
3677
+ label: "Obligation completion follows exact aggregate repayment",
3678
+ tenantTunable: false,
3679
+ });
3680
+ const paymentNoun = (index) => {
3681
+ const anchor = index + 1;
3682
+ const id = paymentNouns[index];
3683
+ const permittedParentStates = liveStates;
3684
+ const payerAccountField = `${settlement.payer.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase())}AccountId`;
3685
+ const payeeAccountField = `${settlement.payee.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase())}AccountId`;
3686
+ const createRequirement = {
3687
+ [obligationIdField]: {
3688
+ bind: {
3689
+ currency: "fields.currency",
3690
+ [payerAccountField]: `fields.${payerAccountField}`,
3691
+ [payeeAccountField]: `fields.${payeeAccountField}`,
3692
+ },
3693
+ statuses: permittedParentStates,
3694
+ },
3695
+ };
3696
+ const transitionRequirement = {
3697
+ [obligationIdField]: {
3698
+ match: {
3699
+ "fields.currency": "fields.currency",
3700
+ [`fields.${payerAccountField}`]: `fields.${payerAccountField}`,
3701
+ [`fields.${payeeAccountField}`]: `fields.${payeeAccountField}`,
3702
+ },
3703
+ statuses: permittedParentStates,
3704
+ },
3705
+ };
3706
+ return {
3707
+ actors: {
3708
+ [settlement.payee]: "beneficiary",
3709
+ [settlement.payer]: "payer",
3710
+ },
3711
+ desc: `One partial or full payment bound to installment ${anchor} of ${noun}; the operation name fixes the anchor and ${obligationIdField} fixes the obligation`,
3712
+ fields: {
3713
+ amount: moneyFieldSpec(`Positive payment amount capped with its paid siblings at ${installmentFields[index]}`),
3714
+ currency: {
3715
+ desc: "ISO 4217 currency derived from the obligation",
3716
+ type: "currency",
3717
+ },
3718
+ [obligationIdField]: {
3719
+ desc: `The exact ${noun.replaceAll("_", " ")} this payment belongs to`,
3720
+ type: `ref:${noun}`,
3721
+ },
3722
+ },
3723
+ id,
3724
+ summary: `Anchor-bound payment for installment ${anchor}`,
3725
+ title: `${titleize(noun)} Installment ${anchor} Payment`,
3726
+ verbs: {
3727
+ create: {
3728
+ requires: createRequirement,
3729
+ summary: `Create a payment record for installment ${anchor}`,
3730
+ to: "created",
3731
+ },
3732
+ repay: {
3733
+ ...(collection && collectionMandate
3734
+ ? {
3735
+ captureInput: {
3736
+ mandateEvidenceReference: "evidenceReference",
3737
+ },
3738
+ port: {
3739
+ allowed: collectionMandate.allowed,
3740
+ fields: { evidenceReference: "text" },
3741
+ },
3742
+ }
3743
+ : {}),
3744
+ from: ["created"],
3745
+ moneyEvent: repaymentEvent,
3746
+ moves: [
3747
+ {
3748
+ amount: "amount",
3749
+ from: settlement.payer,
3750
+ key: "repayment",
3751
+ operation: "create",
3752
+ to: settlement.payee,
3753
+ },
3754
+ ],
3755
+ requires: transitionRequirement,
3756
+ requiresExposure: [
3757
+ {
3758
+ amountField: "amount",
3759
+ anchorField: obligationIdField,
3760
+ capField: installmentFields[index],
3761
+ capOnAnchor: true,
3762
+ childNounId: id,
3763
+ statuses: ["paid"],
3764
+ },
3765
+ ],
3766
+ summary: `Pay a partial or full amount against installment ${anchor}`,
3767
+ to: "paid",
3768
+ },
3769
+ refund: {
3770
+ from: ["paid"],
3771
+ moneyEvent: refundEvent,
3772
+ moves: [
3773
+ {
3774
+ amount: "amount",
3775
+ from: settlement.payee,
3776
+ key: "refund",
3777
+ operation: "create",
3778
+ to: settlement.payer,
3779
+ },
3780
+ ],
3781
+ requires: transitionRequirement,
3782
+ summary: `Refund this one stored installment ${anchor} payment whole`,
3783
+ to: "refunded",
3784
+ },
3785
+ },
3786
+ };
3787
+ };
3788
+ return {
3789
+ design: [
3790
+ `${noun}: existing scheduled mechanism in obligation mode; principal partitions into ${settlement.schedule.count} stored anchors; each payment operation names one anchor and one obligation`,
3791
+ `${noun}: partial and early payments serialize under per-anchor aggregate caps; each refund reverses one paid row whole; rescheduling is refused by the checker`,
3792
+ `${noun}: due-only delinquency and write-off change state without money; ${settlement.advanceTo ? "advance is an internal ledger movement with no provider claim" : "no advance is emitted"}`,
3793
+ ...(collection
3794
+ ? [
3795
+ `${collection.name}: explicit collection attempts reuse ${noun}'s anchor-bound repayment verbs; mandate evidence is captured per attempt; failures remain receipted failures and delinquency stays on ${noun}`,
3796
+ ]
3797
+ : []),
3798
+ ],
3799
+ extraNouns: paymentNouns.map((_, index) => paymentNoun(index)),
3800
+ generatedPrefixNounIds: paymentNouns,
3801
+ feeLines: [],
3802
+ moneyEvents: [
3803
+ ...(settlement.advanceTo
3804
+ ? [
3805
+ mintEvent({
3806
+ amount: `The full ${amountName}`,
3807
+ fromActor: settlement.payee,
3808
+ key: advanceEvent,
3809
+ kind: "payout",
3810
+ toActor: settlement.advanceTo,
3811
+ trigger: "Advance the approved principal once",
3812
+ }),
3813
+ ]
3814
+ : []),
3815
+ mintEvent({
3816
+ amount: "A positive amount capped by its stored installment anchor",
3817
+ fromActor: settlement.payer,
3818
+ key: repaymentEvent,
3819
+ kind: "installment",
3820
+ occurrence: "repeatable",
3821
+ toActor: settlement.payee,
3822
+ trigger: "Pay one anchor-bound partial or full installment amount",
3823
+ }),
3824
+ mintEvent({
3825
+ amount: "Exactly one stored paid installment payment",
3826
+ fromActor: settlement.payee,
3827
+ key: refundEvent,
3828
+ kind: "refund",
3829
+ occurrence: "repeatable",
3830
+ toActor: settlement.payer,
3831
+ trigger: "Refund one linked paid installment payment whole",
3832
+ }),
3833
+ ],
3834
+ noun: {
3835
+ actors: {
3836
+ ...(settlement.advanceTo
3837
+ ? { [settlement.advanceTo]: "beneficiary" }
3838
+ : {}),
3839
+ [settlement.payee]: "beneficiary",
3840
+ [settlement.payer]: "payer",
3841
+ [settlement.debtor]: "party",
3842
+ },
3843
+ aggregateInvariants,
3844
+ desc: `Installment obligation for ${settlement.debtor.replaceAll("_", " ")}; ${settlement.payer.replaceAll("_", " ")} pays ${settlement.payee.replaceAll("_", " ")} against exact stored anchors`,
3845
+ fields,
3846
+ id: noun,
3847
+ ...partitionsSpread(partitionClause(amountName, installmentFields)),
3848
+ summary: `${settlement.schedule.count}-anchor obligation for ${settlement.debtor.replaceAll("_", " ")}`,
3849
+ title: titleize(noun),
3850
+ verbs,
3851
+ },
3852
+ rules,
3853
+ settlement: { name: noun, pieces: [] },
3854
+ };
3855
+ }
1411
3856
  function lowerScheduled(settlement) {
1412
3857
  const noun = settlement.name;
1413
3858
  const amountName = settlement.amount.name;
@@ -1505,9 +3950,9 @@ function lowerScheduled(settlement) {
1505
3950
  settlement: { name: noun, pieces: [] },
1506
3951
  };
1507
3952
  }
1508
- function lowerAdvance(settlement) {
3953
+ function lowerAdvance(settlement, recourses) {
1509
3954
  return settlement.source.kind === "carve"
1510
- ? lowerCarvedAdvance(settlement, settlement.source.settlement)
3955
+ ? lowerCarvedAdvance(settlement, settlement.source.settlement, recourses)
1511
3956
  : lowerScheduledAdvance(settlement, settlement.source.schedule);
1512
3957
  }
1513
3958
  /**
@@ -1517,15 +3962,31 @@ function lowerAdvance(settlement) {
1517
3962
  * owed on, and the close that records the carve landing. An advance carved
1518
3963
  * this way can never pay out more than the hold already holds.
1519
3964
  */
1520
- function lowerCarvedAdvance(settlement, hold) {
3965
+ function lowerCarvedAdvance(settlement, hold, recourses) {
1521
3966
  const noun = settlement.name;
1522
3967
  const amountName = settlement.amount.name;
1523
3968
  const hasFee = settlement.feeBps > 0;
1524
3969
  const advancedWords = settlement.advanced.replaceAll("_", " ");
1525
3970
  const funderWords = settlement.funder.replaceAll("_", " ");
1526
3971
  const holdWords = hold.replaceAll("_", " ");
3972
+ const holdRefField = "carveHoldId";
3973
+ const referenceBindings = [
3974
+ { field: holdRefField, statuses: ["funded"], target: hold },
3975
+ ...recourses.map((recourse, index) => ({
3976
+ field: `carveRecourse${index + 1}Id`,
3977
+ statuses: ["active"],
3978
+ target: recourse.name,
3979
+ })),
3980
+ ];
1527
3981
  const fields = {
1528
3982
  [amountName]: moneyFieldSpec(`The advanced amount in ${settlement.amount.currency} minor units, disbursed to the ${advancedWords} up front`),
3983
+ ...Object.fromEntries(referenceBindings.map((binding) => [
3984
+ binding.field,
3985
+ {
3986
+ desc: `The ${binding.target.replaceAll("_", " ")} bound to this advance`,
3987
+ type: `ref:${binding.target}`,
3988
+ },
3989
+ ])),
1529
3990
  ...(hasFee
1530
3991
  ? {
1531
3992
  feeAmount: moneyFieldSpec(`${formatBps(settlement.feeBps)} of ${amountName}, the funder's discount owed on top of the advance`),
@@ -1586,6 +4047,16 @@ function lowerCarvedAdvance(settlement, hold) {
1586
4047
  to: settlement.advanced,
1587
4048
  },
1588
4049
  ],
4050
+ requires: Object.fromEntries(referenceBindings.map((binding) => [
4051
+ binding.field,
4052
+ {
4053
+ match: {
4054
+ [`fields.${amountName}`]: `fields.${amountName}`,
4055
+ "fields.currency": "fields.currency",
4056
+ },
4057
+ statuses: binding.statuses,
4058
+ },
4059
+ ])),
1589
4060
  summary: `Disburse the ${amountName} to the ${advancedWords}`,
1590
4061
  to: "advanced",
1591
4062
  },
@@ -2047,6 +4518,8 @@ function summarize(program) {
2047
4518
  : `the ${settlement.payee.replaceAll("_", " ")} is paid on confirmed release`;
2048
4519
  return `The ${settlement.payer.replaceAll("_", " ")} funds ${settlement.amount.name} into escrow and ${paid}${cancel}`;
2049
4520
  }
4521
+ case "captured_payment":
4522
+ return `The ${settlement.payer.replaceAll("_", " ")}'s ${settlement.amount.name} is reserved until ${settlement.reserveUntilField}, captured by the ${settlement.payee.replaceAll("_", " ")} in strict partial slices, then settled or released`;
2050
4523
  case "instant_transfer":
2051
4524
  return `The ${settlement.payer.replaceAll("_", " ")} pays ${settlement.amount.name} straight through to the ${settlement.payee.replaceAll("_", " ")}`;
2052
4525
  case "premium_forward":
@@ -2054,7 +4527,9 @@ function summarize(program) {
2054
4527
  case "deposit":
2055
4528
  return `The ${settlement.payer.replaceAll("_", " ")}'s ${settlement.amount.name} is reserved for the ${settlement.holder.replaceAll("_", " ")} until claimed or returned`;
2056
4529
  case "scheduled":
2057
- return `The ${settlement.payer.replaceAll("_", " ")} pays ${settlement.amount.name} to the ${settlement.payee.replaceAll("_", " ")} over ${settlement.schedule.count} scheduled installments`;
4530
+ return settlement.mode === "obligation"
4531
+ ? `The ${settlement.debtor.replaceAll("_", " ")} owes ${settlement.amount.name}; ${settlement.advanceTo ? `the ${settlement.payee.replaceAll("_", " ")} advances it to the ${settlement.advanceTo.replaceAll("_", " ")}, then ` : ""}the ${settlement.payer.replaceAll("_", " ")} repays the ${settlement.payee.replaceAll("_", " ")} over ${settlement.schedule.count} anchor-bound installments`
4532
+ : `The ${settlement.payer.replaceAll("_", " ")} pays ${settlement.amount.name} to the ${settlement.payee.replaceAll("_", " ")} over ${settlement.schedule.count} scheduled installments`;
2058
4533
  case "advance":
2059
4534
  return settlement.source.kind === "carve"
2060
4535
  ? `The ${settlement.funder.replaceAll("_", " ")} advances ${settlement.amount.name} to the ${settlement.advanced.replaceAll("_", " ")}, repaid out of the ${settlement.source.settlement.replaceAll("_", " ")} release`
@@ -2063,6 +4538,20 @@ function summarize(program) {
2063
4538
  return `The ${settlement.payer.replaceAll("_", " ")} is charged per metered unit at a committed rate card until the period closes`;
2064
4539
  case "pooled_split":
2065
4540
  return `The ${settlement.payer.replaceAll("_", " ")} pools ${settlement.amount.name} and it distributes ${settlement.shares.length} ways on the payout date`;
4541
+ case "settlement_batch":
4542
+ return `The ${settlement.settlementAccount.replaceAll("_", " ")} freezes capture lineage and pays one signed net amount to the ${settlement.payoutDestination.replaceAll("_", " ")}`;
4543
+ case "funding_round":
4544
+ return `The ${settlement.contributor.replaceAll("_", " ")} commits under ${settlement.target.name} until the stored close anchor activates or fails the round`;
4545
+ case "weighted_distribution":
4546
+ return `The ${settlement.source.replaceAll("_", " ")} pays a frozen claimant set by deterministic largest remainder`;
4547
+ case "credit_facility":
4548
+ return `The ${settlement.lender.replaceAll("_", " ")} admits draws under ${settlement.limit.name}; ${settlement.obligation.settlement.replaceAll("_", " ")} owns repayment`;
4549
+ case "recurring_collection":
4550
+ return `${settlement.name.replaceAll("_", " ")} adds explicit mandate evidence to ${settlement.obligation.settlement.replaceAll("_", " ")} repayment attempts`;
4551
+ case "conditional_disbursement":
4552
+ return `The ${settlement.source.replaceAll("_", " ")} pays one evidence-approved amount under ${settlement.cap.name} to the ${settlement.destination.replaceAll("_", " ")}`;
4553
+ case "rotating_pool":
4554
+ return `${settlement.members.length} fixed members contribute one exact amount per cycle in a stored payout order`;
2066
4555
  case "swap":
2067
4556
  return `The ${settlement.sides[0].party.replaceAll("_", " ")} and ${settlement.sides[1].party.replaceAll("_", " ")} fund one shared escrow and the entire two-sided trade releases or reverses together`;
2068
4557
  }