@tiangong-lca/cli 0.0.30 → 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.
@@ -446,11 +446,17 @@ function schemaForConfig(config) {
446
446
  }
447
447
  function validatePayload(payload, type, config) {
448
448
  const { schema, createEntity } = schemaForConfig(config);
449
- const outcome = validateSchemaWithDeepFallback(schema, payload, createEntity);
449
+ // SDK schema/entity validation may apply defaults by mutating its input. Keep validation
450
+ // isolated so execution-contract hashing, dispatch, and readback all use the exact input.
451
+ const validationPayload = structuredClone(payload);
452
+ const outcome = validateSchemaWithDeepFallback(schema, validationPayload, createEntity);
450
453
  const processIssues = type === 'process'
451
- ? [...collectProcessRequiredFieldIssues(payload), ...collectProcessPlaceholderIssues(payload)]
454
+ ? [
455
+ ...collectProcessRequiredFieldIssues(validationPayload),
456
+ ...collectProcessPlaceholderIssues(validationPayload),
457
+ ]
452
458
  : [];
453
- const importIssues = type === 'process' ? [] : collectImportContentIssues(payload);
459
+ const importIssues = type === 'process' ? [] : collectImportContentIssues(validationPayload);
454
460
  const issues = [
455
461
  ...outcome.issues.map(normalizeValidationIssue),
456
462
  ...processIssues,
@@ -902,6 +908,42 @@ async function readbackIsDesiredExact(options) {
902
908
  return false;
903
909
  }
904
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
+ }
905
947
  async function runExecutionContractBatch(options) {
906
948
  const contractSha256 = sha256Json(options.contract);
907
949
  const ledgerRoot = executionLedgerRoot(options.env, options.contract);
@@ -913,11 +955,20 @@ async function runExecutionContractBatch(options) {
913
955
  executionContractError('Owner session or project does not match the execution contract.');
914
956
  }
915
957
  options.files.execution_ledger = ledgerRoot;
916
- 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);
917
963
  const statuses = new Map();
918
964
  const referenceOnlySupportCache = new Map();
919
- for (const [index, action] of options.contract.actions.entries()) {
965
+ const executeAction = async (index) => {
966
+ const action = options.contract.actions[index];
920
967
  const row = options.preparedRows[index];
968
+ const storeReport = (report) => {
969
+ reports[index] = report;
970
+ statuses.set(action.action_id, report.status);
971
+ };
921
972
  const preparedFailure = buildPreparedFailure(row, options.allowReferenceOnlySupport);
922
973
  if (preparedFailure) {
923
974
  const report = {
@@ -928,9 +979,8 @@ async function runExecutionContractBatch(options) {
928
979
  replayed: false,
929
980
  readback: 'not_performed',
930
981
  };
931
- reports.push(report);
932
- statuses.set(action.action_id, report.status);
933
- continue;
982
+ storeReport(report);
983
+ return;
934
984
  }
935
985
  const priorOutcome = ledger.outcomes.get(action.action_id);
936
986
  if (priorOutcome) {
@@ -959,9 +1009,8 @@ async function runExecutionContractBatch(options) {
959
1009
  }
960
1010
  : {}),
961
1011
  });
962
- reports.push(report);
963
- statuses.set(action.action_id, status);
964
- continue;
1012
+ storeReport(report);
1013
+ return;
965
1014
  }
966
1015
  if (ledger.attempts.has(action.action_id)) {
967
1016
  const report = await finalizeAttemptedAction({
@@ -976,9 +1025,8 @@ async function runExecutionContractBatch(options) {
976
1025
  now: options.now,
977
1026
  recovered: true,
978
1027
  });
979
- reports.push(report);
980
- statuses.set(action.action_id, report.status);
981
- continue;
1028
+ storeReport(report);
1029
+ return;
982
1030
  }
983
1031
  const blockingDependencies = action.dependency_action_ids.filter((dependency) => statuses.get(dependency) !== 'executed');
984
1032
  if (blockingDependencies.length > 0) {
@@ -994,9 +1042,8 @@ async function runExecutionContractBatch(options) {
994
1042
  details: { dependency_action_ids: blockingDependencies },
995
1043
  },
996
1044
  });
997
- reports.push(report);
998
- statuses.set(action.action_id, report.status);
999
- continue;
1045
+ storeReport(report);
1046
+ return;
1000
1047
  }
1001
1048
  let beforeRows;
1002
1049
  let transportFailed = false;
@@ -1042,6 +1089,11 @@ async function runExecutionContractBatch(options) {
1042
1089
  });
1043
1090
  }
1044
1091
  }
1092
+ await renewExecutionOwnerToken({
1093
+ runtime: options.runtime,
1094
+ commandTransport: options.commandTransport,
1095
+ contract: options.contract,
1096
+ });
1045
1097
  }
1046
1098
  catch (error) {
1047
1099
  const report = contractRowReport({
@@ -1053,9 +1105,8 @@ async function runExecutionContractBatch(options) {
1053
1105
  readback: 'not_performed',
1054
1106
  error: serializeError(error),
1055
1107
  });
1056
- reports.push(report);
1057
- statuses.set(action.action_id, report.status);
1058
- continue;
1108
+ storeReport(report);
1109
+ return;
1059
1110
  }
1060
1111
  try {
1061
1112
  const beforeDispatch = () => {
@@ -1111,15 +1162,40 @@ async function runExecutionContractBatch(options) {
1111
1162
  now: options.now,
1112
1163
  recovered: transportFailed,
1113
1164
  });
1114
- reports.push(report);
1115
- statuses.set(action.action_id, report.status);
1165
+ storeReport(report);
1166
+ };
1167
+ for (let index = 0; index < serialPrefixLength; index += 1) {
1168
+ await executeAction(index);
1116
1169
  }
1117
- const failures = reports.filter((row) => ['failed', 'unknown', 'blocked'].includes(row.status));
1118
- 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);
1119
1195
  writeJsonLinesArtifact(options.files.failures_jsonl, failures);
1120
- const unknown = reports.filter((row) => row.status === 'unknown').length;
1121
- const failed = reports.filter((row) => row.status === 'failed').length;
1122
- 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;
1123
1199
  const report = {
1124
1200
  schema_version: 2,
1125
1201
  generated_at_utc: options.now(),
@@ -1136,21 +1212,24 @@ async function runExecutionContractBatch(options) {
1136
1212
  counts: {
1137
1213
  selected: options.preparedRows.length,
1138
1214
  prepared: 0,
1139
- executed: reports.filter((row) => row.status === 'executed').length,
1215
+ executed: completedReports.filter((row) => row.status === 'executed').length,
1140
1216
  failed,
1141
1217
  unknown,
1142
1218
  blocked,
1143
1219
  attempts_consumed: ledger.attempts.size,
1144
1220
  by_table: byTable(options.preparedRows),
1145
- operations: operationCount(reports),
1221
+ operations: operationCount(completedReports),
1146
1222
  },
1147
1223
  files: options.files,
1148
- rows: reports,
1224
+ rows: completedReports,
1149
1225
  execution_contract: {
1150
1226
  path: path.resolve(options.contractPath),
1151
1227
  sha256: contractSha256,
1152
1228
  execution_id: options.contract.execution_id,
1153
1229
  target_mode: 'owner_draft',
1230
+ max_parallel: options.maxParallel,
1231
+ serial_prefix_actions: serialPrefixLength,
1232
+ parallel_suffix_actions: options.contract.actions.length - serialPrefixLength,
1154
1233
  },
1155
1234
  };
1156
1235
  writeJsonArtifact(options.files.summary_json, report);
@@ -1167,6 +1246,7 @@ export async function runDatasetSaveDraft(options) {
1167
1246
  const files = buildFiles(outDir);
1168
1247
  const preparedRows = prepareRows(inputPath, options.rawInput, requestedType);
1169
1248
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1249
+ const maxParallel = normalizeExecutionMaxParallel(options.maxParallel);
1170
1250
  const executionContractPath = options.executionContractPath
1171
1251
  ? path.resolve(options.executionContractPath)
1172
1252
  : null;
@@ -1176,6 +1256,9 @@ export async function runDatasetSaveDraft(options) {
1176
1256
  if (executionContract && !commit) {
1177
1257
  executionContractError('Execution contract mode requires --commit.');
1178
1258
  }
1259
+ if (!executionContract && maxParallel !== 1) {
1260
+ executionContractError('--max-parallel greater than 1 requires --execution-contract.');
1261
+ }
1179
1262
  if (executionContract) {
1180
1263
  bindExecutionContractRows(executionContract, preparedRows);
1181
1264
  }
@@ -1221,6 +1304,7 @@ export async function runDatasetSaveDraft(options) {
1221
1304
  fetchImpl: options.fetchImpl,
1222
1305
  timeoutMs,
1223
1306
  now: () => now.toISOString(),
1307
+ maxParallel,
1224
1308
  });
1225
1309
  }
1226
1310
  const reports = [];