@tiangong-lca/cli 0.0.32 → 0.0.33

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.
@@ -1,14 +1,15 @@
1
- import { existsSync } from 'node:fs';
1
+ import { closeSync, existsSync, fchmodSync, fsyncSync, openSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { writeJsonArtifact } from './artifacts.js';
4
+ import { collectRemoteReferences } from './dataset-remote-verify.js';
4
5
  import { CliError } from './errors.js';
5
6
  import { withStateFileLock } from './state-lock.js';
6
7
  import { buildAliasBatchRequest, buildAliasPlanRequest, loadMaintenanceDesiredPayload, orderedAliasBatches, } from './dataset-maintenance-alias-request.js';
7
- import { MAINTENANCE_SCAN_TABLES, appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, sha256Json, snapshotRemoteRow, writeImmutableJson, } from './dataset-maintenance-contract.js';
8
+ import { MAINTENANCE_SCAN_TABLES, appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, sha256Json, snapshotRemoteRow, stableJsonText, writeImmutableJson, } from './dataset-maintenance-contract.js';
8
9
  import { buildDerivativePlanRequest, derivativePlanAction, parseDerivativeSnapshotResponse, parseDerivativeSubmitResponse, } from './dataset-maintenance-derivatives.js';
9
10
  import { maintenanceProjectedReferenceFingerprint } from './dataset-maintenance-plan.js';
10
11
  import { isSnapshotCompletenessCompatible } from './dataset-maintenance-pagination.js';
11
- import { applyMaintenanceAliasPlan, applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
12
+ import { applyMaintenanceAliasPlan, applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, fetchMaintenanceVisibleTableRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
12
13
  const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
13
14
  function clock(options) {
14
15
  return (options.now ?? new Date()).toISOString();
@@ -16,6 +17,252 @@ function clock(options) {
16
17
  function errorMessage(error) {
17
18
  return error instanceof Error ? error.message : String(error);
18
19
  }
20
+ const ABSENT_READBACK_SHA256 = sha256Json([]);
21
+ function parallelDeleteDesiredSha256(action) {
22
+ return sha256Json({
23
+ action: 'delete',
24
+ table: action.table,
25
+ id: action.id,
26
+ version: action.version,
27
+ desired: 'absent',
28
+ });
29
+ }
30
+ function normalizeMaintenanceMaxParallel(value) {
31
+ const normalized = value ?? 1;
32
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 8) {
33
+ throw new CliError('--max-parallel must be an integer from 1 to 8.', {
34
+ code: 'DATASET_MAINTENANCE_MAX_PARALLEL_INVALID',
35
+ exitCode: 2,
36
+ });
37
+ }
38
+ return normalized;
39
+ }
40
+ function assertParallelDeletePlan(plan) {
41
+ const targets = new Set();
42
+ if (plan.operation !== 'delete' ||
43
+ plan.actions.length === 0 ||
44
+ plan.actions.some((action) => action.action !== 'delete' || action.table !== 'flows') ||
45
+ plan.summary.delete !== plan.actions.length ||
46
+ plan.summary.save_draft !== 0 ||
47
+ (plan.summary.update_json_ordered ?? 0) !== 0 ||
48
+ (plan.summary.rebuild_derivatives ?? 0) !== 0 ||
49
+ plan.summary.current_reference_impacts !== 0 ||
50
+ plan.summary.projected_reference_impacts !== 0) {
51
+ throw new CliError('--max-parallel maintenance apply requires a non-empty flow delete-only plan with zero current and projected inbound references.', {
52
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_PLAN_REQUIRED',
53
+ exitCode: 1,
54
+ });
55
+ }
56
+ for (const action of plan.actions) {
57
+ const target = `${action.table}\u0000${action.id}\u0000${action.version}`;
58
+ if (targets.has(target)) {
59
+ throw new CliError('Parallel delete plan contains a repeated table/id/version target.', {
60
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_TARGET_DUPLICATE',
61
+ exitCode: 1,
62
+ details: { table: action.table, id: action.id, version: action.version },
63
+ });
64
+ }
65
+ targets.add(target);
66
+ }
67
+ }
68
+ function parseParallelDeleteExecutionLog(plan, executionLogPath) {
69
+ const actions = new Map(plan.actions.map((action) => [action.action_id, action]));
70
+ const entries = [];
71
+ const byAction = new Map();
72
+ const dispatched = new Set();
73
+ const committed = new Set();
74
+ for (const value of readJsonLinesIfPresent(executionLogPath)) {
75
+ const action = isJsonObject(value) && typeof value.action_id === 'string'
76
+ ? actions.get(value.action_id)
77
+ : null;
78
+ const expectedDesired = action ? parallelDeleteDesiredSha256(action) : null;
79
+ const actionEntries = action ? (byAction.get(action.action_id) ?? []) : [];
80
+ const priorDispatched = actionEntries.some((entry) => entry.status === 'DISPATCHED');
81
+ const priorCommitted = actionEntries.some((entry) => entry.status === 'COMMITTED');
82
+ const status = isJsonObject(value) ? value.status : null;
83
+ const auditIdValid = isJsonObject(value) &&
84
+ (value.audit_id === null ||
85
+ (typeof value.audit_id === 'string' && POSITIVE_INTEGER_TEXT.test(value.audit_id)));
86
+ const remoteHashValid = isJsonObject(value) &&
87
+ (value.remote_result_sha256 === null ||
88
+ (typeof value.remote_result_sha256 === 'string' &&
89
+ /^[a-f0-9]{64}$/u.test(value.remote_result_sha256)));
90
+ const commonOutcomeFieldsValid = isJsonObject(value) &&
91
+ typeof value.attempt_consumed === 'boolean' &&
92
+ typeof value.recovered === 'boolean' &&
93
+ (value.readback_sha256 === null ||
94
+ (typeof value.readback_sha256 === 'string' &&
95
+ /^[a-f0-9]{64}$/u.test(value.readback_sha256))) &&
96
+ (value.error === null || typeof value.error === 'string');
97
+ const statusFieldsValid = isJsonObject(value) &&
98
+ ((status === 'PREPARED' &&
99
+ value.attempt_consumed === false &&
100
+ value.recovered === false &&
101
+ value.audit_id === null &&
102
+ value.readback_sha256 === null &&
103
+ value.remote_result_sha256 === null &&
104
+ value.error === null &&
105
+ !priorDispatched &&
106
+ !priorCommitted) ||
107
+ (status === 'DISPATCHED' &&
108
+ value.attempt_consumed === true &&
109
+ value.recovered === false &&
110
+ value.audit_id === null &&
111
+ value.readback_sha256 === null &&
112
+ value.remote_result_sha256 === null &&
113
+ value.error === null &&
114
+ actionEntries.some((entry) => entry.status === 'PREPARED') &&
115
+ !priorDispatched &&
116
+ !priorCommitted) ||
117
+ (status === 'UNKNOWN' &&
118
+ value.attempt_consumed === true &&
119
+ typeof value.error === 'string' &&
120
+ value.error.length > 0 &&
121
+ priorDispatched &&
122
+ !priorCommitted) ||
123
+ (status === 'COMMITTED' &&
124
+ value.attempt_consumed === true &&
125
+ value.readback_sha256 === ABSENT_READBACK_SHA256 &&
126
+ value.error === null &&
127
+ priorDispatched &&
128
+ !priorCommitted));
129
+ if (!isJsonObject(value) ||
130
+ value.schema_version !== 1 ||
131
+ value.plan_sha256 !== plan.plan_sha256 ||
132
+ value.operation_id !== plan.operation_id ||
133
+ !action ||
134
+ action.action !== 'delete' ||
135
+ action.table !== 'flows' ||
136
+ value.attempt_key !== `${action.action_id}@${expectedDesired}` ||
137
+ value.action !== 'delete' ||
138
+ value.table !== 'flows' ||
139
+ value.id !== action.id ||
140
+ value.version !== action.version ||
141
+ value.desired_sha256 !== expectedDesired ||
142
+ value.before_sha256 !== action.before?.row_sha256 ||
143
+ !isJsonObject(value.actor) ||
144
+ value.actor.user_id !== plan.account.user_id ||
145
+ value.actor.email !== plan.account.email ||
146
+ !isJsonObject(value.audit_context) ||
147
+ value.audit_context.plan_sha256 !== plan.plan_sha256 ||
148
+ value.audit_context.operation_id !== plan.operation_id ||
149
+ value.audit_context.action_id !== action.action_id ||
150
+ value.audit_context.reason_code !== action.reason_code ||
151
+ value.audit_context.source !== 'tiangong-lca dataset maintenance apply' ||
152
+ typeof value.recorded_at_utc !== 'string' ||
153
+ !Number.isFinite(Date.parse(value.recorded_at_utc)) ||
154
+ !auditIdValid ||
155
+ !remoteHashValid ||
156
+ !commonOutcomeFieldsValid ||
157
+ !statusFieldsValid) {
158
+ throw new CliError('Parallel delete execution log contains an invalid or foreign entry.', {
159
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_LOG_INVALID',
160
+ exitCode: 1,
161
+ details: value,
162
+ });
163
+ }
164
+ const entry = value;
165
+ entries.push(entry);
166
+ actionEntries.push(entry);
167
+ byAction.set(action.action_id, actionEntries);
168
+ if (entry.status === 'DISPATCHED')
169
+ dispatched.add(action.action_id);
170
+ if (entry.status === 'COMMITTED')
171
+ committed.add(action.action_id);
172
+ }
173
+ return { entries, byAction, dispatched, committed };
174
+ }
175
+ function appendParallelDeleteExecutionEntry(options) {
176
+ const desiredSha256 = parallelDeleteDesiredSha256(options.action);
177
+ const entry = {
178
+ schema_version: 1,
179
+ plan_sha256: options.plan.plan_sha256,
180
+ operation_id: options.plan.operation_id,
181
+ action_id: options.action.action_id,
182
+ attempt_key: `${options.action.action_id}@${desiredSha256}`,
183
+ action: 'delete',
184
+ table: 'flows',
185
+ id: options.action.id,
186
+ version: options.action.version,
187
+ desired_sha256: desiredSha256,
188
+ before_sha256: options.action.before.row_sha256,
189
+ actor: {
190
+ user_id: options.context.account.user_id,
191
+ email: options.context.account.email,
192
+ },
193
+ status: options.status,
194
+ recorded_at_utc: options.recordedAtUtc,
195
+ attempt_consumed: options.status !== 'PREPARED',
196
+ recovered: options.recovered ?? false,
197
+ audit_context: {
198
+ plan_sha256: options.plan.plan_sha256,
199
+ operation_id: options.plan.operation_id,
200
+ action_id: options.action.action_id,
201
+ reason_code: options.action.reason_code,
202
+ source: 'tiangong-lca dataset maintenance apply',
203
+ },
204
+ audit_id: options.auditId ?? null,
205
+ readback_sha256: options.readbackSha256 ?? null,
206
+ remote_result_sha256: options.remoteResultSha256 ?? null,
207
+ error: options.error ?? null,
208
+ };
209
+ const descriptor = openSync(options.path, 'a', 0o600);
210
+ try {
211
+ fchmodSync(descriptor, 0o600);
212
+ writeFileSync(descriptor, `${stableJsonText(entry)}\n`, 'utf8');
213
+ fsyncSync(descriptor);
214
+ }
215
+ finally {
216
+ closeSync(descriptor);
217
+ }
218
+ options.state.entries.push(entry);
219
+ const actionEntries = options.state.byAction.get(options.action.action_id) ?? [];
220
+ actionEntries.push(entry);
221
+ options.state.byAction.set(options.action.action_id, actionEntries);
222
+ if (entry.status === 'DISPATCHED')
223
+ options.state.dispatched.add(options.action.action_id);
224
+ if (entry.status === 'COMMITTED')
225
+ options.state.committed.add(options.action.action_id);
226
+ return entry;
227
+ }
228
+ function assertNoVisibleProcessInboundReferences(options) {
229
+ const targets = new Set(options.plan.actions.map((action) => `${action.id}\u0000${action.version}`));
230
+ const targetIds = new Set(options.plan.actions.map((action) => action.id));
231
+ const payloadRows = options.rows
232
+ .filter((row) => row.table === 'processes' && row.json_ordered !== null)
233
+ .map((row) => ({
234
+ table: row.table,
235
+ id: row.id,
236
+ version: row.version,
237
+ json_ordered: row.json_ordered,
238
+ }));
239
+ const references = collectRemoteReferences(payloadRows).filter((reference) => reference.role === 'reference' &&
240
+ reference.table === 'flows' &&
241
+ typeof reference.id === 'string');
242
+ const inbound = references.filter((reference) => reference.version
243
+ ? targets.has(`${reference.id}\u0000${reference.version}`)
244
+ : targetIds.has(reference.id));
245
+ if (inbound.length > 0) {
246
+ throw new CliError('Parallel flow delete admission found visible process inbound references.', {
247
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_INBOUND_REFERENCES',
248
+ exitCode: 1,
249
+ details: {
250
+ inbound_count: inbound.length,
251
+ first: inbound.slice(0, 20),
252
+ },
253
+ });
254
+ }
255
+ return {
256
+ process_rows: payloadRows.length,
257
+ process_references: references.length,
258
+ snapshot_sha256: sha256Json(payloadRows.map((row) => ({
259
+ table: row.table,
260
+ id: row.id,
261
+ version: row.version,
262
+ payload_sha256: sha256Json(row.json_ordered),
263
+ }))),
264
+ };
265
+ }
19
266
  function loadDesiredPayload(planDir, action) {
20
267
  return loadMaintenanceDesiredPayload(planDir, action);
21
268
  }
@@ -413,6 +660,13 @@ function assertApplyPreconditions(options) {
413
660
  }
414
661
  continue;
415
662
  }
663
+ if (action.action === 'delete' && options.attemptedDeleteActionIds?.has(action.action_id)) {
664
+ // A request may have committed before its response or progress row was
665
+ // persisted, or the target may have drifted after dispatch. The execution
666
+ // ledger owns both cases and permits only read-only recovery, never an
667
+ // automatic replay.
668
+ continue;
669
+ }
416
670
  if (!currentRow || currentRow.user_id !== action.expected_user_id) {
417
671
  throw new CliError(`Action row is missing, non-draft, or not owned: ${action.action_id}`, {
418
672
  code: 'DATASET_MAINTENANCE_ACTION_ROW_DRIFT',
@@ -1026,6 +1280,335 @@ async function executeAction(options) {
1026
1280
  }
1027
1281
  return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
1028
1282
  }
1283
+ function appendParallelDeleteProgress(options) {
1284
+ const entry = {
1285
+ schema_version: 1,
1286
+ plan_sha256: options.plan.plan_sha256,
1287
+ operation_id: options.plan.operation_id,
1288
+ action_id: options.action.action_id,
1289
+ action: 'delete',
1290
+ table: options.action.table,
1291
+ id: options.action.id,
1292
+ version: options.action.version,
1293
+ reason_code: options.action.reason_code,
1294
+ audit_context: {
1295
+ plan_sha256: options.plan.plan_sha256,
1296
+ operation_id: options.plan.operation_id,
1297
+ action_id: options.action.action_id,
1298
+ reason_code: options.action.reason_code,
1299
+ source: 'tiangong-lca dataset maintenance apply',
1300
+ },
1301
+ actor: {
1302
+ user_id: options.context.account.user_id,
1303
+ email: options.context.account.email,
1304
+ },
1305
+ started_at_utc: options.startedAtUtc,
1306
+ ended_at_utc: options.endedAtUtc,
1307
+ before_sha256: options.action.before.row_sha256,
1308
+ after_sha256: null,
1309
+ remote_result_sha256: options.remoteResultSha256,
1310
+ result: options.result,
1311
+ error: options.error,
1312
+ rollback: options.action.rollback,
1313
+ };
1314
+ appendStableJsonLine(options.progressPath, entry);
1315
+ options.progress.entries.push(entry);
1316
+ if (entry.result === 'success') {
1317
+ options.progress.successes.set(entry.action_id, entry);
1318
+ options.progress.latestFailures.delete(entry.action_id);
1319
+ }
1320
+ else if (!options.progress.successes.has(entry.action_id)) {
1321
+ options.progress.latestFailures.set(entry.action_id, entry);
1322
+ }
1323
+ return entry;
1324
+ }
1325
+ function remoteAuditId(value) {
1326
+ const candidate = value.audit_id;
1327
+ return typeof candidate === 'string' && POSITIVE_INTEGER_TEXT.test(candidate) ? candidate : null;
1328
+ }
1329
+ async function exactParallelDeleteRows(options) {
1330
+ return (await fetchMaintenanceExactRows({
1331
+ context: options.context,
1332
+ table: options.action.table,
1333
+ id: options.action.id,
1334
+ version: options.action.version,
1335
+ })).rows;
1336
+ }
1337
+ function exactParallelDeleteBefore(options) {
1338
+ const row = options.rows[0];
1339
+ return Boolean(options.rows.length === 1 &&
1340
+ row &&
1341
+ row.user_id === options.action.expected_user_id &&
1342
+ row.state_code === 0 &&
1343
+ snapshotRemoteRow(row).row_sha256 === options.action.before?.row_sha256);
1344
+ }
1345
+ async function executeParallelDeletePlan(options) {
1346
+ const statuses = new Map();
1347
+ const recoverAttempted = async (action) => {
1348
+ const startedAt = options.now();
1349
+ let rows;
1350
+ try {
1351
+ rows = await exactParallelDeleteRows({ context: options.context, action });
1352
+ }
1353
+ catch (error) {
1354
+ statuses.set(action.action_id, 'unknown');
1355
+ if (options.execution.byAction.get(action.action_id)?.at(-1)?.status !== 'UNKNOWN') {
1356
+ appendParallelDeleteExecutionEntry({
1357
+ path: options.executionLogPath,
1358
+ state: options.execution,
1359
+ plan: options.plan,
1360
+ action,
1361
+ context: options.context,
1362
+ status: 'UNKNOWN',
1363
+ recordedAtUtc: options.now(),
1364
+ recovered: true,
1365
+ error: `Read-only recovery failed: ${errorMessage(error)}`,
1366
+ });
1367
+ }
1368
+ return;
1369
+ }
1370
+ if (rows.length === 0) {
1371
+ const priorCommitted = options.execution.committed.has(action.action_id);
1372
+ if (!priorCommitted) {
1373
+ appendParallelDeleteExecutionEntry({
1374
+ path: options.executionLogPath,
1375
+ state: options.execution,
1376
+ plan: options.plan,
1377
+ action,
1378
+ context: options.context,
1379
+ status: 'COMMITTED',
1380
+ recordedAtUtc: options.now(),
1381
+ recovered: true,
1382
+ readbackSha256: ABSENT_READBACK_SHA256,
1383
+ });
1384
+ }
1385
+ if (!options.progress.successes.has(action.action_id)) {
1386
+ appendParallelDeleteProgress({
1387
+ progressPath: options.progressPath,
1388
+ progress: options.progress,
1389
+ plan: options.plan,
1390
+ action,
1391
+ context: options.context,
1392
+ startedAtUtc: startedAt,
1393
+ endedAtUtc: options.now(),
1394
+ result: 'success',
1395
+ remoteResultSha256: sha256Json({
1396
+ recovery: 'desired_absent',
1397
+ action_id: action.action_id,
1398
+ desired_sha256: parallelDeleteDesiredSha256(action),
1399
+ }),
1400
+ error: null,
1401
+ });
1402
+ }
1403
+ statuses.set(action.action_id, 'success');
1404
+ return;
1405
+ }
1406
+ const message = exactParallelDeleteBefore({ rows, action })
1407
+ ? 'Prior dispatch has exact-before readback but no zero-dispatch/zero-mutation audit proof; replay is forbidden.'
1408
+ : 'Prior dispatch has ambiguous or drifted readback; replay is forbidden.';
1409
+ if (!options.execution.committed.has(action.action_id) &&
1410
+ options.execution.byAction.get(action.action_id)?.at(-1)?.status !== 'UNKNOWN') {
1411
+ appendParallelDeleteExecutionEntry({
1412
+ path: options.executionLogPath,
1413
+ state: options.execution,
1414
+ plan: options.plan,
1415
+ action,
1416
+ context: options.context,
1417
+ status: 'UNKNOWN',
1418
+ recordedAtUtc: options.now(),
1419
+ recovered: true,
1420
+ readbackSha256: sha256Json(rows.map(snapshotRemoteRow)),
1421
+ error: message,
1422
+ });
1423
+ }
1424
+ if (!options.progress.successes.has(action.action_id)) {
1425
+ appendParallelDeleteProgress({
1426
+ progressPath: options.progressPath,
1427
+ progress: options.progress,
1428
+ plan: options.plan,
1429
+ action,
1430
+ context: options.context,
1431
+ startedAtUtc: startedAt,
1432
+ endedAtUtc: options.now(),
1433
+ result: 'failed',
1434
+ remoteResultSha256: null,
1435
+ error: `UNKNOWN: ${message}`,
1436
+ });
1437
+ }
1438
+ statuses.set(action.action_id, 'unknown');
1439
+ };
1440
+ const executeOne = async (action) => {
1441
+ if (options.progress.successes.has(action.action_id)) {
1442
+ statuses.set(action.action_id, 'success');
1443
+ return;
1444
+ }
1445
+ if (options.execution.dispatched.has(action.action_id)) {
1446
+ await recoverAttempted(action);
1447
+ return;
1448
+ }
1449
+ const startedAt = options.now();
1450
+ let rows;
1451
+ try {
1452
+ rows = await exactParallelDeleteRows({ context: options.context, action });
1453
+ if (!exactParallelDeleteBefore({ rows, action })) {
1454
+ throw new CliError(`Action row drifted immediately before write: ${action.action_id}`, {
1455
+ code: 'DATASET_MAINTENANCE_ACTION_JUST_IN_TIME_DRIFT',
1456
+ exitCode: 1,
1457
+ });
1458
+ }
1459
+ }
1460
+ catch (error) {
1461
+ appendParallelDeleteProgress({
1462
+ progressPath: options.progressPath,
1463
+ progress: options.progress,
1464
+ plan: options.plan,
1465
+ action,
1466
+ context: options.context,
1467
+ startedAtUtc: startedAt,
1468
+ endedAtUtc: options.now(),
1469
+ result: 'failed',
1470
+ remoteResultSha256: null,
1471
+ error: errorMessage(error),
1472
+ });
1473
+ statuses.set(action.action_id, 'failed');
1474
+ return;
1475
+ }
1476
+ appendParallelDeleteExecutionEntry({
1477
+ path: options.executionLogPath,
1478
+ state: options.execution,
1479
+ plan: options.plan,
1480
+ action,
1481
+ context: options.context,
1482
+ status: 'PREPARED',
1483
+ recordedAtUtc: options.now(),
1484
+ });
1485
+ appendParallelDeleteExecutionEntry({
1486
+ path: options.executionLogPath,
1487
+ state: options.execution,
1488
+ plan: options.plan,
1489
+ action,
1490
+ context: options.context,
1491
+ status: 'DISPATCHED',
1492
+ recordedAtUtc: options.now(),
1493
+ });
1494
+ let remoteResult = null;
1495
+ let dispatchError = null;
1496
+ try {
1497
+ remoteResult = await deleteMaintenanceRow({
1498
+ context: options.context,
1499
+ table: action.table,
1500
+ id: action.id,
1501
+ version: action.version,
1502
+ audit: {
1503
+ plan_sha256: options.plan.plan_sha256,
1504
+ operation_id: options.plan.operation_id,
1505
+ action_id: action.action_id,
1506
+ reason_code: action.reason_code,
1507
+ desired_sha256: parallelDeleteDesiredSha256(action),
1508
+ source: 'tiangong-lca dataset maintenance apply',
1509
+ },
1510
+ });
1511
+ }
1512
+ catch (error) {
1513
+ dispatchError = error;
1514
+ }
1515
+ let readbackRows = null;
1516
+ let readbackError = null;
1517
+ try {
1518
+ readbackRows = await exactParallelDeleteRows({ context: options.context, action });
1519
+ }
1520
+ catch (error) {
1521
+ readbackError = error;
1522
+ }
1523
+ if (readbackRows?.length === 0) {
1524
+ const remoteResultSha256 = remoteResult ? sha256Json(remoteResult) : null;
1525
+ appendParallelDeleteExecutionEntry({
1526
+ path: options.executionLogPath,
1527
+ state: options.execution,
1528
+ plan: options.plan,
1529
+ action,
1530
+ context: options.context,
1531
+ status: 'COMMITTED',
1532
+ recordedAtUtc: options.now(),
1533
+ recovered: dispatchError !== null,
1534
+ auditId: remoteResult ? remoteAuditId(remoteResult) : null,
1535
+ readbackSha256: ABSENT_READBACK_SHA256,
1536
+ remoteResultSha256,
1537
+ });
1538
+ appendParallelDeleteProgress({
1539
+ progressPath: options.progressPath,
1540
+ progress: options.progress,
1541
+ plan: options.plan,
1542
+ action,
1543
+ context: options.context,
1544
+ startedAtUtc: startedAt,
1545
+ endedAtUtc: options.now(),
1546
+ result: 'success',
1547
+ remoteResultSha256: remoteResultSha256 ??
1548
+ sha256Json({
1549
+ recovery: 'desired_absent',
1550
+ action_id: action.action_id,
1551
+ desired_sha256: parallelDeleteDesiredSha256(action),
1552
+ }),
1553
+ error: null,
1554
+ });
1555
+ statuses.set(action.action_id, 'success');
1556
+ return;
1557
+ }
1558
+ const error = readbackError
1559
+ ? `Readback failed after dispatch: ${errorMessage(readbackError)}`
1560
+ : dispatchError
1561
+ ? `Dispatch outcome ambiguous and desired absence was not observed: ${errorMessage(dispatchError)}`
1562
+ : 'Delete RPC returned but exact absent readback was not observed.';
1563
+ appendParallelDeleteExecutionEntry({
1564
+ path: options.executionLogPath,
1565
+ state: options.execution,
1566
+ plan: options.plan,
1567
+ action,
1568
+ context: options.context,
1569
+ status: 'UNKNOWN',
1570
+ recordedAtUtc: options.now(),
1571
+ recovered: dispatchError !== null,
1572
+ auditId: remoteResult ? remoteAuditId(remoteResult) : null,
1573
+ readbackSha256: readbackRows ? sha256Json(readbackRows.map(snapshotRemoteRow)) : null,
1574
+ remoteResultSha256: remoteResult ? sha256Json(remoteResult) : null,
1575
+ error,
1576
+ });
1577
+ appendParallelDeleteProgress({
1578
+ progressPath: options.progressPath,
1579
+ progress: options.progress,
1580
+ plan: options.plan,
1581
+ action,
1582
+ context: options.context,
1583
+ startedAtUtc: startedAt,
1584
+ endedAtUtc: options.now(),
1585
+ result: 'failed',
1586
+ remoteResultSha256: null,
1587
+ error: `UNKNOWN: ${error}`,
1588
+ });
1589
+ statuses.set(action.action_id, 'unknown');
1590
+ };
1591
+ let nextIndex = 0;
1592
+ let fatalError = null;
1593
+ const worker = async () => {
1594
+ while (fatalError === null) {
1595
+ const index = nextIndex;
1596
+ nextIndex += 1;
1597
+ if (index >= options.plan.actions.length)
1598
+ return;
1599
+ try {
1600
+ await executeOne(options.plan.actions[index]);
1601
+ }
1602
+ catch (error) {
1603
+ fatalError ??= error;
1604
+ }
1605
+ }
1606
+ };
1607
+ await Promise.all(Array.from({ length: Math.min(options.maxParallel, options.plan.actions.length) }, () => worker()));
1608
+ if (fatalError !== null)
1609
+ throw fatalError;
1610
+ return statuses;
1611
+ }
1029
1612
  async function executeDerivativeAdmission(options) {
1030
1613
  const action = derivativePlanAction(options.plan);
1031
1614
  const plannedSnapshot = action.derivative_before;
@@ -1087,6 +1670,13 @@ function nextAttemptPath(planDir) {
1087
1670
  }
1088
1671
  return path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`);
1089
1672
  }
1673
+ function nextParallelDeleteInboundBarrierPath(planDir) {
1674
+ let attempt = 1;
1675
+ while (existsSync(path.join(planDir, `parallel-delete-inbound-barrier.attempt-${String(attempt).padStart(4, '0')}.json`))) {
1676
+ attempt += 1;
1677
+ }
1678
+ return path.join(planDir, `parallel-delete-inbound-barrier.attempt-${String(attempt).padStart(4, '0')}.json`);
1679
+ }
1090
1680
  export async function runDatasetMaintenanceApply(options) {
1091
1681
  if (!options.commit) {
1092
1682
  throw new CliError('Dataset maintenance apply requires commit=true.', {
@@ -1097,6 +1687,11 @@ export async function runDatasetMaintenanceApply(options) {
1097
1687
  const planPath = path.resolve(options.planPath);
1098
1688
  const planDir = path.dirname(planPath);
1099
1689
  const plan = parseMaintenancePlan(readJsonFile(planPath, 'Maintenance plan'));
1690
+ const parallelDeleteMode = options.maxParallel !== undefined;
1691
+ const maxParallel = normalizeMaintenanceMaxParallel(options.maxParallel);
1692
+ if (parallelDeleteMode) {
1693
+ assertParallelDeletePlan(plan);
1694
+ }
1100
1695
  if (options.approvePlan !== plan.plan_sha256) {
1101
1696
  throw new CliError('approvePlan must exactly match the canonical maintenance plan hash.', {
1102
1697
  code: 'DATASET_MAINTENANCE_PLAN_APPROVAL_REQUIRED',
@@ -1153,18 +1748,46 @@ export async function runDatasetMaintenanceApply(options) {
1153
1748
  const progress = plan.operation === 'rebuild-derivatives'
1154
1749
  ? { entries: [], successes: new Map(), latestFailures: new Map() }
1155
1750
  : parseProgress(plan, progressPath);
1751
+ const executionLogPath = path.join(planDir, 'apply-execution-log.jsonl');
1752
+ const parallelDeleteExecution = parallelDeleteMode
1753
+ ? parseParallelDeleteExecutionLog(plan, executionLogPath)
1754
+ : { entries: [], byAction: new Map(), dispatched: new Set(), committed: new Set() };
1156
1755
  const resumedSuccesses = progress.successes.size;
1157
- const current = await fetchMaintenanceAccountRows({
1158
- context,
1159
- userId: plan.account.user_id,
1160
- });
1756
+ const [current, visibleProcesses] = await Promise.all([
1757
+ fetchMaintenanceAccountRows({
1758
+ context,
1759
+ userId: plan.account.user_id,
1760
+ }),
1761
+ parallelDeleteMode
1762
+ ? fetchMaintenanceVisibleTableRows({ context, table: 'processes' })
1763
+ : Promise.resolve(null),
1764
+ ]);
1161
1765
  assertApplyPreconditions({
1162
1766
  plan,
1163
1767
  planDir,
1164
1768
  currentRows: current.rows,
1165
1769
  progress,
1166
1770
  aliasPlanProgress: { entries: [], success: null, latestFailure: null },
1771
+ attemptedDeleteActionIds: parallelDeleteExecution.dispatched,
1167
1772
  });
1773
+ const inboundBarrierPath = nextParallelDeleteInboundBarrierPath(planDir);
1774
+ const inboundBarrier = parallelDeleteMode && visibleProcesses
1775
+ ? assertNoVisibleProcessInboundReferences({ plan, rows: visibleProcesses.rows })
1776
+ : null;
1777
+ if (inboundBarrier && visibleProcesses) {
1778
+ writeImmutableJson(inboundBarrierPath, {
1779
+ schema_version: 1,
1780
+ generated_at_utc: clock(options),
1781
+ plan_sha256: plan.plan_sha256,
1782
+ operation_id: plan.operation_id,
1783
+ actor: { user_id: context.account.user_id, email: context.account.email },
1784
+ target_table: 'flows',
1785
+ target_count: plan.actions.length,
1786
+ inbound_reference_count: 0,
1787
+ ...inboundBarrier,
1788
+ completeness: visibleProcesses.completeness,
1789
+ });
1790
+ }
1168
1791
  const approvalPath = path.join(planDir, 'approval-record.json');
1169
1792
  const approvalAlreadyExisted = existsSync(approvalPath);
1170
1793
  validateApprovalRecord({ path: approvalPath, plan, context });
@@ -1284,6 +1907,80 @@ export async function runDatasetMaintenanceApply(options) {
1284
1907
  writeJsonArtifact(report.artifacts.commit_report, report);
1285
1908
  return report;
1286
1909
  }
1910
+ if (parallelDeleteMode) {
1911
+ const executionStatuses = await executeParallelDeletePlan({
1912
+ plan,
1913
+ context,
1914
+ progress,
1915
+ progressPath,
1916
+ executionLogPath,
1917
+ execution: parallelDeleteExecution,
1918
+ maxParallel,
1919
+ now: () => clock(options),
1920
+ });
1921
+ const actions = plan.actions.map((action) => {
1922
+ const status = executionStatuses.get(action.action_id);
1923
+ return {
1924
+ action_id: action.action_id,
1925
+ action: action.action,
1926
+ table: action.table,
1927
+ id: action.id,
1928
+ version: action.version,
1929
+ status,
1930
+ error: progress.latestFailures.get(action.action_id)?.error ?? null,
1931
+ };
1932
+ });
1933
+ const successCount = actions.filter((action) => action.status === 'success').length;
1934
+ const failureCount = actions.filter((action) => action.status === 'failed').length;
1935
+ const unknownCount = actions.filter((action) => action.status === 'unknown').length;
1936
+ const attemptPath = nextAttemptPath(planDir);
1937
+ const report = {
1938
+ schema_version: 1,
1939
+ generated_at_utc: clock(options),
1940
+ status: unknownCount > 0
1941
+ ? 'completed_with_unknowns'
1942
+ : successCount === actions.length
1943
+ ? 'completed'
1944
+ : 'completed_with_failures',
1945
+ task_id: plan.task_id,
1946
+ operation: plan.operation,
1947
+ operation_id: plan.operation_id,
1948
+ target_mode: plan.target_mode,
1949
+ plan_sha256: plan.plan_sha256,
1950
+ actor: { user_id: context.account.user_id, email: context.account.email },
1951
+ summary: {
1952
+ actions: actions.length,
1953
+ success: successCount,
1954
+ failed: failureCount,
1955
+ unknown: unknownCount,
1956
+ pending: actions.length - successCount - failureCount - unknownCount,
1957
+ resumed_successes: resumedSuccesses,
1958
+ },
1959
+ actions,
1960
+ artifacts: {
1961
+ approval_record: approvalPath,
1962
+ apply_progress: progressPath,
1963
+ execution_log: executionLogPath,
1964
+ inbound_reference_barrier: inboundBarrierPath,
1965
+ commit_report: path.join(planDir, 'commit-report.json'),
1966
+ attempt_report: attemptPath,
1967
+ },
1968
+ database_audit: {
1969
+ rpc_transaction_log: 'public.command_audit_log',
1970
+ source: 'tiangong-lca dataset maintenance apply',
1971
+ correlation_fields: [
1972
+ 'plan_sha256',
1973
+ 'operation_id',
1974
+ 'action_id',
1975
+ 'reason_code',
1976
+ 'desired_sha256',
1977
+ ],
1978
+ },
1979
+ };
1980
+ writeImmutableJson(attemptPath, report);
1981
+ writeJsonArtifact(report.artifacts.commit_report, report);
1982
+ return report;
1983
+ }
1287
1984
  const ordered = [...plan.actions].sort((left, right) => {
1288
1985
  const rank = {
1289
1986
  save_draft: 0,
@@ -1426,8 +2123,11 @@ export const __testInternals = {
1426
2123
  aliasExchangeProgressKey,
1427
2124
  appendAliasSuccessLogs,
1428
2125
  appendAliasProofProgress,
2126
+ appendParallelDeleteExecutionEntry,
2127
+ assertNoVisibleProcessInboundReferences,
1429
2128
  assertApplyPreconditions,
1430
2129
  assertAliasSupportSnapshots,
2130
+ assertParallelDeletePlan,
1431
2131
  buildAliasBatchRequest,
1432
2132
  buildAliasPlanRequest,
1433
2133
  clock,
@@ -1435,14 +2135,20 @@ export const __testInternals = {
1435
2135
  executeDerivativeAdmission,
1436
2136
  executeAliasPlan,
1437
2137
  executeAction,
2138
+ executeParallelDeletePlan,
1438
2139
  finalProjectedRows,
1439
2140
  loadDesiredPayload,
1440
2141
  nextAttemptPath,
2142
+ nextParallelDeleteInboundBarrierPath,
2143
+ normalizeMaintenanceMaxParallel,
2144
+ parallelDeleteDesiredSha256,
1441
2145
  parseDerivativeSubmitProgress,
1442
2146
  validateDerivativeAdmissionAttempt,
1443
2147
  parseAliasBatchProgress,
1444
2148
  parseAliasPlanProgress,
1445
2149
  parseProgress,
2150
+ parseParallelDeleteExecutionLog,
2151
+ remoteAuditId,
1446
2152
  validateAliasRpcResult,
1447
2153
  validateAliasPlanRpcResult,
1448
2154
  validateApprovalRecord,