@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.
@@ -915,76 +915,272 @@ function validateInstrument(instrument, instrumentIndex, instruments, subjects,
915
915
  }
916
916
  }
917
917
  }
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));
918
+ for (const finIssue of instrumentFinanceIssues(instrument, {
919
+ plans: planResolution.plans,
920
+ })) {
921
+ add([...base, ...finIssue.path], finIssue.message, finIssue.code);
922
+ }
923
+ validateAggregates(instrument, base, instruments, references, add);
924
+ }
925
+ /**
926
+ * The finance oracle over an instrument's resolved action plans, with paths
927
+ * rooted at the instrument. A piece-plan instrument is unfolded over the
928
+ * runtime's piece progress; any other instrument runs once per action plan
929
+ * combination. Contract-side callers pass the UDL projection of a blueprint
930
+ * definition so both boundaries prove the same machine.
931
+ */
932
+ export function instrumentFinanceIssues(instrument, options = {}) {
933
+ const plans = options.plans ?? resolveUdlActionPlans(instrument).plans;
934
+ const financeOptions = options.penaltyMayBeNonzero === undefined
935
+ ? {}
936
+ : { penaltyMayBeNonzero: options.penaltyMayBeNonzero };
937
+ const issues = [];
938
+ const seen = new Set();
939
+ const add = (path, message, code) => {
940
+ const key = `${code}:${path.join(".")}:${message}`;
941
+ if (seen.has(key))
942
+ return;
943
+ seen.add(key);
944
+ issues.push({ code, message, path });
945
+ };
946
+ const expansion = instrument.piecePlan
947
+ ? expandPieceProgress(instrument, plans)
948
+ : undefined;
949
+ if (expansion === "bound") {
950
+ add(["actions"], `piece progress expansion exceeds reachable variant bound of ${UDL_LIMITS.maxActionExpansion}`, "UDL2010");
951
+ return issues;
952
+ }
953
+ if (expansion) {
954
+ for (const finIssue of analyzeInstrumentFinance(expansion.instrument, financeOptions)) {
955
+ const message = expansion.displayNames.reduce((text, [expanded, display]) => text.replaceAll(expanded, display), finIssue.message);
956
+ add(originFinancePath(expansion, finIssue.path), message, finIssue.code);
957
+ }
958
+ return issues;
959
+ }
960
+ const actionsWithPlans = Object.keys(instrument.actions).filter((aName) => plans.some((p) => p.action === aName));
920
961
  let totalCombinations = 1;
921
962
  const actionPlanMap = {};
922
963
  for (const aName of actionsWithPlans) {
923
- const actionPlans = planResolution.plans.filter((p) => p.action === aName);
964
+ const actionPlans = plans.filter((p) => p.action === aName);
924
965
  actionPlanMap[aName] = actionPlans;
925
966
  totalCombinations *= actionPlans.length;
926
967
  }
927
968
  if (actionsWithPlans.length > 0 &&
928
969
  totalCombinations > UDL_LIMITS.maxActionExpansion) {
929
- add([...base, "actions"], `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`, "UDL2010");
970
+ add(["actions"], `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`, "UDL2010");
971
+ return issues;
930
972
  }
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
- }
973
+ const generateCombos = (keys) => {
974
+ if (keys.length === 0)
975
+ return [{}];
976
+ const [first, ...rest] = keys;
977
+ const restCombos = generateCombos(rest);
978
+ const result = [];
979
+ for (const plan of actionPlanMap[first]) {
980
+ for (const combo of restCombos) {
981
+ result.push({ ...combo, [first]: plan });
942
982
  }
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
- }
983
+ }
984
+ return result;
985
+ };
986
+ const combinations = actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
987
+ for (const combo of combinations) {
988
+ const expandedActions = {};
989
+ for (const [aName, aDef] of Object.entries(instrument.actions)) {
990
+ expandedActions[aName] = planExpandedAction(aDef, combo[aName]);
991
+ }
992
+ for (const finIssue of analyzeInstrumentFinance({ ...instrument, actions: expandedActions }, financeOptions)) {
993
+ add(finIssue.path, finIssue.message, finIssue.code);
994
+ }
995
+ }
996
+ return issues;
997
+ }
998
+ /**
999
+ * Only an action that moves money through calls is replaced by its expanded
1000
+ * leaves. Authored moves and steps always reach the oracle.
1001
+ */
1002
+ function planExpandedAction(definition, plan) {
1003
+ if (!plan || (definition.calls?.length ?? 0) === 0)
1004
+ return definition;
1005
+ const steps = [];
1006
+ const moves = [];
1007
+ for (const leaf of plan.leaves) {
1008
+ if ("key" in leaf.step) {
1009
+ moves.push(leaf.step);
1010
+ }
1011
+ else {
1012
+ steps.push(leaf.step);
1013
+ }
1014
+ }
1015
+ return { ...definition, moves, steps };
1016
+ }
1017
+ function progressKey(progress) {
1018
+ return `${progress.funded.join(",")}|${progress.consumed.join(",")}`;
1019
+ }
1020
+ /** Mirrors eligiblePieces in the engine's piece-plan dispatcher. */
1021
+ function eligiblePieces(plan, stage, progress) {
1022
+ return plan[`${stage}_order`].filter((id) => stage === "fund"
1023
+ ? !progress.funded.includes(id)
1024
+ : progress.funded.includes(id) && !progress.consumed.includes(id));
1025
+ }
1026
+ /**
1027
+ * Unfolds a piece-plan instrument over the runtime's piece progress so the
1028
+ * ordinary lifecycle oracle sees exactly the states the dispatcher admits: a
1029
+ * piece-stage action moves the next eligible piece of its stage order, the
1030
+ * lifecycle state is retained until the stage's last piece moves, funding
1031
+ * cannot resume once a piece has left escrow, and an action gated on a drained
1032
+ * account is closed while a funded piece is still held. Every expanded state is
1033
+ * one (lifecycle state, progress) pair; expanded action names carry the piece
1034
+ * and the source state so each has exactly one transition. A quote-commit pair
1035
+ * is not carried through the expansion.
1036
+ */
1037
+ function expandPieceProgress(instrument, plans) {
1038
+ const plan = instrument.piecePlan;
1039
+ if (!plan)
1040
+ return undefined;
1041
+ const stateName = (state, progress) => `${state}#${progressKey(progress)}`;
1042
+ const initialProgress = { funded: [], consumed: [] };
1043
+ const stateOrigins = new Map();
1044
+ const actionOrigins = new Map();
1045
+ const displayNames = [];
1046
+ const actions = {};
1047
+ const transitions = {};
1048
+ const pending = [
1049
+ { state: instrument.lifecycle.initial, progress: initialProgress },
1050
+ ];
1051
+ stateOrigins.set(stateName(instrument.lifecycle.initial, initialProgress), instrument.lifecycle.initial);
1052
+ const visit = (state, progress) => {
1053
+ const name = stateName(state, progress);
1054
+ if (stateOrigins.has(name))
1055
+ return;
1056
+ stateOrigins.set(name, state);
1057
+ pending.push({ state, progress });
1058
+ };
1059
+ while (pending.length > 0) {
1060
+ const { state, progress } = pending.shift();
1061
+ if (stateOrigins.size > UDL_LIMITS.maxActionExpansion)
1062
+ return "bound";
1063
+ const held = progress.funded.filter((id) => !progress.consumed.includes(id));
1064
+ for (const [actionName, transition] of Object.entries(instrument.lifecycle.transitions)) {
1065
+ if (!transition.from.includes(state))
1066
+ continue;
1067
+ const definition = instrument.actions[actionName];
1068
+ if (!definition)
1069
+ continue;
1070
+ if (definition.requiresDrainedAccount && held.length > 0)
1071
+ continue;
1072
+ const stage = definition.pieceStage;
1073
+ if (!stage) {
1074
+ const expanded = `${actionName}#${state}#${progressKey(progress)}`;
1075
+ const variant = plans.find((candidate) => candidate.action === actionName);
1076
+ actions[expanded] = planExpandedAction(definition, variant);
1077
+ transitions[expanded] = {
1078
+ from: [stateName(state, progress)],
1079
+ to: stateName(transition.to, progress),
1080
+ };
1081
+ actionOrigins.set(expanded, {
1082
+ action: actionName,
1083
+ ...(variant && (definition.calls?.length ?? 0) > 0
1084
+ ? { leafOrigins: variant.leaves.map((leaf) => leaf.originPath) }
1085
+ : {}),
1086
+ });
1087
+ displayNames.push([expanded, actionName]);
1088
+ visit(transition.to, progress);
1089
+ continue;
973
1090
  }
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);
1091
+ if (stage.plan !== plan.id)
1092
+ continue;
1093
+ if (stage.stage === "fund" && progress.consumed.length > 0)
1094
+ continue;
1095
+ const eligible = eligiblePieces(plan, stage.stage, progress);
1096
+ const pieceId = eligible[0];
1097
+ if (pieceId === undefined)
1098
+ continue;
1099
+ const variant = plans.find((candidate) => candidate.action === actionName && candidate.pieceId === pieceId);
1100
+ if (!variant)
1101
+ continue;
1102
+ const next = stage.stage === "fund"
1103
+ ? {
1104
+ funded: [...progress.funded, pieceId],
1105
+ consumed: progress.consumed,
983
1106
  }
984
- }
1107
+ : {
1108
+ funded: progress.funded,
1109
+ consumed: [...progress.consumed, pieceId],
1110
+ };
1111
+ const stageComplete = eligiblePieces(plan, stage.stage, next).length === 0;
1112
+ const target = stageComplete ? transition.to : state;
1113
+ const expanded = `${actionName}@${pieceId}#${state}#${progressKey(progress)}`;
1114
+ actions[expanded] = planExpandedAction(definition, variant);
1115
+ transitions[expanded] = {
1116
+ from: [stateName(state, progress)],
1117
+ to: stateName(target, next),
1118
+ };
1119
+ actionOrigins.set(expanded, {
1120
+ action: actionName,
1121
+ leafOrigins: variant.leaves.map((leaf) => leaf.originPath),
1122
+ });
1123
+ displayNames.push([expanded, `${actionName}[${pieceId}]`]);
1124
+ visit(target, next);
985
1125
  }
986
1126
  }
987
- validateAggregates(instrument, base, instruments, references, add);
1127
+ for (const [actionName, definition] of Object.entries(instrument.actions)) {
1128
+ if (Object.hasOwn(instrument.lifecycle.transitions, actionName))
1129
+ continue;
1130
+ actions[actionName] = planExpandedAction(definition, plans.find((candidate) => candidate.action === actionName));
1131
+ actionOrigins.set(actionName, { action: actionName });
1132
+ }
1133
+ return {
1134
+ instrument: {
1135
+ ...instrument,
1136
+ actions,
1137
+ lifecycle: {
1138
+ initial: stateName(instrument.lifecycle.initial, initialProgress),
1139
+ states: [...stateOrigins.keys()],
1140
+ transitions,
1141
+ },
1142
+ },
1143
+ displayNames: [
1144
+ ...displayNames,
1145
+ ...[...stateOrigins.entries()].map(([expanded, origin]) => [expanded, origin]),
1146
+ ].sort((left, right) => right[0].length - left[0].length),
1147
+ actionOrigins,
1148
+ stateOrigins,
1149
+ originStates: instrument.lifecycle.states,
1150
+ };
1151
+ }
1152
+ function originFinancePath(expansion, path) {
1153
+ const [head, second, third, fourth, ...rest] = path;
1154
+ if (head === "lifecycle" &&
1155
+ second === "states" &&
1156
+ typeof third === "number") {
1157
+ const expandedState = expansion.instrument.lifecycle.states[third];
1158
+ const origin = expandedState === undefined
1159
+ ? undefined
1160
+ : expansion.stateOrigins.get(expandedState);
1161
+ const index = origin === undefined ? -1 : expansion.originStates.indexOf(origin);
1162
+ return index >= 0
1163
+ ? ["lifecycle", "states", index]
1164
+ : ["lifecycle", "states"];
1165
+ }
1166
+ if (head === "actions" && typeof second === "string") {
1167
+ const origin = expansion.actionOrigins.get(second);
1168
+ if (!origin)
1169
+ return path;
1170
+ if (third === "moves" &&
1171
+ typeof fourth === "number" &&
1172
+ origin.leafOrigins?.[fourth]) {
1173
+ return ["actions", ...origin.leafOrigins[fourth], ...rest];
1174
+ }
1175
+ return [
1176
+ "actions",
1177
+ origin.action,
1178
+ ...(third === undefined ? [] : [third]),
1179
+ ...(fourth === undefined ? [] : [fourth]),
1180
+ ...rest,
1181
+ ];
1182
+ }
1183
+ return path;
988
1184
  }
989
1185
  function validatePiecePlan(instrument, base, references, add) {
990
1186
  const plan = instrument.piecePlan;