@hyperscale0/hsx 1.0.0-alpha.6 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +48 -10
  2. package/LICENSING.md +3 -3
  3. package/README.md +37 -149
  4. package/TRADEMARKS.md +4 -4
  5. package/dist/src/ast.d.ts +129 -10
  6. package/dist/src/ast.d.ts.map +1 -1
  7. package/dist/src/cli.d.ts +2 -1
  8. package/dist/src/cli.d.ts.map +1 -1
  9. package/dist/src/cli.js +148 -12
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/compile.d.ts +41 -4
  12. package/dist/src/compile.d.ts.map +1 -1
  13. package/dist/src/compile.js +82 -15
  14. package/dist/src/compile.js.map +1 -1
  15. package/dist/src/cost.d.ts +78 -0
  16. package/dist/src/cost.d.ts.map +1 -0
  17. package/dist/src/cost.js +369 -0
  18. package/dist/src/cost.js.map +1 -0
  19. package/dist/src/diagnostics.d.ts +11 -0
  20. package/dist/src/diagnostics.d.ts.map +1 -0
  21. package/dist/src/diagnostics.js +420 -0
  22. package/dist/src/diagnostics.js.map +1 -0
  23. package/dist/src/emit.d.ts +35 -0
  24. package/dist/src/emit.d.ts.map +1 -0
  25. package/dist/src/emit.js +359 -0
  26. package/dist/src/emit.js.map +1 -0
  27. package/dist/src/entry-overrides.d.ts +17 -3
  28. package/dist/src/entry-overrides.d.ts.map +1 -1
  29. package/dist/src/entry-overrides.js +180 -35
  30. package/dist/src/entry-overrides.js.map +1 -1
  31. package/dist/src/format.d.ts +10 -0
  32. package/dist/src/format.d.ts.map +1 -0
  33. package/dist/src/format.js +183 -0
  34. package/dist/src/format.js.map +1 -0
  35. package/dist/src/index.d.ts +10 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +8 -3
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/ir.d.ts +66 -0
  40. package/dist/src/ir.d.ts.map +1 -0
  41. package/dist/src/ir.js +15 -0
  42. package/dist/src/ir.js.map +1 -0
  43. package/dist/src/lex.d.ts +9 -1
  44. package/dist/src/lex.d.ts.map +1 -1
  45. package/dist/src/lex.js +50 -4
  46. package/dist/src/lex.js.map +1 -1
  47. package/dist/src/limits.d.ts +2 -3
  48. package/dist/src/limits.d.ts.map +1 -1
  49. package/dist/src/limits.js +2 -3
  50. package/dist/src/limits.js.map +1 -1
  51. package/dist/src/modules.d.ts +36 -0
  52. package/dist/src/modules.d.ts.map +1 -0
  53. package/dist/src/modules.js +372 -0
  54. package/dist/src/modules.js.map +1 -0
  55. package/dist/src/parse.d.ts.map +1 -1
  56. package/dist/src/parse.js +590 -17
  57. package/dist/src/parse.js.map +1 -1
  58. package/dist/src/typecheck.d.ts +8 -0
  59. package/dist/src/typecheck.d.ts.map +1 -0
  60. package/dist/src/typecheck.js +2958 -0
  61. package/dist/src/typecheck.js.map +1 -0
  62. package/dist/src/version.d.ts +4 -15
  63. package/dist/src/version.d.ts.map +1 -1
  64. package/dist/src/version.js +4 -15
  65. package/dist/src/version.js.map +1 -1
  66. package/docs/README.md +23 -0
  67. package/docs/guide/01-first-program.md +24 -0
  68. package/docs/guide/02-money.md +25 -0
  69. package/docs/guide/03-instruments.md +22 -0
  70. package/docs/guide/04-lifecycles.md +25 -0
  71. package/docs/guide/05-fees-and-splits.md +45 -0
  72. package/docs/guide/06-schedules.md +22 -0
  73. package/docs/guide/07-composition.md +46 -0
  74. package/docs/guide/08-writing-a-module.md +26 -0
  75. package/docs/guide/09-cost.md +24 -0
  76. package/docs/guide/10-diagnostics.md +27 -0
  77. package/docs/llms-full.txt +1738 -0
  78. package/docs/llms.txt +45 -0
  79. package/docs/reference/cli.md +33 -0
  80. package/docs/reference/diagnostics.md +619 -0
  81. package/docs/reference/grammar.md +39 -0
  82. package/docs/reference/std/advance.md +35 -0
  83. package/docs/reference/std/cancellable_booking.md +35 -0
  84. package/docs/reference/std/captured_payment.md +43 -0
  85. package/docs/reference/std/conditional_disbursement.md +33 -0
  86. package/docs/reference/std/credit_facility.md +35 -0
  87. package/docs/reference/std/held_payment.md +64 -0
  88. package/docs/reference/std/instant_transfer.md +30 -0
  89. package/docs/reference/std/metered.md +27 -0
  90. package/docs/reference/std/pooled_split.md +26 -0
  91. package/docs/reference/std/premium_forward.md +39 -0
  92. package/docs/reference/std/reconciled_payout.md +33 -0
  93. package/docs/reference/std/recurring_collection.md +25 -0
  94. package/docs/reference/std/rotating_pool.md +42 -0
  95. package/docs/reference/std/scheduled.md +51 -0
  96. package/docs/reference/std/security_deposit.md +46 -0
  97. package/docs/reference/std/settlement_batch.md +42 -0
  98. package/docs/reference/std/swap.md +46 -0
  99. package/docs/reference/std/threshold_pool.md +47 -0
  100. package/docs/reference/std/weighted_distribution.md +39 -0
  101. package/docs/reference/types.md +20 -0
  102. package/docs/reference/udl-output.md +14 -0
  103. package/docs/sessions/2026-09-02-two-instruments.hsx +21 -0
  104. package/docs/sessions/2026-09-02-two-instruments.md +41 -0
  105. package/examples/01-first-program/README.md +45 -0
  106. package/examples/01-first-program/tip-jar.hsx +17 -0
  107. package/examples/02-imports-and-archetypes/README.md +99 -0
  108. package/examples/02-imports-and-archetypes/photo-booth.hsx +37 -0
  109. package/examples/03-diagnostics/README.md +68 -0
  110. package/examples/03-diagnostics/corner-shop-fixed.hsx +22 -0
  111. package/examples/03-diagnostics/corner-shop.hsx +19 -0
  112. package/examples/04-complete-product/README.md +87 -0
  113. package/examples/04-complete-product/study-hall.hsx +67 -0
  114. package/examples/05-watch-club/README.md +9 -0
  115. package/examples/05-watch-club/watch-club.hsx +124 -0
  116. package/examples/README.md +18 -0
  117. package/examples/advance/README.md +3 -0
  118. package/examples/advance/advance.hsx +13 -0
  119. package/examples/advance/advance.udl +339 -0
  120. package/examples/cancellable_booking/README.md +3 -0
  121. package/examples/cancellable_booking/cancellable_booking.hsx +14 -0
  122. package/examples/cancellable_booking/cancellable_booking.udl +391 -0
  123. package/examples/captured_payment/README.md +3 -0
  124. package/examples/captured_payment/captured_payment.hsx +21 -0
  125. package/examples/captured_payment/captured_payment.udl +677 -0
  126. package/examples/conditional_disbursement/README.md +3 -0
  127. package/examples/conditional_disbursement/conditional_disbursement.hsx +17 -0
  128. package/examples/conditional_disbursement/conditional_disbursement.udl +412 -0
  129. package/examples/cost-table.json +386 -0
  130. package/examples/credit_facility/README.md +3 -0
  131. package/examples/credit_facility/credit_facility.hsx +27 -0
  132. package/examples/credit_facility/credit_facility.udl +1373 -0
  133. package/examples/held_payment/README.md +3 -0
  134. package/examples/held_payment/held_payment.hsx +13 -0
  135. package/examples/held_payment/held_payment.udl +507 -0
  136. package/examples/instant_transfer/README.md +3 -0
  137. package/examples/instant_transfer/instant_transfer.hsx +16 -0
  138. package/examples/instant_transfer/instant_transfer.udl +364 -0
  139. package/examples/metered/README.md +3 -0
  140. package/examples/metered/metered.hsx +13 -0
  141. package/examples/metered/metered.udl +247 -0
  142. package/examples/pooled_split/README.md +3 -0
  143. package/examples/pooled_split/pooled_split.hsx +15 -0
  144. package/examples/pooled_split/pooled_split.udl +425 -0
  145. package/examples/premium_forward/README.md +3 -0
  146. package/examples/premium_forward/premium_forward.hsx +19 -0
  147. package/examples/premium_forward/premium_forward.udl +594 -0
  148. package/examples/reconciled_payout/README.md +3 -0
  149. package/examples/reconciled_payout/reconciled_payout.hsx +13 -0
  150. package/examples/reconciled_payout/reconciled_payout.udl +303 -0
  151. package/examples/recurring_collection/README.md +3 -0
  152. package/examples/recurring_collection/recurring_collection.hsx +21 -0
  153. package/examples/recurring_collection/recurring_collection.udl +1130 -0
  154. package/examples/rotating_pool/README.md +3 -0
  155. package/examples/rotating_pool/rotating_pool.hsx +18 -0
  156. package/examples/rotating_pool/rotating_pool.udl +4352 -0
  157. package/examples/scheduled/README.md +3 -0
  158. package/examples/scheduled/scheduled.hsx +12 -0
  159. package/examples/scheduled/scheduled.udl +335 -0
  160. package/examples/security_deposit/README.md +3 -0
  161. package/examples/security_deposit/security_deposit.hsx +21 -0
  162. package/examples/security_deposit/security_deposit.udl +293 -0
  163. package/examples/settlement_batch/README.md +3 -0
  164. package/examples/settlement_batch/settlement_batch.hsx +19 -0
  165. package/examples/settlement_batch/settlement_batch.udl +753 -0
  166. package/examples/swap/README.md +3 -0
  167. package/examples/swap/swap.hsx +19 -0
  168. package/examples/swap/swap.udl +876 -0
  169. package/examples/threshold_pool/README.md +3 -0
  170. package/examples/threshold_pool/threshold_pool.hsx +16 -0
  171. package/examples/threshold_pool/threshold_pool.udl +667 -0
  172. package/examples/weighted_distribution/README.md +3 -0
  173. package/examples/weighted_distribution/weighted_distribution.hsx +20 -0
  174. package/examples/weighted_distribution/weighted_distribution.udl +337 -0
  175. package/package.json +19 -6
  176. package/skills/hsx/SKILL.md +483 -0
  177. package/src/ast.ts +163 -9
  178. package/src/cli.ts +164 -12
  179. package/src/compile.ts +160 -20
  180. package/src/cost.ts +579 -0
  181. package/src/diagnostics.ts +442 -0
  182. package/src/emit.ts +447 -0
  183. package/src/entry-overrides.ts +263 -48
  184. package/src/format.ts +261 -0
  185. package/src/index.ts +32 -4
  186. package/src/ir.ts +99 -0
  187. package/src/lex.ts +59 -4
  188. package/src/limits.ts +2 -3
  189. package/src/modules.ts +444 -0
  190. package/src/parse.ts +655 -21
  191. package/src/typecheck.ts +3876 -0
  192. package/src/version.ts +4 -16
  193. package/std/SEMANTICS.md +130 -0
  194. package/std/settlements/advance.hsx +177 -0
  195. package/std/settlements/cancellable_booking.hsx +87 -0
  196. package/std/settlements/captured_payment.hsx +144 -0
  197. package/std/settlements/conditional_disbursement.hsx +185 -0
  198. package/std/settlements/credit_facility.hsx +167 -0
  199. package/std/settlements/held_payment.hsx +578 -0
  200. package/std/settlements/index.hsx +3 -0
  201. package/std/settlements/instant_transfer.hsx +190 -0
  202. package/std/settlements/metered.hsx +67 -0
  203. package/std/settlements/pooled_split.hsx +68 -0
  204. package/std/settlements/premium_forward.hsx +257 -0
  205. package/std/settlements/reconciled_payout.hsx +77 -0
  206. package/std/settlements/recurring_collection.hsx +27 -0
  207. package/std/settlements/rotating_pool.hsx +252 -0
  208. package/std/settlements/scheduled.hsx +641 -0
  209. package/std/settlements/security_deposit.hsx +166 -0
  210. package/std/settlements/settlement_batch.hsx +122 -0
  211. package/std/settlements/swap.hsx +1114 -0
  212. package/std/settlements/threshold_pool.hsx +221 -0
  213. package/std/settlements/weighted_distribution.hsx +141 -0
  214. package/dist/src/archetypes.d.ts +0 -17
  215. package/dist/src/archetypes.d.ts.map +0 -1
  216. package/dist/src/archetypes.js +0 -327
  217. package/dist/src/archetypes.js.map +0 -1
  218. package/dist/src/check.d.ts +0 -12
  219. package/dist/src/check.d.ts.map +0 -1
  220. package/dist/src/check.js +0 -1910
  221. package/dist/src/check.js.map +0 -1
  222. package/dist/src/lower.d.ts +0 -146
  223. package/dist/src/lower.d.ts.map +0 -1
  224. package/dist/src/lower.js +0 -4570
  225. package/dist/src/lower.js.map +0 -1
  226. package/dist/src/model.d.ts +0 -467
  227. package/dist/src/model.d.ts.map +0 -1
  228. package/dist/src/model.js +0 -15
  229. package/dist/src/model.js.map +0 -1
  230. package/spec/hsx-ir.schema.json +0 -920
  231. package/src/archetypes.ts +0 -340
  232. package/src/check.ts +0 -2938
  233. package/src/lower.ts +0 -5416
  234. package/src/model.ts +0 -519
package/dist/src/check.js DELETED
@@ -1,1910 +0,0 @@
1
- /**
2
- * The HSX typechecker. Takes the parsed AST, resolves every name, validates
3
- * each archetype's parameter surface, and produces the checked program model
4
- * the lowering consumes. Total: always returns diagnostics; returns the model
5
- * exactly when nothing error-severity was found. Warnings are the compiler's
6
- * lint voice and never block.
7
- */
8
- import { ARCHETYPE_DEFINITIONS, SETTLEMENT_ARCHETYPES, } from "./archetypes.js";
9
- import { ASSET_KINDS, PARTY_KINDS, } from "./model.js";
10
- export { SETTLEMENT_ARCHETYPES } from "./archetypes.js";
11
- /** The three terms that together declare a finite schedule. */
12
- const SCHEDULE_KEYS = ["count", "every", "first_due"];
13
- const SNAKE_CASE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
14
- const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
15
- const CURRENCY = /^[A-Z]{3}$/;
16
- const TOTAL_BPS = 10_000;
17
- export function checkProgram(program) {
18
- const diagnostics = [];
19
- const error = (span, message) => {
20
- diagnostics.push({ message, severity: "error", span });
21
- };
22
- const warning = (span, message) => {
23
- diagnostics.push({ message, severity: "warning", span });
24
- };
25
- const derivedAmounts = [];
26
- // --- Header ---------------------------------------------------------------
27
- const headers = program.decls.filter((decl) => decl.kind === "program");
28
- const header = headers[0];
29
- if (!header) {
30
- error({ end: 0, start: 0 }, 'the file needs a program header naming the company, like: program used_car_escrow "Used-car escrow"');
31
- }
32
- for (const extra of headers.slice(1)) {
33
- error(extra.span, "a file declares exactly one program");
34
- }
35
- if (header && !SNAKE_CASE.test(header.name.name)) {
36
- error(header.name.span, `the program name must be snake_case, like used_car_escrow; "${header.name.name}" is not`);
37
- }
38
- // --- Imports --------------------------------------------------------------
39
- const imported = new Map();
40
- for (const decl of program.decls) {
41
- if (decl.kind !== "import")
42
- continue;
43
- if (decl.from.value !== "settlement") {
44
- error(decl.from.span, `"${decl.from.value}" is not an importable module; the settlement algebra lives in "settlement"`);
45
- continue;
46
- }
47
- for (const name of decl.names) {
48
- if (!SETTLEMENT_ARCHETYPES.includes(name.name)) {
49
- error(name.span, `"settlement" has no archetype named ${name.name}; it offers ${SETTLEMENT_ARCHETYPES.join(", ")}`);
50
- continue;
51
- }
52
- if (imported.has(name.name)) {
53
- warning(name.span, `${name.name} is imported more than once`);
54
- }
55
- imported.set(name.name, name.span);
56
- }
57
- }
58
- // --- One namespace for every declared name --------------------------------
59
- const declared = new Map();
60
- const claim = (name, kind) => {
61
- if (name.name === "platform" || name.name === "escrow") {
62
- error(name.span, `the name ${name.name} is reserved: the platform and each settlement's escrow are always present without being declared`);
63
- return false;
64
- }
65
- const existing = declared.get(name.name);
66
- if (existing) {
67
- error(name.span, `the name ${name.name} is already taken by a ${existing.kind}; every party, asset, settlement, and port needs its own name`);
68
- return false;
69
- }
70
- if (!SNAKE_CASE.test(name.name)) {
71
- error(name.span, `${kind} names are snake_case; "${name.name}" is not`);
72
- return false;
73
- }
74
- declared.set(name.name, { kind, span: name.span });
75
- return true;
76
- };
77
- // --- Parties and assets ---------------------------------------------------
78
- const parties = new Map();
79
- const assets = new Map();
80
- for (const decl of program.decls) {
81
- if (decl.kind === "party") {
82
- if (!claim(decl.name, "party"))
83
- continue;
84
- if (!PARTY_KINDS.includes(decl.partyKind.name)) {
85
- error(decl.partyKind.span, `a party is a ${PARTY_KINDS.join(" or a ")}; "${decl.partyKind.name}" is neither`);
86
- continue;
87
- }
88
- if (decl.attrs) {
89
- error(decl.attrs.span, `party ${decl.name.name} takes no attribute block yet`);
90
- }
91
- parties.set(decl.name.name, {
92
- kind: decl.partyKind.name,
93
- name: decl.name.name,
94
- origin: decl.span,
95
- });
96
- }
97
- if (decl.kind === "asset") {
98
- if (!claim(decl.name, "asset"))
99
- continue;
100
- if (!ASSET_KINDS.includes(decl.assetKind.name)) {
101
- error(decl.assetKind.span, `an asset is one of ${ASSET_KINDS.join(", ")}; "${decl.assetKind.name}" is none of those`);
102
- continue;
103
- }
104
- let titleTransfer = "on_platform";
105
- for (const entry of decl.attrs?.entries ?? []) {
106
- if (entry.key.name !== "title_transfer") {
107
- error(entry.key.span, `asset ${decl.name.name} does not understand "${entry.key.name}"; the only attribute is title_transfer`);
108
- continue;
109
- }
110
- const value = entry.value;
111
- if (value.kind === "ident" &&
112
- (value.name === "off_platform" || value.name === "on_platform")) {
113
- titleTransfer = value.name;
114
- }
115
- else {
116
- error(value.span, "title_transfer is either on_platform or off_platform");
117
- }
118
- }
119
- assets.set(decl.name.name, {
120
- kind: decl.assetKind.name,
121
- name: decl.name.name,
122
- origin: decl.span,
123
- titleTransfer,
124
- });
125
- }
126
- }
127
- // Claim settlement and port names before checking bodies, so declarations
128
- // may reference each other regardless of file order.
129
- const portDecls = program.decls.filter((decl) => decl.kind === "port");
130
- const settlementDecls = program.decls.filter((decl) => decl.kind === "settlement");
131
- for (const decl of settlementDecls)
132
- claim(decl.name, "settlement");
133
- const portNames = new Set();
134
- for (const decl of portDecls) {
135
- if (claim(decl.name, "port"))
136
- portNames.add(decl.name.name);
137
- }
138
- // --- Shared body helpers ----------------------------------------------------
139
- const entriesOf = (block, valid, owner) => {
140
- const seen = new Map();
141
- for (const entry of block.entries) {
142
- if (!valid.includes(entry.key.name)) {
143
- error(entry.key.span, `${owner} does not understand "${entry.key.name}"; it takes ${valid.join(", ")}`);
144
- continue;
145
- }
146
- if (seen.has(entry.key.name)) {
147
- error(entry.key.span, `${owner} sets ${entry.key.name} twice`);
148
- continue;
149
- }
150
- seen.set(entry.key.name, entry);
151
- }
152
- return seen;
153
- };
154
- const noQualifiers = (entry, owner) => {
155
- if (entry.qualifiers.length > 0) {
156
- error(entry.span, `${entry.key.name} on ${owner} takes no ( ... ) qualifier`);
157
- }
158
- };
159
- const referencedParties = new Set();
160
- const partyRef = (expr, role) => {
161
- if (expr.kind === "ident" && parties.has(expr.name)) {
162
- referencedParties.add(expr.name);
163
- return expr.name;
164
- }
165
- error(expr.span, expr.kind === "ident"
166
- ? `${role} must name a declared party; there is no party named ${expr.name}`
167
- : `${role} must name a declared party`);
168
- return undefined;
169
- };
170
- // --- Settlements: shared term parsers ---------------------------------------
171
- const referencedPorts = new Set();
172
- const settlements = [];
173
- // The lowering mints fields with these names; a user field that collides
174
- // would silently overwrite the generated one and corrupt the choreography
175
- // (e.g. an amount named serviceFeeAmount would BE the fee moved to the
176
- // platform). Refused here, at source coordinates, for every archetype.
177
- const RESERVED_FIELD_NAMES = new Set([
178
- "currency",
179
- "feeAmount",
180
- "repayableAmount",
181
- "serviceFeeAmount",
182
- "clawbackAt",
183
- ]);
184
- const RESERVED_FIELD_PATTERNS = [
185
- /^piece\d+Amount$/,
186
- /^installment\d+Amount$/,
187
- /^repayment\d+Amount$/,
188
- /^carve(?:Hold|Recourse\d+)Id$/,
189
- /ShareAmount$/,
190
- /AccountId$/,
191
- ];
192
- const reservedFieldName = (name, span, owner) => {
193
- if (RESERVED_FIELD_NAMES.has(name) ||
194
- RESERVED_FIELD_PATTERNS.some((pattern) => pattern.test(name))) {
195
- error(span, `${owner} names a field ${name}, but the compiler reserves that name for a generated field; pick another name`);
196
- return true;
197
- }
198
- return false;
199
- };
200
- const parseAmount = (entry, owner) => {
201
- if (!entry)
202
- return undefined;
203
- noQualifiers(entry, owner);
204
- const value = entry.value;
205
- if (value.kind === "binding" &&
206
- value.type.kind === "call" &&
207
- value.type.callee.name === "money" &&
208
- value.type.args.length === 1 &&
209
- value.type.args[0]?.kind === "ident") {
210
- const currency = value.type.args[0].name;
211
- if (!CAMEL_CASE.test(value.name.name)) {
212
- error(value.name.span, `the ${entry.key.name} field is a camelCase name like price; "${value.name.name}" is not`);
213
- }
214
- else if (!CURRENCY.test(currency)) {
215
- error(value.type.args[0].span, `"${currency}" is not a currency code; use three capital letters like SAR`);
216
- }
217
- else if (!reservedFieldName(value.name.name, value.name.span, owner)) {
218
- return { currency, name: value.name.name, origin: value.span };
219
- }
220
- return undefined;
221
- }
222
- error(value.span, `${owner} declares ${entry.key.name} as a typed field, like: ${entry.key.name}: price: money(SAR)`);
223
- return undefined;
224
- };
225
- const parsePortEntry = (entry, owner, role, acceptsDeadline = false) => {
226
- if (!entry)
227
- return undefined;
228
- noQualifiers(entry, owner);
229
- const value = entry.value;
230
- if (value.kind !== "port_ref") {
231
- error(value.span, `${owner} needs a ${role} decision, like: ${entry.key.name}: port ${entry.key.name}_decided`);
232
- return undefined;
233
- }
234
- if (value.within) {
235
- error(value.within.span, `${entry.key.name} on ${owner} has no time window; only dispute accepts "within <duration>"`);
236
- return undefined;
237
- }
238
- if (value.deadline && !acceptsDeadline) {
239
- error(value.deadline.span, `${entry.key.name} on ${owner} has no date default; only a held payment's release falls back to a date`);
240
- return undefined;
241
- }
242
- if (!portNames.has(value.name.name)) {
243
- error(value.name.span, `${owner} decides ${role} through port ${value.name.name}, but no port with that name is declared`);
244
- return undefined;
245
- }
246
- let deadlineField;
247
- if (value.deadline) {
248
- if (!CAMEL_CASE.test(value.deadline.name)) {
249
- error(value.deadline.span, `at( ... ) names a camelCase date field, like at(releaseDueAt); "${value.deadline.name}" is not`);
250
- return undefined;
251
- }
252
- if (reservedFieldName(value.deadline.name, value.deadline.span, owner)) {
253
- return undefined;
254
- }
255
- deadlineField = value.deadline.name;
256
- }
257
- referencedPorts.add(value.name.name);
258
- return {
259
- ...(deadlineField ? { deadlineField } : {}),
260
- origin: value.span,
261
- port: value.name.name,
262
- };
263
- };
264
- const parseDisputeEntry = (entry, owner, label = "dispute") => {
265
- if (!entry)
266
- return undefined;
267
- noQualifiers(entry, owner);
268
- const value = entry.value;
269
- if (value.kind !== "port_ref") {
270
- error(value.span, `${owner} needs one ${label} decision through a fixed windowed port`);
271
- return undefined;
272
- }
273
- if (!portNames.has(value.name.name)) {
274
- error(value.name.span, `${owner} routes ${label} through port ${value.name.name}, but no port with that name is declared`);
275
- return undefined;
276
- }
277
- if (!value.within) {
278
- error(value.span, `${owner} ${label} needs a fixed window, like: ${entry.key.name}: port ${value.name.name} within P14D`);
279
- return undefined;
280
- }
281
- const raw = value.within.name;
282
- const match = /^P([1-9]\d{0,3})([DW])$/.exec(raw);
283
- if (!match) {
284
- error(value.within.span, `${label} on ${owner} uses a fixed duration in days or weeks, like P14D; calendar months cannot define an exact money deadline`);
285
- return undefined;
286
- }
287
- const magnitude = Number(match[1]);
288
- const days = match[2] === "W" ? magnitude * 7 : magnitude;
289
- referencedPorts.add(value.name.name);
290
- return {
291
- origin: value.span,
292
- port: value.name.name,
293
- window: { days, raw },
294
- };
295
- };
296
- const parsePercentEntry = (entry, owner, label) => {
297
- if (!entry)
298
- return undefined;
299
- noQualifiers(entry, owner);
300
- if (entry.value.kind !== "percent") {
301
- error(entry.value.span, `${label} on ${owner} must be a percent`);
302
- return undefined;
303
- }
304
- if (entry.value.bps >= TOTAL_BPS) {
305
- error(entry.value.span, `a ${entry.value.raw}% ${label} consumes the whole amount; it stays under 100%`);
306
- return undefined;
307
- }
308
- if (entry.value.bps === 0) {
309
- warning(entry.value.span, `${label} on ${owner} is 0%; drop the entry if none is meant`);
310
- }
311
- return entry.value.bps;
312
- };
313
- const parseFees = (entry, owner, payer, payee) => {
314
- const fees = [];
315
- if (!entry)
316
- return fees;
317
- noQualifiers(entry, owner);
318
- if (entry.value.kind !== "block") {
319
- error(entry.value.span, `fees is a block of party shares, like: fees { buyer: 1% }`);
320
- return fees;
321
- }
322
- for (const fee of entry.value.entries) {
323
- const bearer = fee.key.name;
324
- if (bearer !== payer && bearer !== payee) {
325
- error(fee.key.span, `fees are borne by this settlement's payer or payee; ${bearer} is neither`);
326
- continue;
327
- }
328
- if (fees.some((existing) => existing.bearer === bearer)) {
329
- error(fee.key.span, `${owner} sets a fee for ${bearer} twice`);
330
- continue;
331
- }
332
- if (fee.value.kind !== "percent") {
333
- error(fee.value.span, `the fee for ${bearer} must be a percent`);
334
- continue;
335
- }
336
- if (fee.value.bps >= TOTAL_BPS) {
337
- error(fee.value.span, `a ${fee.value.raw}% fee consumes the whole amount; fees stay under 100%`);
338
- continue;
339
- }
340
- if (fee.value.bps === 0) {
341
- warning(fee.value.span, `the fee for ${bearer} is 0%; drop the entry if no fee is meant`);
342
- }
343
- fees.push({ bearer, bps: fee.value.bps, origin: fee.span });
344
- }
345
- return fees;
346
- };
347
- const parseSwapFees = (entry, owner, sides) => {
348
- const fees = [];
349
- if (!entry)
350
- return fees;
351
- noQualifiers(entry, owner);
352
- if (entry.value.kind !== "block") {
353
- error(entry.value.span, `fees is a block of exact on-top money fields, like: fees { buyer: buyerFee: money(SAR) }`);
354
- return fees;
355
- }
356
- for (const fee of entry.value.entries) {
357
- const bearer = fee.key.name;
358
- if (!sides.includes(bearer)) {
359
- error(fee.key.span, `swap fees are borne by one of its two parties; ${bearer} is not in between`);
360
- continue;
361
- }
362
- if (fees.some((existing) => existing.bearer === bearer)) {
363
- error(fee.key.span, `${owner} sets a fee for ${bearer} twice`);
364
- continue;
365
- }
366
- const amount = parseAmount(fee, `${owner} fee for ${bearer}`);
367
- if (amount)
368
- fees.push({ amount, bearer });
369
- }
370
- return fees;
371
- };
372
- /**
373
- * A percent split block whose shares must sum to exactly 100%. Recipients
374
- * are constrained to `allowed`; `remainder_to` is accepted only when
375
- * `remainderAllowed` (pooled_split), naming the share that carries the
376
- * integer-division remainder.
377
- */
378
- const parseSplitBlock = (entry, owner, allowed, allowedLabel, remainderAllowed) => {
379
- if (entry.value.kind !== "block") {
380
- error(entry.value.span, `${entry.key.name} on ${owner} is a block of percent shares, like: { ${allowed[0] ?? "party"}: 100% }`);
381
- return undefined;
382
- }
383
- const shares = [];
384
- let remainderTo;
385
- let sum = 0;
386
- let sound = true;
387
- for (const share of entry.value.entries) {
388
- const to = share.key.name;
389
- if (to === "remainder_to") {
390
- if (!remainderAllowed) {
391
- error(share.key.span, `${owner} does not take remainder_to here; the first share carries the integer remainder`);
392
- sound = false;
393
- continue;
394
- }
395
- if (share.value.kind !== "ident") {
396
- error(share.value.span, `remainder_to names one of the recipients`);
397
- sound = false;
398
- continue;
399
- }
400
- if (remainderTo) {
401
- error(share.key.span, `${owner} sets remainder_to twice`);
402
- sound = false;
403
- continue;
404
- }
405
- remainderTo = { name: share.value.name, span: share.value.span };
406
- continue;
407
- }
408
- if (!allowed.includes(to)) {
409
- error(share.key.span, `the ${entry.key.name} split goes to ${allowedLabel}; ${to} is not among them`);
410
- sound = false;
411
- continue;
412
- }
413
- referencedParties.add(to);
414
- if (shares.some((existing) => existing.to === to)) {
415
- error(share.key.span, `${owner} splits to ${to} twice`);
416
- sound = false;
417
- continue;
418
- }
419
- if (share.value.kind !== "percent") {
420
- error(share.value.span, `the share for ${to} must be a percent`);
421
- sound = false;
422
- continue;
423
- }
424
- // A 0% share would lower to a zero-amount money movement, which the
425
- // kernel refuses at runtime; a recipient who receives nothing simply
426
- // leaves the block.
427
- if (share.value.bps === 0) {
428
- error(share.value.span, `the share for ${to} is 0%; drop ${to} from the split instead of giving it nothing`);
429
- sound = false;
430
- continue;
431
- }
432
- shares.push({ bps: share.value.bps, origin: share.span, to });
433
- sum += share.value.bps;
434
- }
435
- if (sound && sum !== TOTAL_BPS) {
436
- error(entry.value.span, `the ${entry.key.name} split must account for exactly 100%; these shares total ${(sum / 100).toFixed(2).replace(/\.?0+$/, "")}%`);
437
- sound = false;
438
- }
439
- if (remainderTo &&
440
- !shares.some((share) => share.to === remainderTo?.name)) {
441
- error(remainderTo.span, `remainder_to must name one of the split's recipients; ${remainderTo.name} holds no share`);
442
- sound = false;
443
- }
444
- if (!sound || shares.length === 0)
445
- return undefined;
446
- return {
447
- ...(remainderTo ? { remainderTo: remainderTo.name } : {}),
448
- shares,
449
- };
450
- };
451
- const parseCancelPolicy = (entry, owner, allowed, allowedLabel) => {
452
- if (!entry)
453
- return undefined;
454
- const qualifier = entry.qualifiers[0];
455
- if (entry.qualifiers.length !== 1 || qualifier?.name !== "funded") {
456
- error(entry.span, `on_cancel states the phase it covers; today that is on_cancel(funded)`);
457
- return undefined;
458
- }
459
- const split = parseSplitBlock(entry, owner, allowed, allowedLabel, false);
460
- if (!split)
461
- return undefined;
462
- return { origin: entry.span, shares: split.shares, when: "funded" };
463
- };
464
- const parseDateField = (entry, owner) => {
465
- if (!entry)
466
- return undefined;
467
- noQualifiers(entry, owner);
468
- if (entry.value.kind === "ident" && CAMEL_CASE.test(entry.value.name)) {
469
- return reservedFieldName(entry.value.name, entry.value.span, owner)
470
- ? undefined
471
- : entry.value.name;
472
- }
473
- error(entry.value.span, `${entry.key.name} on ${owner} names a camelCase date field, like: ${entry.key.name}: dueDate`);
474
- return undefined;
475
- };
476
- const parseFieldName = (entry, owner) => {
477
- if (!entry)
478
- return undefined;
479
- noQualifiers(entry, owner);
480
- if (entry.value.kind === "ident" && CAMEL_CASE.test(entry.value.name)) {
481
- return reservedFieldName(entry.value.name, entry.value.span, owner)
482
- ? undefined
483
- : entry.value.name;
484
- }
485
- error(entry.value.span, `${entry.key.name} on ${owner} names a camelCase lineage field, like: ${entry.key.name}: captureReference`);
486
- return undefined;
487
- };
488
- const requireIdentPolicy = (entry, owner, expected, refusal) => {
489
- if (!entry)
490
- return false;
491
- noQualifiers(entry, owner);
492
- if (entry.value.kind === "ident" && entry.value.name === expected) {
493
- return true;
494
- }
495
- error(entry.value.span, `${entry.key.name} on ${owner} is ${expected}; ${refusal}`);
496
- return false;
497
- };
498
- const parseLiteralCount = (entry, owner, label, minimum, maximum) => {
499
- if (!entry)
500
- return undefined;
501
- noQualifiers(entry, owner);
502
- const parsed = entry.value.kind === "number" ? Number(entry.value.raw) : Number.NaN;
503
- if (Number.isInteger(parsed) && parsed >= minimum && parsed <= maximum) {
504
- return parsed;
505
- }
506
- error(entry.value.span, `${label} on ${owner} is a literal integer from ${minimum} through ${maximum}`);
507
- return undefined;
508
- };
509
- const parsePartyList = (entry, owner, label, minimum, maximum) => {
510
- if (!entry)
511
- return undefined;
512
- noQualifiers(entry, owner);
513
- if (entry.value.kind !== "list" ||
514
- entry.value.items.length < minimum ||
515
- entry.value.items.length > maximum) {
516
- error(entry.value.span, `${label} on ${owner} lists ${minimum} through ${maximum} declared parties`);
517
- return undefined;
518
- }
519
- const names = entry.value.items
520
- .map((item) => partyRef(item, `${owner} ${label}`))
521
- .filter((name) => name !== undefined);
522
- if (new Set(names).size !== names.length) {
523
- error(entry.value.span, `${label} on ${owner} must not repeat a party`);
524
- return undefined;
525
- }
526
- return names.length === entry.value.items.length ? names : undefined;
527
- };
528
- const parseSchedule = (entries, owner, origin, maxCount = 12) => {
529
- const countEntry = entries.get("count");
530
- const everyEntry = entries.get("every");
531
- let count;
532
- if (countEntry) {
533
- noQualifiers(countEntry, owner);
534
- const value = countEntry.value;
535
- const parsed = value.kind === "number" ? Number(value.raw) : Number.NaN;
536
- if (Number.isInteger(parsed) && parsed >= 2 && parsed <= maxCount) {
537
- count = parsed;
538
- }
539
- else {
540
- error(value.span, `count on ${owner} is a literal number of anchors between 2 and ${maxCount}; the schedule stays finite by construction`);
541
- }
542
- }
543
- let every;
544
- if (everyEntry) {
545
- noQualifiers(everyEntry, owner);
546
- const value = everyEntry.value;
547
- const raw = value.kind === "ident"
548
- ? value.name
549
- : value.kind === "string"
550
- ? value.value
551
- : undefined;
552
- const match = raw ? /^P([1-9]\d{0,3})([DW])$/.exec(raw) : null;
553
- if (match && raw) {
554
- const magnitude = Number(match[1]);
555
- every = {
556
- days: match[2] === "W" ? magnitude * 7 : magnitude,
557
- raw,
558
- };
559
- }
560
- else {
561
- error(value.span, `every on ${owner} is a fixed duration in days or weeks, like P30D or P2W; calendar months drift and cannot anchor an exact schedule`);
562
- }
563
- }
564
- const firstDueField = parseDateField(entries.get("first_due"), owner);
565
- if (count === undefined || !every || !firstDueField)
566
- return undefined;
567
- return { count, every, firstDueField, origin };
568
- };
569
- const parseSettlementReference = (entry, owner) => {
570
- noQualifiers(entry, owner);
571
- const value = entry.value;
572
- if (value.kind !== "settlement_ref") {
573
- error(value.span, `${owner} draws against a held payment's release, like: against: retention.release`);
574
- return undefined;
575
- }
576
- const target = value.owner.name;
577
- const targetDecl = settlementDecls.find((decl) => decl.name.name === target);
578
- if (!targetDecl) {
579
- const other = declared.get(target);
580
- error(value.owner.span, other
581
- ? `${owner} references ${target}, which is a ${other.kind}; settlement exits belong to settlements`
582
- : `${owner} references ${target}, but no settlement with that name is declared`);
583
- return undefined;
584
- }
585
- const targetArchetype = targetDecl.archetype.name;
586
- if (!SETTLEMENT_ARCHETYPES.includes(targetArchetype)) {
587
- return undefined;
588
- }
589
- const exits = ARCHETYPE_DEFINITIONS[targetArchetype]
590
- .referenceableExits;
591
- if (!exits.includes(value.member.name)) {
592
- const offered = exits.length > 0
593
- ? `it exposes ${exits.join(", ")}`
594
- : "it exposes no referenceable exits";
595
- error(value.member.span, `${owner} references ${target}.${value.member.name}, but ${targetArchetype} does not expose that exit; ${offered}`);
596
- return undefined;
597
- }
598
- return {
599
- exit: value.member.name,
600
- origin: value.span,
601
- settlement: target,
602
- targetArchetype: targetArchetype,
603
- };
604
- };
605
- /**
606
- * An advance draws its repayment from exactly one source: its own finite
607
- * schedule, or a carve out of the release of a hold the advanced party is
608
- * already owed. Which hold that is resolves after every settlement is
609
- * checked, so only the reference's shape is validated here.
610
- */
611
- const parseAdvanceSource = (entries, owner, origin) => {
612
- const againstEntry = entries.get("against");
613
- const scheduleKeys = SCHEDULE_KEYS.filter((key) => entries.has(key));
614
- if (againstEntry && scheduleKeys.length > 0) {
615
- error(againstEntry.span, `${owner} is repaid by a carve and by its own schedule (${scheduleKeys.join(", ")}); an advance draws on one source`);
616
- return undefined;
617
- }
618
- if (!againstEntry) {
619
- if (scheduleKeys.length === 0) {
620
- error(origin, `${owner} needs a repayment source: against: <hold>.release carves it out of a hold's release, or count + every + first_due collects it on a schedule`);
621
- return undefined;
622
- }
623
- for (const key of SCHEDULE_KEYS) {
624
- if (!entries.has(key))
625
- error(origin, `${owner} is missing ${key}`);
626
- }
627
- const schedule = parseSchedule(entries, owner, origin);
628
- return schedule ? { kind: "schedule", schedule } : undefined;
629
- }
630
- const reference = parseSettlementReference(againstEntry, owner);
631
- return reference ? { kind: "carve", ...reference } : undefined;
632
- };
633
- // --- Settlements: one checker per archetype ---------------------------------
634
- for (const decl of settlementDecls) {
635
- const archetype = decl.archetype.name;
636
- if (!imported.has(archetype)) {
637
- error(decl.archetype.span, SETTLEMENT_ARCHETYPES.includes(archetype)
638
- ? `${archetype} must be imported first: import { ${archetype} } from "settlement"`
639
- : `there is no settlement archetype named ${archetype}`);
640
- continue;
641
- }
642
- const owner = `settlement ${decl.name.name}`;
643
- const surface = ARCHETYPE_DEFINITIONS[archetype];
644
- const entries = entriesOf(decl.body, [...surface.keys, "derived_amount"], owner);
645
- for (const required of surface.required) {
646
- if (!entries.has(required)) {
647
- error(decl.span, `${owner} is missing ${required}`);
648
- }
649
- }
650
- const party = (key, role) => {
651
- const entry = entries.get(key);
652
- if (!entry)
653
- return undefined;
654
- noQualifiers(entry, owner);
655
- return partyRef(entry.value, `${owner} ${role}`);
656
- };
657
- const distinct = (left, right, message) => {
658
- if (left && right && left === right) {
659
- error(decl.span, `${owner} ${message}`);
660
- return false;
661
- }
662
- return true;
663
- };
664
- const derived = entries.get("derived_amount");
665
- if (derived) {
666
- noQualifiers(derived, owner);
667
- if (derived.value.kind !== "block") {
668
- error(derived.value.span, "derived_amount is a block with field, source, rule, and bearer");
669
- }
670
- else {
671
- const terms = entriesOf(derived.value, ["field", "source", "rule", "bearer"], `${owner} derived_amount`);
672
- for (const key of ["field", "source", "rule", "bearer"]) {
673
- if (!terms.has(key))
674
- error(derived.span, `${owner} derived_amount is missing ${key}`);
675
- }
676
- const fieldValue = terms.get("field")?.value;
677
- const baseValue = terms.get("source")?.value;
678
- const ruleValue = terms.get("rule")?.value;
679
- const bearerValue = terms.get("bearer")?.value;
680
- const field = fieldValue?.kind === "ident" ? fieldValue.name : undefined;
681
- const baseField = baseValue?.kind === "ident" ? baseValue.name : undefined;
682
- const bearer = bearerValue
683
- ? partyRef(bearerValue, `${owner} derived amount bearer`)
684
- : undefined;
685
- if (fieldValue && fieldValue.kind !== "ident")
686
- error(fieldValue.span, "derived amount field must be a camelCase identifier");
687
- if (baseValue && baseValue.kind !== "ident")
688
- error(baseValue.span, "derived amount source must name one stored money field");
689
- if (ruleValue && ruleValue.kind !== "percent") {
690
- error(ruleValue.span, "derived_amount proves percentage-of rules only; fixed and tiered rules are refused");
691
- }
692
- if (ruleValue?.kind === "percent" &&
693
- (ruleValue.bps <= 0 || ruleValue.bps >= TOTAL_BPS)) {
694
- error(ruleValue.span, "derived amount percentage must be above 0% and below 100%");
695
- }
696
- if (field && !CAMEL_CASE.test(field))
697
- error(fieldValue.span, `derived amount field ${field} must be camelCase`);
698
- if (baseField && !CAMEL_CASE.test(baseField))
699
- error(baseValue.span, `derived amount source ${baseField} must be camelCase`);
700
- if (field && baseField && field === baseField)
701
- error(derived.span, "derived amount field must differ from its source field");
702
- if (field &&
703
- baseField &&
704
- bearer &&
705
- ruleValue?.kind === "percent" &&
706
- ruleValue.bps > 0 &&
707
- ruleValue.bps < TOTAL_BPS) {
708
- derivedAmounts.push({
709
- baseField,
710
- bearer,
711
- bps: ruleValue.bps,
712
- field,
713
- origin: derived.span,
714
- settlement: decl.name.name,
715
- });
716
- }
717
- }
718
- }
719
- switch (archetype) {
720
- case "captured_payment": {
721
- const payer = party("payer", "payer");
722
- const payee = party("payee", "payee");
723
- const sound = distinct(payer, payee, `pays ${payer} from ${payer}; payer and payee must differ`);
724
- const amount = parseAmount(entries.get("amount"), owner);
725
- const reserveUntilField = parseDateField(entries.get("reserve_until"), owner);
726
- const correction = parsePortEntry(entries.get("correction"), owner, "payee correction");
727
- const externalReversal = parseDisputeEntry(entries.get("external_reversal"), owner, "external reversal");
728
- const captureInSlices = requireIdentPolicy(entries.get("capture_mode"), owner, "partial_then_full", "capture calls must leave a remainder and settle posts the final remainder");
729
- const fullCorrection = requireIdentPolicy(entries.get("correction_mode"), owner, "full_only", "repeated partial corrections are not representable yet, so the checker refuses them");
730
- const noNegativePosition = requireIdentPolicy(entries.get("negative_position"), owner, "reject", "a correction or reversal must fail when the payee cannot fund it");
731
- const timeoutRejects = requireIdentPolicy(entries.get("timeout"), owner, "reject", "a timeout records no money movement; only a confirmed decision may reverse funds");
732
- if (entries.has("fees")) {
733
- error(entries.get("fees").span, `${owner} does not price fees inside captured_payment; compose a separate fee settlement so capture and correction amounts stay exact`);
734
- }
735
- if (amount && reserveUntilField === amount.name) {
736
- error(entries.get("reserve_until")?.span ?? decl.span, `${owner} uses ${amount.name} as both the reserved amount and reserve_until date field; they need distinct names`);
737
- break;
738
- }
739
- if (payer &&
740
- payee &&
741
- amount &&
742
- reserveUntilField &&
743
- correction &&
744
- externalReversal &&
745
- captureInSlices &&
746
- fullCorrection &&
747
- noNegativePosition &&
748
- timeoutRejects &&
749
- sound) {
750
- settlements.push({
751
- amount,
752
- archetype: "captured_payment",
753
- correction,
754
- externalReversal,
755
- name: decl.name.name,
756
- origin: decl.span,
757
- payee,
758
- payer,
759
- reserveUntilField,
760
- });
761
- }
762
- break;
763
- }
764
- case "settlement_batch": {
765
- const settlementAccount = party("settlement_account", "settlement account");
766
- const payoutDestination = party("payout_destination", "payout destination");
767
- const sound = distinct(settlementAccount, payoutDestination, `routes payout from ${settlementAccount} back to itself; settlement account and payout destination must differ`);
768
- const sourceCaptureReferenceField = parseFieldName(entries.get("source_capture_refs"), owner);
769
- const feeReferenceField = parseFieldName(entries.get("fee_entries"), owner);
770
- const externalReversalReferenceField = parseFieldName(entries.get("external_reversal_offsets"), owner);
771
- const closeTriggerField = parseDateField(entries.get("close_trigger"), owner);
772
- const payoutAcknowledgement = parsePortEntry(entries.get("payout_acknowledgement"), owner, "payout acknowledgement");
773
- const payoutBeneficiaryReferenceField = parseFieldName(entries.get("payout_beneficiary_ref"), owner);
774
- const rejectsNegative = requireIdentPolicy(entries.get("negative_position"), owner, "reject", "offsets beyond gross capture entries must stop calculation and post no payout");
775
- const lineageFields = [
776
- sourceCaptureReferenceField,
777
- feeReferenceField,
778
- externalReversalReferenceField,
779
- closeTriggerField,
780
- payoutBeneficiaryReferenceField,
781
- ].filter((field) => field !== undefined);
782
- if (new Set(lineageFields).size !== lineageFields.length) {
783
- error(decl.span, `${owner} needs distinct source_capture_refs, fee_entries, external_reversal_offsets, close_trigger, and payout_beneficiary_ref field names`);
784
- break;
785
- }
786
- if (settlementAccount &&
787
- payoutDestination &&
788
- sourceCaptureReferenceField &&
789
- feeReferenceField &&
790
- externalReversalReferenceField &&
791
- closeTriggerField &&
792
- payoutAcknowledgement &&
793
- payoutBeneficiaryReferenceField &&
794
- rejectsNegative &&
795
- sound) {
796
- settlements.push({
797
- archetype: "settlement_batch",
798
- closeTriggerField,
799
- externalReversalReferenceField,
800
- feeReferenceField,
801
- name: decl.name.name,
802
- origin: decl.span,
803
- payoutDestination,
804
- payoutAcknowledgement,
805
- payoutBeneficiaryReferenceField,
806
- settlementAccount,
807
- sourceCaptureReferenceField,
808
- });
809
- }
810
- break;
811
- }
812
- case "funding_round": {
813
- const contributor = party("contributor", "contributor");
814
- const beneficiary = party("beneficiary", "beneficiary");
815
- const target = parseAmount(entries.get("target"), owner);
816
- const commitment = parseAmount(entries.get("commitment"), owner);
817
- const maxContributors = parseLiteralCount(entries.get("max_contributors"), owner, "max_contributors", 2, 10_000);
818
- const closeByField = parseDateField(entries.get("close_by"), owner);
819
- const policiesSound = [
820
- requireIdentPolicy(entries.get("close_policy"), owner, "threshold", "close must follow the locked committed sum at the stored close date"),
821
- requireIdentPolicy(entries.get("overfund_policy"), owner, "reject", "a commitment past the remaining target headroom must refuse"),
822
- requireIdentPolicy(entries.get("cancel_policy"), owner, "before_close", "a commitment may cancel only while its parent remains open"),
823
- requireIdentPolicy(entries.get("fail_policy"), owner, "whole_commitment_refund", "failure refunds each stored commitment whole"),
824
- ].every(Boolean);
825
- const sound = distinct(contributor, beneficiary, `uses ${contributor} as both contributor and beneficiary; the roles must differ`);
826
- if (target && commitment && target.currency !== commitment.currency) {
827
- error(commitment.origin, `${owner} mixes ${target.currency} and ${commitment.currency}; target and commitment need one currency`);
828
- }
829
- if (target && commitment && target.name === commitment.name) {
830
- error(commitment.origin, `${owner} uses ${target.name} for both target and commitment; the parent and child amounts need distinct fields`);
831
- }
832
- if (contributor &&
833
- beneficiary &&
834
- target &&
835
- commitment &&
836
- target.currency === commitment.currency &&
837
- target.name !== commitment.name &&
838
- maxContributors &&
839
- closeByField &&
840
- policiesSound &&
841
- sound) {
842
- settlements.push({
843
- archetype: "funding_round",
844
- beneficiary,
845
- cancelPolicy: "before_close",
846
- closeByField,
847
- closePolicy: "threshold",
848
- commitment,
849
- contributor,
850
- failPolicy: "whole_commitment_refund",
851
- maxContributors,
852
- name: decl.name.name,
853
- origin: decl.span,
854
- overfundPolicy: "reject",
855
- target,
856
- });
857
- }
858
- break;
859
- }
860
- case "weighted_distribution": {
861
- const source = party("source", "source");
862
- const recipient = party("recipient", "recipient template");
863
- const amount = parseAmount(entries.get("amount"), owner);
864
- const weight = parseAmount(entries.get("weight"), owner);
865
- const maxRecipients = parseLiteralCount(entries.get("max_recipients"), owner, "max_recipients", 2, 10_000);
866
- const recordAtField = parseDateField(entries.get("record_at"), owner);
867
- const snapshot = parsePortEntry(entries.get("snapshot"), owner, "entitlement snapshot");
868
- const policiesSound = [
869
- requireIdentPolicy(entries.get("rounding_policy"), owner, "largest_remainder", "floor shares use a deterministic largest-remainder allocation with noun id as the tie-break"),
870
- requireIdentPolicy(entries.get("withholding_policy"), owner, "refuse", "withholding needs its own proved retained-amount mechanism"),
871
- requireIdentPolicy(entries.get("correction_policy"), owner, "new_distribution", "post-payout correction is a new linked distribution, never a rewrite or caller amount"),
872
- ].every(Boolean);
873
- const sound = distinct(source, recipient, `uses ${source} as both source and recipient; the roles must differ`);
874
- if (amount && weight && amount.currency !== weight.currency) {
875
- error(weight.origin, `${owner} mixes ${amount.currency} and ${weight.currency}; pool and stored weights need one denomination`);
876
- }
877
- if (amount && weight && amount.name === weight.name) {
878
- error(weight.origin, `${owner} uses ${amount.name} for both pool and weight; the fields must differ`);
879
- }
880
- if (source &&
881
- recipient &&
882
- amount &&
883
- weight &&
884
- amount.currency === weight.currency &&
885
- amount.name !== weight.name &&
886
- maxRecipients &&
887
- recordAtField &&
888
- snapshot &&
889
- policiesSound &&
890
- sound) {
891
- settlements.push({
892
- amount,
893
- archetype: "weighted_distribution",
894
- correctionPolicy: "new_distribution",
895
- maxRecipients,
896
- name: decl.name.name,
897
- origin: decl.span,
898
- recipient,
899
- recordAtField,
900
- roundingPolicy: "largest_remainder",
901
- snapshot,
902
- source,
903
- weight,
904
- withholdingPolicy: "refuse",
905
- });
906
- }
907
- break;
908
- }
909
- case "credit_facility": {
910
- const lender = party("lender", "lender");
911
- const borrower = party("borrower", "borrower");
912
- const drawDestination = party("draw_destination", "draw destination");
913
- const limit = parseAmount(entries.get("limit"), owner);
914
- const expiresAtField = parseDateField(entries.get("expires_at"), owner);
915
- const obligationEntry = entries.get("obligation");
916
- const obligation = obligationEntry
917
- ? parseSettlementReference(obligationEntry, owner)
918
- : undefined;
919
- const availabilityEntry = entries.get("availability_policy");
920
- let availabilityPolicy;
921
- if (availabilityEntry?.value.kind === "ident" &&
922
- (availabilityEntry.value.name === "revolving" ||
923
- availabilityEntry.value.name === "non_revolving")) {
924
- availabilityPolicy = availabilityEntry.value.name;
925
- }
926
- else if (availabilityEntry) {
927
- error(availabilityEntry.value.span, `availability_policy on ${owner} is revolving or non_revolving; the factpack does not justify an implicit choice`);
928
- }
929
- const policiesSound = [
930
- requireIdentPolicy(entries.get("expiry_policy"), owner, "freeze_draws", "expiry stops new draws without closing linked debt"),
931
- requireIdentPolicy(entries.get("close_policy"), owner, "no_open_draws", "the facility closes only after every linked obligation resolves"),
932
- ].every(Boolean);
933
- const rolesSound = [
934
- distinct(lender, borrower, `uses ${lender} as both lender and borrower; the roles must differ`),
935
- distinct(lender, drawDestination, `draws back to its lender ${lender}; draw_destination must differ`),
936
- ].every(Boolean);
937
- if (lender &&
938
- borrower &&
939
- drawDestination &&
940
- limit &&
941
- expiresAtField &&
942
- obligation &&
943
- availabilityPolicy &&
944
- policiesSound &&
945
- rolesSound) {
946
- settlements.push({
947
- archetype: "credit_facility",
948
- availabilityPolicy,
949
- borrower,
950
- closePolicy: "no_open_draws",
951
- drawDestination,
952
- expiresAtField,
953
- expiryPolicy: "freeze_draws",
954
- lender,
955
- limit,
956
- name: decl.name.name,
957
- obligation,
958
- origin: decl.span,
959
- });
960
- }
961
- break;
962
- }
963
- case "recurring_collection": {
964
- const obligationEntry = entries.get("obligation");
965
- const obligation = obligationEntry
966
- ? parseSettlementReference(obligationEntry, owner)
967
- : undefined;
968
- const mandate = parsePortEntry(entries.get("mandate"), owner, "mandate evidence");
969
- const policiesSound = [
970
- requireIdentPolicy(entries.get("attempt_policy"), owner, "explicit", "every attempt is a separate receipted call"),
971
- requireIdentPolicy(entries.get("period_idempotency"), owner, "obligation_and_anchor", "the parent obligation and stored anchor form the period identity"),
972
- requireIdentPolicy(entries.get("retry_policy"), owner, "explicit_attempt", "PRINCIPLES.md bans hidden retries; a retry needs a new explicit idempotency key"),
973
- requireIdentPolicy(entries.get("failure_policy"), owner, "parent_delinquency", "the scheduled obligation owns due state and delinquency"),
974
- ].every(Boolean);
975
- if (obligation && mandate && policiesSound) {
976
- settlements.push({
977
- archetype: "recurring_collection",
978
- attemptPolicy: "explicit",
979
- failurePolicy: "parent_delinquency",
980
- mandate,
981
- name: decl.name.name,
982
- obligation,
983
- origin: decl.span,
984
- periodIdempotency: "obligation_and_anchor",
985
- retryPolicy: "explicit_attempt",
986
- });
987
- }
988
- break;
989
- }
990
- case "conditional_disbursement": {
991
- const source = party("source", "source");
992
- const destination = party("destination", "destination");
993
- const cap = parseAmount(entries.get("cap"), owner);
994
- const amount = parseAmount(entries.get("amount"), owner);
995
- const decision = parsePortEntry(entries.get("decision"), owner, "capped disbursement decision");
996
- const policiesSound = [
997
- requireIdentPolicy(entries.get("reopen_policy"), owner, "refuse", "a closed decision cannot be reopened; create a new linked disbursement"),
998
- requireIdentPolicy(entries.get("recovery_policy"), owner, "separate_transfer", "recovery is a new money movement with its own receipt"),
999
- ].every(Boolean);
1000
- const sound = distinct(source, destination, `pays ${source} from itself; source and destination must differ`);
1001
- if (cap && amount && cap.currency !== amount.currency) {
1002
- error(amount.origin, `${owner} mixes ${cap.currency} and ${amount.currency}; cap and approved amount need one currency`);
1003
- }
1004
- if (cap && amount && cap.name === amount.name) {
1005
- error(amount.origin, `${owner} uses ${cap.name} for both cap and approved amount; the fields must differ`);
1006
- }
1007
- if (source &&
1008
- destination &&
1009
- cap &&
1010
- amount &&
1011
- cap.currency === amount.currency &&
1012
- cap.name !== amount.name &&
1013
- decision &&
1014
- policiesSound &&
1015
- sound) {
1016
- settlements.push({
1017
- amount,
1018
- archetype: "conditional_disbursement",
1019
- cap,
1020
- decision,
1021
- destination,
1022
- name: decl.name.name,
1023
- origin: decl.span,
1024
- recoveryPolicy: "separate_transfer",
1025
- reopenPolicy: "refuse",
1026
- source,
1027
- });
1028
- }
1029
- break;
1030
- }
1031
- case "rotating_pool": {
1032
- const members = parsePartyList(entries.get("members"), owner, "members", 2, 5);
1033
- const payoutOrder = parsePartyList(entries.get("payout_order"), owner, "payout_order", 2, 5);
1034
- const contribution = parseAmount(entries.get("contribution"), owner);
1035
- const schedule = parseSchedule(entries, owner, decl.span, 12);
1036
- const guarantor = entries.has("guarantor")
1037
- ? party("guarantor", "guarantor")
1038
- : undefined;
1039
- const policiesSound = [
1040
- requireIdentPolicy(entries.get("default_policy"), owner, "due_condition", "default follows an unmet stored contribution due condition"),
1041
- requireIdentPolicy(entries.get("guarantee_policy"), owner, "funded_only", "a guarantee changes money only through an explicit funded contribution"),
1042
- requireIdentPolicy(entries.get("exit_policy"), owner, "before_activation_only", "membership and order freeze before the first contribution"),
1043
- ].every(Boolean);
1044
- let rosterSound = true;
1045
- if (members && payoutOrder) {
1046
- const memberSet = new Set(members);
1047
- if (payoutOrder.length !== members.length ||
1048
- payoutOrder.some((name) => !memberSet.has(name))) {
1049
- error(entries.get("payout_order")?.span ?? decl.span, `${owner} payout_order must contain every member exactly once`);
1050
- rosterSound = false;
1051
- }
1052
- }
1053
- if (members && schedule && schedule.count !== members.length) {
1054
- error(entries.get("count")?.span ?? decl.span, `${owner} count ${schedule.count} must equal its ${members.length}-member roster`);
1055
- rosterSound = false;
1056
- }
1057
- if (members && guarantor && members.includes(guarantor)) {
1058
- error(entries.get("guarantor")?.span ?? decl.span, `${owner} guarantor must differ from every member`);
1059
- rosterSound = false;
1060
- }
1061
- if (members && guarantor && members.length > 4) {
1062
- error(entries.get("members")?.span ?? decl.span, `${owner} supports at most 4 members with a guarantor so every generated child binding stays within the 8-entry cap`);
1063
- rosterSound = false;
1064
- }
1065
- if (members &&
1066
- payoutOrder &&
1067
- contribution &&
1068
- schedule &&
1069
- policiesSound &&
1070
- rosterSound) {
1071
- settlements.push({
1072
- archetype: "rotating_pool",
1073
- contribution,
1074
- defaultPolicy: "due_condition",
1075
- exitPolicy: "before_activation_only",
1076
- guaranteePolicy: "funded_only",
1077
- ...(guarantor ? { guarantor } : {}),
1078
- members,
1079
- name: decl.name.name,
1080
- origin: decl.span,
1081
- payoutOrder,
1082
- schedule,
1083
- });
1084
- }
1085
- break;
1086
- }
1087
- case "swap": {
1088
- const betweenEntry = entries.get("between");
1089
- let sideA;
1090
- let sideB;
1091
- if (betweenEntry) {
1092
- noQualifiers(betweenEntry, owner);
1093
- if (betweenEntry.value.kind !== "list" ||
1094
- betweenEntry.value.items.length !== 2) {
1095
- error(betweenEntry.value.span, `${owner} swaps between exactly two parties, like: between: [buyer, seller]`);
1096
- }
1097
- else {
1098
- sideA = partyRef(betweenEntry.value.items[0], `${owner} first side`);
1099
- sideB = partyRef(betweenEntry.value.items[1], `${owner} second side`);
1100
- distinct(sideA, sideB, `names ${sideA} on both sides; the two swap parties must differ`);
1101
- }
1102
- }
1103
- const amountByParty = new Map();
1104
- const amountsEntry = entries.get("amounts");
1105
- if (amountsEntry) {
1106
- noQualifiers(amountsEntry, owner);
1107
- if (amountsEntry.value.kind !== "block") {
1108
- error(amountsEntry.value.span, `${owner} amounts is a two-party block, like: amounts { buyer: buyerPays: money(SAR), seller: sellerPays: money(SAR) }`);
1109
- }
1110
- else {
1111
- for (const amountEntry of amountsEntry.value.entries) {
1112
- const partyName = amountEntry.key.name;
1113
- if (partyName !== sideA && partyName !== sideB) {
1114
- error(amountEntry.key.span, `${owner} amount belongs to one of its two parties; ${partyName} is not in between`);
1115
- continue;
1116
- }
1117
- if (amountByParty.has(partyName)) {
1118
- error(amountEntry.key.span, `${owner} declares more than one amount for ${partyName}`);
1119
- continue;
1120
- }
1121
- const amount = parseAmount(amountEntry, owner);
1122
- if (amount)
1123
- amountByParty.set(partyName, amount);
1124
- }
1125
- }
1126
- }
1127
- const sideAAmount = sideA ? amountByParty.get(sideA) : undefined;
1128
- const sideBAmount = sideB ? amountByParty.get(sideB) : undefined;
1129
- if (sideA && !sideAAmount) {
1130
- error(amountsEntry?.span ?? decl.span, `${owner} needs exactly one funded amount for ${sideA}`);
1131
- }
1132
- if (sideB && !sideBAmount) {
1133
- error(amountsEntry?.span ?? decl.span, `${owner} needs exactly one funded amount for ${sideB}`);
1134
- }
1135
- if (sideAAmount &&
1136
- sideBAmount &&
1137
- sideAAmount.currency !== sideBAmount.currency) {
1138
- error(amountsEntry?.span ?? decl.span, `${owner} cannot link ${sideAAmount.currency} and ${sideBAmount.currency} in one atomic ledger batch; both amounts need one currency`);
1139
- }
1140
- const release = parsePortEntry(entries.get("release"), owner, "whole-trade release");
1141
- const dispute = parseDisputeEntry(entries.get("dispute"), owner);
1142
- const fees = parseSwapFees(entries.get("fees"), owner, [sideA, sideB]);
1143
- const swapAmounts = [sideAAmount, sideBAmount].filter((amount) => amount !== undefined);
1144
- for (const fee of fees) {
1145
- if (swapAmounts.some((amount) => amount.name === fee.amount.name) ||
1146
- fees.some((other) => other !== fee && other.amount.name === fee.amount.name)) {
1147
- error(fee.amount.origin, `${owner} fee field ${fee.amount.name} must be distinct from both principal and fee fields`);
1148
- }
1149
- if (sideAAmount && fee.amount.currency !== sideAAmount.currency) {
1150
- error(fee.amount.origin, `${owner} fee field ${fee.amount.name} must use the trade currency ${sideAAmount.currency}`);
1151
- }
1152
- }
1153
- if (sideA &&
1154
- sideB &&
1155
- sideAAmount &&
1156
- sideBAmount &&
1157
- sideAAmount.currency === sideBAmount.currency &&
1158
- release) {
1159
- const sideAFee = fees.find((fee) => fee.bearer === sideA);
1160
- const sideBFee = fees.find((fee) => fee.bearer === sideB);
1161
- settlements.push({
1162
- archetype: "swap",
1163
- ...(dispute ? { dispute } : {}),
1164
- name: decl.name.name,
1165
- origin: decl.span,
1166
- release,
1167
- sides: [
1168
- {
1169
- amount: sideAAmount,
1170
- ...(sideAFee ? { fee: sideAFee } : {}),
1171
- party: sideA,
1172
- },
1173
- {
1174
- amount: sideBAmount,
1175
- ...(sideBFee ? { fee: sideBFee } : {}),
1176
- party: sideB,
1177
- },
1178
- ],
1179
- });
1180
- }
1181
- break;
1182
- }
1183
- case "held_payment": {
1184
- const payer = party("payer", "payer");
1185
- const payee = party("payee", "payee");
1186
- const sound = distinct(payer, payee, `pays ${payer} from ${payer}; payer and payee must differ`);
1187
- const amount = parseAmount(entries.get("amount"), owner);
1188
- const release = parsePortEntry(entries.get("release"), owner, "release", true);
1189
- const fees = parseFees(entries.get("fees"), owner, payer, payee);
1190
- const onCancel = parseCancelPolicy(entries.get("on_cancel"), owner, [payer, payee].filter((name) => name !== undefined), "this settlement's payer or payee");
1191
- if (amount && release?.deadlineField === amount.name) {
1192
- error(release.origin, `${owner} uses ${amount.name} as both the held amount and the release date field; they need distinct names`);
1193
- break;
1194
- }
1195
- if (payer && payee && amount && release && sound) {
1196
- settlements.push({
1197
- amount,
1198
- archetype: "held_payment",
1199
- fees,
1200
- name: decl.name.name,
1201
- ...(onCancel ? { onCancel } : {}),
1202
- origin: decl.span,
1203
- payee,
1204
- payer,
1205
- release: { origin: release.origin, port: release.port },
1206
- ...(release.deadlineField
1207
- ? { releaseDeadlineField: release.deadlineField }
1208
- : {}),
1209
- });
1210
- }
1211
- break;
1212
- }
1213
- case "instant_transfer": {
1214
- const payer = party("payer", "payer");
1215
- const payee = party("payee", "payee");
1216
- const sound = distinct(payer, payee, `pays ${payer} from ${payer}; payer and payee must differ`);
1217
- const amount = parseAmount(entries.get("amount"), owner);
1218
- const fees = parseFees(entries.get("fees"), owner, payer, payee);
1219
- if (payer && payee && amount && sound) {
1220
- settlements.push({
1221
- amount,
1222
- archetype: "instant_transfer",
1223
- fees,
1224
- name: decl.name.name,
1225
- origin: decl.span,
1226
- payee,
1227
- payer,
1228
- });
1229
- }
1230
- break;
1231
- }
1232
- case "premium_forward": {
1233
- const payer = party("payer", "payer");
1234
- const carrier = party("carrier", "carrier");
1235
- const sound = distinct(payer, carrier, `forwards the premium from ${payer} to ${payer}; payer and carrier must differ`);
1236
- const amount = parseAmount(entries.get("amount"), owner);
1237
- const bind = parsePortEntry(entries.get("bind"), owner, "binding");
1238
- const commissionBps = parsePercentEntry(entries.get("commission"), owner, "commission") ??
1239
- 0;
1240
- const onCancel = parseCancelPolicy(entries.get("on_cancel"), owner, [payer, carrier].filter((name) => name !== undefined), "this settlement's payer or carrier");
1241
- const extensionKeys = [
1242
- "policy_ref",
1243
- "renewal_due",
1244
- "renewal_policy",
1245
- "endorsement",
1246
- "endorsement_policy",
1247
- "lapse_policy",
1248
- ];
1249
- const extendsPolicy = extensionKeys.some((key) => entries.has(key));
1250
- if (extendsPolicy) {
1251
- for (const key of extensionKeys) {
1252
- if (!entries.has(key))
1253
- error(decl.span, `${owner} is missing ${key}`);
1254
- }
1255
- }
1256
- const policyReferenceField = extendsPolicy
1257
- ? parseFieldName(entries.get("policy_ref"), owner)
1258
- : undefined;
1259
- const renewalDueField = extendsPolicy
1260
- ? parseDateField(entries.get("renewal_due"), owner)
1261
- : undefined;
1262
- const endorsement = extendsPolicy
1263
- ? parsePortEntry(entries.get("endorsement"), owner, "endorsement evidence")
1264
- : undefined;
1265
- const extensionPoliciesSound = !extendsPolicy
1266
- ? true
1267
- : [
1268
- requireIdentPolicy(entries.get("renewal_policy"), owner, "explicit_new_forward", "each renewal creates a new premium_forward instead of moving money from policy state"),
1269
- requireIdentPolicy(entries.get("endorsement_policy"), owner, "non_money_only", "a money adjustment needs a separate corrected movement"),
1270
- requireIdentPolicy(entries.get("lapse_policy"), owner, "due_condition", "lapse follows the stored renewal due condition"),
1271
- ].every(Boolean);
1272
- if (payer &&
1273
- carrier &&
1274
- amount &&
1275
- bind &&
1276
- sound &&
1277
- extensionPoliciesSound &&
1278
- (!extendsPolicy ||
1279
- (policyReferenceField && renewalDueField && endorsement))) {
1280
- const checked = {
1281
- amount,
1282
- archetype: "premium_forward",
1283
- bind,
1284
- carrier,
1285
- commissionBps,
1286
- ...(endorsement ? { endorsement } : {}),
1287
- ...(extendsPolicy &&
1288
- policyReferenceField &&
1289
- renewalDueField &&
1290
- endorsement
1291
- ? {
1292
- endorsementPolicy: "non_money_only",
1293
- lapsePolicy: "due_condition",
1294
- policyReferenceField,
1295
- renewalDueField,
1296
- renewalPolicy: "explicit_new_forward",
1297
- }
1298
- : {}),
1299
- name: decl.name.name,
1300
- ...(onCancel ? { onCancel } : {}),
1301
- origin: decl.span,
1302
- payer,
1303
- };
1304
- settlements.push(checked);
1305
- }
1306
- break;
1307
- }
1308
- case "deposit": {
1309
- const payer = party("payer", "payer");
1310
- const holder = party("holder", "holder");
1311
- const sound = distinct(payer, holder, `holds ${payer}'s deposit for ${payer}; payer and holder must differ`);
1312
- const amount = parseAmount(entries.get("amount"), owner);
1313
- const claim = parsePortEntry(entries.get("claim"), owner, "claim");
1314
- const returnPort = parsePortEntry(entries.get("return"), owner, "return");
1315
- if (claim && returnPort && claim.port === returnPort.port) {
1316
- error(returnPort.origin, `${owner} claims and returns through the same port ${claim.port}; the two exits need their own ports`);
1317
- break;
1318
- }
1319
- if (payer && holder && amount && claim && returnPort && sound) {
1320
- settlements.push({
1321
- amount,
1322
- archetype: "deposit",
1323
- claim,
1324
- holder,
1325
- name: decl.name.name,
1326
- origin: decl.span,
1327
- payer,
1328
- return: returnPort,
1329
- });
1330
- }
1331
- break;
1332
- }
1333
- case "scheduled": {
1334
- const payer = party("payer", "payer");
1335
- const payee = party("payee", "payee");
1336
- const sound = distinct(payer, payee, `pays ${payer} from ${payer}; payer and payee must differ`);
1337
- const amount = parseAmount(entries.get("amount"), owner);
1338
- const requestedMode = entries.get("mode")?.value;
1339
- const obligationModeRequested = requestedMode?.kind === "ident" &&
1340
- requestedMode.name === "obligation";
1341
- const schedule = parseSchedule(entries, owner, decl.span, obligationModeRequested ? 7 : 12);
1342
- if (amount && schedule && amount.name === schedule.firstDueField) {
1343
- error(schedule.origin, `${owner} uses ${amount.name} as both the amount and the first_due date field; they need distinct names`);
1344
- break;
1345
- }
1346
- const modeEntry = entries.get("mode");
1347
- const obligationKeys = [
1348
- "advance_to",
1349
- "debtor",
1350
- "delinquency_policy",
1351
- "partial_payment",
1352
- "refund_policy",
1353
- "repayment_matching",
1354
- "reschedule_policy",
1355
- ];
1356
- const hasObligationKey = obligationKeys.some((key) => entries.has(key));
1357
- if (!modeEntry && hasObligationKey) {
1358
- error(decl.span, `${owner} uses obligation policies without mode: obligation`);
1359
- break;
1360
- }
1361
- if (!modeEntry) {
1362
- if (payer && payee && amount && schedule && sound) {
1363
- settlements.push({
1364
- amount,
1365
- archetype: "scheduled",
1366
- mode: "transfer",
1367
- name: decl.name.name,
1368
- origin: decl.span,
1369
- payee,
1370
- payer,
1371
- schedule,
1372
- });
1373
- }
1374
- break;
1375
- }
1376
- if (!requireIdentPolicy(modeEntry, owner, "obligation", "omit mode for a fixed transfer schedule")) {
1377
- break;
1378
- }
1379
- const requiredPolicies = [
1380
- "debtor",
1381
- "partial_payment",
1382
- "repayment_matching",
1383
- "refund_policy",
1384
- "reschedule_policy",
1385
- "delinquency_policy",
1386
- ];
1387
- for (const key of requiredPolicies) {
1388
- if (!entries.has(key))
1389
- error(decl.span, `${owner} is missing ${key}`);
1390
- }
1391
- const debtor = party("debtor", "debtor");
1392
- const advanceTo = entries.has("advance_to")
1393
- ? party("advance_to", "advance recipient")
1394
- : undefined;
1395
- const policiesSound = [
1396
- requireIdentPolicy(entries.get("partial_payment"), owner, "anchor_bound", "payments must bind to one stored installment anchor"),
1397
- requireIdentPolicy(entries.get("repayment_matching"), owner, "obligation_and_anchor", "each payment must name both the obligation and its installment anchor; fuzzy or balance-wide matching is refused"),
1398
- requireIdentPolicy(entries.get("refund_policy"), owner, "full_payment_only", "a refund reverses one stored payment whole so it cannot exceed that payment"),
1399
- requireIdentPolicy(entries.get("reschedule_policy"), owner, "refuse", "forward-version carryover is not mechanically proven; create a new obligation after closing this one"),
1400
- requireIdentPolicy(entries.get("delinquency_policy"), owner, "due_condition", "delinquency must come from an unmet stored due anchor"),
1401
- ].every(Boolean);
1402
- const rolesSound = [
1403
- distinct(debtor, payee, `${owner} names ${debtor} as both debtor and settlement recipient; they must differ`),
1404
- advanceTo
1405
- ? distinct(advanceTo, debtor, `${owner} advances to its debtor ${debtor}; advance_to must name a distinct recipient`)
1406
- : true,
1407
- ].every(Boolean);
1408
- if (payer &&
1409
- payee &&
1410
- amount &&
1411
- schedule &&
1412
- sound &&
1413
- debtor &&
1414
- policiesSound &&
1415
- rolesSound) {
1416
- settlements.push({
1417
- ...(advanceTo ? { advanceTo } : {}),
1418
- amount,
1419
- archetype: "scheduled",
1420
- debtor,
1421
- mode: "obligation",
1422
- name: decl.name.name,
1423
- origin: decl.span,
1424
- payee,
1425
- payer,
1426
- schedule,
1427
- });
1428
- }
1429
- break;
1430
- }
1431
- case "advance": {
1432
- const funder = party("funder", "funder");
1433
- const advanced = party("to", "advanced party");
1434
- const sound = distinct(funder, advanced, `advances ${funder} their own money; funder and advanced party must differ`);
1435
- const amount = parseAmount(entries.get("amount"), owner);
1436
- const feeBps = parsePercentEntry(entries.get("fee"), owner, "fee") ?? 0;
1437
- const source = parseAdvanceSource(entries, owner, decl.span);
1438
- if (amount &&
1439
- source?.kind === "schedule" &&
1440
- amount.name === source.schedule.firstDueField) {
1441
- error(source.schedule.origin, `${owner} uses ${amount.name} as both the amount and the first_due date field; they need distinct names`);
1442
- break;
1443
- }
1444
- if (funder && advanced && amount && source && sound) {
1445
- settlements.push({
1446
- advanced,
1447
- amount,
1448
- archetype: "advance",
1449
- feeBps,
1450
- funder,
1451
- name: decl.name.name,
1452
- origin: decl.span,
1453
- source,
1454
- });
1455
- }
1456
- break;
1457
- }
1458
- case "metered": {
1459
- const payer = party("payer", "payer");
1460
- const payee = party("payee", "payee");
1461
- const sound = distinct(payer, payee, `meters ${payer} against ${payer}; payer and payee must differ`);
1462
- const closeByField = parseDateField(entries.get("close_by"), owner);
1463
- const rates = [];
1464
- const ratesEntry = entries.get("rates");
1465
- let withinEventCap = true;
1466
- if (ratesEntry) {
1467
- noQualifiers(ratesEntry, owner);
1468
- if (ratesEntry.value.kind !== "block") {
1469
- error(ratesEntry.value.span, `rates is a block of per-unit prices, like: rates { api_call: unitFee: money(SAR) }`);
1470
- }
1471
- else {
1472
- const maxRates = ARCHETYPE_DEFINITIONS.metered.maxRates ??
1473
- ARCHETYPE_DEFINITIONS.metered.eventCap;
1474
- if (ratesEntry.value.entries.length > maxRates) {
1475
- error(ratesEntry.value.span, `${owner} prices ${ratesEntry.value.entries.length} meters, but metered allows at most ${maxRates} so one settlement stays within its ${ARCHETYPE_DEFINITIONS.metered.eventCap}-event cap`);
1476
- withinEventCap = false;
1477
- }
1478
- for (const rate of ratesEntry.value.entries) {
1479
- if (!SNAKE_CASE.test(rate.key.name)) {
1480
- error(rate.key.span, `meter names are snake_case; "${rate.key.name}" is not`);
1481
- continue;
1482
- }
1483
- if (rates.some((existing) => existing.meter === rate.key.name)) {
1484
- error(rate.key.span, `${owner} prices meter ${rate.key.name} twice`);
1485
- continue;
1486
- }
1487
- const field = parseAmount(rate, owner);
1488
- if (!field)
1489
- continue;
1490
- if (rates.some((existing) => existing.field.name === field.name)) {
1491
- error(field.origin, `${owner} reuses the field ${field.name} for two meters`);
1492
- continue;
1493
- }
1494
- const currency = rates[0]?.field.currency;
1495
- if (currency && currency !== field.currency) {
1496
- error(field.origin, `${owner} mixes ${currency} and ${field.currency}; one settlement meters in one currency`);
1497
- continue;
1498
- }
1499
- rates.push({ field, meter: rate.key.name, origin: rate.span });
1500
- }
1501
- if (ratesEntry.value.entries.length === 0) {
1502
- error(ratesEntry.value.span, `${owner} prices no meters`);
1503
- }
1504
- }
1505
- }
1506
- if (closeByField &&
1507
- rates.some((rate) => rate.field.name === closeByField)) {
1508
- error(decl.span, `${owner} uses ${closeByField} as both a rate field and the close_by date field; they need distinct names`);
1509
- break;
1510
- }
1511
- if (payer &&
1512
- payee &&
1513
- closeByField &&
1514
- rates.length > 0 &&
1515
- sound &&
1516
- withinEventCap) {
1517
- settlements.push({
1518
- archetype: "metered",
1519
- closeByField,
1520
- name: decl.name.name,
1521
- origin: decl.span,
1522
- payee,
1523
- payer,
1524
- rates,
1525
- });
1526
- }
1527
- break;
1528
- }
1529
- case "pooled_split": {
1530
- const payer = party("payer", "payer");
1531
- const amount = parseAmount(entries.get("amount"), owner);
1532
- const distributeDueField = parseDateField(entries.get("payout_due"), owner);
1533
- const splitEntry = entries.get("split");
1534
- const recipients = [...parties.keys()].filter((name) => name !== payer);
1535
- const split = splitEntry
1536
- ? parseSplitBlock(splitEntry, owner, recipients, "declared parties other than the funder", true)
1537
- : undefined;
1538
- if (split && split.shares.length < 2) {
1539
- error(splitEntry.value.span, `${owner} splits to a single recipient; a pool distributes between at least two`);
1540
- break;
1541
- }
1542
- const maxRecipients = ARCHETYPE_DEFINITIONS.pooled_split.maxRecipients ??
1543
- Math.floor(ARCHETYPE_DEFINITIONS.pooled_split.eventCap / 2);
1544
- const withinEventCap = !split || split.shares.length <= maxRecipients;
1545
- if (!withinEventCap) {
1546
- error(splitEntry.value.span, `${owner} splits to ${split.shares.length} recipients, but pooled_split allows at most ${maxRecipients} so funding and payout stay within its ${ARCHETYPE_DEFINITIONS.pooled_split.eventCap}-event cap`);
1547
- }
1548
- // The lowering names each share field ${camelCase(party)}ShareAmount;
1549
- // camelCasing is not injective (a_2b and a2b collide), so two shares
1550
- // must never map onto one generated field.
1551
- if (split) {
1552
- const shareFieldOwners = new Map();
1553
- for (const share of split.shares) {
1554
- const [head = "", ...rest] = share.to.split("_");
1555
- const field = `${head +
1556
- rest
1557
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
1558
- .join("")}ShareAmount`;
1559
- const other = shareFieldOwners.get(field);
1560
- if (other) {
1561
- error(share.origin, `${owner} splits to ${other} and ${share.to}, whose generated share fields both come out as ${field}; rename one party`);
1562
- }
1563
- shareFieldOwners.set(field, share.to);
1564
- }
1565
- }
1566
- if (amount &&
1567
- distributeDueField &&
1568
- amount.name === distributeDueField) {
1569
- error(decl.span, `${owner} uses ${amount.name} as both the amount and the payout_due date field; they need distinct names`);
1570
- break;
1571
- }
1572
- if (payer && amount && distributeDueField && split && withinEventCap) {
1573
- settlements.push({
1574
- amount,
1575
- archetype: "pooled_split",
1576
- distributeDueField,
1577
- name: decl.name.name,
1578
- origin: decl.span,
1579
- payer,
1580
- remainderTo: split.remainderTo ?? split.shares[0].to,
1581
- shares: split.shares,
1582
- });
1583
- }
1584
- break;
1585
- }
1586
- }
1587
- }
1588
- // --- Ports ------------------------------------------------------------------
1589
- const ports = [];
1590
- for (const decl of portDecls) {
1591
- const owner = `port ${decl.name.name}`;
1592
- // The tenant's own backend is always the decider, so a port carries only
1593
- // who may ask (allowed) and what the answer looks like (shape).
1594
- const entries = entriesOf(decl.body, ["allowed", "shape"], owner);
1595
- if (!entries.has("allowed")) {
1596
- error(decl.span, `${owner} is missing allowed`);
1597
- }
1598
- const allowed = [];
1599
- const allowedEntry = entries.get("allowed");
1600
- if (allowedEntry) {
1601
- noQualifiers(allowedEntry, owner);
1602
- const value = allowedEntry.value;
1603
- if (value.kind !== "list" || value.items.length === 0) {
1604
- error(value.span, `allowed lists who may ask this question, like: allowed: [buyer]`);
1605
- }
1606
- else {
1607
- for (const item of value.items) {
1608
- const party = partyRef(item, `${owner} allowed`);
1609
- if (!party)
1610
- continue;
1611
- if (allowed.includes(party)) {
1612
- error(item.span, `${owner} allows ${party} twice`);
1613
- continue;
1614
- }
1615
- allowed.push(party);
1616
- }
1617
- }
1618
- }
1619
- const fields = [];
1620
- const shapeEntry = entries.get("shape");
1621
- if (shapeEntry) {
1622
- noQualifiers(shapeEntry, owner);
1623
- if (shapeEntry.value.kind !== "block") {
1624
- error(shapeEntry.value.span, `shape is a block of typed fields, like: shape { vehicleId: id(vehicle) }`);
1625
- }
1626
- else {
1627
- for (const field of shapeEntry.value.entries) {
1628
- if (!CAMEL_CASE.test(field.key.name)) {
1629
- error(field.key.span, `shape fields are camelCase names; "${field.key.name}" is not`);
1630
- continue;
1631
- }
1632
- if (fields.some((existing) => existing.name === field.key.name)) {
1633
- error(field.key.span, `${owner} declares ${field.key.name} twice`);
1634
- continue;
1635
- }
1636
- const type = portFieldType(field.value);
1637
- if (!type) {
1638
- error(field.value.span, `${field.key.name} needs a type: id(<asset>), money(<CUR>), text, or date`);
1639
- continue;
1640
- }
1641
- if (type.kind === "asset_id" && !assets.has(type.asset)) {
1642
- error(field.value.span, `id(${type.asset}) points at an asset that is not declared`);
1643
- continue;
1644
- }
1645
- if (type.kind === "money" && !CURRENCY.test(type.currency)) {
1646
- error(field.value.span, `"${type.currency}" is not a currency code; use three capital letters like SAR`);
1647
- continue;
1648
- }
1649
- fields.push({ name: field.key.name, origin: field.span, type });
1650
- }
1651
- }
1652
- }
1653
- if (allowed.length > 0) {
1654
- ports.push({
1655
- allowed,
1656
- fields,
1657
- name: decl.name.name,
1658
- origin: decl.span,
1659
- });
1660
- }
1661
- }
1662
- const checkedPortByName = new Map(ports.map((port) => [port.name, port]));
1663
- for (const settlement of settlements) {
1664
- if (settlement.archetype === "captured_payment") {
1665
- const correctionPort = checkedPortByName.get(settlement.correction.port);
1666
- if (correctionPort &&
1667
- (correctionPort.allowed.length !== 1 ||
1668
- correctionPort.allowed[0] !== settlement.payee)) {
1669
- error(correctionPort.origin, `captured_payment ${settlement.name} correction port ${correctionPort.name} must allow only its payee ${settlement.payee}`);
1670
- }
1671
- const reversalPort = checkedPortByName.get(settlement.externalReversal.port);
1672
- if (reversalPort &&
1673
- !reversalPort.fields.some((field) => field.name === "externalReference" && field.type.kind === "text")) {
1674
- error(reversalPort.origin, `captured_payment ${settlement.name} external reversal port ${reversalPort.name} needs shape { externalReference: text }`);
1675
- }
1676
- continue;
1677
- }
1678
- if (settlement.archetype === "settlement_batch") {
1679
- const acknowledgementPort = checkedPortByName.get(settlement.payoutAcknowledgement.port);
1680
- if (acknowledgementPort &&
1681
- !acknowledgementPort.fields.some((field) => field.name === "acknowledgementReference" &&
1682
- field.type.kind === "text")) {
1683
- error(acknowledgementPort.origin, `settlement_batch ${settlement.name} payout acknowledgement port ${acknowledgementPort.name} needs shape { acknowledgementReference: text }`);
1684
- }
1685
- continue;
1686
- }
1687
- if (settlement.archetype === "weighted_distribution") {
1688
- const port = checkedPortByName.get(settlement.snapshot.port);
1689
- if (port &&
1690
- !port.fields.some((field) => field.name === "evidenceReference" && field.type.kind === "text")) {
1691
- error(port.origin, `weighted_distribution ${settlement.name} snapshot port ${port.name} needs shape { evidenceReference: text }`);
1692
- }
1693
- continue;
1694
- }
1695
- if (settlement.archetype === "recurring_collection") {
1696
- const port = checkedPortByName.get(settlement.mandate.port);
1697
- if (port &&
1698
- !port.fields.some((field) => field.name === "evidenceReference" && field.type.kind === "text")) {
1699
- error(port.origin, `recurring_collection ${settlement.name} mandate port ${port.name} needs shape { evidenceReference: text }`);
1700
- }
1701
- continue;
1702
- }
1703
- if (settlement.archetype === "conditional_disbursement") {
1704
- const port = checkedPortByName.get(settlement.decision.port);
1705
- if (port &&
1706
- (!port.fields.some((field) => field.name === "evidenceReference" && field.type.kind === "text") ||
1707
- port.allowed.length !== 1 ||
1708
- port.allowed[0] !== settlement.source)) {
1709
- error(port.origin, `conditional_disbursement ${settlement.name} decision port ${port.name} must allow only ${settlement.source} and needs shape { evidenceReference: text }`);
1710
- }
1711
- continue;
1712
- }
1713
- if (settlement.archetype === "premium_forward" && settlement.endorsement) {
1714
- const port = checkedPortByName.get(settlement.endorsement.port);
1715
- if (port &&
1716
- !port.fields.some((field) => field.name === "evidenceReference" && field.type.kind === "text")) {
1717
- error(port.origin, `premium_forward ${settlement.name} endorsement port ${port.name} needs shape { evidenceReference: text }`);
1718
- }
1719
- continue;
1720
- }
1721
- if (settlement.archetype !== "swap")
1722
- continue;
1723
- const parties = new Set(settlement.sides.map((side) => side.party));
1724
- const decisions = [
1725
- { label: "release", portRef: settlement.release },
1726
- ...(settlement.dispute
1727
- ? [{ label: "dispute", portRef: settlement.dispute }]
1728
- : []),
1729
- ];
1730
- for (const decision of decisions) {
1731
- const port = checkedPortByName.get(decision.portRef.port);
1732
- if (!port)
1733
- continue;
1734
- for (const actor of port.allowed) {
1735
- if (!parties.has(actor)) {
1736
- error(port.origin, `swap ${settlement.name} ${decision.label} port ${port.name} allows ${actor}, but whole-trade decisions belong only to ${settlement.sides.map((side) => side.party).join(" or ")}`);
1737
- }
1738
- }
1739
- }
1740
- }
1741
- const scheduledObligations = new Map(settlements
1742
- .filter((settlement) => settlement.archetype === "scheduled" &&
1743
- settlement.mode === "obligation")
1744
- .map((settlement) => [settlement.name, settlement]));
1745
- const collectionOwnerByObligation = new Map();
1746
- for (const settlement of settlements) {
1747
- if (settlement.archetype !== "credit_facility" &&
1748
- settlement.archetype !== "recurring_collection") {
1749
- continue;
1750
- }
1751
- const target = scheduledObligations.get(settlement.obligation.settlement);
1752
- const owner = `settlement ${settlement.name}`;
1753
- if (!target) {
1754
- error(settlement.obligation.origin, `${owner} must compose with scheduled in mode: obligation; ${settlement.obligation.settlement} is not one`);
1755
- continue;
1756
- }
1757
- if (settlement.archetype === "credit_facility") {
1758
- if (target.advanceTo) {
1759
- error(settlement.obligation.origin, `${owner} references ${target.name}, which already owns an advance; facility draws and obligation repayment must have one disbursement owner`);
1760
- }
1761
- if (target.payee !== settlement.lender ||
1762
- target.debtor !== settlement.borrower ||
1763
- target.amount.currency !== settlement.limit.currency) {
1764
- error(settlement.obligation.origin, `${owner} must link an obligation whose payee is ${settlement.lender}, debtor is ${settlement.borrower}, and currency is ${settlement.limit.currency}`);
1765
- }
1766
- continue;
1767
- }
1768
- const mandatePort = checkedPortByName.get(settlement.mandate.port);
1769
- const firstCollection = collectionOwnerByObligation.get(target.name);
1770
- if (firstCollection) {
1771
- error(settlement.obligation.origin, `${owner} and settlement ${firstCollection} both collect ${target.name}; one obligation may have one collection policy owner`);
1772
- }
1773
- else {
1774
- collectionOwnerByObligation.set(target.name, settlement.name);
1775
- }
1776
- if (mandatePort &&
1777
- (mandatePort.allowed.length !== 1 ||
1778
- mandatePort.allowed[0] !== target.payer)) {
1779
- error(mandatePort.origin, `${owner} mandate port ${mandatePort.name} must allow only the obligation payer ${target.payer}`);
1780
- }
1781
- }
1782
- // --- Carved advances: the hold each one draws against -----------------------
1783
- // The generic reference checker above has resolved the named target and exit.
1784
- // This pass proves the business terms required by an advance carve.
1785
- const heldByName = new Map(settlements
1786
- .filter((settlement) => settlement.archetype === "held_payment")
1787
- .map((settlement) => [settlement.name, settlement]));
1788
- const carvedBy = new Map();
1789
- for (const settlement of settlements) {
1790
- if (settlement.archetype !== "advance")
1791
- continue;
1792
- if (settlement.source.kind !== "carve")
1793
- continue;
1794
- const owner = `settlement ${settlement.name}`;
1795
- const target = settlement.source.settlement;
1796
- const origin = settlement.source.origin;
1797
- if (settlement.source.targetArchetype !== "held_payment" ||
1798
- settlement.source.exit !== "release") {
1799
- error(origin, `${owner} can only carve held_payment.release; ${target}.${settlement.source.exit} is a ${settlement.source.targetArchetype} exit`);
1800
- continue;
1801
- }
1802
- const hold = heldByName.get(target);
1803
- // The hold's own body failed to check and already said why; a second
1804
- // complaint about it here would only bury the first.
1805
- if (!hold)
1806
- continue;
1807
- if (hold.payee !== settlement.advanced) {
1808
- error(origin, `${owner} advances ${settlement.advanced}, but ${target} releases to ${hold.payee}; an advance carves the release of the party it finances`);
1809
- continue;
1810
- }
1811
- if (hold.amount.name !== settlement.amount.name) {
1812
- error(origin, `${owner} advances field ${settlement.amount.name}, but ${target}.release carries ${hold.amount.name}; a carve must name the same money field`);
1813
- continue;
1814
- }
1815
- if (hold.amount.currency !== settlement.amount.currency) {
1816
- error(origin, `${owner} advances ${settlement.amount.currency}, but ${target}.release carries ${hold.amount.currency}; a carve must use one currency`);
1817
- continue;
1818
- }
1819
- if (settlement.feeBps > 0) {
1820
- error(origin, `${owner} adds a fee to a carved advance, but ${target}.release can only prove repayment of the principal field; use a fee-free carve or a scheduled advance`);
1821
- continue;
1822
- }
1823
- if (hold.fees.some((fee) => fee.bearer === hold.payee && fee.bps > 0)) {
1824
- error(origin, `${owner} carves ${target}.release after a payee fee reduces it; a carved hold must release the full principal field to the funder`);
1825
- continue;
1826
- }
1827
- if (hold.payer === settlement.funder) {
1828
- error(origin, `${settlement.funder} funds ${target} and finances it too; the party who pays the hold cannot be the funder its release repays`);
1829
- continue;
1830
- }
1831
- const first = carvedBy.get(target);
1832
- if (first) {
1833
- error(origin, `${owner} and settlement ${first} both draw against ${target}; one release repays one advance`);
1834
- continue;
1835
- }
1836
- carvedBy.set(target, settlement.name);
1837
- // The hold's release is the advance's only repayment, so every exit that
1838
- // is not that release leaves the funder unpaid. Cancellation is the exit
1839
- // the author chose to declare, so an uncovered one is an error; the
1840
- // pre-funding abandonment every hold carries is a lint.
1841
- const recourse = settlements.some((other) => other.archetype === "scheduled" &&
1842
- other.mode === "transfer" &&
1843
- other.payer === settlement.advanced &&
1844
- other.payee === settlement.funder &&
1845
- other.amount.name === settlement.amount.name &&
1846
- other.amount.currency === settlement.amount.currency);
1847
- if (recourse)
1848
- continue;
1849
- const repayment = `add a scheduled settlement collecting from the ${settlement.advanced.replaceAll("_", " ")} to the ${settlement.funder.replaceAll("_", " ")}`;
1850
- if (hold.onCancel) {
1851
- error(origin, `${owner} has no repayment path when ${target} is refunded instead of released; ${repayment}`);
1852
- continue;
1853
- }
1854
- warning(origin, `${owner} has no repayment path if ${target} is abandoned before it funds; ${repayment}`);
1855
- }
1856
- // --- Whole-program lint -----------------------------------------------------
1857
- for (const [name, party] of parties) {
1858
- if (!referencedParties.has(name)) {
1859
- warning(party.origin, `party ${name} is declared but no settlement or port involves them`);
1860
- }
1861
- }
1862
- for (const decl of portDecls) {
1863
- if (portNames.has(decl.name.name) && !referencedPorts.has(decl.name.name)) {
1864
- warning(decl.span, `port ${decl.name.name} is declared but nothing releases through it`);
1865
- }
1866
- }
1867
- for (const [name, span] of imported) {
1868
- const used = settlementDecls.some((decl) => decl.archetype.name === name);
1869
- if (!used) {
1870
- warning(span, `${name} is imported but never instantiated`);
1871
- }
1872
- }
1873
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === "error");
1874
- if (hasErrors || !header)
1875
- return { diagnostics };
1876
- return {
1877
- diagnostics,
1878
- program: {
1879
- assets: [...assets.values()],
1880
- derivedAmounts,
1881
- name: header.name.name,
1882
- parties: [...parties.values()],
1883
- ports,
1884
- settlements,
1885
- title: header.title?.value ?? titleize(header.name.name),
1886
- },
1887
- };
1888
- }
1889
- function portFieldType(expr) {
1890
- if (expr.kind === "ident" && expr.name === "text")
1891
- return { kind: "text" };
1892
- if (expr.kind === "ident" && expr.name === "date")
1893
- return { kind: "date" };
1894
- if (expr.kind === "call" &&
1895
- expr.args.length === 1 &&
1896
- expr.args[0]?.kind === "ident") {
1897
- const argument = expr.args[0].name;
1898
- if (expr.callee.name === "id")
1899
- return { asset: argument, kind: "asset_id" };
1900
- if (expr.callee.name === "money") {
1901
- return { currency: argument, kind: "money" };
1902
- }
1903
- }
1904
- return undefined;
1905
- }
1906
- function titleize(snake) {
1907
- const spaced = snake.replaceAll("_", " ");
1908
- return spaced.charAt(0).toUpperCase() + spaced.slice(1);
1909
- }
1910
- //# sourceMappingURL=check.js.map