@hyperscale0/udl 1.0.0 → 1.0.124

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 +5 -216
  2. package/LICENSE.md +356 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +14 -106
  5. package/dist/canonical.d.ts.map +1 -1
  6. package/dist/canonical.js +1 -29
  7. package/dist/canonical.js.map +1 -1
  8. package/dist/diagnostics.d.ts +13 -165
  9. package/dist/diagnostics.d.ts.map +1 -1
  10. package/dist/diagnostics.js +33 -157
  11. package/dist/diagnostics.js.map +1 -1
  12. package/dist/field-value.d.ts +5 -0
  13. package/dist/field-value.d.ts.map +1 -0
  14. package/dist/field-value.js +85 -0
  15. package/dist/field-value.js.map +1 -0
  16. package/dist/finance.d.ts +10 -53
  17. package/dist/finance.d.ts.map +1 -1
  18. package/dist/finance.js +252 -536
  19. package/dist/finance.js.map +1 -1
  20. package/dist/index.d.ts +9 -13
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/json.d.ts +2 -0
  25. package/dist/json.d.ts.map +1 -0
  26. package/dist/json.js +30 -0
  27. package/dist/json.js.map +1 -0
  28. package/dist/limits.d.ts +3 -28
  29. package/dist/limits.d.ts.map +1 -1
  30. package/dist/limits.js +5 -31
  31. package/dist/limits.js.map +1 -1
  32. package/dist/object-contract.d.ts +140 -0
  33. package/dist/object-contract.d.ts.map +1 -0
  34. package/dist/object-contract.js +314 -0
  35. package/dist/object-contract.js.map +1 -0
  36. package/dist/object-semantics-schema.d.ts +5710 -0
  37. package/dist/object-semantics-schema.d.ts.map +1 -0
  38. package/dist/object-semantics-schema.js +119 -0
  39. package/dist/object-semantics-schema.js.map +1 -0
  40. package/dist/object-semantics.d.ts +6 -0
  41. package/dist/object-semantics.d.ts.map +1 -0
  42. package/dist/object-semantics.js +279 -0
  43. package/dist/object-semantics.js.map +1 -0
  44. package/dist/object-structures.d.ts +4 -0
  45. package/dist/object-structures.d.ts.map +1 -0
  46. package/dist/object-structures.js +134 -0
  47. package/dist/object-structures.js.map +1 -0
  48. package/dist/reporting-validation.d.ts +8 -0
  49. package/dist/reporting-validation.d.ts.map +1 -0
  50. package/dist/reporting-validation.js +252 -0
  51. package/dist/reporting-validation.js.map +1 -0
  52. package/dist/reporting.d.ts +477 -0
  53. package/dist/reporting.d.ts.map +1 -0
  54. package/dist/reporting.js +169 -0
  55. package/dist/reporting.js.map +1 -0
  56. package/dist/schema.d.ts +5572 -2710
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +444 -1121
  59. package/dist/schema.js.map +1 -1
  60. package/dist/validation.d.ts +10 -73
  61. package/dist/validation.d.ts.map +1 -1
  62. package/dist/validation.js +1109 -2569
  63. package/dist/validation.js.map +1 -1
  64. package/docs/assets/brand/manifest.json +30 -0
  65. package/docs/assets/brand/udl-horizontal-white.svg +1 -0
  66. package/docs/assets/brand/udl-horizontal.svg +1 -0
  67. package/docs/assets/brand/udl-stacked-white.svg +1 -0
  68. package/docs/assets/brand/udl-stacked.svg +1 -0
  69. package/docs/assets/udl.svg +11 -0
  70. package/package.json +13 -22
  71. package/spec/README.md +300 -172
  72. package/spec/cars.udl.json +249 -0
  73. package/spec/udl.schema.json +3066 -2242
  74. package/src/canonical.ts +1 -35
  75. package/src/diagnostics.ts +76 -209
  76. package/src/field-value.ts +87 -0
  77. package/src/finance.ts +327 -832
  78. package/src/index.ts +18 -95
  79. package/src/json.ts +34 -0
  80. package/src/limits.ts +5 -31
  81. package/src/object-contract.ts +559 -0
  82. package/src/object-semantics-schema.ts +148 -0
  83. package/src/object-semantics.ts +378 -0
  84. package/src/object-structures.ts +255 -0
  85. package/src/reporting-validation.ts +383 -0
  86. package/src/reporting.ts +178 -0
  87. package/src/schema.ts +510 -1277
  88. package/src/validation.ts +1630 -4545
  89. package/LICENSE +0 -661
  90. package/LICENSING.md +0 -51
  91. package/TRADEMARKS.md +0 -35
  92. package/conformance/README.md +0 -82
  93. package/conformance/evolution/action-contract.expected.json +0 -10
  94. package/conformance/evolution/action-contract.live.udl +0 -44
  95. package/conformance/evolution/action-contract.next.udl +0 -45
  96. package/conformance/evolution/product-identity.expected.json +0 -10
  97. package/conformance/evolution/product-identity.live.udl +0 -55
  98. package/conformance/evolution/product-identity.next.udl +0 -55
  99. package/conformance/evolution/version-required.expected.json +0 -10
  100. package/conformance/evolution/version-required.live.udl +0 -55
  101. package/conformance/evolution/version-required.next.udl +0 -58
  102. package/conformance/invalid/action-without-transition.expected.json +0 -10
  103. package/conformance/invalid/action-without-transition.udl +0 -60
  104. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  105. package/conformance/invalid/agent-description-too-long.udl +0 -57
  106. package/conformance/invalid/blank-title.expected.json +0 -10
  107. package/conformance/invalid/blank-title.udl +0 -54
  108. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  109. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  110. package/conformance/invalid/depth-budget.expected.json +0 -10
  111. package/conformance/invalid/depth-budget.udl +0 -49
  112. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  113. package/conformance/invalid/duplicate-subject.udl +0 -230
  114. package/conformance/invalid/forged-effects.expected.json +0 -10
  115. package/conformance/invalid/forged-effects.udl +0 -76
  116. package/conformance/invalid/format-version.expected.json +0 -10
  117. package/conformance/invalid/format-version.udl +0 -54
  118. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  119. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  120. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  121. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  122. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  123. package/conformance/invalid/invalid-check-duration.udl +0 -219
  124. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  125. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  126. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  127. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  128. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  129. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  130. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  131. package/conformance/invalid/invalid-remainder.udl +0 -220
  132. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  133. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  134. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  135. package/conformance/invalid/invalid-utf8.udl +0 -1
  136. package/conformance/invalid/malformed-json.expected.json +0 -10
  137. package/conformance/invalid/malformed-json.udl +0 -1
  138. package/conformance/invalid/missing-create-action.expected.json +0 -10
  139. package/conformance/invalid/missing-create-action.udl +0 -48
  140. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  141. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  142. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  143. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  144. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  145. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  146. package/conformance/invalid/not-an-object.expected.json +0 -10
  147. package/conformance/invalid/not-an-object.udl +0 -1
  148. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  149. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  150. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  151. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  152. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  153. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  154. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  155. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  156. package/conformance/invalid/unknown-key.expected.json +0 -10
  157. package/conformance/invalid/unknown-key.udl +0 -55
  158. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  159. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  160. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  161. package/conformance/invalid/unknown-required-field.udl +0 -220
  162. package/conformance/invalid/unreachable-state.expected.json +0 -10
  163. package/conformance/invalid/unreachable-state.udl +0 -55
  164. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  165. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  166. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  167. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  168. package/conformance/valid/agent-description.expected.json +0 -6
  169. package/conformance/valid/agent-description.udl +0 -66
  170. package/conformance/valid/cards.expected.json +0 -6
  171. package/conformance/valid/cards.udl +0 -1579
  172. package/conformance/valid/commerce-escrow.expected.json +0 -6
  173. package/conformance/valid/commerce-escrow.udl +0 -1512
  174. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  175. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  176. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  177. package/conformance/valid/compiled-watch-club.udl +0 -2486
  178. package/conformance/valid/complete-contract.expected.json +0 -6
  179. package/conformance/valid/complete-contract.udl +0 -218
  180. package/conformance/valid/effect-signatures.expected.json +0 -6
  181. package/conformance/valid/effect-signatures.udl +0 -75
  182. package/conformance/valid/hand-edited.expected.json +0 -6
  183. package/conformance/valid/hand-edited.udl +0 -1
  184. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  185. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  186. package/conformance/valid/insured-travel.expected.json +0 -6
  187. package/conformance/valid/insured-travel.udl +0 -3469
  188. package/conformance/valid/minimal.expected.json +0 -6
  189. package/conformance/valid/minimal.udl +0 -54
  190. package/conformance/valid/protection.expected.json +0 -6
  191. package/conformance/valid/protection.udl +0 -1551
  192. package/conformance/valid/string-escaping.expected.json +0 -6
  193. package/conformance/valid/string-escaping.udl +0 -54
  194. package/dist/check-profiles.d.ts +0 -57
  195. package/dist/check-profiles.d.ts.map +0 -1
  196. package/dist/check-profiles.js +0 -62
  197. package/dist/check-profiles.js.map +0 -1
  198. package/dist/cli.d.ts +0 -3
  199. package/dist/cli.d.ts.map +0 -1
  200. package/dist/cli.js +0 -184
  201. package/dist/cli.js.map +0 -1
  202. package/dist/effects.d.ts +0 -41
  203. package/dist/effects.d.ts.map +0 -1
  204. package/dist/effects.js +0 -110
  205. package/dist/effects.js.map +0 -1
  206. package/dist/evolution.d.ts +0 -120
  207. package/dist/evolution.d.ts.map +0 -1
  208. package/dist/evolution.js +0 -567
  209. package/dist/evolution.js.map +0 -1
  210. package/dist/parser.d.ts +0 -4
  211. package/dist/parser.d.ts.map +0 -1
  212. package/dist/parser.js +0 -44
  213. package/dist/parser.js.map +0 -1
  214. package/docs/README.md +0 -14
  215. package/docs/guide/01-a-document.md +0 -37
  216. package/docs/guide/02-money-steps.md +0 -23
  217. package/docs/guide/03-laws.md +0 -18
  218. package/docs/guide/04-fees-and-remainder.md +0 -15
  219. package/docs/guide/05-checks-updates-dials.md +0 -7
  220. package/docs/guide/06-effects.md +0 -11
  221. package/docs/guide/07-evolution.md +0 -11
  222. package/docs/guide/08-implementing.md +0 -13
  223. package/docs/llms-full.txt +0 -1241
  224. package/docs/llms.txt +0 -14
  225. package/docs/reference/canonical.md +0 -16
  226. package/docs/reference/clauses.md +0 -986
  227. package/docs/reference/cli.md +0 -22
  228. package/docs/reference/diagnostics.md +0 -32
  229. package/skills/udl/SKILL.md +0 -28
  230. package/src/check-profiles.ts +0 -80
  231. package/src/cli.ts +0 -196
  232. package/src/effects.ts +0 -163
  233. package/src/evolution.ts +0 -965
  234. package/src/parser.ts +0 -58
@@ -0,0 +1,559 @@
1
+ import * as z from "zod";
2
+ import { udlFieldValueSchema } from "./field-value.js";
3
+ import {
4
+ projectDocumentSemantics,
5
+ presentationLabel,
6
+ } from "./object-semantics.js";
7
+ import type { DocumentSemantics } from "./object-semantics-schema.js";
8
+ export * from "./object-semantics-schema.js";
9
+ export {
10
+ projectDocumentSemantics,
11
+ presentationLabel,
12
+ } from "./object-semantics.js";
13
+ import {
14
+ udlExternalIdSchema,
15
+ type UdlDocument,
16
+ type UdlObjectField,
17
+ type UdlObjectKind,
18
+ type UdlSubjectRequirement,
19
+ type UdlValue,
20
+ type UdlInstrument,
21
+ } from "./schema.js";
22
+
23
+ export type JsonSchemaDocument = z.core.JSONSchema.BaseSchema;
24
+
25
+ export interface ObjectKindDiscovery {
26
+ attachments: readonly {
27
+ name: string;
28
+ title: string;
29
+ instrument: string;
30
+ parentAttachment: string | null;
31
+ block: { key: string; recordPath: string; headerDigest: string } | null;
32
+ }[];
33
+ productBuildId: string;
34
+ digest: string;
35
+ creation: true;
36
+ kind: string;
37
+ title: string;
38
+ fields: readonly UdlObjectField[];
39
+ authoredFields: readonly string[];
40
+ columns: readonly string[];
41
+ filters: readonly {
42
+ field: string;
43
+ type: "text" | "enum" | "date" | "boolean" | "integer";
44
+ }[];
45
+ createSchema: JsonSchemaDocument;
46
+ actions: readonly ObjectActionDiscovery[];
47
+ }
48
+ export function objectKindFilters(
49
+ kind: UdlObjectKind,
50
+ ): ObjectKindDiscovery["filters"] {
51
+ return kind.columns.flatMap((column) => {
52
+ const field = kind.fields.find((item) => item.name === column);
53
+ return field &&
54
+ !field.sensitive &&
55
+ ["text", "enum", "date", "boolean", "integer"].includes(field.type)
56
+ ? [
57
+ {
58
+ field: column,
59
+ type: field.type as ObjectKindDiscovery["filters"][number]["type"],
60
+ },
61
+ ]
62
+ : [];
63
+ });
64
+ }
65
+ export type ObjectActionTarget =
66
+ | { kind: "attachment"; attachment: string }
67
+ | { kind: "instance"; attachment: string; instanceId: string };
68
+ export interface ObjectActionContext {
69
+ productBuildId: string;
70
+ digest: string;
71
+ target: ObjectActionTarget;
72
+ }
73
+ export interface ObjectActionDiscovery extends ObjectActionContext {
74
+ summary: string;
75
+ name: string;
76
+ title: string;
77
+ instrument: string;
78
+ action: string;
79
+ requirements: readonly UdlObjectField[];
80
+ requirementConditions?: Record<
81
+ string,
82
+ NonNullable<UdlSubjectRequirement["when"]>
83
+ >;
84
+ creationRequirements?: readonly UdlObjectField[];
85
+ creationOnlyNames?: readonly string[];
86
+ inputSchema: JsonSchemaDocument;
87
+ fieldsSchema: JsonSchemaDocument;
88
+ availability: ObjectActionAvailability;
89
+ }
90
+ export type ObjectActionBlockerCode =
91
+ | "permission_required"
92
+ | "state_changed"
93
+ | "not_due"
94
+ | "deadline_passed"
95
+ | "clock_pending"
96
+ | "condition_not_met"
97
+ | "already_used"
98
+ | "evidence_required"
99
+ | "account_unavailable"
100
+ | "insufficient_money"
101
+ | "setup_required"
102
+ | "invalid_input"
103
+ | "operation_pending";
104
+ export type ObjectActionAvailability =
105
+ | { status: "available" }
106
+ | {
107
+ status: "requires_input";
108
+ subjectFields: readonly string[];
109
+ actionInputs: readonly string[];
110
+ parties: readonly string[];
111
+ reason: string;
112
+ }
113
+ | {
114
+ status: "unavailable";
115
+ blockers: readonly {
116
+ code: ObjectActionBlockerCode;
117
+ reason: string;
118
+ recheckAt?: string;
119
+ }[];
120
+ }
121
+ | { status: "unknown"; reason: string };
122
+
123
+ export type EvidenceState =
124
+ | "missing"
125
+ | "pending"
126
+ | "satisfied"
127
+ | "refused"
128
+ | "stale";
129
+
130
+ export interface ObjectEvidenceSummary {
131
+ requirementId: string;
132
+ subjectId: string;
133
+ protectedInputDigest: string;
134
+ adapter: string;
135
+ declarationDigest: string;
136
+ productBuildId: string;
137
+ state: EvidenceState;
138
+ reference?: string;
139
+ observedAt?: string;
140
+ reasonCode?: string;
141
+ }
142
+
143
+ export interface ObjectActionState extends ObjectActionDiscovery {
144
+ requiredNow: readonly string[];
145
+ evidence?: readonly ObjectEvidenceSummary[];
146
+ }
147
+ /** GET /v1/products/{productId}/objects. Core supplies the frozen Build identity. */
148
+ export type RetainedObjectKind = Pick<
149
+ ObjectKindDiscovery,
150
+ "kind" | "title" | "fields" | "authoredFields" | "columns" | "filters"
151
+ > & {
152
+ attachments: ObjectKindDiscovery["attachments"];
153
+ productBuildId: string;
154
+ digest: string;
155
+ creation: false;
156
+ };
157
+ export interface ObjectDiscovery {
158
+ semantics: DocumentSemantics;
159
+ navigation: readonly { kind: string; title: string; creation: boolean }[];
160
+ retainedKinds: readonly RetainedObjectKind[];
161
+ productBuildId: string;
162
+ digest: string;
163
+ kinds: readonly ObjectKindDiscovery[];
164
+ }
165
+
166
+ /** Creation accepts the full metadata union, with every value optional. */
167
+ export function objectCreateSchema(kind: UdlObjectKind): z.ZodObject {
168
+ return z.strictObject({
169
+ externalId: udlExternalIdSchema.optional(),
170
+ fields: z
171
+ .strictObject(
172
+ Object.fromEntries(
173
+ kind.fields.map((field) => [
174
+ field.name,
175
+ udlFieldValueSchema(field).optional(),
176
+ ]),
177
+ ),
178
+ )
179
+ .optional(),
180
+ });
181
+ }
182
+
183
+ /** Canonical UDL supplies meaning; core adds authority, instance state and readiness. */
184
+ export function projectObjectDiscovery(
185
+ document: UdlDocument,
186
+ build: Pick<ObjectDiscovery, "productBuildId" | "digest">,
187
+ ): ObjectDiscovery {
188
+ const semantics = projectDocumentSemantics(document);
189
+ return {
190
+ ...build,
191
+ semantics,
192
+ navigation: document.objects.map((kind) => ({
193
+ kind: kind.id,
194
+ title: kind.title,
195
+ creation: true,
196
+ })),
197
+ retainedKinds: [],
198
+ kinds: semantics.objects.map((kind) => ({
199
+ ...build,
200
+ creation: true,
201
+ kind: kind.id,
202
+ title: kind.title,
203
+ fields: kind.fields,
204
+ authoredFields: kind.authoredFields,
205
+ columns: kind.columns,
206
+ filters: objectKindFilters(kind),
207
+ attachments: kind.attachments.map((attachment) => ({
208
+ name: attachment.name,
209
+ title:
210
+ attachment.title ??
211
+ document.instruments.find((item) => item.id === attachment.instrument)
212
+ ?.title ??
213
+ attachment.name,
214
+ instrument: attachment.instrument,
215
+ parentAttachment: attachment.parent ?? null,
216
+ block: null,
217
+ })),
218
+ createSchema: z.toJSONSchema(objectCreateSchema(kind), { io: "input" }),
219
+ actions: document.instruments
220
+ .filter((instrument) => instrument.subject === kind.id)
221
+ .flatMap((instrument) => {
222
+ const createHidden =
223
+ !!instrument.actions.create &&
224
+ !instrument.actions.create.publicAction;
225
+ const attachmentRefListFields = instrument.fields.filter(
226
+ (field) =>
227
+ field.type === "list" &&
228
+ field.item === "ref" &&
229
+ field.targetKind !== "object",
230
+ );
231
+ return instrument.actionOrder.flatMap((name) => {
232
+ const action = instrument.actions[name]!;
233
+ if (!action.publicAction) return [];
234
+ const transition = instrument.lifecycle.transitions[name];
235
+ const canStartFromInitial = Boolean(
236
+ transition?.from.includes(instrument.lifecycle.initial),
237
+ );
238
+ const isCreationAction =
239
+ name === "create" || (createHidden && canStartFromInitial);
240
+
241
+ const rawCreateReqs: UdlSubjectRequirement[] = isCreationAction
242
+ ? [
243
+ ...(instrument.actions.create?.subject?.requirements ?? []),
244
+ ...attachmentRefListFields.map((field) => ({
245
+ field: field as UdlObjectField,
246
+ })),
247
+ ]
248
+ : [];
249
+ const actionReqs = action.subject?.requirements ?? [];
250
+ const actionReqNames = new Set(
251
+ actionReqs.map((r) => r.objectField ?? r.field.name),
252
+ );
253
+ const mergedReqs: UdlSubjectRequirement[] = actionReqs.map(
254
+ (requirement) => ({ ...requirement }),
255
+ );
256
+ for (const req of rawCreateReqs) {
257
+ const reqName = req.objectField ?? req.field.name;
258
+ const existing = mergedReqs.find(
259
+ (r) => (r.objectField ?? r.field.name) === reqName,
260
+ );
261
+ if (!existing) mergedReqs.push({ ...req });
262
+ else if (!req.when) delete existing.when;
263
+ else if (existing.when)
264
+ existing.when = [...existing.when, ...req.when];
265
+ }
266
+ const requirementConditions = Object.fromEntries(
267
+ mergedReqs.flatMap((req) =>
268
+ req.when ? [[req.objectField ?? req.field.name, req.when]] : [],
269
+ ),
270
+ );
271
+ const unbound = [
272
+ ...(action.subject?.adapters ?? []),
273
+ ...(isCreationAction
274
+ ? (instrument.actions.create?.subject?.adapters ?? [])
275
+ : []),
276
+ ].find((adapter) => adapter.snapshot === null);
277
+ const seenCreation = new Set<string>();
278
+ const creationRequirements: UdlObjectField[] = [];
279
+ for (const requirement of rawCreateReqs) {
280
+ const name = requirement.objectField ?? requirement.field.name;
281
+ if (!seenCreation.has(name)) {
282
+ seenCreation.add(name);
283
+ creationRequirements.push({
284
+ ...requirement.field,
285
+ name,
286
+ } as UdlObjectField);
287
+ }
288
+ }
289
+ const creationOnlyNames = creationRequirements
290
+ .map((field) => field.name)
291
+ .filter((name) => !actionReqNames.has(name));
292
+ return [
293
+ {
294
+ ...build,
295
+ target: {
296
+ kind: "attachment" as const,
297
+ attachment: kind.attachments.find(
298
+ (attachment) => attachment.instrument === instrument.id,
299
+ )!.name,
300
+ },
301
+ name: action.publicAction,
302
+ title: action.title ?? presentationLabel(action.publicAction),
303
+ summary: action.summary,
304
+ instrument: instrument.id,
305
+ action: name,
306
+ requirements: mergedReqs.map(
307
+ (requirement) =>
308
+ ({
309
+ ...requirement.field,
310
+ name: requirement.objectField ?? requirement.field.name,
311
+ }) as UdlObjectField,
312
+ ),
313
+ ...(Object.keys(requirementConditions).length
314
+ ? { requirementConditions }
315
+ : {}),
316
+ ...(creationRequirements.length > 0
317
+ ? { creationRequirements }
318
+ : {}),
319
+ ...(creationRequirements.length > 0
320
+ ? { creationOnlyNames }
321
+ : {}),
322
+ fieldsSchema: z.toJSONSchema(
323
+ z.strictObject(
324
+ Object.fromEntries(
325
+ mergedReqs.map((requirement) => [
326
+ requirement.objectField ?? requirement.field.name,
327
+ udlFieldValueSchema(requirement.field).optional(),
328
+ ]),
329
+ ),
330
+ ),
331
+ { io: "input" },
332
+ ),
333
+ inputSchema: z.toJSONSchema(
334
+ z.strictObject(
335
+ Object.fromEntries(
336
+ action.input.map((field) => [
337
+ field.name,
338
+ field.optional
339
+ ? udlFieldValueSchema(field).optional()
340
+ : udlFieldValueSchema(field),
341
+ ]),
342
+ ),
343
+ ),
344
+ { io: "input" },
345
+ ),
346
+ availability: unbound
347
+ ? {
348
+ status: "unavailable" as const,
349
+ blockers: [
350
+ {
351
+ code: "setup_required" as const,
352
+ reason:
353
+ "A required external check has not been set up.",
354
+ },
355
+ ],
356
+ }
357
+ : { status: "available" as const },
358
+ },
359
+ ];
360
+ });
361
+ }),
362
+ })),
363
+ };
364
+ }
365
+
366
+ /** Values overwritten before invocation cannot decide a guard during discovery. */
367
+ export function objectActionSelf(
368
+ instrument: UdlInstrument,
369
+ actionName: string,
370
+ fields: Readonly<Record<string, unknown>> = {},
371
+ ): Record<string, unknown> {
372
+ const action = instrument.actions[actionName]!;
373
+ const known = {
374
+ ...Object.fromEntries(
375
+ instrument.fields.flatMap((field) =>
376
+ "value" in field ? [[field.name, field.value]] : [],
377
+ ),
378
+ ),
379
+ ...fields,
380
+ };
381
+ for (const calculation of [
382
+ ...instrument.calculate,
383
+ ...(action.calculate ?? []),
384
+ ])
385
+ delete known[calculation.target];
386
+ for (const name of Object.keys(action.set ?? {})) delete known[name];
387
+ return known;
388
+ }
389
+
390
+ export interface ObjectActionStateOptions {
391
+ attached?: boolean;
392
+ self?: Readonly<Record<string, unknown>>;
393
+ input?: Readonly<Record<string, unknown>>;
394
+ }
395
+
396
+ /** Unknown invocation state remains possible; a known false guard suppresses a prompt. */
397
+ function requirementPossible(
398
+ paths: NonNullable<UdlSubjectRequirement["when"]>,
399
+ action: ObjectActionDiscovery,
400
+ fields: Readonly<Record<string, unknown>>,
401
+ options: ObjectActionStateOptions,
402
+ ): boolean {
403
+ return paths.some((path) =>
404
+ path.every((condition) => {
405
+ const read = (value: UdlValue): unknown => {
406
+ if ("literal" in value) return value.literal;
407
+ const [root, name, ...tail] = value.field.split(".");
408
+ if (!name || tail.length) return undefined;
409
+ if (root === "subject") return fields[name];
410
+ if (
411
+ condition.instrument !== action.instrument ||
412
+ condition.action !== action.action
413
+ )
414
+ return undefined;
415
+ if (root === "self") return options.self?.[name];
416
+ if (root === "input") return options.input?.[name];
417
+ return undefined;
418
+ };
419
+ let left = read(condition.guard.left);
420
+ let right = read(condition.guard.right);
421
+ if (left === undefined || right === undefined) return true;
422
+ try {
423
+ if (condition.valueType === "money") {
424
+ left = BigInt(String(left));
425
+ right = BigInt(String(right));
426
+ } else if (condition.valueType === "date") {
427
+ left = Date.parse(String(left));
428
+ right = Date.parse(String(right));
429
+ if (!Number.isFinite(left) || !Number.isFinite(right)) return true;
430
+ }
431
+ } catch {
432
+ return true;
433
+ }
434
+ if (
435
+ typeof left !== typeof right ||
436
+ !["string", "number", "boolean", "bigint"].includes(typeof left)
437
+ )
438
+ return true;
439
+ const a = left as string | number | bigint;
440
+ const b = right as string | number | bigint;
441
+ switch (condition.guard.operator) {
442
+ case "==":
443
+ return left === right;
444
+ case "!=":
445
+ return left !== right;
446
+ case "<":
447
+ return a < b;
448
+ case "<=":
449
+ return a <= b;
450
+ case ">":
451
+ return a > b;
452
+ case ">=":
453
+ return a >= b;
454
+ }
455
+ }),
456
+ );
457
+ }
458
+
459
+ /** Object-specific values determine missing fields; global discovery cannot. */
460
+ export function objectActionState(
461
+ action: ObjectActionDiscovery,
462
+ fields: Readonly<Record<string, unknown>>,
463
+ options?: ObjectActionStateOptions | boolean,
464
+ ): ObjectActionState {
465
+ const attached =
466
+ typeof options === "boolean" ? options : (options?.attached ?? false);
467
+ const creationFilter = new Set(action.creationOnlyNames ?? []);
468
+ return {
469
+ ...action,
470
+ requiredNow: action.requirements
471
+ .filter((field) => {
472
+ const conditions =
473
+ action.requirementConditions &&
474
+ Object.hasOwn(action.requirementConditions, field.name)
475
+ ? action.requirementConditions[field.name]
476
+ : undefined;
477
+ if (
478
+ conditions &&
479
+ !requirementPossible(
480
+ conditions,
481
+ action,
482
+ fields,
483
+ typeof options === "object" ? options : {},
484
+ )
485
+ )
486
+ return false;
487
+ const value = fields[field.name];
488
+ const optional =
489
+ field.optional || (attached && creationFilter.has(field.name));
490
+ return (
491
+ !(optional && value === undefined) &&
492
+ !udlFieldValueSchema(field).safeParse(value).success
493
+ );
494
+ })
495
+ .map((field) => field.name),
496
+ };
497
+ }
498
+ export interface SubjectRequirementIssue {
499
+ code:
500
+ | "subject_requirement_missing"
501
+ | "subject_field_unknown"
502
+ | "subject_adapter_unbound";
503
+ action: string;
504
+ field?: string;
505
+ origin: string;
506
+ message: string;
507
+ }
508
+
509
+ /** Check merged metadata before dispatch. Runtime guards add their active child requirements. */
510
+ export function validateObjectActionSubject(
511
+ action: ObjectActionDiscovery,
512
+ storedFields: Readonly<Record<string, unknown>>,
513
+ submittedFields: Readonly<Record<string, unknown>>,
514
+ options?: ObjectActionStateOptions | boolean,
515
+ ): readonly SubjectRequirementIssue[] {
516
+ const origin = `${action.instrument}.${action.action}.subject`;
517
+ const issues: SubjectRequirementIssue[] = [];
518
+ if (
519
+ action.availability.status === "unavailable" &&
520
+ action.availability.blockers.some(
521
+ (blocker) => blocker.code === "setup_required",
522
+ )
523
+ ) {
524
+ issues.push({
525
+ code: "subject_adapter_unbound",
526
+ action: action.name,
527
+ origin,
528
+ message: "A required external check has not been set up.",
529
+ });
530
+ }
531
+ const admitted = new Set(action.requirements.map((field) => field.name));
532
+ for (const field of Object.keys(submittedFields)) {
533
+ if (!admitted.has(field))
534
+ issues.push({
535
+ code: "subject_field_unknown",
536
+ action: action.name,
537
+ field,
538
+ origin,
539
+ message: `${field} is not admitted by ${action.name}`,
540
+ });
541
+ }
542
+ for (const field of objectActionState(
543
+ action,
544
+ {
545
+ ...storedFields,
546
+ ...submittedFields,
547
+ },
548
+ options,
549
+ ).requiredNow) {
550
+ issues.push({
551
+ code: "subject_requirement_missing",
552
+ action: action.name,
553
+ field,
554
+ origin: `${origin}.${field}`,
555
+ message: `${action.name} requires a valid ${field}`,
556
+ });
557
+ }
558
+ return issues;
559
+ }
@@ -0,0 +1,148 @@
1
+ import * as z from "zod";
2
+ import {
3
+ udlActionSchema,
4
+ udlCalculationSchema,
5
+ udlFieldSchema,
6
+ udlInstrumentSchema,
7
+ udlObjectKindSchema,
8
+ udlObjectAttachmentSchema,
9
+ udlPartySchema,
10
+ udlRequirementSchema,
11
+ udlValueSchema,
12
+ udlKernelOperationSchema,
13
+ subjectPartyRoles,
14
+ } from "./schema.js";
15
+
16
+ const text = z.string();
17
+ export const cardinalitySchema = z.strictObject({
18
+ min: z.number().int().nonnegative(),
19
+ // null means the declarations supply no upper bound.
20
+ max: z.number().int().nonnegative().nullable(),
21
+ });
22
+ export const semanticOwnerSchema = z.union([
23
+ z.strictObject({ kind: z.literal("instrument"), instrument: text }),
24
+ z.strictObject({ kind: z.literal("role"), role: z.enum(subjectPartyRoles) }),
25
+ z.strictObject({ kind: z.literal("party"), party: text }),
26
+ z.strictObject({ kind: z.literal("adapter"), adapter: text }),
27
+ ]);
28
+ export const semanticAccountSchema = z.strictObject({
29
+ path: text,
30
+ scope: text,
31
+ instrument: text,
32
+ field: text,
33
+ owner: semanticOwnerSchema,
34
+ book: z.enum(["cash", "claim"]),
35
+ key: text.optional(),
36
+ contra: z.boolean(),
37
+ });
38
+ export const semanticRoleSchema = z.union([
39
+ semanticOwnerSchema,
40
+ z.strictObject({ kind: z.enum(["caller", "clock"]) }),
41
+ z.strictObject({ kind: z.literal("parent"), instruments: z.array(text) }),
42
+ ]);
43
+ export const semanticRelationshipSchema = z.strictObject({
44
+ kind: z.enum(["reference", "child"]),
45
+ via: z.enum(["field", "attachment", "invocation", "parent_actor"]),
46
+ name: text,
47
+ targetKind: z.enum(["instrument", "object"]),
48
+ targets: z.array(text),
49
+ cardinality: cardinalitySchema,
50
+ });
51
+ export const semanticMoneyEffectSchema = z.strictObject({
52
+ key: text,
53
+ operation: udlKernelOperationSchema,
54
+ amount: udlValueSchema.nullable(),
55
+ from: z.array(semanticAccountSchema),
56
+ to: z.array(semanticAccountSchema),
57
+ // Expressions describe declared effects, never a receipt or evaluated quote.
58
+ disposition: z.enum(["held", "paid", "released", "claim", "unresolved"]),
59
+ transfer: text.nullable(),
60
+ reservations: z.array(
61
+ z.strictObject({ instrument: text, action: text, key: text }),
62
+ ),
63
+ });
64
+ export const semanticActionSchema = z.strictObject({
65
+ name: text,
66
+ alias: text.nullable(),
67
+ title: text,
68
+ actor: udlActionSchema.shape.actor,
69
+ roles: z.array(semanticRoleSchema),
70
+ from: z.array(text),
71
+ to: text.nullable(),
72
+ references: z.array(udlFieldSchema).optional(),
73
+ invokedBy: z
74
+ .array(
75
+ z.strictObject({
76
+ instrument: text,
77
+ action: text,
78
+ alias: text.nullable(),
79
+ invocation: udlActionSchema.shape.invoke.unwrap().element,
80
+ }),
81
+ )
82
+ .optional(),
83
+ inputs: udlActionSchema.shape.input,
84
+ subject: udlActionSchema.shape.subject,
85
+ prerequisites: udlActionSchema.shape.requires,
86
+ due: udlActionSchema.shape.due.unwrap().nullable(),
87
+ deadline: udlActionSchema.shape.deadline.unwrap().nullable(),
88
+ calculations: z.array(udlCalculationSchema),
89
+ assignments: udlActionSchema.shape.set.unwrap(),
90
+ moves: udlActionSchema.shape.moves,
91
+ moneyEffects: z.array(semanticMoneyEffectSchema),
92
+ invokes: udlActionSchema.shape.invoke.unwrap(),
93
+ });
94
+ export const semanticStructureSchema = z.enum([
95
+ "stage_and_decisions",
96
+ "payment_review",
97
+ "held_funds",
98
+ "reserved_payment",
99
+ "payment_calendar",
100
+ "funding_progress",
101
+ "cancellation_review",
102
+ "external_checks",
103
+ "report_view",
104
+ ]);
105
+ export const semanticInstrumentSchema = z.strictObject({
106
+ id: text,
107
+ title: text,
108
+ subject: text.optional(),
109
+ fields: z.array(udlFieldSchema),
110
+ accounts: z.array(semanticAccountSchema),
111
+ relationships: z.array(semanticRelationshipSchema),
112
+ calculations: z.array(udlCalculationSchema),
113
+ invariants: z.array(udlRequirementSchema),
114
+ initialState: text,
115
+ lifecycle: z.strictObject({
116
+ initial: text,
117
+ states: z.array(
118
+ z.strictObject({ name: text, label: text, terminal: z.boolean() }),
119
+ ),
120
+ cancellationActions: z.array(text),
121
+ }),
122
+ actions: z.array(semanticActionSchema),
123
+ reports: udlInstrumentSchema.shape.reports.unwrap(),
124
+ structures: z.array(semanticStructureSchema),
125
+ });
126
+ export const documentSemanticsSchema = z.strictObject({
127
+ currency: text,
128
+ parties: z.record(text, udlPartySchema),
129
+ objects: z.array(
130
+ z.strictObject({
131
+ ...udlObjectKindSchema.shape,
132
+ attachments: z.array(
133
+ z.strictObject({
134
+ ...udlObjectAttachmentSchema.shape,
135
+ cardinality: cardinalitySchema,
136
+ }),
137
+ ),
138
+ }),
139
+ ),
140
+ instruments: z.array(semanticInstrumentSchema),
141
+ });
142
+ export type DocumentSemantics = z.infer<typeof documentSemanticsSchema>;
143
+ export type SemanticInstrument = z.infer<typeof semanticInstrumentSchema>;
144
+ export type SemanticAction = z.infer<typeof semanticActionSchema>;
145
+ export type SemanticAccount = z.infer<typeof semanticAccountSchema>;
146
+ export type SemanticOwner = z.infer<typeof semanticOwnerSchema>;
147
+ export type SemanticRelationship = z.infer<typeof semanticRelationshipSchema>;
148
+ export type SemanticMoneyEffect = z.infer<typeof semanticMoneyEffectSchema>;