@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
@@ -1,3 +1,5 @@
1
+ import { referencePatternPrefix } from "./reference.js";
2
+ import { validateVocabulary } from "./vocabulary.js";
1
3
  import { Validator, } from "@cfworker/json-schema";
2
4
  import { z } from "zod";
3
5
  import { udlClauseVocabulary, quoteExpiresAtRefKey, quoteFrozenRefKey, quoteSeededRefKeys, udlDocumentSchema, } from "./schema.js";
@@ -17,6 +19,12 @@ export class UdlError extends Error {
17
19
  this.issues = Object.freeze([...issues]);
18
20
  }
19
21
  }
22
+ /**
23
+ * Validate a complete document. Clause references to instruments must resolve
24
+ * within this document, even when the reference field is optional. A catalogue
25
+ * slice must include its referenced instruments; an external catalogue cannot
26
+ * supply missing product laws. Reference fields use the sealed public ID grammar.
27
+ */
20
28
  export function validateUdl(value, options = {}) {
21
29
  const resourceIssue = structuralBudgetIssue(value);
22
30
  if (resourceIssue)
@@ -211,11 +219,21 @@ function semanticIssues(document, references, options) {
211
219
  validateDocumentSchemas(document, add);
212
220
  if (issues.length > 0)
213
221
  return issues;
222
+ // Vocabulary laws must accumulate with the existing semantic diagnostics.
223
+ validateVocabulary(document.instruments, (path, message) => add(path, message, path.some((part) => [
224
+ "attests",
225
+ "requiresInput",
226
+ "engineOwned",
227
+ "captureEngine",
228
+ "unique",
229
+ "port",
230
+ ].includes(String(part)))
231
+ ? "UDL5001"
232
+ : shapeIssueCode(path)));
214
233
  addDuplicateIssues(document.subjects.map((subject) => subject.kind), ["subjects"], "subject kind", add);
215
234
  addDuplicateIssues(document.instruments.map((instrument) => instrument.id), ["instruments"], "instrument id", add);
216
235
  addDuplicateIssues(document.instruments.map((instrument) => instrument.idPrefix), ["instruments"], "instrument idPrefix", add);
217
236
  validateCompositionDials(document, add);
218
- validateInstrumentJourneys(document, add);
219
237
  const subjects = new Map(document.subjects.map((subject) => [subject.kind, subject]));
220
238
  const instruments = new Map(document.instruments.map((instrument) => [instrument.id, instrument]));
221
239
  for (const [subjectIndex, subject] of document.subjects.entries()) {
@@ -280,121 +298,6 @@ function semanticIssues(document, references, options) {
280
298
  }
281
299
  return issues;
282
300
  }
283
- const scopedIdPattern = /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/;
284
- function camel(value) {
285
- return value.replace(/_([a-z0-9])/g, (_match, character) => character.toUpperCase());
286
- }
287
- function journeyReferenceFields(instrument, actionName) {
288
- const action = instrument.actions[actionName];
289
- if (!action)
290
- return new Map();
291
- const fields = new Map();
292
- const addSchema = (name, schema) => {
293
- const pattern = recordValue(schema).pattern;
294
- if (typeof pattern !== "string")
295
- return;
296
- const prefix = scopedIdPattern.exec(pattern)?.[1];
297
- if (prefix)
298
- fields.set(name, prefix);
299
- };
300
- if (actionName === "create") {
301
- const derivedFields = new Set((action.requiresRefs ?? []).flatMap((gate) => Object.keys(gate.bind ?? {})));
302
- for (const name of instrument.required) {
303
- if (!derivedFields.has(name))
304
- addSchema(name, instrument.fields[name]);
305
- }
306
- }
307
- else {
308
- fields.set(`${camel(instrument.id)}Id`, instrument.idPrefix);
309
- }
310
- const input = recordValue(action.input);
311
- const required = Array.isArray(input.required)
312
- ? input.required.filter((name) => typeof name === "string")
313
- : [];
314
- const properties = recordValue(input.properties);
315
- for (const name of required)
316
- addSchema(name, properties[name]);
317
- return fields;
318
- }
319
- /**
320
- * Validate the part of authored journeys that canonical UDL can prove alone.
321
- * A caller with the operation catalog validates root-operation examples and
322
- * cross-kind bindings. UDL owns local examples, lifecycle order, and local
323
- * reference completeness so raw UDL cannot bypass those laws.
324
- */
325
- function validateInstrumentJourneys(document, add) {
326
- const instruments = new Map(document.instruments.map((instrument) => [instrument.id, instrument]));
327
- for (const [instrumentIndex, owner] of document.instruments.entries()) {
328
- for (const [journeyIndex, journey] of (owner.journeys ?? []).entries()) {
329
- const base = [
330
- "instruments",
331
- instrumentIndex,
332
- "journeys",
333
- journeyIndex,
334
- ];
335
- const seen = new Set();
336
- const createdKindByStep = new Map();
337
- const stateByStep = new Map();
338
- for (const [stepIndex, step] of journey.steps.entries()) {
339
- const stepBase = [...base, "steps", stepIndex];
340
- if (step.id) {
341
- if (seen.has(step.id)) {
342
- add([...stepBase, "id"], `journey ${journey.id} declares step id ${step.id} more than once`, "journey_duplicate_step_id");
343
- }
344
- }
345
- const [instrumentId, actionName] = step.operation.split(".");
346
- const instrument = instrumentId
347
- ? instruments.get(instrumentId)
348
- : undefined;
349
- const action = instrument && actionName ? instrument.actions[actionName] : undefined;
350
- if (!instrument) {
351
- if (step.id)
352
- seen.add(step.id);
353
- continue;
354
- }
355
- if (!action || !actionName) {
356
- add([...stepBase, "operation"], `journey ${journey.id} names unknown operation ${step.operation}`, "journey_unknown_operation");
357
- continue;
358
- }
359
- if (!action.examples?.some((example) => example.name === step.example)) {
360
- add([...stepBase, "example"], `journey ${journey.id} names unknown example ${step.example} on ${step.operation}`, "journey_unknown_example");
361
- }
362
- for (const [field, prefix] of journeyReferenceFields(instrument, actionName)) {
363
- const producer = step.bind[field];
364
- if (!producer || !seen.has(producer)) {
365
- add([...stepBase, "bind", field], `journey ${journey.id} must bind ${step.operation}.${field} to an earlier step`, "journey_unbound_reference");
366
- continue;
367
- }
368
- const producedPrefix = createdKindByStep.get(producer);
369
- if (producedPrefix && producedPrefix !== prefix) {
370
- add([...stepBase, "bind", field], `journey ${journey.id} binds ${step.operation}.${field} to ${producer}, which creates ${producedPrefix} instead of ${prefix}`, "journey_unbound_reference");
371
- }
372
- }
373
- if (actionName === "create") {
374
- if (step.id) {
375
- createdKindByStep.set(step.id, instrument.idPrefix);
376
- stateByStep.set(step.id, instrument.lifecycle.initial);
377
- seen.add(step.id);
378
- }
379
- continue;
380
- }
381
- const transition = instrument.lifecycle.transitions[actionName];
382
- if (!transition)
383
- continue;
384
- const ownProducer = step.bind[`${camel(instrument.id)}Id`];
385
- const state = ownProducer ? stateByStep.get(ownProducer) : undefined;
386
- if (state && !transition.from.includes(state)) {
387
- add([...stepBase, "operation"], `journey ${journey.id} runs ${step.operation} from ${state}; allowed states are ${transition.from.join(", ")}`, "journey_invalid_transition");
388
- }
389
- else if (ownProducer) {
390
- stateByStep.set(ownProducer, transition.to);
391
- }
392
- if (step.id)
393
- seen.add(step.id);
394
- }
395
- }
396
- }
397
- }
398
301
  function structuralBudgetIssue(value) {
399
302
  let discovered = 1;
400
303
  let nodes = 0;
@@ -915,76 +818,272 @@ function validateInstrument(instrument, instrumentIndex, instruments, subjects,
915
818
  }
916
819
  }
917
820
  }
918
- // Multi-variant finance oracle check across action plan combinations
919
- const actionsWithPlans = Object.keys(instrument.actions).filter((aName) => planResolution.plans.some((p) => p.action === aName));
821
+ for (const finIssue of instrumentFinanceIssues(instrument, {
822
+ plans: planResolution.plans,
823
+ })) {
824
+ add([...base, ...finIssue.path], finIssue.message, finIssue.code);
825
+ }
826
+ validateAggregates(instrument, base, instruments, references, add);
827
+ }
828
+ /**
829
+ * The finance oracle over an instrument's resolved action plans, with paths
830
+ * rooted at the instrument. A piece-plan instrument is unfolded over the
831
+ * runtime's piece progress; any other instrument runs once per action plan
832
+ * combination. Contract-side callers pass the UDL projection of a blueprint
833
+ * definition so both boundaries prove the same machine.
834
+ */
835
+ export function instrumentFinanceIssues(instrument, options = {}) {
836
+ const plans = options.plans ?? resolveUdlActionPlans(instrument).plans;
837
+ const financeOptions = options.penaltyMayBeNonzero === undefined
838
+ ? {}
839
+ : { penaltyMayBeNonzero: options.penaltyMayBeNonzero };
840
+ const issues = [];
841
+ const seen = new Set();
842
+ const add = (path, message, code) => {
843
+ const key = `${code}:${path.join(".")}:${message}`;
844
+ if (seen.has(key))
845
+ return;
846
+ seen.add(key);
847
+ issues.push({ code, message, path });
848
+ };
849
+ const expansion = instrument.piecePlan
850
+ ? expandPieceProgress(instrument, plans)
851
+ : undefined;
852
+ if (expansion === "bound") {
853
+ add(["actions"], `piece progress expansion exceeds reachable variant bound of ${UDL_LIMITS.maxActionExpansion}`, "UDL2010");
854
+ return issues;
855
+ }
856
+ if (expansion) {
857
+ for (const finIssue of analyzeInstrumentFinance(expansion.instrument, financeOptions)) {
858
+ const message = expansion.displayNames.reduce((text, [expanded, display]) => text.replaceAll(expanded, display), finIssue.message);
859
+ add(originFinancePath(expansion, finIssue.path), message, finIssue.code);
860
+ }
861
+ return issues;
862
+ }
863
+ const actionsWithPlans = Object.keys(instrument.actions).filter((aName) => plans.some((p) => p.action === aName));
920
864
  let totalCombinations = 1;
921
865
  const actionPlanMap = {};
922
866
  for (const aName of actionsWithPlans) {
923
- const actionPlans = planResolution.plans.filter((p) => p.action === aName);
867
+ const actionPlans = plans.filter((p) => p.action === aName);
924
868
  actionPlanMap[aName] = actionPlans;
925
869
  totalCombinations *= actionPlans.length;
926
870
  }
927
871
  if (actionsWithPlans.length > 0 &&
928
872
  totalCombinations > UDL_LIMITS.maxActionExpansion) {
929
- add([...base, "actions"], `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`, "UDL2010");
873
+ add(["actions"], `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`, "UDL2010");
874
+ return issues;
930
875
  }
931
- else {
932
- const generateCombos = (keys) => {
933
- if (keys.length === 0)
934
- return [{}];
935
- const [first, ...rest] = keys;
936
- const restCombos = generateCombos(rest);
937
- const result = [];
938
- for (const plan of actionPlanMap[first]) {
939
- for (const combo of restCombos) {
940
- result.push({ ...combo, [first]: plan });
941
- }
876
+ const generateCombos = (keys) => {
877
+ if (keys.length === 0)
878
+ return [{}];
879
+ const [first, ...rest] = keys;
880
+ const restCombos = generateCombos(rest);
881
+ const result = [];
882
+ for (const plan of actionPlanMap[first]) {
883
+ for (const combo of restCombos) {
884
+ result.push({ ...combo, [first]: plan });
942
885
  }
943
- return result;
944
- };
945
- const combinations = actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
946
- const seenFinanceIssues = new Set();
947
- for (const combo of combinations) {
948
- const expandedActions = {};
949
- for (const [aName, aDef] of Object.entries(instrument.actions)) {
950
- const plan = combo[aName];
951
- // Only an action that moves money through calls is replaced by its
952
- // expanded leaves. Authored moves and steps always reach the oracle.
953
- if (plan && (aDef.calls?.length ?? 0) > 0) {
954
- const steps = [];
955
- const moves = [];
956
- for (const leaf of plan.leaves) {
957
- if ("key" in leaf.step) {
958
- moves.push(leaf.step);
959
- }
960
- else {
961
- steps.push(leaf.step);
962
- }
963
- }
964
- expandedActions[aName] = {
965
- ...aDef,
966
- moves,
967
- steps,
968
- };
969
- }
970
- else {
971
- expandedActions[aName] = aDef;
972
- }
886
+ }
887
+ return result;
888
+ };
889
+ const combinations = actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
890
+ for (const combo of combinations) {
891
+ const expandedActions = {};
892
+ for (const [aName, aDef] of Object.entries(instrument.actions)) {
893
+ expandedActions[aName] = planExpandedAction(aDef, combo[aName]);
894
+ }
895
+ for (const finIssue of analyzeInstrumentFinance({ ...instrument, actions: expandedActions }, financeOptions)) {
896
+ add(finIssue.path, finIssue.message, finIssue.code);
897
+ }
898
+ }
899
+ return issues;
900
+ }
901
+ /**
902
+ * Only an action that moves money through calls is replaced by its expanded
903
+ * leaves. Authored moves and steps always reach the oracle.
904
+ */
905
+ function planExpandedAction(definition, plan) {
906
+ if (!plan || (definition.calls?.length ?? 0) === 0)
907
+ return definition;
908
+ const steps = [];
909
+ const moves = [];
910
+ for (const leaf of plan.leaves) {
911
+ if ("key" in leaf.step) {
912
+ moves.push(leaf.step);
913
+ }
914
+ else {
915
+ steps.push(leaf.step);
916
+ }
917
+ }
918
+ return { ...definition, moves, steps };
919
+ }
920
+ function progressKey(progress) {
921
+ return `${progress.funded.join(",")}|${progress.consumed.join(",")}`;
922
+ }
923
+ /** Mirrors eligiblePieces in the engine's piece-plan dispatcher. */
924
+ function eligiblePieces(plan, stage, progress) {
925
+ return plan[`${stage}_order`].filter((id) => stage === "fund"
926
+ ? !progress.funded.includes(id)
927
+ : progress.funded.includes(id) && !progress.consumed.includes(id));
928
+ }
929
+ /**
930
+ * Unfolds a piece-plan instrument over the runtime's piece progress so the
931
+ * ordinary lifecycle oracle sees exactly the states the dispatcher admits: a
932
+ * piece-stage action moves the next eligible piece of its stage order, the
933
+ * lifecycle state is retained until the stage's last piece moves, funding
934
+ * cannot resume once a piece has left escrow, and an action gated on a drained
935
+ * account is closed while a funded piece is still held. Every expanded state is
936
+ * one (lifecycle state, progress) pair; expanded action names carry the piece
937
+ * and the source state so each has exactly one transition. A quote-commit pair
938
+ * is not carried through the expansion.
939
+ */
940
+ function expandPieceProgress(instrument, plans) {
941
+ const plan = instrument.piecePlan;
942
+ if (!plan)
943
+ return undefined;
944
+ const stateName = (state, progress) => `${state}#${progressKey(progress)}`;
945
+ const initialProgress = { funded: [], consumed: [] };
946
+ const stateOrigins = new Map();
947
+ const actionOrigins = new Map();
948
+ const displayNames = [];
949
+ const actions = {};
950
+ const transitions = {};
951
+ const pending = [
952
+ { state: instrument.lifecycle.initial, progress: initialProgress },
953
+ ];
954
+ stateOrigins.set(stateName(instrument.lifecycle.initial, initialProgress), instrument.lifecycle.initial);
955
+ const visit = (state, progress) => {
956
+ const name = stateName(state, progress);
957
+ if (stateOrigins.has(name))
958
+ return;
959
+ stateOrigins.set(name, state);
960
+ pending.push({ state, progress });
961
+ };
962
+ while (pending.length > 0) {
963
+ const { state, progress } = pending.shift();
964
+ if (stateOrigins.size > UDL_LIMITS.maxActionExpansion)
965
+ return "bound";
966
+ const held = progress.funded.filter((id) => !progress.consumed.includes(id));
967
+ for (const [actionName, transition] of Object.entries(instrument.lifecycle.transitions)) {
968
+ if (!transition.from.includes(state))
969
+ continue;
970
+ const definition = instrument.actions[actionName];
971
+ if (!definition)
972
+ continue;
973
+ if (definition.requiresDrainedAccount && held.length > 0)
974
+ continue;
975
+ const stage = definition.pieceStage;
976
+ if (!stage) {
977
+ const expanded = `${actionName}#${state}#${progressKey(progress)}`;
978
+ const variant = plans.find((candidate) => candidate.action === actionName);
979
+ actions[expanded] = planExpandedAction(definition, variant);
980
+ transitions[expanded] = {
981
+ from: [stateName(state, progress)],
982
+ to: stateName(transition.to, progress),
983
+ };
984
+ actionOrigins.set(expanded, {
985
+ action: actionName,
986
+ ...(variant && (definition.calls?.length ?? 0) > 0
987
+ ? { leafOrigins: variant.leaves.map((leaf) => leaf.originPath) }
988
+ : {}),
989
+ });
990
+ displayNames.push([expanded, actionName]);
991
+ visit(transition.to, progress);
992
+ continue;
973
993
  }
974
- const financeInstrument = {
975
- ...instrument,
976
- actions: expandedActions,
977
- };
978
- for (const finIssue of analyzeInstrumentFinance(financeInstrument)) {
979
- const issueKey = `${finIssue.code}:${finIssue.path.join(".")}:${finIssue.message}`;
980
- if (!seenFinanceIssues.has(issueKey)) {
981
- seenFinanceIssues.add(issueKey);
982
- add([...base, ...finIssue.path], finIssue.message, finIssue.code);
994
+ if (stage.plan !== plan.id)
995
+ continue;
996
+ if (stage.stage === "fund" && progress.consumed.length > 0)
997
+ continue;
998
+ const eligible = eligiblePieces(plan, stage.stage, progress);
999
+ const pieceId = eligible[0];
1000
+ if (pieceId === undefined)
1001
+ continue;
1002
+ const variant = plans.find((candidate) => candidate.action === actionName && candidate.pieceId === pieceId);
1003
+ if (!variant)
1004
+ continue;
1005
+ const next = stage.stage === "fund"
1006
+ ? {
1007
+ funded: [...progress.funded, pieceId],
1008
+ consumed: progress.consumed,
983
1009
  }
984
- }
1010
+ : {
1011
+ funded: progress.funded,
1012
+ consumed: [...progress.consumed, pieceId],
1013
+ };
1014
+ const stageComplete = eligiblePieces(plan, stage.stage, next).length === 0;
1015
+ const target = stageComplete ? transition.to : state;
1016
+ const expanded = `${actionName}@${pieceId}#${state}#${progressKey(progress)}`;
1017
+ actions[expanded] = planExpandedAction(definition, variant);
1018
+ transitions[expanded] = {
1019
+ from: [stateName(state, progress)],
1020
+ to: stateName(target, next),
1021
+ };
1022
+ actionOrigins.set(expanded, {
1023
+ action: actionName,
1024
+ leafOrigins: variant.leaves.map((leaf) => leaf.originPath),
1025
+ });
1026
+ displayNames.push([expanded, `${actionName}[${pieceId}]`]);
1027
+ visit(target, next);
985
1028
  }
986
1029
  }
987
- validateAggregates(instrument, base, instruments, references, add);
1030
+ for (const [actionName, definition] of Object.entries(instrument.actions)) {
1031
+ if (Object.hasOwn(instrument.lifecycle.transitions, actionName))
1032
+ continue;
1033
+ actions[actionName] = planExpandedAction(definition, plans.find((candidate) => candidate.action === actionName));
1034
+ actionOrigins.set(actionName, { action: actionName });
1035
+ }
1036
+ return {
1037
+ instrument: {
1038
+ ...instrument,
1039
+ actions,
1040
+ lifecycle: {
1041
+ initial: stateName(instrument.lifecycle.initial, initialProgress),
1042
+ states: [...stateOrigins.keys()],
1043
+ transitions,
1044
+ },
1045
+ },
1046
+ displayNames: [
1047
+ ...displayNames,
1048
+ ...[...stateOrigins.entries()].map(([expanded, origin]) => [expanded, origin]),
1049
+ ].sort((left, right) => right[0].length - left[0].length),
1050
+ actionOrigins,
1051
+ stateOrigins,
1052
+ originStates: instrument.lifecycle.states,
1053
+ };
1054
+ }
1055
+ function originFinancePath(expansion, path) {
1056
+ const [head, second, third, fourth, ...rest] = path;
1057
+ if (head === "lifecycle" &&
1058
+ second === "states" &&
1059
+ typeof third === "number") {
1060
+ const expandedState = expansion.instrument.lifecycle.states[third];
1061
+ const origin = expandedState === undefined
1062
+ ? undefined
1063
+ : expansion.stateOrigins.get(expandedState);
1064
+ const index = origin === undefined ? -1 : expansion.originStates.indexOf(origin);
1065
+ return index >= 0
1066
+ ? ["lifecycle", "states", index]
1067
+ : ["lifecycle", "states"];
1068
+ }
1069
+ if (head === "actions" && typeof second === "string") {
1070
+ const origin = expansion.actionOrigins.get(second);
1071
+ if (!origin)
1072
+ return path;
1073
+ if (third === "moves" &&
1074
+ typeof fourth === "number" &&
1075
+ origin.leafOrigins?.[fourth]) {
1076
+ return ["actions", ...origin.leafOrigins[fourth], ...rest];
1077
+ }
1078
+ return [
1079
+ "actions",
1080
+ origin.action,
1081
+ ...(third === undefined ? [] : [third]),
1082
+ ...(fourth === undefined ? [] : [fourth]),
1083
+ ...rest,
1084
+ ];
1085
+ }
1086
+ return path;
988
1087
  }
989
1088
  function validatePiecePlan(instrument, base, references, add) {
990
1089
  const plan = instrument.piecePlan;
@@ -1633,7 +1732,9 @@ function validateActionUpdates(instrument, action, definition, actionBase, add)
1633
1732
  add(fieldPath, `updated field ${field} is an aggregate cap`, "UDL5008");
1634
1733
  }
1635
1734
  });
1636
- if (definition.moves.length > 0) {
1735
+ if (definition.moves.length > 0 ||
1736
+ definition.allocate ||
1737
+ definition.contributionStage) {
1637
1738
  add([...actionBase, "updates"], "an action cannot update fields while moving money", "UDL5008");
1638
1739
  }
1639
1740
  }
@@ -1666,6 +1767,19 @@ function validateRemainder(instrument, action, definition, actionBase, instrumen
1666
1767
  remainder.accumulateRef === remainder.amountRef) {
1667
1768
  add([...remainderBase, "accumulateRef"], "remainder accumulateRef must differ from amountRef", "UDL4001");
1668
1769
  }
1770
+ addDuplicateIssues(remainder.subtractPaths ?? [], [...remainderBase, "subtractPaths"], "remainder operand", add);
1771
+ for (const path of remainder.subtractPaths ?? []) {
1772
+ const [root, key] = path.split(".");
1773
+ const declared = root === "fields" &&
1774
+ key !== undefined &&
1775
+ isMoneySchema(instrument.fields[key] ?? {}) &&
1776
+ !instrument.update?.fields.includes(key) &&
1777
+ !Object.values(instrument.actions).some((action) => action.updates?.includes(key));
1778
+ if (!declared ||
1779
+ path === remainder.totalPath ||
1780
+ path === `refs.${remainder.amountRef}`)
1781
+ add([...remainderBase, "subtractPaths"], `remainder subtraction ${path} must name distinct immutable money`, "UDL4001");
1782
+ }
1669
1783
  const [totalRoot, totalKey] = remainder.totalPath.split(".");
1670
1784
  const totalDeclared = (totalRoot === "fields" &&
1671
1785
  totalKey !== undefined &&
@@ -1750,7 +1864,17 @@ function validateActions(instrument, base, instruments, references, add) {
1750
1864
  definition.steps.forEach((step, stepIndex) => validateStep(instrument, definition, step, [...actionBase, "steps", stepIndex], add));
1751
1865
  definition.moves.forEach((move, moveIndex) => validateStep(instrument, definition, move, [...actionBase, "moves", moveIndex], add));
1752
1866
  if (definition.requiresRefs) {
1753
- addDuplicateIssues(definition.requiresRefs.map((gate) => gate.field), [...actionBase, "requiresRefs"], "gate field", add);
1867
+ const boundFields = new Map();
1868
+ for (const gate of definition.requiresRefs) {
1869
+ for (const [field, path] of Object.entries(gate.bind ?? {})) {
1870
+ const source = `${gate.field}:${path}`;
1871
+ const previous = boundFields.get(field);
1872
+ if (previous !== undefined && previous !== source)
1873
+ add([...actionBase, "requiresRefs"], `reference gates bind ${field} from conflicting sources`, "UDL5001");
1874
+ boundFields.set(field, source);
1875
+ }
1876
+ }
1877
+ addDuplicateIssues(definition.requiresRefs.map((gate) => JSON.stringify(sortObject(gate))), [...actionBase, "requiresRefs"], "gate", add);
1754
1878
  definition.requiresRefs.forEach((gate, gateIndex) => {
1755
1879
  addDuplicateIssues(gate.statuses, [...actionBase, "requiresRefs", gateIndex, "statuses"], "gate status", add);
1756
1880
  const schema = instrument.fields[gate.field];
@@ -1811,6 +1935,7 @@ function validateActions(instrument, base, instruments, references, add) {
1811
1935
  const existingRefs = new Set([
1812
1936
  ...Object.entries(instrument.actions).flatMap(([candidateAction, candidate]) => [
1813
1937
  ...Object.keys(candidate.captureInput ?? {}),
1938
+ ...Object.keys(candidate.captureEngine ?? {}),
1814
1939
  ...[...candidate.steps, ...candidate.moves].flatMap((step) => Object.keys(step.capture ?? {})),
1815
1940
  ...(candidateAction !== action && candidate.signedSum
1816
1941
  ? [
@@ -1914,7 +2039,7 @@ function validateActions(instrument, base, instruments, references, add) {
1914
2039
  else if (!isDateTimeFormat(schema.format)) {
1915
2040
  add([...actionBase, "due", "field"], "due field must be a date-time field", "UDL5008");
1916
2041
  }
1917
- if (definition.due.offset &&
2042
+ if (typeof definition.due.offset === "string" &&
1918
2043
  fixedIsoDurationMs(definition.due.offset) === null) {
1919
2044
  add([...actionBase, "due", "offset"], "due.offset must be a fixed ISO-8601 duration using weeks, days, hours, minutes, or seconds", "UDL3001");
1920
2045
  }
@@ -1931,7 +2056,7 @@ function validateActions(instrument, base, instruments, references, add) {
1931
2056
  else if (!isDateTimeFormat(schema.format)) {
1932
2057
  add([...actionBase, "deadline", "field"], "deadline field must be a date-time field", "UDL5008");
1933
2058
  }
1934
- if (definition.deadline.offset &&
2059
+ if (typeof definition.deadline.offset === "string" &&
1935
2060
  fixedIsoDurationMs(definition.deadline.offset) === null) {
1936
2061
  add([...actionBase, "deadline", "offset"], "deadline.offset must be a fixed ISO-8601 duration using weeks, days, hours, minutes, or seconds", "UDL3001");
1937
2062
  }
@@ -2235,7 +2360,10 @@ function validatePayoutsAndSettlement(instrument, base, instruments, references,
2235
2360
  }
2236
2361
  }
2237
2362
  const reservedRefs = new Set([
2238
- ...Object.values(instrument.actions).flatMap((action) => Object.keys(action.captureInput ?? {})),
2363
+ ...Object.values(instrument.actions).flatMap((action) => [
2364
+ ...Object.keys(action.captureInput ?? {}),
2365
+ ...Object.keys(action.captureEngine ?? {}),
2366
+ ]),
2239
2367
  ...Object.values(instrument.actions).flatMap((action) => [...action.steps, ...action.moves].flatMap((step) => Object.keys(step.capture ?? {}))),
2240
2368
  ...Object.values(instrument.actions).flatMap((action) => action.signedSum
2241
2369
  ? [
@@ -2252,6 +2380,7 @@ function validatePayoutsAndSettlement(instrument, base, instruments, references,
2252
2380
  : []),
2253
2381
  ]
2254
2382
  : []),
2383
+ ...Object.values(instrument.actions).flatMap((action) => action.allocate ? [action.allocate.capture] : []),
2255
2384
  ...quoteRefKeys(instrument),
2256
2385
  ...(instrument.subject ? ["subject"] : []),
2257
2386
  ]);
@@ -2552,7 +2681,10 @@ function declaredRefKeys(instrument) {
2552
2681
  return new Set([
2553
2682
  ...Object.values(instrument.actions).flatMap((action) => action.payout ? [action.payout.capture] : []),
2554
2683
  ...Object.values(instrument.actions).flatMap((action) => (action.reconcile ?? []).map((reconcile) => reconcile.capture)),
2555
- ...Object.values(instrument.actions).flatMap((action) => Object.keys(action.captureInput ?? {})),
2684
+ ...Object.values(instrument.actions).flatMap((action) => [
2685
+ ...Object.keys(action.captureInput ?? {}),
2686
+ ...Object.keys(action.captureEngine ?? {}),
2687
+ ]),
2556
2688
  ...Object.values(instrument.actions).flatMap((action) => [...action.steps, ...action.moves].flatMap((step) => Object.keys(step.capture ?? {}))),
2557
2689
  ...Object.values(instrument.actions).flatMap((action) => action.signedSum
2558
2690
  ? [
@@ -2560,6 +2692,7 @@ function declaredRefKeys(instrument) {
2560
2692
  ...action.signedSum.sources.map((source) => source.subtotalRef),
2561
2693
  ]
2562
2694
  : []),
2695
+ ...Object.values(instrument.actions).flatMap((action) => action.allocate ? [action.allocate.capture] : []),
2563
2696
  ...quoteRefKeys(instrument),
2564
2697
  ...(instrument.subject ? ["subject"] : []),
2565
2698
  ]);
@@ -3034,25 +3167,23 @@ function reconcileExceptionProblemMessage(code, parent, child, exception) {
3034
3167
  * are written once. The value is disposable: no document ever carries it, and
3035
3168
  * a host's real id grammar stays the host's business.
3036
3169
  */
3037
- const probeIdFor = (prefix) => `${prefix}_sandbox_0123456789abcdef`;
3038
- /** A prefix no host mints, so a schema that accepts it accepts anything. */
3039
- const UNCLAIMED_PREFIX = "zzzz";
3170
+ /** Classify the sealed ID pattern without compiling document-authored regexes. */
3040
3171
  export function openReferenceShapeBudget() {
3041
3172
  const answers = new WeakMap();
3042
3173
  let probes = 0;
3174
+ let exhausted = false;
3043
3175
  return {
3044
3176
  accepts(schema, prefix) {
3045
3177
  const seen = answers.get(schema);
3046
3178
  const cached = seen?.get(prefix);
3047
3179
  if (cached !== undefined)
3048
3180
  return cached;
3049
- if (probes >= UDL_LIMITS.maxSchemaProbes)
3181
+ if (probes >= UDL_LIMITS.maxSchemaProbes) {
3182
+ exhausted = true;
3050
3183
  return false;
3184
+ }
3051
3185
  probes += 1;
3052
- const answer = validateUdlSchemaValue(schema, probeIdFor(prefix)).errors.length ===
3053
- 0 &&
3054
- validateUdlSchemaValue(schema, probeIdFor(UNCLAIMED_PREFIX)).errors
3055
- .length > 0;
3186
+ const answer = referencePatternPrefix(schema) === prefix;
3056
3187
  if (seen)
3057
3188
  seen.set(prefix, answer);
3058
3189
  else
@@ -3060,7 +3191,7 @@ export function openReferenceShapeBudget() {
3060
3191
  return answer;
3061
3192
  },
3062
3193
  get exhausted() {
3063
- return probes >= UDL_LIMITS.maxSchemaProbes;
3194
+ return exhausted;
3064
3195
  },
3065
3196
  };
3066
3197
  }
@@ -3155,4 +3286,13 @@ function jsonPath(path) {
3155
3286
  }
3156
3287
  return result;
3157
3288
  }
3289
+ function sortObject(value) {
3290
+ if (Array.isArray(value))
3291
+ return value.map(sortObject);
3292
+ if (value !== null && typeof value === "object")
3293
+ return Object.fromEntries(Object.entries(value)
3294
+ .sort(([a], [b]) => a.localeCompare(b))
3295
+ .map(([key, item]) => [key, sortObject(item)]));
3296
+ return value;
3297
+ }
3158
3298
  //# sourceMappingURL=validation.js.map