@tiangong-lca/cli 0.0.32 → 0.1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -12
  3. package/assets/import-smoke/simapro.csv +20 -0
  4. package/bin/tiangong-lca.js +1 -1
  5. package/dist/src/cli.js +103 -65
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/lib/dataset-bilingual.js.map +1 -1
  8. package/dist/src/lib/dataset-classification.js.map +1 -1
  9. package/dist/src/lib/dataset-contract.js.map +1 -1
  10. package/dist/src/lib/dataset-evidence-search.js.map +1 -1
  11. package/dist/src/lib/dataset-import-lca.js +302 -131
  12. package/dist/src/lib/dataset-import-lca.js.map +1 -1
  13. package/dist/src/lib/dataset-maintenance-apply.js +916 -43
  14. package/dist/src/lib/dataset-maintenance-apply.js.map +1 -1
  15. package/dist/src/lib/dataset-maintenance-clear-account.js +10 -4
  16. package/dist/src/lib/dataset-maintenance-clear-account.js.map +1 -1
  17. package/dist/src/lib/dataset-maintenance-contract.js +1 -6
  18. package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
  19. package/dist/src/lib/dataset-maintenance-derivatives.js +0 -1
  20. package/dist/src/lib/dataset-maintenance-derivatives.js.map +1 -1
  21. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js +1 -0
  22. package/dist/src/lib/dataset-maintenance-flow-identity-capture.js.map +1 -1
  23. package/dist/src/lib/dataset-maintenance-pagination.js.map +1 -1
  24. package/dist/src/lib/dataset-maintenance-protected-before.js.map +1 -1
  25. package/dist/src/lib/dataset-maintenance-protected-contract.js +0 -1
  26. package/dist/src/lib/dataset-maintenance-protected-contract.js.map +1 -1
  27. package/dist/src/lib/dataset-maintenance-protected-freeze.js.map +1 -1
  28. package/dist/src/lib/dataset-maintenance-protected-preparation.js +0 -2
  29. package/dist/src/lib/dataset-maintenance-protected-preparation.js.map +1 -1
  30. package/dist/src/lib/dataset-maintenance-protected-seal.js.map +1 -1
  31. package/dist/src/lib/dataset-maintenance-protected-verify.js +0 -3
  32. package/dist/src/lib/dataset-maintenance-protected-verify.js.map +1 -1
  33. package/dist/src/lib/dataset-maintenance-remote.js +68 -12
  34. package/dist/src/lib/dataset-maintenance-remote.js.map +1 -1
  35. package/dist/src/lib/dataset-maintenance-verify.js +1 -2
  36. package/dist/src/lib/dataset-maintenance-verify.js.map +1 -1
  37. package/dist/src/lib/dataset-remote-refresh.js.map +1 -1
  38. package/dist/src/lib/dataset-remote-verify.js +2 -0
  39. package/dist/src/lib/dataset-remote-verify.js.map +1 -1
  40. package/dist/src/lib/dataset-save-draft-run.js +2 -0
  41. package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
  42. package/dist/src/lib/flow-build-alias-map.js.map +1 -1
  43. package/dist/src/lib/flow-fetch-rows.js.map +1 -1
  44. package/dist/src/lib/flow-get.js.map +1 -1
  45. package/dist/src/lib/flow-governance.js.map +1 -1
  46. package/dist/src/lib/flow-list.js +1 -0
  47. package/dist/src/lib/flow-list.js.map +1 -1
  48. package/dist/src/lib/flow-payload-validation.js.map +1 -1
  49. package/dist/src/lib/flow-publish-version.js +1 -0
  50. package/dist/src/lib/flow-publish-version.js.map +1 -1
  51. package/dist/src/lib/flow-read.js +1 -0
  52. package/dist/src/lib/flow-read.js.map +1 -1
  53. package/dist/src/lib/flow-regen-product.js.map +1 -1
  54. package/dist/src/lib/flow-remediate.js.map +1 -1
  55. package/dist/src/lib/identity-preflight.js +4 -10
  56. package/dist/src/lib/identity-preflight.js.map +1 -1
  57. package/dist/src/lib/lifecyclemodel-bundle-save.js +1 -0
  58. package/dist/src/lib/lifecyclemodel-bundle-save.js.map +1 -1
  59. package/dist/src/lib/lifecyclemodel-orchestrate.js.map +1 -1
  60. package/dist/src/lib/lifecyclemodel-publish-resulting-process.js.map +1 -1
  61. package/dist/src/lib/lifecyclemodel-resulting-process.js.map +1 -1
  62. package/dist/src/lib/lifecyclemodel-save-draft-run.js.map +1 -1
  63. package/dist/src/lib/lifecyclemodel-validate-build.js.map +1 -1
  64. package/dist/src/lib/package-version.js.map +1 -1
  65. package/dist/src/lib/process-auto-build.js.map +1 -1
  66. package/dist/src/lib/process-dedup-review.js +14 -7
  67. package/dist/src/lib/process-dedup-review.js.map +1 -1
  68. package/dist/src/lib/process-flow-build-plan.js +111 -30
  69. package/dist/src/lib/process-flow-build-plan.js.map +1 -1
  70. package/dist/src/lib/process-get.js +1 -0
  71. package/dist/src/lib/process-get.js.map +1 -1
  72. package/dist/src/lib/process-list.js +1 -0
  73. package/dist/src/lib/process-list.js.map +1 -1
  74. package/dist/src/lib/process-payload-validation.js.map +1 -1
  75. package/dist/src/lib/process-qa.js.map +1 -1
  76. package/dist/src/lib/process-refresh-references.js +15 -10
  77. package/dist/src/lib/process-refresh-references.js.map +1 -1
  78. package/dist/src/lib/process-save-draft.js +9 -3
  79. package/dist/src/lib/process-save-draft.js.map +1 -1
  80. package/dist/src/lib/process-scope-statistics.js +11 -4
  81. package/dist/src/lib/process-scope-statistics.js.map +1 -1
  82. package/dist/src/lib/process-verify-rows.js.map +1 -1
  83. package/dist/src/lib/publish.js.map +1 -1
  84. package/dist/src/lib/run.js.map +1 -1
  85. package/dist/src/lib/supabase-client.js +13 -1
  86. package/dist/src/lib/supabase-client.js.map +1 -1
  87. package/dist/src/lib/supabase-data-api-contract.js +328 -0
  88. package/dist/src/lib/supabase-data-api-contract.js.map +1 -0
  89. package/dist/src/lib/supabase-data-api-replay.js +69 -0
  90. package/dist/src/lib/supabase-data-api-replay.js.map +1 -0
  91. package/dist/src/lib/supabase-json-ordered-write.js +2 -0
  92. package/dist/src/lib/supabase-json-ordered-write.js.map +1 -1
  93. package/dist/src/lib/supabase-rest.js +1 -0
  94. package/dist/src/lib/supabase-rest.js.map +1 -1
  95. package/dist/src/lib/tidas-sdk-package-validator.js +5 -5
  96. package/dist/src/lib/tidas-sdk-package-validator.js.map +1 -1
  97. package/dist/src/lib/validation.js.map +1 -1
  98. package/dist/src/main.js.map +1 -1
  99. package/package.json +40 -40
  100. package/dist/scripts/assert-full-coverage.js +0 -77
  101. package/dist/scripts/assert-full-coverage.js.map +0 -1
@@ -1,14 +1,21 @@
1
- import { existsSync } from 'node:fs';
1
+ import { closeSync, existsSync, fchmodSync, fsyncSync, openSync, readFileSync, 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, sha256Text, 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 { applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, fetchMaintenanceVisibleTableRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
13
+ // Process payloads can be large enough for the default 1,000-row page to exceed
14
+ // the database statement timeout. Keep the destructive all-visible RLS fence,
15
+ // but bound each SELECT-only page so admission can complete without weakening it.
16
+ const PARALLEL_DELETE_VISIBLE_PROCESS_PAGE_SIZE = 250;
17
+ const GLOBAL_INBOUND_PROOF_MAX_AGE_MS = 30 * 60 * 1_000;
18
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
12
19
  const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
13
20
  function clock(options) {
14
21
  return (options.now ?? new Date()).toISOString();
@@ -16,6 +23,405 @@ function clock(options) {
16
23
  function errorMessage(error) {
17
24
  return error instanceof Error ? error.message : String(error);
18
25
  }
26
+ const ABSENT_READBACK_SHA256 = sha256Json([]);
27
+ function parallelDeleteDesiredSha256(action) {
28
+ return sha256Json({
29
+ action: 'delete',
30
+ table: action.table,
31
+ id: action.id,
32
+ version: action.version,
33
+ desired: 'absent',
34
+ });
35
+ }
36
+ function normalizeMaintenanceMaxParallel(value) {
37
+ const normalized = value ?? 1;
38
+ if (!Number.isInteger(normalized) || normalized < 1 || normalized > 8) {
39
+ throw new CliError('--max-parallel must be an integer from 1 to 8.', {
40
+ code: 'DATASET_MAINTENANCE_MAX_PARALLEL_INVALID',
41
+ exitCode: 2,
42
+ });
43
+ }
44
+ return normalized;
45
+ }
46
+ function assertParallelDeletePlan(plan) {
47
+ const targets = new Set();
48
+ if (plan.operation !== 'delete' ||
49
+ plan.actions.length === 0 ||
50
+ plan.actions.some((action) => action.action !== 'delete' || action.table !== 'flows') ||
51
+ plan.summary.delete !== plan.actions.length ||
52
+ plan.summary.save_draft !== 0 ||
53
+ (plan.summary.update_json_ordered ?? 0) !== 0 ||
54
+ (plan.summary.rebuild_derivatives ?? 0) !== 0 ||
55
+ plan.summary.current_reference_impacts !== 0 ||
56
+ plan.summary.projected_reference_impacts !== 0) {
57
+ throw new CliError('--max-parallel maintenance apply requires a non-empty flow delete-only plan with zero current and projected inbound references.', {
58
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_PLAN_REQUIRED',
59
+ exitCode: 1,
60
+ });
61
+ }
62
+ for (const action of plan.actions) {
63
+ const target = `${action.table}\u0000${action.id}\u0000${action.version}`;
64
+ if (targets.has(target)) {
65
+ throw new CliError('Parallel delete plan contains a repeated table/id/version target.', {
66
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_TARGET_DUPLICATE',
67
+ exitCode: 1,
68
+ details: { table: action.table, id: action.id, version: action.version },
69
+ });
70
+ }
71
+ targets.add(target);
72
+ }
73
+ }
74
+ function parallelDeleteTargetBindingSha256(plan) {
75
+ return sha256Json(plan.actions.map((action, index) => ({
76
+ index,
77
+ ordinal: action.ordinal,
78
+ action_id: action.action_id,
79
+ table: action.table,
80
+ id: action.id,
81
+ version: action.version,
82
+ expected_user_id: action.expected_user_id,
83
+ expected_state_code: action.expected_state_code,
84
+ before_sha256: action.before?.row_sha256 ?? null,
85
+ })));
86
+ }
87
+ function invalidGlobalInboundProof(message, details) {
88
+ throw new CliError(message, {
89
+ code: 'DATASET_MAINTENANCE_GLOBAL_INBOUND_PROOF_INVALID',
90
+ exitCode: 1,
91
+ details,
92
+ });
93
+ }
94
+ function assertGlobalInboundProofOptionShape(options) {
95
+ const hasPath = typeof options.proofPath === 'string' && options.proofPath.length > 0;
96
+ const hasApproval = typeof options.approveProof === 'string' && options.approveProof.length > 0;
97
+ if (hasPath !== hasApproval) {
98
+ throw new CliError('--global-inbound-proof and --approve-global-inbound-proof must be provided together.', {
99
+ code: 'DATASET_MAINTENANCE_GLOBAL_INBOUND_PROOF_OPTIONS_INCOMPLETE',
100
+ exitCode: 2,
101
+ });
102
+ }
103
+ if ((hasPath || hasApproval) && !options.parallelDeleteMode) {
104
+ throw new CliError('A global inbound proof is allowed only with --max-parallel flow delete-only execution.', {
105
+ code: 'DATASET_MAINTENANCE_GLOBAL_INBOUND_PROOF_MODE_INVALID',
106
+ exitCode: 2,
107
+ });
108
+ }
109
+ if (hasApproval && !SHA256_PATTERN.test(options.approveProof)) {
110
+ throw new CliError('--approve-global-inbound-proof must be a lowercase SHA-256.', {
111
+ code: 'DATASET_MAINTENANCE_GLOBAL_INBOUND_PROOF_APPROVAL_INVALID',
112
+ exitCode: 2,
113
+ });
114
+ }
115
+ }
116
+ function validateParallelDeleteGlobalInboundProof(options) {
117
+ if (!options.proofPath || !options.approveProof)
118
+ return null;
119
+ if (!path.isAbsolute(options.proofPath)) {
120
+ invalidGlobalInboundProof('Global inbound proof path must be absolute.');
121
+ }
122
+ let proofText;
123
+ try {
124
+ proofText = readFileSync(options.proofPath, 'utf8');
125
+ }
126
+ catch (error) {
127
+ invalidGlobalInboundProof('Global inbound proof file could not be read.', errorMessage(error));
128
+ }
129
+ const proofSha256 = sha256Text(proofText);
130
+ if (proofSha256 !== options.approveProof) {
131
+ invalidGlobalInboundProof('Global inbound proof file SHA-256 does not match approval.', {
132
+ expected: options.approveProof,
133
+ actual: proofSha256,
134
+ });
135
+ }
136
+ let value;
137
+ try {
138
+ value = JSON.parse(proofText);
139
+ }
140
+ catch (error) {
141
+ invalidGlobalInboundProof('Global inbound proof is not valid JSON.', errorMessage(error));
142
+ }
143
+ if (!isJsonObject(value)) {
144
+ invalidGlobalInboundProof('Global inbound proof must be a JSON object.');
145
+ }
146
+ const expectedBinding = parallelDeleteTargetBindingSha256(options.plan);
147
+ const capturedAt = typeof value.captured_at_utc === 'string' ? Date.parse(value.captured_at_utc) : NaN;
148
+ const nowMs = options.now.getTime();
149
+ const capturedAgeMs = nowMs - capturedAt;
150
+ const safePositiveInteger = (candidate) => Number.isSafeInteger(candidate) && Number(candidate) > 0;
151
+ if (value.schema_version !== 'dataset-maintenance-global-inbound-proof.v1' ||
152
+ value.status !== 'PASS_GLOBAL_ALL_PROCESS_INBOUND_ZERO' ||
153
+ value.statement_kind !== 'SELECT' ||
154
+ value.source !== 'supabase_select_only_raw_sql' ||
155
+ value.process_scope !== 'all_process_rows_without_rls_restriction' ||
156
+ !Number.isFinite(capturedAt) ||
157
+ capturedAgeMs < -30_000 ||
158
+ capturedAgeMs > GLOBAL_INBOUND_PROOF_MAX_AGE_MS ||
159
+ value.project_ref !== options.context.project_ref ||
160
+ value.actor_user_id !== options.context.account.user_id ||
161
+ value.plan_sha256 !== options.plan.plan_sha256 ||
162
+ value.operation !== 'delete' ||
163
+ value.target_table !== 'flows' ||
164
+ value.target_count !== options.plan.actions.length ||
165
+ value.target_binding_sha256 !== expectedBinding ||
166
+ !safePositiveInteger(value.global_process_rows) ||
167
+ !safePositiveInteger(value.global_exchange_rows) ||
168
+ value.inbound_exchanges !== 0 ||
169
+ value.old_flow_identities_with_inbound !== 0 ||
170
+ value.process_identities_with_inbound !== 0 ||
171
+ value.p0 !== 0 ||
172
+ value.p1 !== 0 ||
173
+ !Array.isArray(value.chunks) ||
174
+ value.chunks.length === 0) {
175
+ invalidGlobalInboundProof('Global inbound proof metadata or zero-inbound result is invalid.', {
176
+ expected_project_ref: options.context.project_ref,
177
+ expected_actor_user_id: options.context.account.user_id,
178
+ expected_plan_sha256: options.plan.plan_sha256,
179
+ expected_target_count: options.plan.actions.length,
180
+ expected_target_binding_sha256: expectedBinding,
181
+ });
182
+ }
183
+ let expectedStart = 0;
184
+ for (const [index, rawChunk] of value.chunks.entries()) {
185
+ if (!isJsonObject(rawChunk)) {
186
+ invalidGlobalInboundProof('Global inbound proof contains a non-object chunk.', { index });
187
+ }
188
+ const chunkCapturedAt = typeof rawChunk.captured_at_utc === 'string' ? Date.parse(rawChunk.captured_at_utc) : NaN;
189
+ const chunkAgeMs = nowMs - chunkCapturedAt;
190
+ if (rawChunk.index !== index ||
191
+ rawChunk.start !== expectedStart ||
192
+ !Number.isSafeInteger(rawChunk.end_exclusive) ||
193
+ Number(rawChunk.end_exclusive) <= expectedStart ||
194
+ Number(rawChunk.end_exclusive) > options.plan.actions.length ||
195
+ rawChunk.target_count !== Number(rawChunk.end_exclusive) - expectedStart ||
196
+ !Number.isFinite(chunkCapturedAt) ||
197
+ chunkAgeMs < -30_000 ||
198
+ chunkAgeMs > GLOBAL_INBOUND_PROOF_MAX_AGE_MS ||
199
+ rawChunk.inbound_exchanges !== 0 ||
200
+ rawChunk.old_flow_identities_with_inbound !== 0 ||
201
+ rawChunk.process_identities_with_inbound !== 0 ||
202
+ rawChunk.owner_draft_inbound !== 0 ||
203
+ rawChunk.public_inbound !== 0 ||
204
+ rawChunk.foreign_private_inbound !== 0 ||
205
+ rawChunk.other_state_inbound !== 0 ||
206
+ typeof rawChunk.sql_sha256 !== 'string' ||
207
+ !SHA256_PATTERN.test(rawChunk.sql_sha256)) {
208
+ invalidGlobalInboundProof('Global inbound proof contains an invalid or incomplete chunk.', {
209
+ index,
210
+ expected_start: expectedStart,
211
+ });
212
+ }
213
+ expectedStart = Number(rawChunk.end_exclusive);
214
+ }
215
+ if (expectedStart !== options.plan.actions.length) {
216
+ invalidGlobalInboundProof('Global inbound proof chunks do not cover every delete target.', {
217
+ expected_end: options.plan.actions.length,
218
+ actual_end: expectedStart,
219
+ });
220
+ }
221
+ return {
222
+ ...value,
223
+ proof_path: options.proofPath,
224
+ proof_sha256: proofSha256,
225
+ };
226
+ }
227
+ function parseParallelDeleteExecutionLog(plan, executionLogPath) {
228
+ const actions = new Map(plan.actions.map((action) => [action.action_id, action]));
229
+ const entries = [];
230
+ const byAction = new Map();
231
+ const dispatched = new Set();
232
+ const committed = new Set();
233
+ for (const value of readJsonLinesIfPresent(executionLogPath)) {
234
+ const action = isJsonObject(value) && typeof value.action_id === 'string'
235
+ ? actions.get(value.action_id)
236
+ : null;
237
+ const expectedDesired = action ? parallelDeleteDesiredSha256(action) : null;
238
+ const actionEntries = action ? (byAction.get(action.action_id) ?? []) : [];
239
+ const priorDispatched = actionEntries.some((entry) => entry.status === 'DISPATCHED');
240
+ const priorCommitted = actionEntries.some((entry) => entry.status === 'COMMITTED');
241
+ const status = isJsonObject(value) ? value.status : null;
242
+ const auditIdValid = isJsonObject(value) &&
243
+ (value.audit_id === null ||
244
+ (typeof value.audit_id === 'string' && POSITIVE_INTEGER_TEXT.test(value.audit_id)));
245
+ const remoteHashValid = isJsonObject(value) &&
246
+ (value.remote_result_sha256 === null ||
247
+ (typeof value.remote_result_sha256 === 'string' &&
248
+ /^[a-f0-9]{64}$/u.test(value.remote_result_sha256)));
249
+ const commonOutcomeFieldsValid = isJsonObject(value) &&
250
+ typeof value.attempt_consumed === 'boolean' &&
251
+ typeof value.recovered === 'boolean' &&
252
+ (value.readback_sha256 === null ||
253
+ (typeof value.readback_sha256 === 'string' &&
254
+ /^[a-f0-9]{64}$/u.test(value.readback_sha256))) &&
255
+ (value.error === null || typeof value.error === 'string');
256
+ const statusFieldsValid = isJsonObject(value) &&
257
+ ((status === 'PREPARED' &&
258
+ value.attempt_consumed === false &&
259
+ value.recovered === false &&
260
+ value.audit_id === null &&
261
+ value.readback_sha256 === null &&
262
+ value.remote_result_sha256 === null &&
263
+ value.error === null &&
264
+ !priorDispatched &&
265
+ !priorCommitted) ||
266
+ (status === 'DISPATCHED' &&
267
+ value.attempt_consumed === true &&
268
+ value.recovered === false &&
269
+ value.audit_id === null &&
270
+ value.readback_sha256 === null &&
271
+ value.remote_result_sha256 === null &&
272
+ value.error === null &&
273
+ actionEntries.some((entry) => entry.status === 'PREPARED') &&
274
+ !priorDispatched &&
275
+ !priorCommitted) ||
276
+ (status === 'UNKNOWN' &&
277
+ value.attempt_consumed === true &&
278
+ typeof value.error === 'string' &&
279
+ value.error.length > 0 &&
280
+ priorDispatched &&
281
+ !priorCommitted) ||
282
+ (status === 'COMMITTED' &&
283
+ value.attempt_consumed === true &&
284
+ value.readback_sha256 === ABSENT_READBACK_SHA256 &&
285
+ value.error === null &&
286
+ priorDispatched &&
287
+ !priorCommitted));
288
+ if (!isJsonObject(value) ||
289
+ value.schema_version !== 1 ||
290
+ value.plan_sha256 !== plan.plan_sha256 ||
291
+ value.operation_id !== plan.operation_id ||
292
+ !action ||
293
+ action.action !== 'delete' ||
294
+ action.table !== 'flows' ||
295
+ value.attempt_key !== `${action.action_id}@${expectedDesired}` ||
296
+ value.action !== 'delete' ||
297
+ value.table !== 'flows' ||
298
+ value.id !== action.id ||
299
+ value.version !== action.version ||
300
+ value.desired_sha256 !== expectedDesired ||
301
+ value.before_sha256 !== action.before?.row_sha256 ||
302
+ !isJsonObject(value.actor) ||
303
+ value.actor.user_id !== plan.account.user_id ||
304
+ value.actor.email !== plan.account.email ||
305
+ !isJsonObject(value.audit_context) ||
306
+ value.audit_context.plan_sha256 !== plan.plan_sha256 ||
307
+ value.audit_context.operation_id !== plan.operation_id ||
308
+ value.audit_context.action_id !== action.action_id ||
309
+ value.audit_context.reason_code !== action.reason_code ||
310
+ value.audit_context.source !== 'tiangong-lca dataset maintenance apply' ||
311
+ typeof value.recorded_at_utc !== 'string' ||
312
+ !Number.isFinite(Date.parse(value.recorded_at_utc)) ||
313
+ !auditIdValid ||
314
+ !remoteHashValid ||
315
+ !commonOutcomeFieldsValid ||
316
+ !statusFieldsValid) {
317
+ throw new CliError('Parallel delete execution log contains an invalid or foreign entry.', {
318
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_LOG_INVALID',
319
+ exitCode: 1,
320
+ details: value,
321
+ });
322
+ }
323
+ const entry = value;
324
+ entries.push(entry);
325
+ actionEntries.push(entry);
326
+ byAction.set(action.action_id, actionEntries);
327
+ if (entry.status === 'DISPATCHED')
328
+ dispatched.add(action.action_id);
329
+ if (entry.status === 'COMMITTED')
330
+ committed.add(action.action_id);
331
+ }
332
+ return { entries, byAction, dispatched, committed };
333
+ }
334
+ function appendParallelDeleteExecutionEntry(options) {
335
+ const desiredSha256 = parallelDeleteDesiredSha256(options.action);
336
+ const entry = {
337
+ schema_version: 1,
338
+ plan_sha256: options.plan.plan_sha256,
339
+ operation_id: options.plan.operation_id,
340
+ action_id: options.action.action_id,
341
+ attempt_key: `${options.action.action_id}@${desiredSha256}`,
342
+ action: 'delete',
343
+ table: 'flows',
344
+ id: options.action.id,
345
+ version: options.action.version,
346
+ desired_sha256: desiredSha256,
347
+ before_sha256: options.action.before.row_sha256,
348
+ actor: {
349
+ user_id: options.context.account.user_id,
350
+ email: options.context.account.email,
351
+ },
352
+ status: options.status,
353
+ recorded_at_utc: options.recordedAtUtc,
354
+ attempt_consumed: options.status !== 'PREPARED',
355
+ recovered: options.recovered ?? false,
356
+ audit_context: {
357
+ plan_sha256: options.plan.plan_sha256,
358
+ operation_id: options.plan.operation_id,
359
+ action_id: options.action.action_id,
360
+ reason_code: options.action.reason_code,
361
+ source: 'tiangong-lca dataset maintenance apply',
362
+ },
363
+ audit_id: options.auditId ?? null,
364
+ readback_sha256: options.readbackSha256 ?? null,
365
+ remote_result_sha256: options.remoteResultSha256 ?? null,
366
+ error: options.error ?? null,
367
+ };
368
+ const descriptor = openSync(options.path, 'a', 0o600);
369
+ try {
370
+ fchmodSync(descriptor, 0o600);
371
+ writeFileSync(descriptor, `${stableJsonText(entry)}\n`, 'utf8');
372
+ fsyncSync(descriptor);
373
+ }
374
+ finally {
375
+ closeSync(descriptor);
376
+ }
377
+ options.state.entries.push(entry);
378
+ const actionEntries = options.state.byAction.get(options.action.action_id) ?? [];
379
+ actionEntries.push(entry);
380
+ options.state.byAction.set(options.action.action_id, actionEntries);
381
+ if (entry.status === 'DISPATCHED')
382
+ options.state.dispatched.add(options.action.action_id);
383
+ if (entry.status === 'COMMITTED')
384
+ options.state.committed.add(options.action.action_id);
385
+ return entry;
386
+ }
387
+ function assertNoVisibleProcessInboundReferences(options) {
388
+ const targets = new Set(options.plan.actions.map((action) => `${action.id}\u0000${action.version}`));
389
+ const targetIds = new Set(options.plan.actions.map((action) => action.id));
390
+ const payloadRows = options.rows
391
+ .filter((row) => row.table === 'processes' && row.json_ordered !== null)
392
+ .map((row) => ({
393
+ table: row.table,
394
+ id: row.id,
395
+ version: row.version,
396
+ json_ordered: row.json_ordered,
397
+ }));
398
+ const references = collectRemoteReferences(payloadRows).filter((reference) => reference.role === 'reference' &&
399
+ reference.table === 'flows' &&
400
+ typeof reference.id === 'string');
401
+ const inbound = references.filter((reference) => reference.version
402
+ ? targets.has(`${reference.id}\u0000${reference.version}`)
403
+ : targetIds.has(reference.id));
404
+ if (inbound.length > 0) {
405
+ throw new CliError('Parallel flow delete admission found visible process inbound references.', {
406
+ code: 'DATASET_MAINTENANCE_PARALLEL_DELETE_INBOUND_REFERENCES',
407
+ exitCode: 1,
408
+ details: {
409
+ inbound_count: inbound.length,
410
+ first: inbound.slice(0, 20),
411
+ },
412
+ });
413
+ }
414
+ return {
415
+ process_rows: payloadRows.length,
416
+ process_references: references.length,
417
+ snapshot_sha256: sha256Json(payloadRows.map((row) => ({
418
+ table: row.table,
419
+ id: row.id,
420
+ version: row.version,
421
+ payload_sha256: sha256Json(row.json_ordered),
422
+ }))),
423
+ };
424
+ }
19
425
  function loadDesiredPayload(planDir, action) {
20
426
  return loadMaintenanceDesiredPayload(planDir, action);
21
427
  }
@@ -413,6 +819,13 @@ function assertApplyPreconditions(options) {
413
819
  }
414
820
  continue;
415
821
  }
822
+ if (action.action === 'delete' && options.attemptedDeleteActionIds?.has(action.action_id)) {
823
+ // A request may have committed before its response or progress row was
824
+ // persisted, or the target may have drifted after dispatch. The execution
825
+ // ledger owns both cases and permits only read-only recovery, never an
826
+ // automatic replay.
827
+ continue;
828
+ }
416
829
  if (!currentRow || currentRow.user_id !== action.expected_user_id) {
417
830
  throw new CliError(`Action row is missing, non-draft, or not owned: ${action.action_id}`, {
418
831
  code: 'DATASET_MAINTENANCE_ACTION_ROW_DRIFT',
@@ -627,39 +1040,6 @@ function validateAliasPlanRpcResult(value, plan) {
627
1040
  raw: value,
628
1041
  };
629
1042
  }
630
- async function executeAliasPlan(options) {
631
- const request = buildAliasPlanRequest(options);
632
- const remoteResult = await applyMaintenanceAliasPlan({
633
- context: options.context,
634
- plan: request,
635
- });
636
- const rpc = validateAliasPlanRpcResult(remoteResult, options.plan);
637
- const afterByAction = new Map();
638
- for (const action of options.plan.actions) {
639
- const exact = await fetchMaintenanceExactRows({
640
- context: options.context,
641
- table: action.table,
642
- id: action.id,
643
- version: action.version,
644
- });
645
- const row = exact.rows.length === 1 ? exact.rows[0] : null;
646
- const snapshot = row ? snapshotRemoteRow(row) : null;
647
- if (!row ||
648
- row.user_id !== action.expected_user_id ||
649
- row.state_code !== 0 ||
650
- snapshot?.payload_sha256 !== action.desired_payload?.sha256 ||
651
- row.model_id !== action.before?.model_id ||
652
- row.rule_verification !== action.before?.rule_verification) {
653
- throw new CliError(`Alias plan readback failed for action ${action.action_id}.`, {
654
- code: 'DATASET_MAINTENANCE_ALIAS_READBACK_FAILED',
655
- exitCode: 1,
656
- });
657
- }
658
- afterByAction.set(action.action_id, snapshot.row_sha256);
659
- }
660
- await assertAliasSupportSnapshots({ plan: options.plan, context: options.context });
661
- return { rpc, after_by_action: afterByAction };
662
- }
663
1043
  function aliasExchangeProgressKey(value) {
664
1044
  return `${value.batch_id}\u0000${value.action_id}\u0000${value.exchange_index}\u0000${value.data_set_internal_id}`;
665
1045
  }
@@ -1026,6 +1406,335 @@ async function executeAction(options) {
1026
1406
  }
1027
1407
  return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
1028
1408
  }
1409
+ function appendParallelDeleteProgress(options) {
1410
+ const entry = {
1411
+ schema_version: 1,
1412
+ plan_sha256: options.plan.plan_sha256,
1413
+ operation_id: options.plan.operation_id,
1414
+ action_id: options.action.action_id,
1415
+ action: 'delete',
1416
+ table: options.action.table,
1417
+ id: options.action.id,
1418
+ version: options.action.version,
1419
+ reason_code: options.action.reason_code,
1420
+ audit_context: {
1421
+ plan_sha256: options.plan.plan_sha256,
1422
+ operation_id: options.plan.operation_id,
1423
+ action_id: options.action.action_id,
1424
+ reason_code: options.action.reason_code,
1425
+ source: 'tiangong-lca dataset maintenance apply',
1426
+ },
1427
+ actor: {
1428
+ user_id: options.context.account.user_id,
1429
+ email: options.context.account.email,
1430
+ },
1431
+ started_at_utc: options.startedAtUtc,
1432
+ ended_at_utc: options.endedAtUtc,
1433
+ before_sha256: options.action.before.row_sha256,
1434
+ after_sha256: null,
1435
+ remote_result_sha256: options.remoteResultSha256,
1436
+ result: options.result,
1437
+ error: options.error,
1438
+ rollback: options.action.rollback,
1439
+ };
1440
+ appendStableJsonLine(options.progressPath, entry);
1441
+ options.progress.entries.push(entry);
1442
+ if (entry.result === 'success') {
1443
+ options.progress.successes.set(entry.action_id, entry);
1444
+ options.progress.latestFailures.delete(entry.action_id);
1445
+ }
1446
+ else if (!options.progress.successes.has(entry.action_id)) {
1447
+ options.progress.latestFailures.set(entry.action_id, entry);
1448
+ }
1449
+ return entry;
1450
+ }
1451
+ function remoteAuditId(value) {
1452
+ const candidate = value.audit_id;
1453
+ return typeof candidate === 'string' && POSITIVE_INTEGER_TEXT.test(candidate) ? candidate : null;
1454
+ }
1455
+ async function exactParallelDeleteRows(options) {
1456
+ return (await fetchMaintenanceExactRows({
1457
+ context: options.context,
1458
+ table: options.action.table,
1459
+ id: options.action.id,
1460
+ version: options.action.version,
1461
+ })).rows;
1462
+ }
1463
+ function exactParallelDeleteBefore(options) {
1464
+ const row = options.rows[0];
1465
+ return Boolean(options.rows.length === 1 &&
1466
+ row &&
1467
+ row.user_id === options.action.expected_user_id &&
1468
+ row.state_code === 0 &&
1469
+ snapshotRemoteRow(row).row_sha256 === options.action.before?.row_sha256);
1470
+ }
1471
+ async function executeParallelDeletePlan(options) {
1472
+ const statuses = new Map();
1473
+ const recoverAttempted = async (action) => {
1474
+ const startedAt = options.now();
1475
+ let rows;
1476
+ try {
1477
+ rows = await exactParallelDeleteRows({ context: options.context, action });
1478
+ }
1479
+ catch (error) {
1480
+ statuses.set(action.action_id, 'unknown');
1481
+ if (options.execution.byAction.get(action.action_id)?.at(-1)?.status !== 'UNKNOWN') {
1482
+ appendParallelDeleteExecutionEntry({
1483
+ path: options.executionLogPath,
1484
+ state: options.execution,
1485
+ plan: options.plan,
1486
+ action,
1487
+ context: options.context,
1488
+ status: 'UNKNOWN',
1489
+ recordedAtUtc: options.now(),
1490
+ recovered: true,
1491
+ error: `Read-only recovery failed: ${errorMessage(error)}`,
1492
+ });
1493
+ }
1494
+ return;
1495
+ }
1496
+ if (rows.length === 0) {
1497
+ const priorCommitted = options.execution.committed.has(action.action_id);
1498
+ if (!priorCommitted) {
1499
+ appendParallelDeleteExecutionEntry({
1500
+ path: options.executionLogPath,
1501
+ state: options.execution,
1502
+ plan: options.plan,
1503
+ action,
1504
+ context: options.context,
1505
+ status: 'COMMITTED',
1506
+ recordedAtUtc: options.now(),
1507
+ recovered: true,
1508
+ readbackSha256: ABSENT_READBACK_SHA256,
1509
+ });
1510
+ }
1511
+ if (!options.progress.successes.has(action.action_id)) {
1512
+ appendParallelDeleteProgress({
1513
+ progressPath: options.progressPath,
1514
+ progress: options.progress,
1515
+ plan: options.plan,
1516
+ action,
1517
+ context: options.context,
1518
+ startedAtUtc: startedAt,
1519
+ endedAtUtc: options.now(),
1520
+ result: 'success',
1521
+ remoteResultSha256: sha256Json({
1522
+ recovery: 'desired_absent',
1523
+ action_id: action.action_id,
1524
+ desired_sha256: parallelDeleteDesiredSha256(action),
1525
+ }),
1526
+ error: null,
1527
+ });
1528
+ }
1529
+ statuses.set(action.action_id, 'success');
1530
+ return;
1531
+ }
1532
+ const message = exactParallelDeleteBefore({ rows, action })
1533
+ ? 'Prior dispatch has exact-before readback but no zero-dispatch/zero-mutation audit proof; replay is forbidden.'
1534
+ : 'Prior dispatch has ambiguous or drifted readback; replay is forbidden.';
1535
+ if (!options.execution.committed.has(action.action_id) &&
1536
+ options.execution.byAction.get(action.action_id)?.at(-1)?.status !== 'UNKNOWN') {
1537
+ appendParallelDeleteExecutionEntry({
1538
+ path: options.executionLogPath,
1539
+ state: options.execution,
1540
+ plan: options.plan,
1541
+ action,
1542
+ context: options.context,
1543
+ status: 'UNKNOWN',
1544
+ recordedAtUtc: options.now(),
1545
+ recovered: true,
1546
+ readbackSha256: sha256Json(rows.map(snapshotRemoteRow)),
1547
+ error: message,
1548
+ });
1549
+ }
1550
+ if (!options.progress.successes.has(action.action_id)) {
1551
+ appendParallelDeleteProgress({
1552
+ progressPath: options.progressPath,
1553
+ progress: options.progress,
1554
+ plan: options.plan,
1555
+ action,
1556
+ context: options.context,
1557
+ startedAtUtc: startedAt,
1558
+ endedAtUtc: options.now(),
1559
+ result: 'failed',
1560
+ remoteResultSha256: null,
1561
+ error: `UNKNOWN: ${message}`,
1562
+ });
1563
+ }
1564
+ statuses.set(action.action_id, 'unknown');
1565
+ };
1566
+ const executeOne = async (action) => {
1567
+ if (options.progress.successes.has(action.action_id)) {
1568
+ statuses.set(action.action_id, 'success');
1569
+ return;
1570
+ }
1571
+ if (options.execution.dispatched.has(action.action_id)) {
1572
+ await recoverAttempted(action);
1573
+ return;
1574
+ }
1575
+ const startedAt = options.now();
1576
+ let rows;
1577
+ try {
1578
+ rows = await exactParallelDeleteRows({ context: options.context, action });
1579
+ if (!exactParallelDeleteBefore({ rows, action })) {
1580
+ throw new CliError(`Action row drifted immediately before write: ${action.action_id}`, {
1581
+ code: 'DATASET_MAINTENANCE_ACTION_JUST_IN_TIME_DRIFT',
1582
+ exitCode: 1,
1583
+ });
1584
+ }
1585
+ }
1586
+ catch (error) {
1587
+ appendParallelDeleteProgress({
1588
+ progressPath: options.progressPath,
1589
+ progress: options.progress,
1590
+ plan: options.plan,
1591
+ action,
1592
+ context: options.context,
1593
+ startedAtUtc: startedAt,
1594
+ endedAtUtc: options.now(),
1595
+ result: 'failed',
1596
+ remoteResultSha256: null,
1597
+ error: errorMessage(error),
1598
+ });
1599
+ statuses.set(action.action_id, 'failed');
1600
+ return;
1601
+ }
1602
+ appendParallelDeleteExecutionEntry({
1603
+ path: options.executionLogPath,
1604
+ state: options.execution,
1605
+ plan: options.plan,
1606
+ action,
1607
+ context: options.context,
1608
+ status: 'PREPARED',
1609
+ recordedAtUtc: options.now(),
1610
+ });
1611
+ appendParallelDeleteExecutionEntry({
1612
+ path: options.executionLogPath,
1613
+ state: options.execution,
1614
+ plan: options.plan,
1615
+ action,
1616
+ context: options.context,
1617
+ status: 'DISPATCHED',
1618
+ recordedAtUtc: options.now(),
1619
+ });
1620
+ let remoteResult = null;
1621
+ let dispatchError = null;
1622
+ try {
1623
+ remoteResult = await deleteMaintenanceRow({
1624
+ context: options.context,
1625
+ table: action.table,
1626
+ id: action.id,
1627
+ version: action.version,
1628
+ audit: {
1629
+ plan_sha256: options.plan.plan_sha256,
1630
+ operation_id: options.plan.operation_id,
1631
+ action_id: action.action_id,
1632
+ reason_code: action.reason_code,
1633
+ desired_sha256: parallelDeleteDesiredSha256(action),
1634
+ source: 'tiangong-lca dataset maintenance apply',
1635
+ },
1636
+ });
1637
+ }
1638
+ catch (error) {
1639
+ dispatchError = error;
1640
+ }
1641
+ let readbackRows = null;
1642
+ let readbackError = null;
1643
+ try {
1644
+ readbackRows = await exactParallelDeleteRows({ context: options.context, action });
1645
+ }
1646
+ catch (error) {
1647
+ readbackError = error;
1648
+ }
1649
+ if (readbackRows?.length === 0) {
1650
+ const remoteResultSha256 = remoteResult ? sha256Json(remoteResult) : null;
1651
+ appendParallelDeleteExecutionEntry({
1652
+ path: options.executionLogPath,
1653
+ state: options.execution,
1654
+ plan: options.plan,
1655
+ action,
1656
+ context: options.context,
1657
+ status: 'COMMITTED',
1658
+ recordedAtUtc: options.now(),
1659
+ recovered: dispatchError !== null,
1660
+ auditId: remoteResult ? remoteAuditId(remoteResult) : null,
1661
+ readbackSha256: ABSENT_READBACK_SHA256,
1662
+ remoteResultSha256,
1663
+ });
1664
+ appendParallelDeleteProgress({
1665
+ progressPath: options.progressPath,
1666
+ progress: options.progress,
1667
+ plan: options.plan,
1668
+ action,
1669
+ context: options.context,
1670
+ startedAtUtc: startedAt,
1671
+ endedAtUtc: options.now(),
1672
+ result: 'success',
1673
+ remoteResultSha256: remoteResultSha256 ??
1674
+ sha256Json({
1675
+ recovery: 'desired_absent',
1676
+ action_id: action.action_id,
1677
+ desired_sha256: parallelDeleteDesiredSha256(action),
1678
+ }),
1679
+ error: null,
1680
+ });
1681
+ statuses.set(action.action_id, 'success');
1682
+ return;
1683
+ }
1684
+ const error = readbackError
1685
+ ? `Readback failed after dispatch: ${errorMessage(readbackError)}`
1686
+ : dispatchError
1687
+ ? `Dispatch outcome ambiguous and desired absence was not observed: ${errorMessage(dispatchError)}`
1688
+ : 'Delete RPC returned but exact absent readback was not observed.';
1689
+ appendParallelDeleteExecutionEntry({
1690
+ path: options.executionLogPath,
1691
+ state: options.execution,
1692
+ plan: options.plan,
1693
+ action,
1694
+ context: options.context,
1695
+ status: 'UNKNOWN',
1696
+ recordedAtUtc: options.now(),
1697
+ recovered: dispatchError !== null,
1698
+ auditId: remoteResult ? remoteAuditId(remoteResult) : null,
1699
+ readbackSha256: readbackRows ? sha256Json(readbackRows.map(snapshotRemoteRow)) : null,
1700
+ remoteResultSha256: remoteResult ? sha256Json(remoteResult) : null,
1701
+ error,
1702
+ });
1703
+ appendParallelDeleteProgress({
1704
+ progressPath: options.progressPath,
1705
+ progress: options.progress,
1706
+ plan: options.plan,
1707
+ action,
1708
+ context: options.context,
1709
+ startedAtUtc: startedAt,
1710
+ endedAtUtc: options.now(),
1711
+ result: 'failed',
1712
+ remoteResultSha256: null,
1713
+ error: `UNKNOWN: ${error}`,
1714
+ });
1715
+ statuses.set(action.action_id, 'unknown');
1716
+ };
1717
+ let nextIndex = 0;
1718
+ let fatalError = null;
1719
+ const worker = async () => {
1720
+ while (fatalError === null) {
1721
+ const index = nextIndex;
1722
+ nextIndex += 1;
1723
+ if (index >= options.plan.actions.length)
1724
+ return;
1725
+ try {
1726
+ await executeOne(options.plan.actions[index]);
1727
+ }
1728
+ catch (error) {
1729
+ fatalError ??= error;
1730
+ }
1731
+ }
1732
+ };
1733
+ await Promise.all(Array.from({ length: Math.min(options.maxParallel, options.plan.actions.length) }, () => worker()));
1734
+ if (fatalError !== null)
1735
+ throw fatalError;
1736
+ return statuses;
1737
+ }
1029
1738
  async function executeDerivativeAdmission(options) {
1030
1739
  const action = derivativePlanAction(options.plan);
1031
1740
  const plannedSnapshot = action.derivative_before;
@@ -1036,8 +1745,7 @@ async function executeDerivativeAdmission(options) {
1036
1745
  }), { id: action.id, version: action.version, userId: action.expected_user_id });
1037
1746
  if (preflight.modified_at !== plannedSnapshot.modified_at ||
1038
1747
  preflight.json_sha256 !== plannedSnapshot.json_sha256 ||
1039
- preflight.json_ordered_sha256 !== plannedSnapshot.json_ordered_sha256 ||
1040
- preflight.extracted_text_sha256 !== plannedSnapshot.extracted_text_sha256) {
1748
+ preflight.json_ordered_sha256 !== plannedSnapshot.json_ordered_sha256) {
1041
1749
  throw new CliError('Derivative action primary preconditions drifted after planning.', {
1042
1750
  code: 'DATASET_MAINTENANCE_DERIVATIVE_PRIMARY_DRIFT',
1043
1751
  exitCode: 1,
@@ -1087,6 +1795,13 @@ function nextAttemptPath(planDir) {
1087
1795
  }
1088
1796
  return path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`);
1089
1797
  }
1798
+ function nextParallelDeleteInboundBarrierPath(planDir) {
1799
+ let attempt = 1;
1800
+ while (existsSync(path.join(planDir, `parallel-delete-inbound-barrier.attempt-${String(attempt).padStart(4, '0')}.json`))) {
1801
+ attempt += 1;
1802
+ }
1803
+ return path.join(planDir, `parallel-delete-inbound-barrier.attempt-${String(attempt).padStart(4, '0')}.json`);
1804
+ }
1090
1805
  export async function runDatasetMaintenanceApply(options) {
1091
1806
  if (!options.commit) {
1092
1807
  throw new CliError('Dataset maintenance apply requires commit=true.', {
@@ -1097,6 +1812,16 @@ export async function runDatasetMaintenanceApply(options) {
1097
1812
  const planPath = path.resolve(options.planPath);
1098
1813
  const planDir = path.dirname(planPath);
1099
1814
  const plan = parseMaintenancePlan(readJsonFile(planPath, 'Maintenance plan'));
1815
+ const parallelDeleteMode = options.maxParallel !== undefined;
1816
+ const maxParallel = normalizeMaintenanceMaxParallel(options.maxParallel);
1817
+ assertGlobalInboundProofOptionShape({
1818
+ parallelDeleteMode,
1819
+ proofPath: options.globalInboundProofPath,
1820
+ approveProof: options.approveGlobalInboundProof,
1821
+ });
1822
+ if (parallelDeleteMode) {
1823
+ assertParallelDeletePlan(plan);
1824
+ }
1100
1825
  if (options.approvePlan !== plan.plan_sha256) {
1101
1826
  throw new CliError('approvePlan must exactly match the canonical maintenance plan hash.', {
1102
1827
  code: 'DATASET_MAINTENANCE_PLAN_APPROVAL_REQUIRED',
@@ -1147,24 +1872,87 @@ export async function runDatasetMaintenanceApply(options) {
1147
1872
  exitCode: 2,
1148
1873
  });
1149
1874
  }
1875
+ const globalInboundProof = parallelDeleteMode
1876
+ ? validateParallelDeleteGlobalInboundProof({
1877
+ proofPath: options.globalInboundProofPath,
1878
+ approveProof: options.approveGlobalInboundProof,
1879
+ plan,
1880
+ context,
1881
+ now: options.now ?? new Date(),
1882
+ })
1883
+ : null;
1150
1884
  const derivativeProgress = plan.operation === 'rebuild-derivatives'
1151
1885
  ? parseDerivativeSubmitProgress(plan, progressPath)
1152
1886
  : { entries: [], latest: null };
1153
1887
  const progress = plan.operation === 'rebuild-derivatives'
1154
1888
  ? { entries: [], successes: new Map(), latestFailures: new Map() }
1155
1889
  : parseProgress(plan, progressPath);
1890
+ const executionLogPath = path.join(planDir, 'apply-execution-log.jsonl');
1891
+ const parallelDeleteExecution = parallelDeleteMode
1892
+ ? parseParallelDeleteExecutionLog(plan, executionLogPath)
1893
+ : { entries: [], byAction: new Map(), dispatched: new Set(), committed: new Set() };
1156
1894
  const resumedSuccesses = progress.successes.size;
1157
- const current = await fetchMaintenanceAccountRows({
1158
- context,
1159
- userId: plan.account.user_id,
1160
- });
1895
+ const [current, visibleProcesses] = await Promise.all([
1896
+ fetchMaintenanceAccountRows({
1897
+ context,
1898
+ userId: plan.account.user_id,
1899
+ }),
1900
+ parallelDeleteMode && !globalInboundProof
1901
+ ? fetchMaintenanceVisibleTableRows({
1902
+ context,
1903
+ table: 'processes',
1904
+ pageSize: PARALLEL_DELETE_VISIBLE_PROCESS_PAGE_SIZE,
1905
+ })
1906
+ : Promise.resolve(null),
1907
+ ]);
1161
1908
  assertApplyPreconditions({
1162
1909
  plan,
1163
1910
  planDir,
1164
1911
  currentRows: current.rows,
1165
1912
  progress,
1166
1913
  aliasPlanProgress: { entries: [], success: null, latestFailure: null },
1914
+ attemptedDeleteActionIds: parallelDeleteExecution.dispatched,
1167
1915
  });
1916
+ const inboundBarrierPath = nextParallelDeleteInboundBarrierPath(planDir);
1917
+ const inboundBarrier = globalInboundProof
1918
+ ? {
1919
+ process_rows: globalInboundProof.global_process_rows,
1920
+ process_references: globalInboundProof.global_exchange_rows,
1921
+ snapshot_sha256: globalInboundProof.proof_sha256,
1922
+ }
1923
+ : parallelDeleteMode && visibleProcesses
1924
+ ? assertNoVisibleProcessInboundReferences({ plan, rows: visibleProcesses.rows })
1925
+ : null;
1926
+ if (inboundBarrier && (visibleProcesses || globalInboundProof)) {
1927
+ writeImmutableJson(inboundBarrierPath, {
1928
+ schema_version: 1,
1929
+ generated_at_utc: clock(options),
1930
+ plan_sha256: plan.plan_sha256,
1931
+ operation_id: plan.operation_id,
1932
+ actor: { user_id: context.account.user_id, email: context.account.email },
1933
+ target_table: 'flows',
1934
+ target_count: plan.actions.length,
1935
+ inbound_reference_count: 0,
1936
+ ...inboundBarrier,
1937
+ completeness: globalInboundProof
1938
+ ? {
1939
+ status: 'complete',
1940
+ complete: true,
1941
+ strategy: 'sha256_approved_global_select_only_all_processes',
1942
+ statement_kind: globalInboundProof.statement_kind,
1943
+ process_scope: globalInboundProof.process_scope,
1944
+ proof_path: globalInboundProof.proof_path,
1945
+ proof_sha256: globalInboundProof.proof_sha256,
1946
+ captured_at_utc: globalInboundProof.captured_at_utc,
1947
+ target_binding_sha256: globalInboundProof.target_binding_sha256,
1948
+ chunk_count: globalInboundProof.chunks.length,
1949
+ global_process_rows: globalInboundProof.global_process_rows,
1950
+ global_exchange_rows: globalInboundProof.global_exchange_rows,
1951
+ inbound_exchanges: globalInboundProof.inbound_exchanges,
1952
+ }
1953
+ : visibleProcesses.completeness,
1954
+ });
1955
+ }
1168
1956
  const approvalPath = path.join(planDir, 'approval-record.json');
1169
1957
  const approvalAlreadyExisted = existsSync(approvalPath);
1170
1958
  validateApprovalRecord({ path: approvalPath, plan, context });
@@ -1284,6 +2072,80 @@ export async function runDatasetMaintenanceApply(options) {
1284
2072
  writeJsonArtifact(report.artifacts.commit_report, report);
1285
2073
  return report;
1286
2074
  }
2075
+ if (parallelDeleteMode) {
2076
+ const executionStatuses = await executeParallelDeletePlan({
2077
+ plan,
2078
+ context,
2079
+ progress,
2080
+ progressPath,
2081
+ executionLogPath,
2082
+ execution: parallelDeleteExecution,
2083
+ maxParallel,
2084
+ now: () => clock(options),
2085
+ });
2086
+ const actions = plan.actions.map((action) => {
2087
+ const status = executionStatuses.get(action.action_id);
2088
+ return {
2089
+ action_id: action.action_id,
2090
+ action: action.action,
2091
+ table: action.table,
2092
+ id: action.id,
2093
+ version: action.version,
2094
+ status,
2095
+ error: progress.latestFailures.get(action.action_id)?.error ?? null,
2096
+ };
2097
+ });
2098
+ const successCount = actions.filter((action) => action.status === 'success').length;
2099
+ const failureCount = actions.filter((action) => action.status === 'failed').length;
2100
+ const unknownCount = actions.filter((action) => action.status === 'unknown').length;
2101
+ const attemptPath = nextAttemptPath(planDir);
2102
+ const report = {
2103
+ schema_version: 1,
2104
+ generated_at_utc: clock(options),
2105
+ status: unknownCount > 0
2106
+ ? 'completed_with_unknowns'
2107
+ : successCount === actions.length
2108
+ ? 'completed'
2109
+ : 'completed_with_failures',
2110
+ task_id: plan.task_id,
2111
+ operation: plan.operation,
2112
+ operation_id: plan.operation_id,
2113
+ target_mode: plan.target_mode,
2114
+ plan_sha256: plan.plan_sha256,
2115
+ actor: { user_id: context.account.user_id, email: context.account.email },
2116
+ summary: {
2117
+ actions: actions.length,
2118
+ success: successCount,
2119
+ failed: failureCount,
2120
+ unknown: unknownCount,
2121
+ pending: actions.length - successCount - failureCount - unknownCount,
2122
+ resumed_successes: resumedSuccesses,
2123
+ },
2124
+ actions,
2125
+ artifacts: {
2126
+ approval_record: approvalPath,
2127
+ apply_progress: progressPath,
2128
+ execution_log: executionLogPath,
2129
+ inbound_reference_barrier: inboundBarrierPath,
2130
+ commit_report: path.join(planDir, 'commit-report.json'),
2131
+ attempt_report: attemptPath,
2132
+ },
2133
+ database_audit: {
2134
+ rpc_transaction_log: 'public.command_audit_log',
2135
+ source: 'tiangong-lca dataset maintenance apply',
2136
+ correlation_fields: [
2137
+ 'plan_sha256',
2138
+ 'operation_id',
2139
+ 'action_id',
2140
+ 'reason_code',
2141
+ 'desired_sha256',
2142
+ ],
2143
+ },
2144
+ };
2145
+ writeImmutableJson(attemptPath, report);
2146
+ writeJsonArtifact(report.artifacts.commit_report, report);
2147
+ return report;
2148
+ }
1287
2149
  const ordered = [...plan.actions].sort((left, right) => {
1288
2150
  const rank = {
1289
2151
  save_draft: 0,
@@ -1426,25 +2288,36 @@ export const __testInternals = {
1426
2288
  aliasExchangeProgressKey,
1427
2289
  appendAliasSuccessLogs,
1428
2290
  appendAliasProofProgress,
2291
+ appendParallelDeleteExecutionEntry,
2292
+ assertNoVisibleProcessInboundReferences,
1429
2293
  assertApplyPreconditions,
1430
2294
  assertAliasSupportSnapshots,
2295
+ assertGlobalInboundProofOptionShape,
2296
+ assertParallelDeletePlan,
1431
2297
  buildAliasBatchRequest,
1432
2298
  buildAliasPlanRequest,
1433
2299
  clock,
1434
2300
  errorMessage,
1435
2301
  executeDerivativeAdmission,
1436
- executeAliasPlan,
1437
2302
  executeAction,
2303
+ executeParallelDeletePlan,
1438
2304
  finalProjectedRows,
1439
2305
  loadDesiredPayload,
1440
2306
  nextAttemptPath,
2307
+ nextParallelDeleteInboundBarrierPath,
2308
+ normalizeMaintenanceMaxParallel,
2309
+ parallelDeleteDesiredSha256,
2310
+ parallelDeleteTargetBindingSha256,
1441
2311
  parseDerivativeSubmitProgress,
1442
2312
  validateDerivativeAdmissionAttempt,
1443
2313
  parseAliasBatchProgress,
1444
2314
  parseAliasPlanProgress,
1445
2315
  parseProgress,
2316
+ parseParallelDeleteExecutionLog,
2317
+ remoteAuditId,
1446
2318
  validateAliasRpcResult,
1447
2319
  validateAliasPlanRpcResult,
1448
2320
  validateApprovalRecord,
2321
+ validateParallelDeleteGlobalInboundProof,
1449
2322
  };
1450
2323
  //# sourceMappingURL=dataset-maintenance-apply.js.map