@hyperscale0/udl 1.0.0-beta.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +78 -25
  2. package/README.md +37 -99
  3. package/conformance/README.md +44 -41
  4. package/conformance/evolution/action-contract.expected.json +10 -0
  5. package/conformance/evolution/action-contract.live.udl +44 -0
  6. package/conformance/evolution/action-contract.next.udl +45 -0
  7. package/conformance/evolution/product-identity.expected.json +10 -0
  8. package/conformance/evolution/product-identity.live.udl +55 -0
  9. package/conformance/evolution/product-identity.next.udl +55 -0
  10. package/conformance/evolution/version-required.expected.json +10 -0
  11. package/conformance/evolution/version-required.live.udl +55 -0
  12. package/conformance/evolution/version-required.next.udl +58 -0
  13. package/conformance/invalid/action-without-transition.expected.json +10 -0
  14. package/conformance/invalid/{verb-without-transition.udl → action-without-transition.udl} +7 -2
  15. package/conformance/invalid/agent-description-too-long.expected.json +10 -0
  16. package/conformance/invalid/agent-description-too-long.udl +57 -0
  17. package/conformance/invalid/blank-title.expected.json +1 -1
  18. package/conformance/invalid/blank-title.udl +6 -2
  19. package/conformance/invalid/composition-dial-duplicate-key.expected.json +10 -0
  20. package/conformance/invalid/composition-dial-duplicate-key.udl +73 -0
  21. package/conformance/invalid/depth-budget.expected.json +1 -1
  22. package/conformance/invalid/duplicate-subject.expected.json +10 -0
  23. package/conformance/invalid/duplicate-subject.udl +230 -0
  24. package/conformance/invalid/forged-effects.expected.json +10 -0
  25. package/conformance/invalid/forged-effects.udl +76 -0
  26. package/conformance/invalid/format-version.expected.json +1 -1
  27. package/conformance/invalid/format-version.udl +6 -2
  28. package/conformance/invalid/instrument-id-not-snake-case.expected.json +10 -0
  29. package/conformance/invalid/{noun-id-not-snake-case.udl → instrument-id-not-snake-case.udl} +6 -2
  30. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +10 -0
  31. package/conformance/invalid/invalid-aggregate-gate-shape.udl +1819 -0
  32. package/conformance/invalid/invalid-check-duration.expected.json +10 -0
  33. package/conformance/invalid/invalid-check-duration.udl +219 -0
  34. package/conformance/invalid/invalid-dial-anchor.expected.json +10 -0
  35. package/conformance/invalid/invalid-dial-anchor.udl +219 -0
  36. package/conformance/invalid/invalid-exception-parent-ref.expected.json +10 -0
  37. package/conformance/invalid/invalid-exception-parent-ref.udl +2487 -0
  38. package/conformance/invalid/invalid-exposure-shape.expected.json +10 -0
  39. package/conformance/invalid/invalid-exposure-shape.udl +1819 -0
  40. package/conformance/invalid/invalid-remainder.expected.json +10 -0
  41. package/conformance/invalid/invalid-remainder.udl +220 -0
  42. package/conformance/invalid/invalid-schema-keyword.expected.json +10 -0
  43. package/conformance/invalid/invalid-schema-keyword.udl +220 -0
  44. package/conformance/invalid/invalid-utf8.expected.json +1 -1
  45. package/conformance/invalid/malformed-json.expected.json +1 -1
  46. package/conformance/invalid/missing-create-action.expected.json +10 -0
  47. package/conformance/invalid/{missing-create-verb.udl → missing-create-action.udl} +5 -2
  48. package/conformance/invalid/missing-exception-amount-field.expected.json +10 -0
  49. package/conformance/invalid/missing-exception-amount-field.udl +2487 -0
  50. package/conformance/invalid/missing-exception-contract.expected.json +14 -0
  51. package/conformance/invalid/missing-exception-contract.udl +2523 -0
  52. package/conformance/invalid/missing-exception-reason-field.expected.json +10 -0
  53. package/conformance/invalid/missing-exception-reason-field.udl +2487 -0
  54. package/conformance/invalid/not-an-object.expected.json +1 -1
  55. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +10 -0
  56. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +259 -0
  57. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +10 -0
  58. package/conformance/invalid/quote-freeze-set-incomplete.udl +261 -0
  59. package/conformance/invalid/quote-named-reference-gate.expected.json +10 -0
  60. package/conformance/invalid/quote-named-reference-gate.udl +50 -0
  61. package/conformance/invalid/reconcile-named-reference-gate.expected.json +10 -0
  62. package/conformance/invalid/reconcile-named-reference-gate.udl +50 -0
  63. package/conformance/invalid/unknown-key.expected.json +1 -1
  64. package/conformance/invalid/unknown-key.udl +6 -2
  65. package/conformance/invalid/unknown-reference-gate-field.expected.json +10 -0
  66. package/conformance/invalid/unknown-reference-gate-field.udl +1819 -0
  67. package/conformance/invalid/unknown-required-field.expected.json +10 -0
  68. package/conformance/invalid/unknown-required-field.udl +220 -0
  69. package/conformance/invalid/unreachable-state.expected.json +2 -2
  70. package/conformance/invalid/unreachable-state.udl +6 -2
  71. package/conformance/invalid/wrong-exception-amount-field.expected.json +10 -0
  72. package/conformance/invalid/wrong-exception-amount-field.udl +2487 -0
  73. package/conformance/invalid/wrong-exception-reason-field.expected.json +10 -0
  74. package/conformance/invalid/wrong-exception-reason-field.udl +2487 -0
  75. package/conformance/valid/agent-description.expected.json +6 -0
  76. package/conformance/valid/agent-description.udl +66 -0
  77. package/conformance/valid/cards.expected.json +1 -0
  78. package/conformance/valid/cards.udl +843 -356
  79. package/conformance/valid/commerce-escrow.expected.json +1 -0
  80. package/conformance/valid/commerce-escrow.udl +792 -291
  81. package/conformance/valid/compiled-crowdfunding.expected.json +6 -0
  82. package/conformance/valid/compiled-crowdfunding.udl +1843 -0
  83. package/conformance/valid/compiled-watch-club.expected.json +6 -0
  84. package/conformance/valid/compiled-watch-club.udl +2486 -0
  85. package/conformance/valid/complete-contract.expected.json +6 -0
  86. package/conformance/valid/complete-contract.udl +218 -0
  87. package/conformance/valid/effect-signatures.expected.json +6 -0
  88. package/conformance/valid/effect-signatures.udl +75 -0
  89. package/conformance/valid/hand-edited.expected.json +1 -0
  90. package/conformance/valid/hand-edited.udl +1 -1
  91. package/conformance/valid/insured-car-marketplace.expected.json +1 -0
  92. package/conformance/valid/insured-car-marketplace.udl +534 -315
  93. package/conformance/valid/insured-travel.expected.json +2 -1
  94. package/conformance/valid/insured-travel.udl +1489 -641
  95. package/conformance/valid/minimal.expected.json +1 -0
  96. package/conformance/valid/minimal.udl +18 -14
  97. package/conformance/valid/protection.expected.json +1 -0
  98. package/conformance/valid/protection.udl +719 -304
  99. package/conformance/valid/string-escaping.expected.json +6 -0
  100. package/conformance/valid/string-escaping.udl +54 -0
  101. package/dist/canonical.d.ts +2 -0
  102. package/dist/canonical.d.ts.map +1 -1
  103. package/dist/canonical.js +7 -0
  104. package/dist/canonical.js.map +1 -1
  105. package/dist/check-profiles.d.ts +57 -0
  106. package/dist/check-profiles.d.ts.map +1 -0
  107. package/dist/check-profiles.js +62 -0
  108. package/dist/check-profiles.js.map +1 -0
  109. package/dist/cli.js +48 -4
  110. package/dist/cli.js.map +1 -1
  111. package/dist/diagnostics.d.ts +168 -0
  112. package/dist/diagnostics.d.ts.map +1 -0
  113. package/dist/diagnostics.js +164 -0
  114. package/dist/diagnostics.js.map +1 -0
  115. package/dist/effects.d.ts +41 -0
  116. package/dist/effects.d.ts.map +1 -0
  117. package/dist/effects.js +110 -0
  118. package/dist/effects.js.map +1 -0
  119. package/dist/evolution.d.ts +51 -18
  120. package/dist/evolution.d.ts.map +1 -1
  121. package/dist/evolution.js +244 -102
  122. package/dist/evolution.js.map +1 -1
  123. package/dist/finance.d.ts +17 -13
  124. package/dist/finance.d.ts.map +1 -1
  125. package/dist/finance.js +165 -109
  126. package/dist/finance.js.map +1 -1
  127. package/dist/index.d.ts +12 -8
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +8 -5
  130. package/dist/index.js.map +1 -1
  131. package/dist/limits.d.ts +2 -2
  132. package/dist/limits.d.ts.map +1 -1
  133. package/dist/limits.js +4 -3
  134. package/dist/limits.js.map +1 -1
  135. package/dist/parser.d.ts.map +1 -1
  136. package/dist/parser.js +6 -16
  137. package/dist/parser.js.map +1 -1
  138. package/dist/schema.d.ts +2059 -135
  139. package/dist/schema.d.ts.map +1 -1
  140. package/dist/schema.js +759 -73
  141. package/dist/schema.js.map +1 -1
  142. package/dist/validation.d.ts +22 -9
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +1463 -457
  145. package/dist/validation.js.map +1 -1
  146. package/docs/README.md +14 -0
  147. package/docs/guide/01-a-document.md +37 -0
  148. package/docs/guide/02-money-steps.md +23 -0
  149. package/docs/guide/03-laws.md +18 -0
  150. package/docs/guide/04-fees-and-remainder.md +15 -0
  151. package/docs/guide/05-checks-updates-dials.md +7 -0
  152. package/docs/guide/06-effects.md +11 -0
  153. package/docs/guide/07-evolution.md +11 -0
  154. package/docs/guide/08-implementing.md +13 -0
  155. package/docs/llms-full.txt +1241 -0
  156. package/docs/llms.txt +14 -0
  157. package/docs/reference/canonical.md +16 -0
  158. package/docs/reference/clauses.md +986 -0
  159. package/docs/reference/cli.md +22 -0
  160. package/docs/reference/diagnostics.md +32 -0
  161. package/package.json +6 -2
  162. package/skills/udl/SKILL.md +28 -0
  163. package/spec/README.md +82 -51
  164. package/spec/udl.schema.json +1191 -119
  165. package/src/canonical.ts +11 -0
  166. package/src/check-profiles.ts +80 -0
  167. package/src/cli.ts +49 -3
  168. package/src/diagnostics.ts +221 -0
  169. package/src/effects.ts +163 -0
  170. package/src/evolution.ts +375 -125
  171. package/src/finance.ts +255 -160
  172. package/src/index.ts +50 -14
  173. package/src/limits.ts +4 -3
  174. package/src/parser.ts +15 -18
  175. package/src/schema.ts +855 -81
  176. package/src/validation.ts +2666 -560
  177. package/conformance/invalid/missing-create-verb.expected.json +0 -10
  178. package/conformance/invalid/noun-id-not-snake-case.expected.json +0 -10
  179. package/conformance/invalid/verb-without-transition.expected.json +0 -10
package/src/canonical.ts CHANGED
@@ -7,6 +7,17 @@ export function serializeUdl(value: unknown): string {
7
7
  return `${writeJson(document, 0)}\n`;
8
8
  }
9
9
 
10
+ /** SHA-256 over the canonical UTF-8 bytes, encoded as lowercase hexadecimal. */
11
+ export async function canonicalDigest(value: unknown): Promise<string> {
12
+ const digest = await globalThis.crypto.subtle.digest(
13
+ "SHA-256",
14
+ new TextEncoder().encode(serializeUdl(value)),
15
+ );
16
+ return [...new Uint8Array(digest)]
17
+ .map((byte) => byte.toString(16).padStart(2, "0"))
18
+ .join("");
19
+ }
20
+
10
21
  function writeJson(value: unknown, depth: number): string {
11
22
  if (value === null || typeof value !== "object") {
12
23
  const encoded = JSON.stringify(value);
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Evidence profiles that an instrument action may require before it runs.
3
+ * The public grammar and the runtime both read this table. A pair absent here
4
+ * cannot gate an action.
5
+ */
6
+ export interface UdlCheckEvidenceProfile {
7
+ readonly family: string;
8
+ readonly checkKind: string;
9
+ readonly kindField?: string;
10
+ readonly statusField: string;
11
+ readonly statuses: readonly string[];
12
+ }
13
+
14
+ export const udlCheckEvidenceProfiles = [
15
+ {
16
+ family: "national_identity",
17
+ checkKind: "identity_verification",
18
+ kindField: "checkKind",
19
+ statusField: "verificationStatus",
20
+ statuses: [
21
+ "requested",
22
+ "waiting",
23
+ "completed",
24
+ "rejected",
25
+ "expired",
26
+ "error",
27
+ ],
28
+ },
29
+ {
30
+ family: "national_identity",
31
+ checkKind: "contact_ownership_verification",
32
+ kindField: "checkKind",
33
+ statusField: "matchResult",
34
+ statuses: ["match", "no_match"],
35
+ },
36
+ {
37
+ family: "credit_bureau",
38
+ checkKind: "consumer_inquiry",
39
+ kindField: "inquiryType",
40
+ statusField: "bureauStatus",
41
+ statuses: ["returned", "acknowledged", "correction_required"],
42
+ },
43
+ {
44
+ family: "credit_bureau",
45
+ checkKind: "commercial_inquiry",
46
+ kindField: "inquiryType",
47
+ statusField: "bureauStatus",
48
+ statuses: ["returned", "acknowledged", "correction_required"],
49
+ },
50
+ {
51
+ family: "credit_bureau",
52
+ checkKind: "facility_report",
53
+ kindField: "inquiryType",
54
+ statusField: "bureauStatus",
55
+ statuses: ["returned", "acknowledged", "correction_required"],
56
+ },
57
+ {
58
+ family: "sanctions_screening",
59
+ checkKind: "screen",
60
+ kindField: "checkKind",
61
+ statusField: "screeningStatus",
62
+ statuses: ["clear", "hit", "manual_review"],
63
+ },
64
+ {
65
+ family: "enforcement_instrument",
66
+ checkKind: "note_request",
67
+ statusField: "requestStatus",
68
+ statuses: ["debtor_approved", "debtor_rejected", "auto_cancelled"],
69
+ },
70
+ ] as const satisfies readonly UdlCheckEvidenceProfile[];
71
+
72
+ /** Returns the tenant-gateable evidence profile for one family and check. */
73
+ export function udlCheckEvidenceProfile(
74
+ family: string,
75
+ checkKind: string,
76
+ ): UdlCheckEvidenceProfile | undefined {
77
+ return udlCheckEvidenceProfiles.find(
78
+ (profile) => profile.family === family && profile.checkKind === checkKind,
79
+ );
80
+ }
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * The `udl` command. Three verbs over the same library the compiler uses:
3
+ * The `udl` command. Actions over the same library the compiler uses:
4
4
  * `validate` answers whether a document is admissible, `fmt` writes the
5
5
  * canonical bytes, and `diff` answers whether a change is legal against a
6
6
  * document that already has live instances.
@@ -9,10 +9,12 @@ import { readFile, writeFile } from "node:fs/promises";
9
9
 
10
10
  import {
11
11
  canonicalizeUdl,
12
+ canonicalDigest,
12
13
  diffValidatedUdlEvolution,
13
14
  parseUdl,
14
15
  UdlError,
15
16
  type UdlDocument,
17
+ udlDiagnostic,
16
18
  } from "./index.js";
17
19
 
18
20
  const USAGE = `udl - the Universal Domain Language toolchain
@@ -20,7 +22,9 @@ const USAGE = `udl - the Universal Domain Language toolchain
20
22
  usage:
21
23
  udl validate <file> parse <file> and report every issue found
22
24
  udl fmt <file> [--write] print the canonical form, or rewrite the file
25
+ udl canon <file> [--digest] print canonical bytes, or their SHA-256 digest
23
26
  udl diff <live> <next> check <next> against the append-only law
27
+ udl explain UDL#### explain one stable diagnostic code
24
28
  udl help print this
25
29
 
26
30
  exit codes:
@@ -40,8 +44,12 @@ async function main(argv: readonly string[]): Promise<number> {
40
44
  return validateCommand(rest);
41
45
  case "fmt":
42
46
  return formatCommand(rest);
47
+ case "canon":
48
+ return canonCommand(rest);
43
49
  case "diff":
44
50
  return diffCommand(rest);
51
+ case "explain":
52
+ return explainCommand(rest);
45
53
  case undefined:
46
54
  return usageError("udl: no command given");
47
55
  default:
@@ -49,6 +57,41 @@ async function main(argv: readonly string[]): Promise<number> {
49
57
  }
50
58
  }
51
59
 
60
+ async function canonCommand(args: readonly string[]): Promise<number> {
61
+ const digest = args.includes("--digest");
62
+ const [file, ...extra] = args.filter((argument) => argument !== "--digest");
63
+ if (file === undefined || extra.length > 0) {
64
+ return usageError("usage: udl canon <file> [--digest]");
65
+ }
66
+ const bytes = await readDocument(file);
67
+ if (bytes === null) return 2;
68
+ let document: UdlDocument;
69
+ try {
70
+ document = parseUdl(bytes);
71
+ } catch (error) {
72
+ return reportIssues(file, error);
73
+ }
74
+ if (digest) console.log(await canonicalDigest(document));
75
+ else process.stdout.write(canonicalizeUdl(bytes));
76
+ return 0;
77
+ }
78
+
79
+ function explainCommand(args: readonly string[]): number {
80
+ const [code, ...extra] = args;
81
+ if (code === undefined || extra.length > 0) {
82
+ return usageError("usage: udl explain UDL####");
83
+ }
84
+ const diagnostic = udlDiagnostic(code);
85
+ if (!diagnostic) {
86
+ return usageError(`udl: unknown diagnostic code ${code}`);
87
+ }
88
+ console.log(`${code} ${diagnostic.title}`);
89
+ console.log(`category: ${diagnostic.category}`);
90
+ console.log(`family: ${diagnostic.family}`);
91
+ console.log(`fix: ${diagnostic.fix}`);
92
+ return 0;
93
+ }
94
+
52
95
  async function validateCommand(args: readonly string[]): Promise<number> {
53
96
  const [file, ...extra] = args;
54
97
  if (file === undefined || extra.length > 0) {
@@ -108,8 +151,11 @@ async function diffCommand(args: readonly string[]): Promise<number> {
108
151
  console.log(`${nextPath}: additive`);
109
152
  return 0;
110
153
  }
111
- for (const violation of violations)
112
- console.error(`${nextPath}: ${violation}`);
154
+ for (const violation of violations) {
155
+ console.error(
156
+ `${nextPath}: ${violation.code} ${violation.path}: ${violation.message}`,
157
+ );
158
+ }
113
159
  return 1;
114
160
  }
115
161
 
@@ -0,0 +1,221 @@
1
+ export type UdlIssueCategory =
2
+ | "invalid_evolution"
3
+ | "invalid_json"
4
+ | "invalid_semantics"
5
+ | "invalid_shape"
6
+ | "invalid_utf8"
7
+ | "resource_limit";
8
+
9
+ export type UdlDiagnosticFamily =
10
+ | "admission"
11
+ | "document"
12
+ | "evolution"
13
+ | "finance"
14
+ | "gates"
15
+ | "lifecycle"
16
+ | "schema";
17
+
18
+ const diagnosticDefinitions = {
19
+ UDL1001: {
20
+ category: "invalid_utf8",
21
+ family: "admission",
22
+ title: "Invalid UTF-8",
23
+ fix: "Encode the document as valid UTF-8.",
24
+ },
25
+ UDL1002: {
26
+ category: "invalid_json",
27
+ family: "admission",
28
+ title: "Invalid JSON",
29
+ fix: "Repair the JSON syntax before validation.",
30
+ },
31
+ UDL1003: {
32
+ category: "invalid_shape",
33
+ family: "admission",
34
+ title: "Invalid document shape",
35
+ fix: "Match the published UDL JSON Schema.",
36
+ },
37
+ UDL1004: {
38
+ category: "resource_limit",
39
+ family: "admission",
40
+ title: "Resource limit exceeded",
41
+ fix: "Reduce the source size, nesting, values, strings, references, or financial paths named by the message.",
42
+ },
43
+
44
+ UDL2001: {
45
+ category: "invalid_semantics",
46
+ family: "document",
47
+ title: "Duplicate declaration",
48
+ fix: "Give each declaration a unique name.",
49
+ },
50
+ UDL2002: {
51
+ category: "invalid_semantics",
52
+ family: "document",
53
+ title: "Document law violation",
54
+ fix: "Repair the declaration, subject contract, or derived effects named by the message.",
55
+ },
56
+ UDL2005: {
57
+ category: "invalid_semantics",
58
+ family: "document",
59
+ title: "Derived effects mismatch",
60
+ fix: "Regenerate the action effects from its clauses.",
61
+ },
62
+
63
+ UDL3001: {
64
+ category: "invalid_semantics",
65
+ family: "lifecycle",
66
+ title: "Lifecycle is not closed",
67
+ fix: "Declare every state and action transition, and make every state reachable.",
68
+ },
69
+
70
+ UDL4001: {
71
+ category: "invalid_semantics",
72
+ family: "finance",
73
+ title: "Money graph violation",
74
+ fix: "Balance every funded amount and close every hold on each lifecycle path.",
75
+ },
76
+
77
+ UDL5001: {
78
+ category: "invalid_semantics",
79
+ family: "gates",
80
+ title: "Reference gate violation",
81
+ fix: "Point the gate at a declared instrument, action, state, field, and reference.",
82
+ },
83
+ UDL5002: {
84
+ category: "invalid_semantics",
85
+ family: "gates",
86
+ title: "Check requirement violation",
87
+ fix: "Use a declared check with compatible evidence and recurrence.",
88
+ },
89
+ UDL5003: {
90
+ category: "invalid_semantics",
91
+ family: "gates",
92
+ title: "Exposure gate violation",
93
+ fix: "Use declared account and money fields for the exposure gate.",
94
+ },
95
+ UDL5004: {
96
+ category: "invalid_semantics",
97
+ family: "gates",
98
+ title: "Aggregate law violation",
99
+ fix: "Point the aggregate at compatible parent and child fields.",
100
+ },
101
+ UDL5005: {
102
+ category: "invalid_semantics",
103
+ family: "gates",
104
+ title: "Settlement or payout violation",
105
+ fix: "Use a declared settlement account and a compatible payout statement line.",
106
+ },
107
+ UDL5006: {
108
+ category: "invalid_semantics",
109
+ family: "gates",
110
+ title: "Quote and commit violation",
111
+ fix: "Declare one complete quote freeze set and one matching commit action.",
112
+ },
113
+ UDL5007: {
114
+ category: "invalid_semantics",
115
+ family: "gates",
116
+ title: "Reconcile exception child violation",
117
+ fix: "Name a declared child whose reference points back to this instrument.",
118
+ },
119
+ UDL5008: {
120
+ category: "invalid_semantics",
121
+ family: "gates",
122
+ title: "Action clause violation",
123
+ fix: "Repair the clause fields and keep incompatible clauses separate.",
124
+ },
125
+ UDL5009: {
126
+ category: "invalid_semantics",
127
+ family: "gates",
128
+ title: "Reconcile exception amount field is missing or optional",
129
+ fix: "Name the exception child's required money field in amountField.",
130
+ },
131
+ UDL5010: {
132
+ category: "invalid_semantics",
133
+ family: "gates",
134
+ title: "Reconcile exception amount field has the wrong type",
135
+ fix: "Point amountField at a money field declared by the exception child.",
136
+ },
137
+ UDL5011: {
138
+ category: "invalid_semantics",
139
+ family: "gates",
140
+ title: "Reconcile exception reason field is missing or optional",
141
+ fix: "Name the exception child's required text field in reasonField.",
142
+ },
143
+ UDL5012: {
144
+ category: "invalid_semantics",
145
+ family: "gates",
146
+ title: "Reconcile exception reason field has the wrong type",
147
+ fix: "Point reasonField at a required plain text field declared by the exception child.",
148
+ },
149
+
150
+ UDL6001: {
151
+ category: "invalid_semantics",
152
+ family: "schema",
153
+ title: "Unsupported JSON Schema",
154
+ fix: "Use only the sealed UDL JSON Schema subset.",
155
+ },
156
+
157
+ UDL7001: {
158
+ category: "invalid_evolution",
159
+ family: "evolution",
160
+ title: "Stored contract changed",
161
+ fix: "Keep stored identities and contracts unchanged, and add only allowed optional declarations.",
162
+ },
163
+ UDL7002: {
164
+ category: "invalid_evolution",
165
+ family: "evolution",
166
+ title: "Version was not increased",
167
+ fix: "Increase the product version for every semantic change.",
168
+ },
169
+ } as const satisfies Record<
170
+ string,
171
+ {
172
+ readonly category: UdlIssueCategory;
173
+ readonly family: UdlDiagnosticFamily;
174
+ readonly fix: string;
175
+ readonly title: string;
176
+ }
177
+ >;
178
+
179
+ export type UdlIssueCode = keyof typeof diagnosticDefinitions;
180
+
181
+ export interface UdlDiagnostic {
182
+ readonly category: UdlIssueCategory;
183
+ readonly code: UdlIssueCode;
184
+ readonly family: UdlDiagnosticFamily;
185
+ readonly fix: string;
186
+ readonly title: string;
187
+ }
188
+
189
+ export const udlDiagnostics: readonly UdlDiagnostic[] = Object.entries(
190
+ diagnosticDefinitions,
191
+ ).map(([code, diagnostic]) => ({
192
+ code: code as UdlIssueCode,
193
+ ...diagnostic,
194
+ }));
195
+
196
+ export function udlDiagnostic(code: string): UdlDiagnostic | undefined {
197
+ return udlDiagnostics.find((diagnostic) => diagnostic.code === code);
198
+ }
199
+
200
+ export interface UdlIssue {
201
+ readonly category: UdlIssueCategory;
202
+ readonly code: UdlIssueCode;
203
+ readonly fix: string;
204
+ readonly message: string;
205
+ readonly path: string;
206
+ }
207
+
208
+ export function issue(
209
+ code: UdlIssueCode,
210
+ path: string,
211
+ messageDetail?: string,
212
+ ): UdlIssue {
213
+ const diagnostic = diagnosticDefinitions[code];
214
+ return {
215
+ category: diagnostic.category,
216
+ code,
217
+ fix: diagnostic.fix,
218
+ message: messageDetail ?? diagnostic.title,
219
+ path,
220
+ };
221
+ }
package/src/effects.ts ADDED
@@ -0,0 +1,163 @@
1
+ export const udlEffectKinds = [
2
+ "decides",
3
+ "holds",
4
+ "moves",
5
+ "notifies",
6
+ "reads",
7
+ "schedules",
8
+ ] as const;
9
+
10
+ export type UdlEffectKind = (typeof udlEffectKinds)[number];
11
+
12
+ type EffectDescriptor = {
13
+ readonly kind: UdlEffectKind;
14
+ readonly per: "clause" | "element";
15
+ readonly signature:
16
+ | { readonly fixed: string }
17
+ | { readonly fromField: string }
18
+ | { readonly movementClass: true };
19
+ };
20
+
21
+ type ActionClauseDescriptor = {
22
+ readonly effects?: readonly EffectDescriptor[];
23
+ readonly scope: "action" | "instrument";
24
+ readonly target: string;
25
+ };
26
+
27
+ type EffectRow = {
28
+ readonly channel?: string;
29
+ readonly role?: string;
30
+ readonly signature: string;
31
+ readonly source: string;
32
+ };
33
+
34
+ export type DerivedUdlEffects = Partial<
35
+ Readonly<Record<UdlEffectKind, readonly EffectRow[]>>
36
+ >;
37
+
38
+ export type UdlMovementClass =
39
+ | "collection.pay_in"
40
+ | "deposit.attributed"
41
+ | "payout.external"
42
+ | "transfer.internal";
43
+
44
+ interface Movement {
45
+ readonly bind?: Readonly<Record<string, unknown>>;
46
+ readonly operation: string;
47
+ }
48
+
49
+ function boundPath(move: Movement, endpoint: string): string | undefined {
50
+ const binding = move.bind?.[endpoint];
51
+ if (binding === null || typeof binding !== "object") return undefined;
52
+ const path = (binding as { readonly path?: unknown }).path;
53
+ return typeof path === "string" ? path : undefined;
54
+ }
55
+
56
+ /**
57
+ * Maps a UDL money operation to the meter and pricing class that owns it.
58
+ * Internal-transfer endpoint bindings are product accounts by construction.
59
+ * External collection, deposit, and payout operations carry their role in the
60
+ * operation family because their remote endpoint is not a UDL account binding.
61
+ */
62
+ export function movementClass(move: Movement): UdlMovementClass {
63
+ if (move.operation.startsWith("internal_transfer.")) {
64
+ const source = boundPath(move, "sourceAccountId");
65
+ const destination = boundPath(move, "destinationAccountId");
66
+ if (
67
+ source?.startsWith("fields.") &&
68
+ destination === "refs.escrowAccountId"
69
+ ) {
70
+ return "collection.pay_in";
71
+ }
72
+ return "transfer.internal";
73
+ }
74
+ if (move.operation.startsWith("collection.pay_in.")) {
75
+ return "collection.pay_in";
76
+ }
77
+ if (move.operation.startsWith("deposit.")) {
78
+ return "deposit.attributed";
79
+ }
80
+ if (move.operation.startsWith("payout.")) {
81
+ return "payout.external";
82
+ }
83
+ throw new Error(`cannot classify UDL movement operation ${move.operation}`);
84
+ }
85
+
86
+ /** Derive the ABI effect rows declared by the action-clause vocabulary. */
87
+ export function deriveUdlActionEffects(
88
+ action: Readonly<Record<string, unknown>>,
89
+ vocabulary: readonly ActionClauseDescriptor[],
90
+ ): DerivedUdlEffects {
91
+ const effects: Partial<Record<UdlEffectKind, EffectRow[]>> = {};
92
+ for (const clause of vocabulary) {
93
+ if (clause.scope !== "action" || !clause.effects) continue;
94
+ const value = actionClauseValue(action, clause.target);
95
+ if (value === undefined) continue;
96
+ for (const descriptor of clause.effects) {
97
+ const values =
98
+ descriptor.per === "element" && Array.isArray(value) ? value : [value];
99
+ for (const [index, candidate] of values.entries()) {
100
+ const object = recordValue(candidate);
101
+ if (
102
+ descriptor.kind === "holds" &&
103
+ "fixed" in descriptor.signature &&
104
+ descriptor.signature.fixed === "reserve" &&
105
+ object?.operation !== "internal_transfer.reserve"
106
+ ) {
107
+ continue;
108
+ }
109
+ const suffix = effectSignatureSuffix(descriptor, object);
110
+ if (!suffix) continue;
111
+ (effects[descriptor.kind] ??= []).push({
112
+ signature: `${descriptor.kind}.${suffix}`,
113
+ source:
114
+ descriptor.per === "element"
115
+ ? `${clause.target}[${index}]`
116
+ : clause.target,
117
+ ...(descriptor.kind === "notifies" &&
118
+ typeof object?.channel === "string" &&
119
+ typeof object.role === "string"
120
+ ? { channel: object.channel, role: object.role }
121
+ : {}),
122
+ });
123
+ }
124
+ }
125
+ }
126
+ return effects;
127
+ }
128
+
129
+ function actionClauseValue(
130
+ action: Readonly<Record<string, unknown>>,
131
+ target: string,
132
+ ): unknown {
133
+ const [head, tail] = target.split(".");
134
+ if (!head) return undefined;
135
+ const value = action[head];
136
+ if (!tail) return value;
137
+ return recordValue(value)?.[tail];
138
+ }
139
+
140
+ function effectSignatureSuffix(
141
+ descriptor: EffectDescriptor,
142
+ value: Readonly<Record<string, unknown>> | undefined,
143
+ ): string | undefined {
144
+ if ("fixed" in descriptor.signature) return descriptor.signature.fixed;
145
+ if ("fromField" in descriptor.signature) {
146
+ const field = value?.[descriptor.signature.fromField];
147
+ return typeof field === "string" ? field : undefined;
148
+ }
149
+ if (!value || !("movementClass" in descriptor.signature)) return undefined;
150
+ const operation = value.operation;
151
+ const bind = recordValue(value.bind);
152
+ return typeof operation === "string"
153
+ ? movementClass({ ...(bind ? { bind } : {}), operation })
154
+ : undefined;
155
+ }
156
+
157
+ function recordValue(
158
+ value: unknown,
159
+ ): Readonly<Record<string, unknown>> | undefined {
160
+ return value !== null && typeof value === "object" && !Array.isArray(value)
161
+ ? (value as Readonly<Record<string, unknown>>)
162
+ : undefined;
163
+ }