@hyperscale0/udl 2.2.0 → 2.3.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.
package/docs/README.md CHANGED
@@ -11,4 +11,7 @@ UDL is the versioned JSON contract between a product definition and an engine th
11
11
  7. [Evolution](guide/07-evolution.md)
12
12
  8. [Implementing UDL](guide/08-implementing.md)
13
13
 
14
+ [Piece plans and private action composition](piece-plans.md) is a worked
15
+ instrument fragment for `piecePlan`, `pieceStage`, `calls` and `actionLibrary`.
16
+
14
17
  The [clause reference](reference/clauses.md) and [diagnostic reference](reference/diagnostics.md) come from the package tables. The [command reference](reference/cli.md) comes from the CLI usage text. Do not edit those generated files by hand.
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.2.0. Edit the source, not llms-full.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.3.0. Edit the source, not llms-full.txt. -->
2
2
 
3
3
  # UDL complete reference
4
4
 
@@ -17,6 +17,9 @@ UDL is the versioned JSON contract between a product definition and an engine th
17
17
  7. [Evolution](guide/07-evolution.md)
18
18
  8. [Implementing UDL](guide/08-implementing.md)
19
19
 
20
+ [Piece plans and private action composition](piece-plans.md) is a worked
21
+ instrument fragment for `piecePlan`, `pieceStage`, `calls` and `actionLibrary`.
22
+
20
23
  The [clause reference](reference/clauses.md) and [diagnostic reference](reference/diagnostics.md) come from the package tables. The [command reference](reference/cli.md) comes from the CLI usage text. Do not edit those generated files by hand.
21
24
 
22
25
  <!-- source: guide/01-a-document.md -->
package/docs/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.2.0. Edit the source, not llms.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.3.0. Edit the source, not llms.txt. -->
2
2
 
3
3
  # UDL documentation
4
4
 
@@ -0,0 +1,148 @@
1
+ # Piece plans and private action composition
2
+
3
+ UDL preserves the authored graph and the order needed to interpret it.
4
+ `piecePlan` declares a finite partition, `pieceStage` names a stage, `calls`
5
+ binds private actions, and `actionLibrary` supplies their definitions. HSX
6
+ spells these clauses `piece_plan`, `piece_stage`, `calls` and `action_library`.
7
+ The shapes live in [`schema.ts`](../src/schema.ts); resolution and finance
8
+ validation live in [`validation.ts`](../src/validation.ts). The
9
+ [clause reference](reference/clauses.md) describes the complete vocabulary.
10
+
11
+ ## Worked sale example
12
+
13
+ The following decoded instrument fragment uses the sale settlement's price
14
+ partition and refund action. Add it to an instrument with its required
15
+ immutable fields, matching partition, lifecycle and captured escrow account.
16
+ It omits the surrounding document and other actions. It is formatted for
17
+ reading; canonical serialization sorts object keys and retains array order.
18
+
19
+ ```json
20
+ {
21
+ "piecePlan": {
22
+ "id": "price",
23
+ "total": "price",
24
+ "pieces": [
25
+ {
26
+ "id": "seller",
27
+ "amount": "piece1Amount",
28
+ "release_to": "sellerAccountId",
29
+ "refund_to": "buyerAccountId"
30
+ },
31
+ {
32
+ "id": "platform_fee",
33
+ "amount": "piece2Amount",
34
+ "release_to": "platformAccountId",
35
+ "refund_to": "buyerAccountId"
36
+ },
37
+ {
38
+ "id": "seller_cancel_fee",
39
+ "amount": "piece3Amount",
40
+ "release_to": "platformAccountId",
41
+ "refund_to": "sellerAccountId"
42
+ }
43
+ ],
44
+ "fund_order": ["seller", "platform_fee", "seller_cancel_fee"],
45
+ "release_order": ["platform_fee", "seller_cancel_fee"],
46
+ "refund_order": ["platform_fee", "seller_cancel_fee"],
47
+ "unfund_order": ["seller_cancel_fee", "platform_fee", "seller"]
48
+ },
49
+ "actionLibrary": {
50
+ "settlement_piece": {
51
+ "actionOrder": ["move"],
52
+ "actions": {
53
+ "move": {
54
+ "parameters": {
55
+ "piece": { "kind": "piece" },
56
+ "source": { "kind": "account" },
57
+ "destination": { "kind": "account" }
58
+ },
59
+ "principal": "api_key",
60
+ "approval": "inherit",
61
+ "recovery": "local",
62
+ "order": ["transfer"],
63
+ "calls": [],
64
+ "leaves": [
65
+ {
66
+ "id": "transfer",
67
+ "operation": "internal_transfer.create",
68
+ "bind": {
69
+ "amount": "$piece.amount",
70
+ "currency": "$piece.currency",
71
+ "sourceAccountId": "$source",
72
+ "destinationAccountId": "$destination"
73
+ },
74
+ "effects": [
75
+ { "kind": "moves", "signature": "moves.transfer.internal" }
76
+ ],
77
+ "evidence": "transferId"
78
+ }
79
+ ]
80
+ }
81
+ }
82
+ }
83
+ },
84
+ "actions": {
85
+ "refund_piece": {
86
+ "pieceStage": { "plan": "price", "stage": "refund" },
87
+ "calls": [
88
+ {
89
+ "id": "move_piece",
90
+ "action": "settlement_piece.move",
91
+ "bind": {
92
+ "piece": "$piece",
93
+ "source": "$instance.refs.escrowAccountId",
94
+ "destination": "$piece.refund_to"
95
+ }
96
+ }
97
+ ],
98
+ "steps": [],
99
+ "summary": "Refund a piece of a sale settlement"
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ The containing instrument's `actionOrder` includes `refund_piece` at its
106
+ authored position. Both library `actionOrder` and private action `order` must
107
+ be exact permutations of their members. The stage derives a required
108
+ `pieceId` enum, here `platform_fee` or `seller_cancel_fee`. The selector does
109
+ not add another transfer; the private leaf moves its amount once.
110
+
111
+ For the sale's example amounts, `piece1Amount` is `245000`, `piece2Amount` is
112
+ `3750`, and `piece3Amount` is `1250`, all SAR minor units. They partition
113
+ `price` of `250000`. Cancellation's separate decision action returns the seller
114
+ piece to the buyer. The refund stage then returns `3750` to the buyer and pays
115
+ `1250` to the seller. Unfund follows its reverse order and returns every funded
116
+ piece to the buyer instead. The service fee stays outside the price partition.
117
+
118
+ `resolveUdlActionPlans` returns plans and issues. A plan identifies its action
119
+ and optional piece ID, with resolved effects and leaves. Each leaf retains
120
+ `originPath`, step, effects and evidence. Resolution expands static calls; it
121
+ does not replace the source graph in canonical UDL. A private library entry is
122
+ not a public action. [`refactor-clauses.spec.ts`](../test/refactor-clauses.spec.ts)
123
+ checks piece constraints, graph order, cycles, bindings, authority and evidence.
124
+
125
+ ## Conservation and diagnostics
126
+
127
+ UDL 2.3.0 checks funded amounts over piece progress. A partially funded path
128
+ must return only the pieces actually funded; selecting one piece is not proof
129
+ that a whole stage completed. For a piece plan, `expandPieceProgress` bounds the reachable lifecycle/progress
130
+ states at 256. Instruments without a piece plan use the separate action-plan
131
+ combination bound. These bounds belong to the proof, not to a runtime retry
132
+ loop. [`udl.spec.ts`](../test/udl.spec.ts) includes the stranded-funded-piece
133
+ refusal; [`validation.ts`](../src/validation.ts) owns the progress expansion.
134
+
135
+ [`diagnostics.ts`](../src/diagnostics.ts) defines the refusal codes:
136
+
137
+ | Code | Refusal | Repair |
138
+ | --------- | ---------------------- | ---------------------------------------------------------------------------------------- |
139
+ | `UDL4001` | Money graph violation | Balance each funded amount and close every hold on each lifecycle path |
140
+ | `UDL5013` | Piece stage violation | Select a declared plan/stage, valid ordered piece IDs and a call-based stage action |
141
+ | `UDL2010` | Action graph violation | Resolve targets and remove cycles, collisions or invalid order within depth/count limits |
142
+
143
+ A partition mismatch or incompatible immutable field is `UDL4002`. A typed
144
+ binding failure is `UDL2011`; an authority boundary is `UDL2012`; incomplete
145
+ effects or evidence are `UDL2013`. These failures are not permission to skip
146
+ the independent conservation oracle. Keep actions that need another principal,
147
+ independent approval or external recovery as separate calls at the public
148
+ boundary, not flattened private work.
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.2.0. Edit the source, not clauses.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.3.0. Edit the source, not clauses.md. -->
2
2
 
3
3
  # Clause reference
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.2.0. Edit the source, not cli.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.3.0. Edit the source, not cli.md. -->
2
2
 
3
3
  # Command reference
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.2.0. Edit the source, not diagnostics.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.3.0. Edit the source, not diagnostics.md. -->
2
2
 
3
3
  # Diagnostic reference
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/udl",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "The Universal Domain Language: format spec, parser, validator, canonical serializer, and evolution diff.",
5
5
  "keywords": [
6
6
  "udl",
@@ -84,5 +84,5 @@
84
84
  "Amir Ayub",
85
85
  "Sara AlBakaawi"
86
86
  ],
87
- "gitHead": "982ff68eee44e481944fe0bc50c6eee6fee17059"
87
+ "gitHead": "ed8866f5e5201aad938ad0219da06e430a513467"
88
88
  }
package/src/effects.ts CHANGED
@@ -66,6 +66,26 @@ function boundPath(move: Movement, endpoint: string): string | undefined {
66
66
  * External collection, deposit, and payout operations carry their role in the
67
67
  * operation family because their remote endpoint is not a UDL account binding.
68
68
  */
69
+ /**
70
+ * The typed kind of an instance ref a call binding may name. Refs exist only
71
+ * through step and move captures on the same instrument; a capture of a leaf's
72
+ * `accountId` output is an account, every other capture is opaque text.
73
+ */
74
+ function capturedRefKind(
75
+ instrument: UdlInstrument,
76
+ refName: string,
77
+ ): "account" | "text" | undefined {
78
+ for (const action of Object.values(instrument.actions)) {
79
+ for (const step of [...action.steps, ...action.moves]) {
80
+ const output = step.capture?.[refName];
81
+ if (output !== undefined) {
82
+ return output === "accountId" ? "account" : "text";
83
+ }
84
+ }
85
+ }
86
+ return undefined;
87
+ }
88
+
69
89
  export function movementClass(move: Movement): UdlMovementClass {
70
90
  if (move.operation.startsWith("internal_transfer.")) {
71
91
  const source = boundPath(move, "sourceAccountId");
@@ -231,6 +251,35 @@ function encodeOriginPathKey(originPath: readonly string[]): string {
231
251
  return `k_${parts.join("_")}`;
232
252
  }
233
253
 
254
+ function sameEffectSignatures(
255
+ left: readonly { readonly kind: UdlEffectKind; readonly signature: string }[],
256
+ right: readonly {
257
+ readonly kind: UdlEffectKind;
258
+ readonly signature: string;
259
+ }[],
260
+ ): boolean {
261
+ const count = (
262
+ effects: readonly {
263
+ readonly kind: UdlEffectKind;
264
+ readonly signature: string;
265
+ }[],
266
+ ) => {
267
+ const counts = new Map<string, number>();
268
+ for (const eff of effects) {
269
+ const key = `${eff.kind}:${eff.signature}`;
270
+ counts.set(key, (counts.get(key) ?? 0) + 1);
271
+ }
272
+ return counts;
273
+ };
274
+ const leftCounts = count(left);
275
+ const rightCounts = count(right);
276
+ if (leftCounts.size !== rightCounts.size) return false;
277
+ for (const [key, n] of leftCounts) {
278
+ if (rightCounts.get(key) !== n) return false;
279
+ }
280
+ return true;
281
+ }
282
+
234
283
  function expectedLeafEffects(
235
284
  step: UdlStep | UdlMove,
236
285
  ): readonly { readonly kind: UdlEffectKind; readonly signature: string }[] {
@@ -925,6 +974,35 @@ export function resolveUdlActionPlans(
925
974
  path: parts[1],
926
975
  };
927
976
  }
977
+ if (parts[1] === "refs") {
978
+ if (parts.length !== 3) {
979
+ issues.push(
980
+ issue(
981
+ "UDL2011",
982
+ leafPath,
983
+ `invalid trailing member access on instance ref: ${rawBind}`,
984
+ ),
985
+ );
986
+ return undefined;
987
+ }
988
+ const refName = parts[2]!;
989
+ const captured = capturedRefKind(instrument, refName);
990
+ if (!captured) {
991
+ issues.push(
992
+ issue(
993
+ "UDL2011",
994
+ leafPath,
995
+ `referenced ref ${refName} is not captured by any step or move on instrument`,
996
+ ),
997
+ );
998
+ return undefined;
999
+ }
1000
+ return {
1001
+ binding: { from: "instance", path: `refs.${refName}` },
1002
+ kind: captured,
1003
+ path: `refs.${refName}`,
1004
+ };
1005
+ }
928
1006
  issues.push(
929
1007
  issue(
930
1008
  "UDL2011",
@@ -1275,27 +1353,17 @@ export function resolveUdlActionPlans(
1275
1353
  consumedSources.add(holdKey);
1276
1354
  }
1277
1355
 
1356
+ // A library leaf is written once and expanded under several callers,
1357
+ // so its declaration names the operation's generic class (the class
1358
+ // with no bindings). Each expansion then carries the class its
1359
+ // resolved bindings select, exactly as an inline move would: a buyer
1360
+ // funding escrow is a pay-in, the same leaf paying out is internal.
1278
1361
  const expectedEffects = expectedLeafEffects(step);
1279
- const actualCounts = new Map<string, number>();
1280
- for (const eff of leaf.effects) {
1281
- const key = `${eff.kind}:${eff.signature}`;
1282
- actualCounts.set(key, (actualCounts.get(key) ?? 0) + 1);
1283
- }
1284
- const expectedCounts = new Map<string, number>();
1285
- for (const eff of expectedEffects) {
1286
- const key = `${eff.kind}:${eff.signature}`;
1287
- expectedCounts.set(key, (expectedCounts.get(key) ?? 0) + 1);
1288
- }
1289
- let effectsMatch = actualCounts.size === expectedCounts.size;
1290
- if (effectsMatch) {
1291
- for (const [k, count] of actualCounts.entries()) {
1292
- if (expectedCounts.get(k) !== count) {
1293
- effectsMatch = false;
1294
- break;
1295
- }
1296
- }
1297
- }
1298
- if (!effectsMatch) {
1362
+ const genericEffects = expectedLeafEffects({ ...step, bind: {} });
1363
+ if (
1364
+ !sameEffectSignatures(leaf.effects, expectedEffects) &&
1365
+ !sameEffectSignatures(leaf.effects, genericEffects)
1366
+ ) {
1299
1367
  issues.push(
1300
1368
  issue(
1301
1369
  "UDL2013",
@@ -1306,7 +1374,7 @@ export function resolveUdlActionPlans(
1306
1374
  }
1307
1375
 
1308
1376
  expandedLeaves.push({
1309
- effects: leaf.effects,
1377
+ effects: expectedEffects,
1310
1378
  evidence: leaf.evidence,
1311
1379
  originPath,
1312
1380
  step,
package/src/index.ts CHANGED
@@ -109,6 +109,7 @@ export {
109
109
  reconcileExceptionChildProblems,
110
110
  openReferenceShapeBudget,
111
111
  UdlError,
112
+ instrumentFinanceIssues,
112
113
  validateUdl,
113
114
  validateUdlJsonSchema,
114
115
  validateUdlSchemaValue,