@tiangong-lca/cli 0.0.31 → 0.0.32

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.
@@ -908,6 +908,42 @@ async function readbackIsDesiredExact(options) {
908
908
  return false;
909
909
  }
910
910
  }
911
+ function normalizeExecutionMaxParallel(value) {
912
+ const normalized = value ?? 1;
913
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 8) {
914
+ executionContractError('Execution contract --max-parallel must be an integer from 1 to 8.');
915
+ }
916
+ return normalized;
917
+ }
918
+ function executionSerialPrefixLength(contract) {
919
+ const actionIndexes = new Map(contract.actions.map((action, index) => [action.action_id, index]));
920
+ let highestDependencyIndex = -1;
921
+ for (const action of contract.actions) {
922
+ for (const dependencyActionId of action.dependency_action_ids) {
923
+ highestDependencyIndex = Math.max(highestDependencyIndex, actionIndexes.get(dependencyActionId));
924
+ }
925
+ }
926
+ return highestDependencyIndex + 1;
927
+ }
928
+ function assertParallelSuffixTargetsAreUnique(contract, serialPrefixLength) {
929
+ const targets = new Set();
930
+ for (const action of contract.actions.slice(serialPrefixLength)) {
931
+ const target = `${action.table}\u0000${action.id}\u0000${action.version}`;
932
+ if (targets.has(target)) {
933
+ executionContractError('Execution contract parallel suffix contains a repeated table/id/version target.');
934
+ }
935
+ targets.add(target);
936
+ }
937
+ }
938
+ async function renewExecutionOwnerToken(options) {
939
+ const accessToken = await options.runtime.getAccessToken();
940
+ const actor = decodeExecutionActor(accessToken);
941
+ if (actor.user_id !== options.contract.owner.user_id ||
942
+ actor.email !== options.contract.owner.email) {
943
+ executionContractError('Renewed owner session does not match the execution contract.');
944
+ }
945
+ options.commandTransport.accessToken = accessToken;
946
+ }
911
947
  async function runExecutionContractBatch(options) {
912
948
  const contractSha256 = sha256Json(options.contract);
913
949
  const ledgerRoot = executionLedgerRoot(options.env, options.contract);
@@ -919,11 +955,20 @@ async function runExecutionContractBatch(options) {
919
955
  executionContractError('Owner session or project does not match the execution contract.');
920
956
  }
921
957
  options.files.execution_ledger = ledgerRoot;
922
- const reports = [];
958
+ const serialPrefixLength = executionSerialPrefixLength(options.contract);
959
+ if (options.maxParallel > 1) {
960
+ assertParallelSuffixTargetsAreUnique(options.contract, serialPrefixLength);
961
+ }
962
+ const reports = new Array(options.contract.actions.length);
923
963
  const statuses = new Map();
924
964
  const referenceOnlySupportCache = new Map();
925
- for (const [index, action] of options.contract.actions.entries()) {
965
+ const executeAction = async (index) => {
966
+ const action = options.contract.actions[index];
926
967
  const row = options.preparedRows[index];
968
+ const storeReport = (report) => {
969
+ reports[index] = report;
970
+ statuses.set(action.action_id, report.status);
971
+ };
927
972
  const preparedFailure = buildPreparedFailure(row, options.allowReferenceOnlySupport);
928
973
  if (preparedFailure) {
929
974
  const report = {
@@ -934,9 +979,8 @@ async function runExecutionContractBatch(options) {
934
979
  replayed: false,
935
980
  readback: 'not_performed',
936
981
  };
937
- reports.push(report);
938
- statuses.set(action.action_id, report.status);
939
- continue;
982
+ storeReport(report);
983
+ return;
940
984
  }
941
985
  const priorOutcome = ledger.outcomes.get(action.action_id);
942
986
  if (priorOutcome) {
@@ -965,9 +1009,8 @@ async function runExecutionContractBatch(options) {
965
1009
  }
966
1010
  : {}),
967
1011
  });
968
- reports.push(report);
969
- statuses.set(action.action_id, status);
970
- continue;
1012
+ storeReport(report);
1013
+ return;
971
1014
  }
972
1015
  if (ledger.attempts.has(action.action_id)) {
973
1016
  const report = await finalizeAttemptedAction({
@@ -982,9 +1025,8 @@ async function runExecutionContractBatch(options) {
982
1025
  now: options.now,
983
1026
  recovered: true,
984
1027
  });
985
- reports.push(report);
986
- statuses.set(action.action_id, report.status);
987
- continue;
1028
+ storeReport(report);
1029
+ return;
988
1030
  }
989
1031
  const blockingDependencies = action.dependency_action_ids.filter((dependency) => statuses.get(dependency) !== 'executed');
990
1032
  if (blockingDependencies.length > 0) {
@@ -1000,9 +1042,8 @@ async function runExecutionContractBatch(options) {
1000
1042
  details: { dependency_action_ids: blockingDependencies },
1001
1043
  },
1002
1044
  });
1003
- reports.push(report);
1004
- statuses.set(action.action_id, report.status);
1005
- continue;
1045
+ storeReport(report);
1046
+ return;
1006
1047
  }
1007
1048
  let beforeRows;
1008
1049
  let transportFailed = false;
@@ -1048,6 +1089,11 @@ async function runExecutionContractBatch(options) {
1048
1089
  });
1049
1090
  }
1050
1091
  }
1092
+ await renewExecutionOwnerToken({
1093
+ runtime: options.runtime,
1094
+ commandTransport: options.commandTransport,
1095
+ contract: options.contract,
1096
+ });
1051
1097
  }
1052
1098
  catch (error) {
1053
1099
  const report = contractRowReport({
@@ -1059,9 +1105,8 @@ async function runExecutionContractBatch(options) {
1059
1105
  readback: 'not_performed',
1060
1106
  error: serializeError(error),
1061
1107
  });
1062
- reports.push(report);
1063
- statuses.set(action.action_id, report.status);
1064
- continue;
1108
+ storeReport(report);
1109
+ return;
1065
1110
  }
1066
1111
  try {
1067
1112
  const beforeDispatch = () => {
@@ -1117,15 +1162,40 @@ async function runExecutionContractBatch(options) {
1117
1162
  now: options.now,
1118
1163
  recovered: transportFailed,
1119
1164
  });
1120
- reports.push(report);
1121
- statuses.set(action.action_id, report.status);
1165
+ storeReport(report);
1166
+ };
1167
+ for (let index = 0; index < serialPrefixLength; index += 1) {
1168
+ await executeAction(index);
1122
1169
  }
1123
- const failures = reports.filter((row) => ['failed', 'unknown', 'blocked'].includes(row.status));
1124
- writeJsonLinesArtifact(options.files.progress_jsonl, reports);
1170
+ let nextParallelIndex = serialPrefixLength;
1171
+ let fatalWorkerError = null;
1172
+ const runParallelWorker = async () => {
1173
+ while (fatalWorkerError === null) {
1174
+ const index = nextParallelIndex;
1175
+ nextParallelIndex += 1;
1176
+ if (index >= options.contract.actions.length) {
1177
+ return;
1178
+ }
1179
+ try {
1180
+ await executeAction(index);
1181
+ }
1182
+ catch (error) {
1183
+ fatalWorkerError ??= error;
1184
+ }
1185
+ }
1186
+ };
1187
+ const parallelWorkerCount = Math.min(options.maxParallel, options.contract.actions.length - serialPrefixLength);
1188
+ await Promise.all(Array.from({ length: parallelWorkerCount }, () => runParallelWorker()));
1189
+ if (fatalWorkerError !== null) {
1190
+ throw fatalWorkerError;
1191
+ }
1192
+ const completedReports = reports;
1193
+ const failures = completedReports.filter((row) => ['failed', 'unknown', 'blocked'].includes(row.status));
1194
+ writeJsonLinesArtifact(options.files.progress_jsonl, completedReports);
1125
1195
  writeJsonLinesArtifact(options.files.failures_jsonl, failures);
1126
- const unknown = reports.filter((row) => row.status === 'unknown').length;
1127
- const failed = reports.filter((row) => row.status === 'failed').length;
1128
- const blocked = reports.filter((row) => row.status === 'blocked').length;
1196
+ const unknown = completedReports.filter((row) => row.status === 'unknown').length;
1197
+ const failed = completedReports.filter((row) => row.status === 'failed').length;
1198
+ const blocked = completedReports.filter((row) => row.status === 'blocked').length;
1129
1199
  const report = {
1130
1200
  schema_version: 2,
1131
1201
  generated_at_utc: options.now(),
@@ -1142,21 +1212,24 @@ async function runExecutionContractBatch(options) {
1142
1212
  counts: {
1143
1213
  selected: options.preparedRows.length,
1144
1214
  prepared: 0,
1145
- executed: reports.filter((row) => row.status === 'executed').length,
1215
+ executed: completedReports.filter((row) => row.status === 'executed').length,
1146
1216
  failed,
1147
1217
  unknown,
1148
1218
  blocked,
1149
1219
  attempts_consumed: ledger.attempts.size,
1150
1220
  by_table: byTable(options.preparedRows),
1151
- operations: operationCount(reports),
1221
+ operations: operationCount(completedReports),
1152
1222
  },
1153
1223
  files: options.files,
1154
- rows: reports,
1224
+ rows: completedReports,
1155
1225
  execution_contract: {
1156
1226
  path: path.resolve(options.contractPath),
1157
1227
  sha256: contractSha256,
1158
1228
  execution_id: options.contract.execution_id,
1159
1229
  target_mode: 'owner_draft',
1230
+ max_parallel: options.maxParallel,
1231
+ serial_prefix_actions: serialPrefixLength,
1232
+ parallel_suffix_actions: options.contract.actions.length - serialPrefixLength,
1160
1233
  },
1161
1234
  };
1162
1235
  writeJsonArtifact(options.files.summary_json, report);
@@ -1173,6 +1246,7 @@ export async function runDatasetSaveDraft(options) {
1173
1246
  const files = buildFiles(outDir);
1174
1247
  const preparedRows = prepareRows(inputPath, options.rawInput, requestedType);
1175
1248
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1249
+ const maxParallel = normalizeExecutionMaxParallel(options.maxParallel);
1176
1250
  const executionContractPath = options.executionContractPath
1177
1251
  ? path.resolve(options.executionContractPath)
1178
1252
  : null;
@@ -1182,6 +1256,9 @@ export async function runDatasetSaveDraft(options) {
1182
1256
  if (executionContract && !commit) {
1183
1257
  executionContractError('Execution contract mode requires --commit.');
1184
1258
  }
1259
+ if (!executionContract && maxParallel !== 1) {
1260
+ executionContractError('--max-parallel greater than 1 requires --execution-contract.');
1261
+ }
1185
1262
  if (executionContract) {
1186
1263
  bindExecutionContractRows(executionContract, preparedRows);
1187
1264
  }
@@ -1227,6 +1304,7 @@ export async function runDatasetSaveDraft(options) {
1227
1304
  fetchImpl: options.fetchImpl,
1228
1305
  timeoutMs,
1229
1306
  now: () => now.toISOString(),
1307
+ maxParallel,
1230
1308
  });
1231
1309
  }
1232
1310
  const reports = [];