@hyperscale0/udl 2.2.0 → 2.4.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 (88) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +5 -1
  3. package/conformance/invalid/invalid-journeys.expected.json +3 -19
  4. package/conformance/invalid/invalid-journeys.udl +47 -49
  5. package/conformance/valid/attested.expected.json +6 -0
  6. package/conformance/valid/attested.udl +251 -0
  7. package/conformance/valid/hand-edited.expected.json +1 -1
  8. package/conformance/valid/hand-edited.udl +1 -1
  9. package/conformance/valid/minimal.expected.json +1 -1
  10. package/conformance/valid/minimal.udl +0 -15
  11. package/conformance/valid/vocabulary.expected.json +6 -0
  12. package/conformance/valid/vocabulary.udl +1999 -0
  13. package/dist/allocation.d.ts +60 -0
  14. package/dist/allocation.d.ts.map +1 -0
  15. package/dist/allocation.js +177 -0
  16. package/dist/allocation.js.map +1 -0
  17. package/dist/diagnostics.d.ts +1 -31
  18. package/dist/diagnostics.d.ts.map +1 -1
  19. package/dist/diagnostics.js +0 -30
  20. package/dist/diagnostics.js.map +1 -1
  21. package/dist/distribution.d.ts +15 -0
  22. package/dist/distribution.d.ts.map +1 -0
  23. package/dist/distribution.js +49 -0
  24. package/dist/distribution.js.map +1 -0
  25. package/dist/effects.d.ts +0 -6
  26. package/dist/effects.d.ts.map +1 -1
  27. package/dist/effects.js +84 -22
  28. package/dist/effects.js.map +1 -1
  29. package/dist/evolution.d.ts +12 -0
  30. package/dist/evolution.d.ts.map +1 -1
  31. package/dist/evolution.js +42 -1
  32. package/dist/evolution.js.map +1 -1
  33. package/dist/finance.d.ts +18 -1
  34. package/dist/finance.d.ts.map +1 -1
  35. package/dist/finance.js +158 -27
  36. package/dist/finance.js.map +1 -1
  37. package/dist/index.d.ts +9 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/instrument-references.d.ts +5 -0
  42. package/dist/instrument-references.d.ts.map +1 -0
  43. package/dist/instrument-references.js +69 -0
  44. package/dist/instrument-references.js.map +1 -0
  45. package/dist/limits.d.ts +3 -3
  46. package/dist/limits.d.ts.map +1 -1
  47. package/dist/limits.js +3 -7
  48. package/dist/limits.js.map +1 -1
  49. package/dist/reference.d.ts +3 -0
  50. package/dist/reference.d.ts.map +1 -0
  51. package/dist/reference.js +28 -0
  52. package/dist/reference.js.map +1 -0
  53. package/dist/schema.d.ts +1232 -83
  54. package/dist/schema.d.ts.map +1 -1
  55. package/dist/schema.js +398 -60
  56. package/dist/schema.js.map +1 -1
  57. package/dist/validation.d.ts +27 -1
  58. package/dist/validation.d.ts.map +1 -1
  59. package/dist/validation.js +327 -187
  60. package/dist/validation.js.map +1 -1
  61. package/dist/vocabulary.d.ts +23 -0
  62. package/dist/vocabulary.d.ts.map +1 -0
  63. package/dist/vocabulary.js +965 -0
  64. package/dist/vocabulary.js.map +1 -0
  65. package/docs/README.md +5 -1
  66. package/docs/funding-custody.md +165 -0
  67. package/docs/guide/09-schedules-and-allocation.md +130 -0
  68. package/docs/llms-full.txt +623 -101
  69. package/docs/llms.txt +1 -1
  70. package/docs/piece-plans.md +148 -0
  71. package/docs/reference/clauses.md +451 -63
  72. package/docs/reference/cli.md +3 -1
  73. package/docs/reference/diagnostics.md +33 -38
  74. package/package.json +5 -6
  75. package/spec/udl.schema.json +1095 -118
  76. package/src/allocation.ts +259 -0
  77. package/src/diagnostics.ts +0 -32
  78. package/src/distribution.ts +61 -0
  79. package/src/effects.ts +116 -22
  80. package/src/evolution.ts +63 -3
  81. package/src/finance.ts +218 -24
  82. package/src/index.ts +31 -3
  83. package/src/instrument-references.ts +98 -0
  84. package/src/limits.ts +3 -7
  85. package/src/reference.ts +31 -0
  86. package/src/schema.ts +417 -66
  87. package/src/validation.ts +452 -243
  88. package/src/vocabulary.ts +1508 -0
package/src/validation.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { referencePatternPrefix } from "./reference.js";
2
+ import { validateVocabulary } from "./vocabulary.js";
1
3
  import {
2
4
  Validator,
3
5
  type OutputUnit,
@@ -19,11 +21,14 @@ import {
19
21
  type UdlInstrument,
20
22
  type UdlStep,
21
23
  type UdlAction,
24
+ type UdlPieceStageStage,
22
25
  } from "./schema.js";
23
26
  import { fixedIsoDurationMs } from "./duration.js";
24
27
  import {
25
28
  analyzeInstrumentFinance,
26
29
  financeAdmissionProblem,
30
+ type FinanceIssue,
31
+ type FinanceOptions,
27
32
  } from "./finance.js";
28
33
  import { UDL_LIMITS } from "./limits.js";
29
34
  import {
@@ -65,6 +70,12 @@ export interface UdlValidationOptions {
65
70
  readonly requireDecisionPartyBindings?: boolean;
66
71
  }
67
72
 
73
+ /**
74
+ * Validate a complete document. Clause references to instruments must resolve
75
+ * within this document, even when the reference field is optional. A catalogue
76
+ * slice must include its referenced instruments; an external catalogue cannot
77
+ * supply missing product laws. Reference fields use the sealed public ID grammar.
78
+ */
68
79
  export function validateUdl(
69
80
  value: unknown,
70
81
  options: UdlValidationOptions = {},
@@ -293,6 +304,26 @@ function semanticIssues(
293
304
  validateDocumentSchemas(document, add);
294
305
  if (issues.length > 0) return issues;
295
306
 
307
+ // Vocabulary laws must accumulate with the existing semantic diagnostics.
308
+ validateVocabulary(document.instruments, (path, message) =>
309
+ add(
310
+ path,
311
+ message,
312
+ path.some((part) =>
313
+ [
314
+ "attests",
315
+ "requiresInput",
316
+ "engineOwned",
317
+ "captureEngine",
318
+ "unique",
319
+ "port",
320
+ ].includes(String(part)),
321
+ )
322
+ ? "UDL5001"
323
+ : shapeIssueCode(path),
324
+ ),
325
+ );
326
+
296
327
  addDuplicateIssues(
297
328
  document.subjects.map((subject) => subject.kind),
298
329
  ["subjects"],
@@ -313,7 +344,6 @@ function semanticIssues(
313
344
  );
314
345
 
315
346
  validateCompositionDials(document, add);
316
- validateInstrumentJourneys(document, add);
317
347
 
318
348
  const subjects = new Map(
319
349
  document.subjects.map((subject) => [subject.kind, subject] as const),
@@ -413,162 +443,6 @@ function semanticIssues(
413
443
  return issues;
414
444
  }
415
445
 
416
- const scopedIdPattern =
417
- /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/;
418
-
419
- function camel(value: string): string {
420
- return value.replace(/_([a-z0-9])/g, (_match, character: string) =>
421
- character.toUpperCase(),
422
- );
423
- }
424
-
425
- function journeyReferenceFields(
426
- instrument: UdlInstrument,
427
- actionName: string,
428
- ): ReadonlyMap<string, string> {
429
- const action = instrument.actions[actionName];
430
- if (!action) return new Map();
431
- const fields = new Map<string, string>();
432
- const addSchema = (name: string, schema: unknown) => {
433
- const pattern = recordValue(schema).pattern;
434
- if (typeof pattern !== "string") return;
435
- const prefix = scopedIdPattern.exec(pattern)?.[1];
436
- if (prefix) fields.set(name, prefix);
437
- };
438
- if (actionName === "create") {
439
- const derivedFields = new Set(
440
- (action.requiresRefs ?? []).flatMap((gate) =>
441
- Object.keys(gate.bind ?? {}),
442
- ),
443
- );
444
- for (const name of instrument.required) {
445
- if (!derivedFields.has(name)) addSchema(name, instrument.fields[name]);
446
- }
447
- } else {
448
- fields.set(`${camel(instrument.id)}Id`, instrument.idPrefix);
449
- }
450
- const input = recordValue(action.input);
451
- const required = Array.isArray(input.required)
452
- ? input.required.filter((name): name is string => typeof name === "string")
453
- : [];
454
- const properties = recordValue(input.properties);
455
- for (const name of required) addSchema(name, properties[name]);
456
- return fields;
457
- }
458
-
459
- /**
460
- * Validate the part of authored journeys that canonical UDL can prove alone.
461
- * A caller with the operation catalog validates root-operation examples and
462
- * cross-kind bindings. UDL owns local examples, lifecycle order, and local
463
- * reference completeness so raw UDL cannot bypass those laws.
464
- */
465
- function validateInstrumentJourneys(
466
- document: UdlDocument,
467
- add: AddIssue,
468
- ): void {
469
- const instruments = new Map(
470
- document.instruments.map(
471
- (instrument) => [instrument.id, instrument] as const,
472
- ),
473
- );
474
- for (const [instrumentIndex, owner] of document.instruments.entries()) {
475
- for (const [journeyIndex, journey] of (owner.journeys ?? []).entries()) {
476
- const base = [
477
- "instruments",
478
- instrumentIndex,
479
- "journeys",
480
- journeyIndex,
481
- ] as const;
482
- const seen = new Set<string>();
483
- const createdKindByStep = new Map<string, string>();
484
- const stateByStep = new Map<string, string>();
485
- for (const [stepIndex, step] of journey.steps.entries()) {
486
- const stepBase = [...base, "steps", stepIndex] as const;
487
- if (step.id) {
488
- if (seen.has(step.id)) {
489
- add(
490
- [...stepBase, "id"],
491
- `journey ${journey.id} declares step id ${step.id} more than once`,
492
- "journey_duplicate_step_id",
493
- );
494
- }
495
- }
496
- const [instrumentId, actionName] = step.operation.split(".");
497
- const instrument = instrumentId
498
- ? instruments.get(instrumentId)
499
- : undefined;
500
- const action =
501
- instrument && actionName ? instrument.actions[actionName] : undefined;
502
- if (!instrument) {
503
- if (step.id) seen.add(step.id);
504
- continue;
505
- }
506
- if (!action || !actionName) {
507
- add(
508
- [...stepBase, "operation"],
509
- `journey ${journey.id} names unknown operation ${step.operation}`,
510
- "journey_unknown_operation",
511
- );
512
- continue;
513
- }
514
- if (
515
- !action.examples?.some((example) => example.name === step.example)
516
- ) {
517
- add(
518
- [...stepBase, "example"],
519
- `journey ${journey.id} names unknown example ${step.example} on ${step.operation}`,
520
- "journey_unknown_example",
521
- );
522
- }
523
- for (const [field, prefix] of journeyReferenceFields(
524
- instrument,
525
- actionName,
526
- )) {
527
- const producer = step.bind[field];
528
- if (!producer || !seen.has(producer)) {
529
- add(
530
- [...stepBase, "bind", field],
531
- `journey ${journey.id} must bind ${step.operation}.${field} to an earlier step`,
532
- "journey_unbound_reference",
533
- );
534
- continue;
535
- }
536
- const producedPrefix = createdKindByStep.get(producer);
537
- if (producedPrefix && producedPrefix !== prefix) {
538
- add(
539
- [...stepBase, "bind", field],
540
- `journey ${journey.id} binds ${step.operation}.${field} to ${producer}, which creates ${producedPrefix} instead of ${prefix}`,
541
- "journey_unbound_reference",
542
- );
543
- }
544
- }
545
- if (actionName === "create") {
546
- if (step.id) {
547
- createdKindByStep.set(step.id, instrument.idPrefix);
548
- stateByStep.set(step.id, instrument.lifecycle.initial);
549
- seen.add(step.id);
550
- }
551
- continue;
552
- }
553
- const transition = instrument.lifecycle.transitions[actionName];
554
- if (!transition) continue;
555
- const ownProducer = step.bind[`${camel(instrument.id)}Id`];
556
- const state = ownProducer ? stateByStep.get(ownProducer) : undefined;
557
- if (state && !transition.from.includes(state)) {
558
- add(
559
- [...stepBase, "operation"],
560
- `journey ${journey.id} runs ${step.operation} from ${state}; allowed states are ${transition.from.join(", ")}`,
561
- "journey_invalid_transition",
562
- );
563
- } else if (ownProducer) {
564
- stateByStep.set(ownProducer, transition.to);
565
- }
566
- if (step.id) seen.add(step.id);
567
- }
568
- }
569
- }
570
- }
571
-
572
446
  function structuralBudgetIssue(value: unknown): UdlIssue | undefined {
573
447
  let discovered = 1;
574
448
  let nodes = 0;
@@ -1365,90 +1239,366 @@ function validateInstrument(
1365
1239
  }
1366
1240
  }
1367
1241
 
1368
- // Multi-variant finance oracle check across action plan combinations
1242
+ for (const finIssue of instrumentFinanceIssues(instrument, {
1243
+ plans: planResolution.plans,
1244
+ })) {
1245
+ add([...base, ...finIssue.path], finIssue.message, finIssue.code);
1246
+ }
1247
+ validateAggregates(instrument, base, instruments, references, add);
1248
+ }
1249
+
1250
+ /**
1251
+ * The finance oracle over an instrument's resolved action plans, with paths
1252
+ * rooted at the instrument. A piece-plan instrument is unfolded over the
1253
+ * runtime's piece progress; any other instrument runs once per action plan
1254
+ * combination. Contract-side callers pass the UDL projection of a blueprint
1255
+ * definition so both boundaries prove the same machine.
1256
+ */
1257
+ export function instrumentFinanceIssues(
1258
+ instrument: UdlInstrument,
1259
+ options: FinanceOptions & {
1260
+ readonly plans?: readonly ResolvedActionPlan[];
1261
+ } = {},
1262
+ ): readonly FinanceIssue[] {
1263
+ const plans = options.plans ?? resolveUdlActionPlans(instrument).plans;
1264
+ const financeOptions: FinanceOptions =
1265
+ options.penaltyMayBeNonzero === undefined
1266
+ ? {}
1267
+ : { penaltyMayBeNonzero: options.penaltyMayBeNonzero };
1268
+ const issues: FinanceIssue[] = [];
1269
+ const seen = new Set<string>();
1270
+ const add = (
1271
+ path: readonly PropertyKey[],
1272
+ message: string,
1273
+ code: UdlIssueCode,
1274
+ ): void => {
1275
+ const key = `${code}:${path.join(".")}:${message}`;
1276
+ if (seen.has(key)) return;
1277
+ seen.add(key);
1278
+ issues.push({ code, message, path });
1279
+ };
1280
+
1281
+ const expansion = instrument.piecePlan
1282
+ ? expandPieceProgress(instrument, plans)
1283
+ : undefined;
1284
+ if (expansion === "bound") {
1285
+ add(
1286
+ ["actions"],
1287
+ `piece progress expansion exceeds reachable variant bound of ${UDL_LIMITS.maxActionExpansion}`,
1288
+ "UDL2010",
1289
+ );
1290
+ return issues;
1291
+ }
1292
+ if (expansion) {
1293
+ for (const finIssue of analyzeInstrumentFinance(
1294
+ expansion.instrument,
1295
+ financeOptions,
1296
+ )) {
1297
+ const message = expansion.displayNames.reduce(
1298
+ (text, [expanded, display]) => text.replaceAll(expanded, display),
1299
+ finIssue.message,
1300
+ );
1301
+ add(originFinancePath(expansion, finIssue.path), message, finIssue.code);
1302
+ }
1303
+ return issues;
1304
+ }
1305
+
1369
1306
  const actionsWithPlans = Object.keys(instrument.actions).filter((aName) =>
1370
- planResolution.plans.some((p) => p.action === aName),
1307
+ plans.some((p) => p.action === aName),
1371
1308
  );
1372
-
1373
1309
  let totalCombinations = 1;
1374
1310
  const actionPlanMap: Record<string, ResolvedActionPlan[]> = {};
1375
1311
  for (const aName of actionsWithPlans) {
1376
- const actionPlans = planResolution.plans.filter((p) => p.action === aName);
1312
+ const actionPlans = plans.filter((p) => p.action === aName);
1377
1313
  actionPlanMap[aName] = actionPlans;
1378
1314
  totalCombinations *= actionPlans.length;
1379
1315
  }
1380
-
1381
1316
  if (
1382
1317
  actionsWithPlans.length > 0 &&
1383
1318
  totalCombinations > UDL_LIMITS.maxActionExpansion
1384
1319
  ) {
1385
1320
  add(
1386
- [...base, "actions"],
1321
+ ["actions"],
1387
1322
  `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`,
1388
1323
  "UDL2010",
1389
1324
  );
1390
- } else {
1391
- const generateCombos = (
1392
- keys: string[],
1393
- ): Record<string, ResolvedActionPlan>[] => {
1394
- if (keys.length === 0) return [{}];
1395
- const [first, ...rest] = keys;
1396
- const restCombos = generateCombos(rest);
1397
- const result: Record<string, ResolvedActionPlan>[] = [];
1398
- for (const plan of actionPlanMap[first!]!) {
1399
- for (const combo of restCombos) {
1400
- result.push({ ...combo, [first!]: plan });
1401
- }
1325
+ return issues;
1326
+ }
1327
+ const generateCombos = (
1328
+ keys: string[],
1329
+ ): Record<string, ResolvedActionPlan>[] => {
1330
+ if (keys.length === 0) return [{}];
1331
+ const [first, ...rest] = keys;
1332
+ const restCombos = generateCombos(rest);
1333
+ const result: Record<string, ResolvedActionPlan>[] = [];
1334
+ for (const plan of actionPlanMap[first!]!) {
1335
+ for (const combo of restCombos) {
1336
+ result.push({ ...combo, [first!]: plan });
1402
1337
  }
1403
- return result;
1404
- };
1338
+ }
1339
+ return result;
1340
+ };
1341
+ const combinations =
1342
+ actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
1343
+ for (const combo of combinations) {
1344
+ const expandedActions: Record<string, UdlAction> = {};
1345
+ for (const [aName, aDef] of Object.entries(instrument.actions)) {
1346
+ expandedActions[aName] = planExpandedAction(aDef, combo[aName]);
1347
+ }
1348
+ for (const finIssue of analyzeInstrumentFinance(
1349
+ { ...instrument, actions: expandedActions },
1350
+ financeOptions,
1351
+ )) {
1352
+ add(finIssue.path, finIssue.message, finIssue.code);
1353
+ }
1354
+ }
1355
+ return issues;
1356
+ }
1405
1357
 
1406
- const combinations =
1407
- actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
1408
- const seenFinanceIssues = new Set<string>();
1358
+ /**
1359
+ * Only an action that moves money through calls is replaced by its expanded
1360
+ * leaves. Authored moves and steps always reach the oracle.
1361
+ */
1362
+ function planExpandedAction(
1363
+ definition: UdlAction,
1364
+ plan: ResolvedActionPlan | undefined,
1365
+ ): UdlAction {
1366
+ if (!plan || (definition.calls?.length ?? 0) === 0) return definition;
1367
+ const steps: UdlStep[] = [];
1368
+ const moves: UdlMove[] = [];
1369
+ for (const leaf of plan.leaves) {
1370
+ if ("key" in leaf.step) {
1371
+ moves.push(leaf.step as UdlMove);
1372
+ } else {
1373
+ steps.push(leaf.step as UdlStep);
1374
+ }
1375
+ }
1376
+ return { ...definition, moves, steps };
1377
+ }
1409
1378
 
1410
- for (const combo of combinations) {
1411
- const expandedActions: Record<
1412
- string,
1413
- (typeof instrument.actions)[string]
1414
- > = {};
1415
- for (const [aName, aDef] of Object.entries(instrument.actions)) {
1416
- const plan = combo[aName];
1417
- // Only an action that moves money through calls is replaced by its
1418
- // expanded leaves. Authored moves and steps always reach the oracle.
1419
- if (plan && (aDef.calls?.length ?? 0) > 0) {
1420
- const steps: UdlStep[] = [];
1421
- const moves: UdlMove[] = [];
1422
- for (const leaf of plan.leaves) {
1423
- if ("key" in leaf.step) {
1424
- moves.push(leaf.step as UdlMove);
1425
- } else {
1426
- steps.push(leaf.step as UdlStep);
1427
- }
1428
- }
1429
- expandedActions[aName] = {
1430
- ...aDef,
1431
- moves,
1432
- steps,
1433
- };
1434
- } else {
1435
- expandedActions[aName] = aDef;
1436
- }
1379
+ interface PieceProgress {
1380
+ readonly funded: readonly string[];
1381
+ readonly consumed: readonly string[];
1382
+ }
1383
+
1384
+ interface PieceProgressExpansion {
1385
+ readonly instrument: UdlInstrument;
1386
+ /** Expanded action name to display name, longest first. */
1387
+ readonly displayNames: readonly (readonly [string, string])[];
1388
+ readonly actionOrigins: ReadonlyMap<
1389
+ string,
1390
+ {
1391
+ readonly action: string;
1392
+ readonly leafOrigins?: readonly (readonly string[])[];
1393
+ }
1394
+ >;
1395
+ readonly stateOrigins: ReadonlyMap<string, string>;
1396
+ readonly originStates: readonly string[];
1397
+ }
1398
+
1399
+ function progressKey(progress: PieceProgress): string {
1400
+ return `${progress.funded.join(",")}|${progress.consumed.join(",")}`;
1401
+ }
1402
+
1403
+ /** Mirrors eligiblePieces in the engine's piece-plan dispatcher. */
1404
+ function eligiblePieces(
1405
+ plan: NonNullable<UdlInstrument["piecePlan"]>,
1406
+ stage: UdlPieceStageStage,
1407
+ progress: PieceProgress,
1408
+ ): readonly string[] {
1409
+ return plan[`${stage}_order`].filter((id) =>
1410
+ stage === "fund"
1411
+ ? !progress.funded.includes(id)
1412
+ : progress.funded.includes(id) && !progress.consumed.includes(id),
1413
+ );
1414
+ }
1415
+
1416
+ /**
1417
+ * Unfolds a piece-plan instrument over the runtime's piece progress so the
1418
+ * ordinary lifecycle oracle sees exactly the states the dispatcher admits: a
1419
+ * piece-stage action moves the next eligible piece of its stage order, the
1420
+ * lifecycle state is retained until the stage's last piece moves, funding
1421
+ * cannot resume once a piece has left escrow, and an action gated on a drained
1422
+ * account is closed while a funded piece is still held. Every expanded state is
1423
+ * one (lifecycle state, progress) pair; expanded action names carry the piece
1424
+ * and the source state so each has exactly one transition. A quote-commit pair
1425
+ * is not carried through the expansion.
1426
+ */
1427
+ function expandPieceProgress(
1428
+ instrument: UdlInstrument,
1429
+ plans: readonly ResolvedActionPlan[],
1430
+ ): PieceProgressExpansion | "bound" | undefined {
1431
+ const plan = instrument.piecePlan;
1432
+ if (!plan) return undefined;
1433
+ const stateName = (state: string, progress: PieceProgress): string =>
1434
+ `${state}#${progressKey(progress)}`;
1435
+ const initialProgress: PieceProgress = { funded: [], consumed: [] };
1436
+ const stateOrigins = new Map<string, string>();
1437
+ const actionOrigins = new Map<
1438
+ string,
1439
+ { action: string; leafOrigins?: readonly (readonly string[])[] }
1440
+ >();
1441
+ const displayNames: [string, string][] = [];
1442
+ const actions: Record<string, UdlAction> = {};
1443
+ const transitions: Record<string, { from: string[]; to: string }> = {};
1444
+ const pending: { state: string; progress: PieceProgress }[] = [
1445
+ { state: instrument.lifecycle.initial, progress: initialProgress },
1446
+ ];
1447
+ stateOrigins.set(
1448
+ stateName(instrument.lifecycle.initial, initialProgress),
1449
+ instrument.lifecycle.initial,
1450
+ );
1451
+
1452
+ const visit = (state: string, progress: PieceProgress): void => {
1453
+ const name = stateName(state, progress);
1454
+ if (stateOrigins.has(name)) return;
1455
+ stateOrigins.set(name, state);
1456
+ pending.push({ state, progress });
1457
+ };
1458
+
1459
+ while (pending.length > 0) {
1460
+ const { state, progress } = pending.shift()!;
1461
+ if (stateOrigins.size > UDL_LIMITS.maxActionExpansion) return "bound";
1462
+ const held = progress.funded.filter(
1463
+ (id) => !progress.consumed.includes(id),
1464
+ );
1465
+ for (const [actionName, transition] of Object.entries(
1466
+ instrument.lifecycle.transitions,
1467
+ )) {
1468
+ if (!transition.from.includes(state)) continue;
1469
+ const definition = instrument.actions[actionName];
1470
+ if (!definition) continue;
1471
+ if (definition.requiresDrainedAccount && held.length > 0) continue;
1472
+ const stage = definition.pieceStage;
1473
+ if (!stage) {
1474
+ const expanded = `${actionName}#${state}#${progressKey(progress)}`;
1475
+ const variant = plans.find(
1476
+ (candidate) => candidate.action === actionName,
1477
+ );
1478
+ actions[expanded] = planExpandedAction(definition, variant);
1479
+ transitions[expanded] = {
1480
+ from: [stateName(state, progress)],
1481
+ to: stateName(transition.to, progress),
1482
+ };
1483
+ actionOrigins.set(expanded, {
1484
+ action: actionName,
1485
+ ...(variant && (definition.calls?.length ?? 0) > 0
1486
+ ? { leafOrigins: variant.leaves.map((leaf) => leaf.originPath) }
1487
+ : {}),
1488
+ });
1489
+ displayNames.push([expanded, actionName]);
1490
+ visit(transition.to, progress);
1491
+ continue;
1437
1492
  }
1438
- const financeInstrument = {
1439
- ...instrument,
1440
- actions: expandedActions,
1493
+ if (stage.plan !== plan.id) continue;
1494
+ if (stage.stage === "fund" && progress.consumed.length > 0) continue;
1495
+ const eligible = eligiblePieces(plan, stage.stage, progress);
1496
+ const pieceId = eligible[0];
1497
+ if (pieceId === undefined) continue;
1498
+ const variant = plans.find(
1499
+ (candidate) =>
1500
+ candidate.action === actionName && candidate.pieceId === pieceId,
1501
+ );
1502
+ if (!variant) continue;
1503
+ const next: PieceProgress =
1504
+ stage.stage === "fund"
1505
+ ? {
1506
+ funded: [...progress.funded, pieceId],
1507
+ consumed: progress.consumed,
1508
+ }
1509
+ : {
1510
+ funded: progress.funded,
1511
+ consumed: [...progress.consumed, pieceId],
1512
+ };
1513
+ const stageComplete =
1514
+ eligiblePieces(plan, stage.stage, next).length === 0;
1515
+ const target = stageComplete ? transition.to : state;
1516
+ const expanded = `${actionName}@${pieceId}#${state}#${progressKey(progress)}`;
1517
+ actions[expanded] = planExpandedAction(definition, variant);
1518
+ transitions[expanded] = {
1519
+ from: [stateName(state, progress)],
1520
+ to: stateName(target, next),
1441
1521
  };
1442
- for (const finIssue of analyzeInstrumentFinance(financeInstrument)) {
1443
- const issueKey = `${finIssue.code}:${finIssue.path.join(".")}:${finIssue.message}`;
1444
- if (!seenFinanceIssues.has(issueKey)) {
1445
- seenFinanceIssues.add(issueKey);
1446
- add([...base, ...finIssue.path], finIssue.message, finIssue.code);
1447
- }
1448
- }
1522
+ actionOrigins.set(expanded, {
1523
+ action: actionName,
1524
+ leafOrigins: variant.leaves.map((leaf) => leaf.originPath),
1525
+ });
1526
+ displayNames.push([expanded, `${actionName}[${pieceId}]`]);
1527
+ visit(target, next);
1449
1528
  }
1450
1529
  }
1451
- validateAggregates(instrument, base, instruments, references, add);
1530
+
1531
+ for (const [actionName, definition] of Object.entries(instrument.actions)) {
1532
+ if (Object.hasOwn(instrument.lifecycle.transitions, actionName)) continue;
1533
+ actions[actionName] = planExpandedAction(
1534
+ definition,
1535
+ plans.find((candidate) => candidate.action === actionName),
1536
+ );
1537
+ actionOrigins.set(actionName, { action: actionName });
1538
+ }
1539
+
1540
+ return {
1541
+ instrument: {
1542
+ ...instrument,
1543
+ actions,
1544
+ lifecycle: {
1545
+ initial: stateName(instrument.lifecycle.initial, initialProgress),
1546
+ states: [...stateOrigins.keys()],
1547
+ transitions,
1548
+ },
1549
+ },
1550
+ displayNames: [
1551
+ ...displayNames,
1552
+ ...[...stateOrigins.entries()].map(
1553
+ ([expanded, origin]): [string, string] => [expanded, origin],
1554
+ ),
1555
+ ].sort((left, right) => right[0].length - left[0].length),
1556
+ actionOrigins,
1557
+ stateOrigins,
1558
+ originStates: instrument.lifecycle.states,
1559
+ };
1560
+ }
1561
+
1562
+ function originFinancePath(
1563
+ expansion: PieceProgressExpansion,
1564
+ path: readonly PropertyKey[],
1565
+ ): readonly PropertyKey[] {
1566
+ const [head, second, third, fourth, ...rest] = path;
1567
+ if (
1568
+ head === "lifecycle" &&
1569
+ second === "states" &&
1570
+ typeof third === "number"
1571
+ ) {
1572
+ const expandedState = expansion.instrument.lifecycle.states[third];
1573
+ const origin =
1574
+ expandedState === undefined
1575
+ ? undefined
1576
+ : expansion.stateOrigins.get(expandedState);
1577
+ const index =
1578
+ origin === undefined ? -1 : expansion.originStates.indexOf(origin);
1579
+ return index >= 0
1580
+ ? ["lifecycle", "states", index]
1581
+ : ["lifecycle", "states"];
1582
+ }
1583
+ if (head === "actions" && typeof second === "string") {
1584
+ const origin = expansion.actionOrigins.get(second);
1585
+ if (!origin) return path;
1586
+ if (
1587
+ third === "moves" &&
1588
+ typeof fourth === "number" &&
1589
+ origin.leafOrigins?.[fourth]
1590
+ ) {
1591
+ return ["actions", ...origin.leafOrigins[fourth], ...rest];
1592
+ }
1593
+ return [
1594
+ "actions",
1595
+ origin.action,
1596
+ ...(third === undefined ? [] : [third]),
1597
+ ...(fourth === undefined ? [] : [fourth]),
1598
+ ...rest,
1599
+ ];
1600
+ }
1601
+ return path;
1452
1602
  }
1453
1603
 
1454
1604
  function validatePiecePlan(
@@ -2637,7 +2787,11 @@ function validateActionUpdates(
2637
2787
  add(fieldPath, `updated field ${field} is an aggregate cap`, "UDL5008");
2638
2788
  }
2639
2789
  });
2640
- if (definition.moves.length > 0) {
2790
+ if (
2791
+ definition.moves.length > 0 ||
2792
+ definition.allocate ||
2793
+ definition.contributionStage
2794
+ ) {
2641
2795
  add(
2642
2796
  [...actionBase, "updates"],
2643
2797
  "an action cannot update fields while moving money",
@@ -2716,6 +2870,33 @@ function validateRemainder(
2716
2870
  "UDL4001",
2717
2871
  );
2718
2872
  }
2873
+ addDuplicateIssues(
2874
+ remainder.subtractPaths ?? [],
2875
+ [...remainderBase, "subtractPaths"],
2876
+ "remainder operand",
2877
+ add,
2878
+ );
2879
+ for (const path of remainder.subtractPaths ?? []) {
2880
+ const [root, key] = path.split(".");
2881
+ const declared =
2882
+ root === "fields" &&
2883
+ key !== undefined &&
2884
+ isMoneySchema(instrument.fields[key] ?? {}) &&
2885
+ !instrument.update?.fields.includes(key) &&
2886
+ !Object.values(instrument.actions).some((action) =>
2887
+ action.updates?.includes(key),
2888
+ );
2889
+ if (
2890
+ !declared ||
2891
+ path === remainder.totalPath ||
2892
+ path === `refs.${remainder.amountRef}`
2893
+ )
2894
+ add(
2895
+ [...remainderBase, "subtractPaths"],
2896
+ `remainder subtraction ${path} must name distinct immutable money`,
2897
+ "UDL4001",
2898
+ );
2899
+ }
2719
2900
  const [totalRoot, totalKey] = remainder.totalPath.split(".");
2720
2901
  const totalDeclared =
2721
2902
  (totalRoot === "fields" &&
@@ -2910,10 +3091,24 @@ function validateActions(
2910
3091
  );
2911
3092
 
2912
3093
  if (definition.requiresRefs) {
3094
+ const boundFields = new Map<string, string>();
3095
+ for (const gate of definition.requiresRefs) {
3096
+ for (const [field, path] of Object.entries(gate.bind ?? {})) {
3097
+ const source = `${gate.field}:${path}`;
3098
+ const previous = boundFields.get(field);
3099
+ if (previous !== undefined && previous !== source)
3100
+ add(
3101
+ [...actionBase, "requiresRefs"],
3102
+ `reference gates bind ${field} from conflicting sources`,
3103
+ "UDL5001",
3104
+ );
3105
+ boundFields.set(field, source);
3106
+ }
3107
+ }
2913
3108
  addDuplicateIssues(
2914
- definition.requiresRefs.map((gate) => gate.field),
3109
+ definition.requiresRefs.map((gate) => JSON.stringify(sortObject(gate))),
2915
3110
  [...actionBase, "requiresRefs"],
2916
- "gate field",
3111
+ "gate",
2917
3112
  add,
2918
3113
  );
2919
3114
  definition.requiresRefs.forEach((gate, gateIndex) => {
@@ -3023,6 +3218,7 @@ function validateActions(
3023
3218
  ...Object.entries(instrument.actions).flatMap(
3024
3219
  ([candidateAction, candidate]) => [
3025
3220
  ...Object.keys(candidate.captureInput ?? {}),
3221
+ ...Object.keys(candidate.captureEngine ?? {}),
3026
3222
  ...[...candidate.steps, ...candidate.moves].flatMap((step) =>
3027
3223
  Object.keys(step.capture ?? {}),
3028
3224
  ),
@@ -3223,7 +3419,7 @@ function validateActions(
3223
3419
  );
3224
3420
  }
3225
3421
  if (
3226
- definition.due.offset &&
3422
+ typeof definition.due.offset === "string" &&
3227
3423
  fixedIsoDurationMs(definition.due.offset) === null
3228
3424
  ) {
3229
3425
  add(
@@ -3258,7 +3454,7 @@ function validateActions(
3258
3454
  );
3259
3455
  }
3260
3456
  if (
3261
- definition.deadline.offset &&
3457
+ typeof definition.deadline.offset === "string" &&
3262
3458
  fixedIsoDurationMs(definition.deadline.offset) === null
3263
3459
  ) {
3264
3460
  add(
@@ -3858,9 +4054,10 @@ function validatePayoutsAndSettlement(
3858
4054
  }
3859
4055
 
3860
4056
  const reservedRefs = new Set([
3861
- ...Object.values(instrument.actions).flatMap((action) =>
3862
- Object.keys(action.captureInput ?? {}),
3863
- ),
4057
+ ...Object.values(instrument.actions).flatMap((action) => [
4058
+ ...Object.keys(action.captureInput ?? {}),
4059
+ ...Object.keys(action.captureEngine ?? {}),
4060
+ ]),
3864
4061
  ...Object.values(instrument.actions).flatMap((action) =>
3865
4062
  [...action.steps, ...action.moves].flatMap((step) =>
3866
4063
  Object.keys(step.capture ?? {}),
@@ -3887,6 +4084,9 @@ function validatePayoutsAndSettlement(
3887
4084
  ]
3888
4085
  : [],
3889
4086
  ),
4087
+ ...Object.values(instrument.actions).flatMap((action) =>
4088
+ action.allocate ? [action.allocate.capture] : [],
4089
+ ),
3890
4090
  ...quoteRefKeys(instrument),
3891
4091
  ...(instrument.subject ? ["subject"] : []),
3892
4092
  ]);
@@ -4419,9 +4619,10 @@ function declaredRefKeys(instrument: UdlInstrument): ReadonlySet<string> {
4419
4619
  ...Object.values(instrument.actions).flatMap((action) =>
4420
4620
  (action.reconcile ?? []).map((reconcile) => reconcile.capture),
4421
4621
  ),
4422
- ...Object.values(instrument.actions).flatMap((action) =>
4423
- Object.keys(action.captureInput ?? {}),
4424
- ),
4622
+ ...Object.values(instrument.actions).flatMap((action) => [
4623
+ ...Object.keys(action.captureInput ?? {}),
4624
+ ...Object.keys(action.captureEngine ?? {}),
4625
+ ]),
4425
4626
  ...Object.values(instrument.actions).flatMap((action) =>
4426
4627
  [...action.steps, ...action.moves].flatMap((step) =>
4427
4628
  Object.keys(step.capture ?? {}),
@@ -4435,6 +4636,9 @@ function declaredRefKeys(instrument: UdlInstrument): ReadonlySet<string> {
4435
4636
  ]
4436
4637
  : [],
4437
4638
  ),
4639
+ ...Object.values(instrument.actions).flatMap((action) =>
4640
+ action.allocate ? [action.allocate.capture] : [],
4641
+ ),
4438
4642
  ...quoteRefKeys(instrument),
4439
4643
  ...(instrument.subject ? ["subject"] : []),
4440
4644
  ]);
@@ -5275,34 +5479,28 @@ function reconcileExceptionProblemMessage(
5275
5479
  * are written once. The value is disposable: no document ever carries it, and
5276
5480
  * a host's real id grammar stays the host's business.
5277
5481
  */
5278
- const probeIdFor = (prefix: string): string =>
5279
- `${prefix}_sandbox_0123456789abcdef`;
5280
-
5281
- /** A prefix no host mints, so a schema that accepts it accepts anything. */
5282
- const UNCLAIMED_PREFIX = "zzzz";
5283
-
5482
+ /** Classify the sealed ID pattern without compiling document-authored regexes. */
5284
5483
  export function openReferenceShapeBudget(): ReferenceShapeBudget {
5285
5484
  const answers = new WeakMap<object, Map<string, boolean>>();
5286
5485
  let probes = 0;
5486
+ let exhausted = false;
5287
5487
  return {
5288
5488
  accepts(schema, prefix) {
5289
5489
  const seen = answers.get(schema);
5290
5490
  const cached = seen?.get(prefix);
5291
5491
  if (cached !== undefined) return cached;
5292
- if (probes >= UDL_LIMITS.maxSchemaProbes) return false;
5492
+ if (probes >= UDL_LIMITS.maxSchemaProbes) {
5493
+ exhausted = true;
5494
+ return false;
5495
+ }
5293
5496
  probes += 1;
5294
-
5295
- const answer =
5296
- validateUdlSchemaValue(schema, probeIdFor(prefix)).errors.length ===
5297
- 0 &&
5298
- validateUdlSchemaValue(schema, probeIdFor(UNCLAIMED_PREFIX)).errors
5299
- .length > 0;
5497
+ const answer = referencePatternPrefix(schema) === prefix;
5300
5498
  if (seen) seen.set(prefix, answer);
5301
5499
  else answers.set(schema, new Map([[prefix, answer]]));
5302
5500
  return answer;
5303
5501
  },
5304
5502
  get exhausted() {
5305
- return probes >= UDL_LIMITS.maxSchemaProbes;
5503
+ return exhausted;
5306
5504
  },
5307
5505
  };
5308
5506
  }
@@ -5447,3 +5645,14 @@ function jsonPath(path: readonly PropertyKey[]): string {
5447
5645
  }
5448
5646
  return result;
5449
5647
  }
5648
+
5649
+ function sortObject(value: unknown): unknown {
5650
+ if (Array.isArray(value)) return value.map(sortObject);
5651
+ if (value !== null && typeof value === "object")
5652
+ return Object.fromEntries(
5653
+ Object.entries(value)
5654
+ .sort(([a], [b]) => a.localeCompare(b))
5655
+ .map(([key, item]) => [key, sortObject(item)]),
5656
+ );
5657
+ return value;
5658
+ }