@tiangong-lca/cli 0.0.21 → 0.0.23
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.
- package/README.md +61 -4
- package/dist/src/cli.js +334 -22
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-alias-rewrite.js +541 -0
- package/dist/src/lib/dataset-maintenance-alias-rewrite.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-apply.js +1540 -0
- package/dist/src/lib/dataset-maintenance-apply.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-clear-account.js +121 -58
- package/dist/src/lib/dataset-maintenance-clear-account.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-contract.js +844 -0
- package/dist/src/lib/dataset-maintenance-contract.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-pagination.js +270 -0
- package/dist/src/lib/dataset-maintenance-pagination.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-plan.js +488 -0
- package/dist/src/lib/dataset-maintenance-plan.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-remote.js +295 -0
- package/dist/src/lib/dataset-maintenance-remote.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-support-validation.js +18 -0
- package/dist/src/lib/dataset-maintenance-support-validation.js.map +1 -0
- package/dist/src/lib/dataset-maintenance-verify.js +693 -0
- package/dist/src/lib/dataset-maintenance-verify.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeJsonArtifact } from './artifacts.js';
|
|
4
|
+
import { CliError } from './errors.js';
|
|
5
|
+
import { withStateFileLock } from './state-lock.js';
|
|
6
|
+
import { MAINTENANCE_SCAN_TABLES, appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, resolveMaintenancePlanArtifactPath, sha256Json, snapshotRemoteRow, writeImmutableJson, } from './dataset-maintenance-contract.js';
|
|
7
|
+
import { maintenanceProjectedReferenceFingerprint } from './dataset-maintenance-plan.js';
|
|
8
|
+
import { isSnapshotCompletenessCompatible } from './dataset-maintenance-pagination.js';
|
|
9
|
+
import { applyMaintenanceAliasPlan, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
|
|
10
|
+
const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
|
|
11
|
+
function clock(options) {
|
|
12
|
+
return (options.now ?? new Date()).toISOString();
|
|
13
|
+
}
|
|
14
|
+
function errorMessage(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
function loadDesiredPayload(planDir, action) {
|
|
18
|
+
if (!action.desired_payload) {
|
|
19
|
+
throw new CliError(`save_draft action lacks desired payload: ${action.action_id}`, {
|
|
20
|
+
code: 'DATASET_MAINTENANCE_PLAN_INVALID',
|
|
21
|
+
exitCode: 2,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const payloadPath = resolveMaintenancePlanArtifactPath(planDir, action.desired_payload.path, 'Maintenance desired payload path');
|
|
25
|
+
const payload = readJsonFile(payloadPath, 'Maintenance desired payload');
|
|
26
|
+
if (!isJsonObject(payload) || sha256Json(payload) !== action.desired_payload.sha256) {
|
|
27
|
+
throw new CliError(`Desired payload hash mismatch for action ${action.action_id}.`, {
|
|
28
|
+
code: 'DATASET_MAINTENANCE_DESIRED_PAYLOAD_HASH_MISMATCH',
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
function parseProgress(plan, progressPath) {
|
|
35
|
+
const rawEntries = readJsonLinesIfPresent(progressPath);
|
|
36
|
+
const entries = [];
|
|
37
|
+
const actionsById = new Map(plan.actions.map((action) => [action.action_id, action]));
|
|
38
|
+
for (const value of rawEntries) {
|
|
39
|
+
const action = isJsonObject(value) && typeof value.action_id === 'string'
|
|
40
|
+
? actionsById.get(value.action_id)
|
|
41
|
+
: null;
|
|
42
|
+
if (!isJsonObject(value) ||
|
|
43
|
+
value.schema_version !== 1 ||
|
|
44
|
+
value.plan_sha256 !== plan.plan_sha256 ||
|
|
45
|
+
value.operation_id !== plan.operation_id ||
|
|
46
|
+
typeof value.action_id !== 'string' ||
|
|
47
|
+
!action ||
|
|
48
|
+
value.action !== action.action ||
|
|
49
|
+
value.reason_code !== action.reason_code ||
|
|
50
|
+
typeof value.before_sha256 !== 'string' ||
|
|
51
|
+
!isJsonObject(value.audit_context) ||
|
|
52
|
+
value.audit_context.plan_sha256 !== plan.plan_sha256 ||
|
|
53
|
+
value.audit_context.operation_id !== plan.operation_id ||
|
|
54
|
+
value.audit_context.action_id !== action.action_id ||
|
|
55
|
+
value.audit_context.reason_code !== action.reason_code ||
|
|
56
|
+
value.audit_context.source !== 'tiangong-lca dataset maintenance apply' ||
|
|
57
|
+
(action.action === 'update_json_ordered' &&
|
|
58
|
+
(value.target_mode !== 'owner_draft' ||
|
|
59
|
+
value.audit_context.target_mode !== 'owner_draft' ||
|
|
60
|
+
value.batch_id !== action.batch_id ||
|
|
61
|
+
typeof value.batch_request_sha256 !== 'string' ||
|
|
62
|
+
!/^[a-f0-9]{64}$/u.test(value.batch_request_sha256) ||
|
|
63
|
+
typeof value.database_audit_id !== 'string' ||
|
|
64
|
+
!POSITIVE_INTEGER_TEXT.test(value.database_audit_id) ||
|
|
65
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
66
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
67
|
+
typeof value.plan_request_sha256 !== 'string' ||
|
|
68
|
+
!/^[a-f0-9]{64}$/u.test(value.plan_request_sha256) ||
|
|
69
|
+
typeof value.plan_summary_audit_id !== 'string' ||
|
|
70
|
+
!POSITIVE_INTEGER_TEXT.test(value.plan_summary_audit_id))) ||
|
|
71
|
+
(action.action !== 'update_json_ordered' &&
|
|
72
|
+
('target_mode' in value ||
|
|
73
|
+
'target_mode' in value.audit_context ||
|
|
74
|
+
'batch_id' in value ||
|
|
75
|
+
'batch_request_sha256' in value ||
|
|
76
|
+
'database_audit_id' in value ||
|
|
77
|
+
'summary_audit_id' in value ||
|
|
78
|
+
'plan_request_sha256' in value ||
|
|
79
|
+
'plan_summary_audit_id' in value)) ||
|
|
80
|
+
!['success', 'failed'].includes(String(value.result)) ||
|
|
81
|
+
(value.result === 'success' && typeof value.remote_result_sha256 !== 'string') ||
|
|
82
|
+
(value.result === 'failed' && value.remote_result_sha256 !== null)) {
|
|
83
|
+
throw new CliError('Apply progress contains an invalid or foreign entry.', {
|
|
84
|
+
code: 'DATASET_MAINTENANCE_PROGRESS_INVALID',
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
details: value,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
entries.push(value);
|
|
90
|
+
}
|
|
91
|
+
const successes = new Map();
|
|
92
|
+
const latestFailures = new Map();
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.result === 'success') {
|
|
95
|
+
successes.set(entry.action_id, entry);
|
|
96
|
+
latestFailures.delete(entry.action_id);
|
|
97
|
+
}
|
|
98
|
+
else if (!successes.has(entry.action_id)) {
|
|
99
|
+
latestFailures.set(entry.action_id, entry);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { entries, successes, latestFailures };
|
|
103
|
+
}
|
|
104
|
+
function parseAliasBatchProgress(plan, progressPath) {
|
|
105
|
+
const batches = new Map(plan.alias_batches.map((batch) => [batch.batch_id, batch]));
|
|
106
|
+
const entries = readJsonLinesIfPresent(progressPath).map((value) => {
|
|
107
|
+
const batch = isJsonObject(value) && typeof value.batch_id === 'string'
|
|
108
|
+
? batches.get(value.batch_id)
|
|
109
|
+
: null;
|
|
110
|
+
if (!isJsonObject(value) ||
|
|
111
|
+
value.schema_version !== 1 ||
|
|
112
|
+
value.plan_sha256 !== plan.plan_sha256 ||
|
|
113
|
+
value.operation_id !== plan.operation_id ||
|
|
114
|
+
!batch ||
|
|
115
|
+
value.target_mode !== 'owner_draft' ||
|
|
116
|
+
value.dimension !== batch.dimension ||
|
|
117
|
+
value.factor !== batch.factor ||
|
|
118
|
+
!isJsonObject(value.actor) ||
|
|
119
|
+
value.actor.user_id !== plan.account.user_id ||
|
|
120
|
+
value.actor.email !== plan.account.email ||
|
|
121
|
+
typeof value.started_at_utc !== 'string' ||
|
|
122
|
+
typeof value.ended_at_utc !== 'string' ||
|
|
123
|
+
value.row_count !== batch.summary.rows ||
|
|
124
|
+
value.exchange_count !== batch.summary.exchanges ||
|
|
125
|
+
value.result !== 'success' ||
|
|
126
|
+
typeof value.batch_request_sha256 !== 'string' ||
|
|
127
|
+
!/^[a-f0-9]{64}$/u.test(value.batch_request_sha256) ||
|
|
128
|
+
typeof value.idempotent_replay !== 'boolean' ||
|
|
129
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
130
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
131
|
+
typeof value.plan_request_sha256 !== 'string' ||
|
|
132
|
+
!/^[a-f0-9]{64}$/u.test(value.plan_request_sha256) ||
|
|
133
|
+
typeof value.plan_summary_audit_id !== 'string' ||
|
|
134
|
+
!POSITIVE_INTEGER_TEXT.test(value.plan_summary_audit_id) ||
|
|
135
|
+
value.error !== null) {
|
|
136
|
+
throw new CliError('Alias batch progress contains an invalid or foreign entry.', {
|
|
137
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PROGRESS_INVALID',
|
|
138
|
+
exitCode: 1,
|
|
139
|
+
details: value,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
});
|
|
144
|
+
const successes = new Map();
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (successes.has(entry.batch_id)) {
|
|
147
|
+
throw new CliError('Alias batch progress contains duplicate success proof.', {
|
|
148
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PROGRESS_INVALID',
|
|
149
|
+
exitCode: 1,
|
|
150
|
+
details: { batch_id: entry.batch_id },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
successes.set(entry.batch_id, entry);
|
|
154
|
+
}
|
|
155
|
+
return { entries, successes };
|
|
156
|
+
}
|
|
157
|
+
function parseAliasPlanProgress(plan, progressPath) {
|
|
158
|
+
const expectedBatches = orderedAliasBatches(plan);
|
|
159
|
+
const entries = readJsonLinesIfPresent(progressPath).map((value) => {
|
|
160
|
+
const batchProofs = isJsonObject(value) && Array.isArray(value.batches) ? value.batches : [];
|
|
161
|
+
const validBatchProofs = batchProofs.length === expectedBatches.length &&
|
|
162
|
+
batchProofs.every((proof, index) => {
|
|
163
|
+
const batch = expectedBatches[index];
|
|
164
|
+
return Boolean(isJsonObject(proof) &&
|
|
165
|
+
proof.batch_id === batch.batch_id &&
|
|
166
|
+
proof.dimension === batch.dimension &&
|
|
167
|
+
typeof proof.batch_request_sha256 === 'string' &&
|
|
168
|
+
/^[a-f0-9]{64}$/u.test(proof.batch_request_sha256) &&
|
|
169
|
+
typeof proof.summary_audit_id === 'string' &&
|
|
170
|
+
POSITIVE_INTEGER_TEXT.test(proof.summary_audit_id));
|
|
171
|
+
});
|
|
172
|
+
const batchRequestHashes = batchProofs
|
|
173
|
+
.filter(isJsonObject)
|
|
174
|
+
.map((proof) => proof.batch_request_sha256);
|
|
175
|
+
const batchSummaryAuditIds = batchProofs
|
|
176
|
+
.filter(isJsonObject)
|
|
177
|
+
.map((proof) => proof.summary_audit_id);
|
|
178
|
+
if (!isJsonObject(value) ||
|
|
179
|
+
value.schema_version !== 1 ||
|
|
180
|
+
value.plan_sha256 !== plan.plan_sha256 ||
|
|
181
|
+
value.operation_id !== plan.operation_id ||
|
|
182
|
+
value.target_mode !== 'owner_draft' ||
|
|
183
|
+
!isJsonObject(value.actor) ||
|
|
184
|
+
value.actor.user_id !== plan.account.user_id ||
|
|
185
|
+
value.actor.email !== plan.account.email ||
|
|
186
|
+
typeof value.started_at_utc !== 'string' ||
|
|
187
|
+
typeof value.ended_at_utc !== 'string' ||
|
|
188
|
+
value.batch_count !== 2 ||
|
|
189
|
+
value.row_count !== 52 ||
|
|
190
|
+
value.exchange_count !== 59 ||
|
|
191
|
+
!['success', 'failed'].includes(String(value.result)) ||
|
|
192
|
+
(value.result === 'success' &&
|
|
193
|
+
(typeof value.plan_request_sha256 !== 'string' ||
|
|
194
|
+
!/^[a-f0-9]{64}$/u.test(value.plan_request_sha256) ||
|
|
195
|
+
typeof value.idempotent_replay !== 'boolean' ||
|
|
196
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
197
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
198
|
+
!validBatchProofs ||
|
|
199
|
+
new Set(batchRequestHashes).size !== 2 ||
|
|
200
|
+
new Set(batchSummaryAuditIds).size !== 2 ||
|
|
201
|
+
batchSummaryAuditIds.includes(value.summary_audit_id) ||
|
|
202
|
+
value.error !== null)) ||
|
|
203
|
+
(value.result === 'failed' &&
|
|
204
|
+
(value.plan_request_sha256 !== null ||
|
|
205
|
+
value.idempotent_replay !== null ||
|
|
206
|
+
value.summary_audit_id !== null ||
|
|
207
|
+
batchProofs.length !== 0 ||
|
|
208
|
+
typeof value.error !== 'string'))) {
|
|
209
|
+
throw new CliError('Alias plan progress contains an invalid or foreign entry.', {
|
|
210
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_PROGRESS_INVALID',
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
details: value,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
});
|
|
217
|
+
let success = null;
|
|
218
|
+
let latestFailure = null;
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (entry.result === 'success') {
|
|
221
|
+
if (success) {
|
|
222
|
+
throw new CliError('Alias plan progress contains duplicate success proof.', {
|
|
223
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_PROGRESS_INVALID',
|
|
224
|
+
exitCode: 1,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
success = entry;
|
|
228
|
+
latestFailure = null;
|
|
229
|
+
}
|
|
230
|
+
else if (!success) {
|
|
231
|
+
latestFailure = entry;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { entries, success, latestFailure };
|
|
235
|
+
}
|
|
236
|
+
function finalProjectedRows(options) {
|
|
237
|
+
const projected = new Map(options.rows.map((row) => [maintenanceRowKey(row), { ...row }]));
|
|
238
|
+
for (const action of options.plan.actions.filter((entry) => ['save_draft', 'update_json_ordered'].includes(entry.action))) {
|
|
239
|
+
const row = projected.get(maintenanceRowKey(action));
|
|
240
|
+
if (row) {
|
|
241
|
+
projected.set(maintenanceRowKey(action), {
|
|
242
|
+
...row,
|
|
243
|
+
json_ordered: loadDesiredPayload(options.planDir, action),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const action of options.plan.actions.filter((entry) => entry.action === 'delete')) {
|
|
248
|
+
projected.delete(maintenanceRowKey(action));
|
|
249
|
+
}
|
|
250
|
+
return [...projected.values()].sort((left, right) => maintenanceRowKey(left).localeCompare(maintenanceRowKey(right)));
|
|
251
|
+
}
|
|
252
|
+
function assertApplyPreconditions(options) {
|
|
253
|
+
const current = new Map(options.currentRows.map((row) => [maintenanceRowKey(row), row]));
|
|
254
|
+
const aliasBatchStates = new Map();
|
|
255
|
+
for (const batch of options.plan.alias_batches ?? []) {
|
|
256
|
+
const states = batch.action_ids.map((actionId) => {
|
|
257
|
+
const action = options.plan.actions.find((entry) => entry.action_id === actionId);
|
|
258
|
+
const row = current.get(maintenanceRowKey(action));
|
|
259
|
+
const snapshot = row ? snapshotRemoteRow(row) : null;
|
|
260
|
+
if (row?.state_code === 0 &&
|
|
261
|
+
row.user_id === action.expected_user_id &&
|
|
262
|
+
snapshot?.row_sha256 === action.before?.row_sha256) {
|
|
263
|
+
return 'before';
|
|
264
|
+
}
|
|
265
|
+
if (row?.state_code === 0 &&
|
|
266
|
+
row.user_id === action.expected_user_id &&
|
|
267
|
+
snapshot?.payload_sha256 === action.desired_payload?.sha256 &&
|
|
268
|
+
row.model_id === action.before?.model_id &&
|
|
269
|
+
row.rule_verification === action.before?.rule_verification) {
|
|
270
|
+
return 'desired';
|
|
271
|
+
}
|
|
272
|
+
return 'invalid';
|
|
273
|
+
});
|
|
274
|
+
const unique = new Set(states);
|
|
275
|
+
if (unique.size !== 1 || unique.has('invalid')) {
|
|
276
|
+
throw new CliError(`Atomic alias batch row state drifted: ${batch.batch_id}`, {
|
|
277
|
+
code: 'DATASET_MAINTENANCE_ALIAS_BATCH_DRIFT',
|
|
278
|
+
exitCode: 1,
|
|
279
|
+
details: { batch_id: batch.batch_id, states },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
aliasBatchStates.set(batch.batch_id, states[0]);
|
|
283
|
+
}
|
|
284
|
+
if (aliasBatchStates.size) {
|
|
285
|
+
const planStates = new Set(aliasBatchStates.values());
|
|
286
|
+
if (planStates.size !== 1 ||
|
|
287
|
+
(Boolean(options.aliasPlanProgress?.success) && !planStates.has('desired'))) {
|
|
288
|
+
throw new CliError('Atomic alias plan rows are split across dimension states.', {
|
|
289
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_DRIFT',
|
|
290
|
+
exitCode: 1,
|
|
291
|
+
details: { batches: Object.fromEntries(aliasBatchStates) },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const baselineKeys = new Set([
|
|
296
|
+
...options.plan.protected_rows.map(maintenanceRowKey),
|
|
297
|
+
...options.plan.actions.filter((action) => action.before).map(maintenanceRowKey),
|
|
298
|
+
]);
|
|
299
|
+
for (const row of options.currentRows) {
|
|
300
|
+
if (!baselineKeys.has(maintenanceRowKey(row))) {
|
|
301
|
+
throw new CliError(`Unexpected current-account row appeared after planning: ${row.id}`, {
|
|
302
|
+
code: 'DATASET_MAINTENANCE_PREFLIGHT_DRIFT',
|
|
303
|
+
exitCode: 1,
|
|
304
|
+
details: { table: row.table, id: row.id, version: row.version },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const protectedRow of options.plan.protected_rows) {
|
|
309
|
+
const currentRow = current.get(maintenanceRowKey(protectedRow));
|
|
310
|
+
if (!currentRow || snapshotRemoteRow(currentRow).row_sha256 !== protectedRow.row_sha256) {
|
|
311
|
+
throw new CliError(`Protected row drifted after planning: ${protectedRow.id}`, {
|
|
312
|
+
code: 'DATASET_MAINTENANCE_PROTECTED_ROW_DRIFT',
|
|
313
|
+
exitCode: 1,
|
|
314
|
+
details: protectedRow,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const action of options.plan.actions) {
|
|
319
|
+
if (!action.before) {
|
|
320
|
+
throw new CliError(`Ready plan action lacks before snapshot: ${action.action_id}`, {
|
|
321
|
+
code: 'DATASET_MAINTENANCE_PLAN_INVALID',
|
|
322
|
+
exitCode: 2,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const currentRow = current.get(maintenanceRowKey(action));
|
|
326
|
+
const alreadySucceeded = options.progress.successes.has(action.action_id);
|
|
327
|
+
if (alreadySucceeded && action.action === 'delete') {
|
|
328
|
+
if (currentRow) {
|
|
329
|
+
throw new CliError(`Previously deleted row is visible again: ${action.action_id}`, {
|
|
330
|
+
code: 'DATASET_MAINTENANCE_RESUME_DRIFT',
|
|
331
|
+
exitCode: 1,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!currentRow || currentRow.user_id !== action.expected_user_id) {
|
|
337
|
+
throw new CliError(`Action row is missing, non-draft, or not owned: ${action.action_id}`, {
|
|
338
|
+
code: 'DATASET_MAINTENANCE_ACTION_ROW_DRIFT',
|
|
339
|
+
exitCode: 1,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const currentSnapshot = snapshotRemoteRow(currentRow);
|
|
343
|
+
if (action.action === 'update_json_ordered') {
|
|
344
|
+
aliasBatchStates.get(action.batch_id);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (alreadySucceeded) {
|
|
348
|
+
const expectedPayloadSha256 = sha256Json(loadDesiredPayload(options.planDir, action));
|
|
349
|
+
if (currentRow.state_code !== 0 ||
|
|
350
|
+
currentSnapshot.payload_sha256 !== expectedPayloadSha256 ||
|
|
351
|
+
currentRow.model_id !== action.before.model_id ||
|
|
352
|
+
currentRow.rule_verification !== action.before.rule_verification) {
|
|
353
|
+
throw new CliError(`Previously saved row payload drifted: ${action.action_id}`, {
|
|
354
|
+
code: 'DATASET_MAINTENANCE_RESUME_DRIFT',
|
|
355
|
+
exitCode: 1,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (currentRow.state_code !== 0 ||
|
|
360
|
+
currentSnapshot.row_sha256 !== action.before.row_sha256) {
|
|
361
|
+
throw new CliError(`Pending action row drifted after planning: ${action.action_id}`, {
|
|
362
|
+
code: 'DATASET_MAINTENANCE_ACTION_ROW_DRIFT',
|
|
363
|
+
exitCode: 1,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const projected = finalProjectedRows({
|
|
368
|
+
rows: options.currentRows,
|
|
369
|
+
plan: options.plan,
|
|
370
|
+
planDir: options.planDir,
|
|
371
|
+
});
|
|
372
|
+
const projectedReferenceSha256 = sha256Json(maintenanceProjectedReferenceFingerprint(projected));
|
|
373
|
+
if (projectedReferenceSha256 !== options.plan.projected_reference_sha256) {
|
|
374
|
+
throw new CliError('Projected reference closure drifted after planning.', {
|
|
375
|
+
code: 'DATASET_MAINTENANCE_REFERENCE_PREFLIGHT_DRIFT',
|
|
376
|
+
exitCode: 1,
|
|
377
|
+
details: {
|
|
378
|
+
expected: options.plan.projected_reference_sha256,
|
|
379
|
+
actual: projectedReferenceSha256,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function validateApprovalRecord(options) {
|
|
385
|
+
if (!existsSync(options.path)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const record = readJsonFile(options.path, 'Maintenance approval record');
|
|
389
|
+
if (!isJsonObject(record) ||
|
|
390
|
+
record.plan_sha256 !== options.plan.plan_sha256 ||
|
|
391
|
+
record.target_mode !== options.plan.target_mode ||
|
|
392
|
+
!isJsonObject(record.account) ||
|
|
393
|
+
record.account.user_id !== options.context.account.user_id ||
|
|
394
|
+
record.account.email !== options.context.account.email ||
|
|
395
|
+
!isSnapshotCompletenessCompatible(record.snapshot_completeness, options.plan.snapshot_completeness, MAINTENANCE_SCAN_TABLES)) {
|
|
396
|
+
throw new CliError('Existing approval record does not match this plan and actor.', {
|
|
397
|
+
code: 'DATASET_MAINTENANCE_APPROVAL_RECORD_MISMATCH',
|
|
398
|
+
exitCode: 1,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function assertAliasSupportSnapshots(options) {
|
|
403
|
+
if (options.plan.target_mode !== 'owner_draft') {
|
|
404
|
+
throw new CliError('Alias apply requires target_mode=owner_draft.', {
|
|
405
|
+
code: 'DATASET_MAINTENANCE_TARGET_MODE_INVALID',
|
|
406
|
+
exitCode: 2,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
for (const batch of options.plan.alias_batches) {
|
|
410
|
+
const snapshots = [
|
|
411
|
+
batch.target_snapshots.unitgroup,
|
|
412
|
+
batch.target_snapshots.flowproperty,
|
|
413
|
+
batch.target_snapshots.source_unitgroup,
|
|
414
|
+
];
|
|
415
|
+
for (const snapshot of snapshots) {
|
|
416
|
+
const exact = await fetchMaintenanceExactRows({
|
|
417
|
+
context: options.context,
|
|
418
|
+
table: snapshot.table,
|
|
419
|
+
id: snapshot.id,
|
|
420
|
+
version: snapshot.version,
|
|
421
|
+
});
|
|
422
|
+
const current = exact.rows.length === 1 && exact.rows[0] ? snapshotRemoteRow(exact.rows[0]) : null;
|
|
423
|
+
if (!current ||
|
|
424
|
+
current.user_id !== options.plan.account.user_id ||
|
|
425
|
+
current.state_code !== 0 ||
|
|
426
|
+
current.row_sha256 !== snapshot.row_sha256) {
|
|
427
|
+
throw new CliError(`Alias support row drifted for batch ${batch.batch_id}.`, {
|
|
428
|
+
code: 'DATASET_MAINTENANCE_ALIAS_SUPPORT_DRIFT',
|
|
429
|
+
exitCode: 1,
|
|
430
|
+
details: { table: snapshot.table, id: snapshot.id, version: snapshot.version },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function buildAliasBatchRequest(options) {
|
|
437
|
+
if (options.plan.target_mode !== 'owner_draft') {
|
|
438
|
+
throw new CliError('Alias batch request requires target_mode=owner_draft.', {
|
|
439
|
+
code: 'DATASET_MAINTENANCE_TARGET_MODE_INVALID',
|
|
440
|
+
exitCode: 2,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const targetSnapshot = (snapshot) => {
|
|
444
|
+
return {
|
|
445
|
+
id: snapshot.id,
|
|
446
|
+
version: snapshot.version,
|
|
447
|
+
expected_modified_at: snapshot.modified_at,
|
|
448
|
+
expected_json_ordered: snapshot.json_ordered,
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
const actions = options.batch.action_ids.map((actionId) => {
|
|
452
|
+
const action = options.plan.actions.find((entry) => entry.action_id === actionId);
|
|
453
|
+
return {
|
|
454
|
+
action_id: action.action_id,
|
|
455
|
+
action: 'update_json_ordered',
|
|
456
|
+
table: action.table,
|
|
457
|
+
id: action.id,
|
|
458
|
+
version: action.version,
|
|
459
|
+
expected_state_code: 0,
|
|
460
|
+
expected_modified_at: action.before.modified_at,
|
|
461
|
+
expected_json_ordered: action.before.json_ordered,
|
|
462
|
+
desired_json_ordered: loadDesiredPayload(options.planDir, action),
|
|
463
|
+
mutation: action.alias_mutation,
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
schema_version: 'dataset-alias-batch.v1',
|
|
468
|
+
target_visibility: 'owner_draft',
|
|
469
|
+
plan_sha256: options.plan.plan_sha256,
|
|
470
|
+
operation_id: options.plan.operation_id,
|
|
471
|
+
batch_id: options.batch.batch_id,
|
|
472
|
+
dimension: options.batch.dimension,
|
|
473
|
+
factor: options.batch.factor,
|
|
474
|
+
target: {
|
|
475
|
+
flowproperty: targetSnapshot(options.batch.target_snapshots.flowproperty),
|
|
476
|
+
unitgroup: targetSnapshot(options.batch.target_snapshots.unitgroup),
|
|
477
|
+
source_unitgroup: targetSnapshot(options.batch.target_snapshots.source_unitgroup),
|
|
478
|
+
},
|
|
479
|
+
actions,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function orderedAliasBatches(plan) {
|
|
483
|
+
const time = plan.alias_batches.find((batch) => batch.dimension === 'time');
|
|
484
|
+
const lengthTime = plan.alias_batches.find((batch) => batch.dimension === 'length_time');
|
|
485
|
+
return [time, lengthTime].filter((batch) => batch !== undefined);
|
|
486
|
+
}
|
|
487
|
+
function buildAliasPlanRequest(options) {
|
|
488
|
+
if (options.plan.target_mode !== 'owner_draft') {
|
|
489
|
+
throw new CliError('Alias plan request requires target_mode=owner_draft.', {
|
|
490
|
+
code: 'DATASET_MAINTENANCE_TARGET_MODE_INVALID',
|
|
491
|
+
exitCode: 2,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const batches = orderedAliasBatches(options.plan);
|
|
495
|
+
if (batches.length !== 2 ||
|
|
496
|
+
batches[0]?.dimension !== 'time' ||
|
|
497
|
+
batches[1]?.dimension !== 'length_time') {
|
|
498
|
+
throw new CliError('Alias plan request requires time followed by length_time exactly once.', {
|
|
499
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_INVALID',
|
|
500
|
+
exitCode: 2,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
schema_version: 'dataset-alias-plan.v1',
|
|
505
|
+
plan_sha256: options.plan.plan_sha256,
|
|
506
|
+
operation_id: options.plan.operation_id,
|
|
507
|
+
target_visibility: 'owner_draft',
|
|
508
|
+
batches: batches.map((batch) => buildAliasBatchRequest({ plan: options.plan, batch, planDir: options.planDir })),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function validateAliasRpcResult(value, batch, plan) {
|
|
512
|
+
const audit = Array.isArray(value.audit) ? value.audit : [];
|
|
513
|
+
const proofs = audit.filter(isJsonObject).map((entry) => ({
|
|
514
|
+
action_id: entry.action_id,
|
|
515
|
+
table: entry.table,
|
|
516
|
+
id: entry.id,
|
|
517
|
+
version: entry.version,
|
|
518
|
+
audit_id: entry.audit_id,
|
|
519
|
+
}));
|
|
520
|
+
const proofByAction = new Map(proofs
|
|
521
|
+
.filter((entry) => typeof entry.action_id === 'string' &&
|
|
522
|
+
typeof entry.table === 'string' &&
|
|
523
|
+
typeof entry.id === 'string' &&
|
|
524
|
+
typeof entry.version === 'string' &&
|
|
525
|
+
typeof entry.audit_id === 'string' &&
|
|
526
|
+
POSITIVE_INTEGER_TEXT.test(entry.audit_id))
|
|
527
|
+
.map((entry) => [entry.action_id, entry]));
|
|
528
|
+
const actionsById = new Map(plan.actions.map((action) => [action.action_id, action]));
|
|
529
|
+
const validProofs = proofByAction.size === batch.action_ids.length &&
|
|
530
|
+
batch.action_ids.every((actionId) => {
|
|
531
|
+
const action = actionsById.get(actionId);
|
|
532
|
+
const proof = proofByAction.get(actionId);
|
|
533
|
+
return Boolean(action &&
|
|
534
|
+
proof &&
|
|
535
|
+
proof.table === action.table &&
|
|
536
|
+
proof.id === action.id &&
|
|
537
|
+
proof.version === action.version);
|
|
538
|
+
});
|
|
539
|
+
if (plan.target_mode !== 'owner_draft' ||
|
|
540
|
+
value.ok !== true ||
|
|
541
|
+
value.command !== 'cmd_dataset_alias_batch_guarded' ||
|
|
542
|
+
value.target_visibility !== 'owner_draft' ||
|
|
543
|
+
value.dimension !== batch.dimension ||
|
|
544
|
+
value.batch_id !== batch.batch_id ||
|
|
545
|
+
value.row_count !== batch.summary.rows ||
|
|
546
|
+
value.exchange_count !== batch.summary.exchanges ||
|
|
547
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
548
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
549
|
+
typeof value.batch_request_sha256 !== 'string' ||
|
|
550
|
+
!/^[a-f0-9]{64}$/u.test(value.batch_request_sha256) ||
|
|
551
|
+
typeof value.idempotent_replay !== 'boolean' ||
|
|
552
|
+
audit.length !== batch.action_ids.length ||
|
|
553
|
+
!validProofs) {
|
|
554
|
+
throw new CliError(`Alias batch RPC returned invalid proof for ${batch.batch_id}.`, {
|
|
555
|
+
code: 'DATASET_MAINTENANCE_ALIAS_RPC_PROOF_INVALID',
|
|
556
|
+
exitCode: 1,
|
|
557
|
+
details: value,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
target_visibility: 'owner_draft',
|
|
562
|
+
batch_request_sha256: value.batch_request_sha256,
|
|
563
|
+
idempotent_replay: value.idempotent_replay,
|
|
564
|
+
exchange_count: value.exchange_count,
|
|
565
|
+
summary_audit_id: value.summary_audit_id,
|
|
566
|
+
audits: proofByAction,
|
|
567
|
+
raw: value,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function validateAliasPlanRpcResult(value, plan) {
|
|
571
|
+
const batches = orderedAliasBatches(plan);
|
|
572
|
+
const rawBatchResults = Array.isArray(value.batches) ? value.batches : [];
|
|
573
|
+
const parsedBatchResults = rawBatchResults.map((entry, index) => isJsonObject(entry) && batches[index]
|
|
574
|
+
? validateAliasRpcResult(entry, batches[index], plan)
|
|
575
|
+
: null);
|
|
576
|
+
const validBatchResults = parsedBatchResults.every((entry) => entry !== null);
|
|
577
|
+
const batchSummaryAuditIds = validBatchResults
|
|
578
|
+
? parsedBatchResults.map((entry) => entry.summary_audit_id)
|
|
579
|
+
: [];
|
|
580
|
+
const rowAuditIds = validBatchResults
|
|
581
|
+
? parsedBatchResults.flatMap((entry) => [...entry.audits.values()].map((proof) => proof.audit_id))
|
|
582
|
+
: [];
|
|
583
|
+
if (plan.target_mode !== 'owner_draft' ||
|
|
584
|
+
value.ok !== true ||
|
|
585
|
+
value.command !== 'cmd_dataset_alias_plan_guarded' ||
|
|
586
|
+
value.schema_version !== 'dataset-alias-plan.v1' ||
|
|
587
|
+
value.plan_sha256 !== plan.plan_sha256 ||
|
|
588
|
+
value.operation_id !== plan.operation_id ||
|
|
589
|
+
value.target_visibility !== 'owner_draft' ||
|
|
590
|
+
typeof value.plan_request_sha256 !== 'string' ||
|
|
591
|
+
!/^[a-f0-9]{64}$/u.test(value.plan_request_sha256) ||
|
|
592
|
+
value.batch_count !== 2 ||
|
|
593
|
+
value.row_count !== 52 ||
|
|
594
|
+
value.exchange_count !== 59 ||
|
|
595
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
596
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
597
|
+
typeof value.idempotent_replay !== 'boolean' ||
|
|
598
|
+
rawBatchResults.length !== 2 ||
|
|
599
|
+
!validBatchResults ||
|
|
600
|
+
parsedBatchResults.some((entry, index) => entry.idempotent_replay !== value.idempotent_replay ||
|
|
601
|
+
entry.exchange_count !== batches[index].summary.exchanges) ||
|
|
602
|
+
new Set(batchSummaryAuditIds).size !== 2 ||
|
|
603
|
+
new Set(rowAuditIds).size !== 52 ||
|
|
604
|
+
batchSummaryAuditIds.includes(value.summary_audit_id) ||
|
|
605
|
+
rowAuditIds.includes(value.summary_audit_id) ||
|
|
606
|
+
rowAuditIds.some((auditId) => batchSummaryAuditIds.includes(auditId))) {
|
|
607
|
+
throw new CliError('Alias plan RPC returned invalid whole-plan proof.', {
|
|
608
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_RPC_PROOF_INVALID',
|
|
609
|
+
exitCode: 1,
|
|
610
|
+
details: value,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
target_visibility: 'owner_draft',
|
|
615
|
+
plan_request_sha256: value.plan_request_sha256,
|
|
616
|
+
idempotent_replay: value.idempotent_replay,
|
|
617
|
+
batch_count: 2,
|
|
618
|
+
row_count: 52,
|
|
619
|
+
exchange_count: 59,
|
|
620
|
+
summary_audit_id: value.summary_audit_id,
|
|
621
|
+
batches: new Map(batches.map((batch, index) => [batch.dimension, parsedBatchResults[index]])),
|
|
622
|
+
raw: value,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async function executeAliasPlan(options) {
|
|
626
|
+
const request = buildAliasPlanRequest(options);
|
|
627
|
+
const remoteResult = await applyMaintenanceAliasPlan({
|
|
628
|
+
context: options.context,
|
|
629
|
+
plan: request,
|
|
630
|
+
});
|
|
631
|
+
const rpc = validateAliasPlanRpcResult(remoteResult, options.plan);
|
|
632
|
+
const afterByAction = new Map();
|
|
633
|
+
for (const action of options.plan.actions) {
|
|
634
|
+
const exact = await fetchMaintenanceExactRows({
|
|
635
|
+
context: options.context,
|
|
636
|
+
table: action.table,
|
|
637
|
+
id: action.id,
|
|
638
|
+
version: action.version,
|
|
639
|
+
});
|
|
640
|
+
const row = exact.rows.length === 1 ? exact.rows[0] : null;
|
|
641
|
+
const snapshot = row ? snapshotRemoteRow(row) : null;
|
|
642
|
+
if (!row ||
|
|
643
|
+
row.user_id !== action.expected_user_id ||
|
|
644
|
+
row.state_code !== 0 ||
|
|
645
|
+
snapshot?.payload_sha256 !== action.desired_payload?.sha256 ||
|
|
646
|
+
row.model_id !== action.before?.model_id ||
|
|
647
|
+
row.rule_verification !== action.before?.rule_verification) {
|
|
648
|
+
throw new CliError(`Alias plan readback failed for action ${action.action_id}.`, {
|
|
649
|
+
code: 'DATASET_MAINTENANCE_ALIAS_READBACK_FAILED',
|
|
650
|
+
exitCode: 1,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
afterByAction.set(action.action_id, snapshot.row_sha256);
|
|
654
|
+
}
|
|
655
|
+
await assertAliasSupportSnapshots({ plan: options.plan, context: options.context });
|
|
656
|
+
return { rpc, after_by_action: afterByAction };
|
|
657
|
+
}
|
|
658
|
+
function aliasExchangeProgressKey(value) {
|
|
659
|
+
return `${value.batch_id}\u0000${value.action_id}\u0000${value.exchange_index}\u0000${value.data_set_internal_id}`;
|
|
660
|
+
}
|
|
661
|
+
function aliasBatchDerivedLogsComplete(options) {
|
|
662
|
+
const planBatchProof = options.planSuccess.batches.find((proof) => proof.batch_id === options.batch.batch_id);
|
|
663
|
+
if (!planBatchProof ||
|
|
664
|
+
planBatchProof.dimension !== options.batch.dimension ||
|
|
665
|
+
planBatchProof.batch_request_sha256 !== options.batchSuccess.batch_request_sha256 ||
|
|
666
|
+
planBatchProof.summary_audit_id !== options.batchSuccess.summary_audit_id ||
|
|
667
|
+
options.batchSuccess.plan_request_sha256 !== options.planSuccess.plan_request_sha256 ||
|
|
668
|
+
options.batchSuccess.plan_summary_audit_id !== options.planSuccess.summary_audit_id ||
|
|
669
|
+
!options.batch.action_ids.every((actionId) => options.progress.successes.get(actionId)?.batch_request_sha256 ===
|
|
670
|
+
options.batchSuccess.batch_request_sha256 &&
|
|
671
|
+
options.progress.successes.get(actionId)?.summary_audit_id ===
|
|
672
|
+
options.batchSuccess.summary_audit_id &&
|
|
673
|
+
options.progress.successes.get(actionId)?.plan_request_sha256 ===
|
|
674
|
+
options.planSuccess.plan_request_sha256 &&
|
|
675
|
+
options.progress.successes.get(actionId)?.plan_summary_audit_id ===
|
|
676
|
+
options.planSuccess.summary_audit_id)) {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const expected = new Map(options.batch.exchange_rewrites.map((rewrite) => [
|
|
680
|
+
aliasExchangeProgressKey({ batch_id: options.batch.batch_id, ...rewrite }),
|
|
681
|
+
rewrite,
|
|
682
|
+
]));
|
|
683
|
+
const exchangeKeys = new Set();
|
|
684
|
+
for (const value of readJsonLinesIfPresent(options.exchangeProgressPath)) {
|
|
685
|
+
if (!isJsonObject(value) || value.batch_id !== options.batch.batch_id)
|
|
686
|
+
continue;
|
|
687
|
+
const key = typeof value.action_id === 'string' &&
|
|
688
|
+
typeof value.exchange_index === 'number' &&
|
|
689
|
+
typeof value.data_set_internal_id === 'string'
|
|
690
|
+
? aliasExchangeProgressKey({
|
|
691
|
+
batch_id: options.batch.batch_id,
|
|
692
|
+
action_id: value.action_id,
|
|
693
|
+
exchange_index: value.exchange_index,
|
|
694
|
+
data_set_internal_id: value.data_set_internal_id,
|
|
695
|
+
})
|
|
696
|
+
: '';
|
|
697
|
+
const rewrite = expected.get(key);
|
|
698
|
+
const rowProof = rewrite ? options.progress.successes.get(rewrite.action_id) : null;
|
|
699
|
+
if (!rewrite ||
|
|
700
|
+
!rowProof ||
|
|
701
|
+
value.schema_version !== 1 ||
|
|
702
|
+
value.plan_sha256 !== options.plan.plan_sha256 ||
|
|
703
|
+
value.operation_id !== options.plan.operation_id ||
|
|
704
|
+
value.target_mode !== 'owner_draft' ||
|
|
705
|
+
value.batch_request_sha256 !== options.batchSuccess.batch_request_sha256 ||
|
|
706
|
+
value.batch_request_sha256 !== rowProof.batch_request_sha256 ||
|
|
707
|
+
value.summary_audit_id !== options.batchSuccess.summary_audit_id ||
|
|
708
|
+
value.summary_audit_id !== rowProof.summary_audit_id ||
|
|
709
|
+
value.plan_request_sha256 !== options.planSuccess.plan_request_sha256 ||
|
|
710
|
+
value.plan_request_sha256 !== rowProof.plan_request_sha256 ||
|
|
711
|
+
value.plan_summary_audit_id !== options.planSuccess.summary_audit_id ||
|
|
712
|
+
value.plan_summary_audit_id !== rowProof.plan_summary_audit_id ||
|
|
713
|
+
value.factor !== options.batch.factor ||
|
|
714
|
+
value.result !== 'success' ||
|
|
715
|
+
!isJsonObject(value.actor) ||
|
|
716
|
+
value.actor.user_id !== options.plan.account.user_id ||
|
|
717
|
+
value.actor.email !== options.plan.account.email ||
|
|
718
|
+
typeof value.logged_at_utc !== 'string' ||
|
|
719
|
+
typeof value.database_audit_id !== 'string' ||
|
|
720
|
+
!POSITIVE_INTEGER_TEXT.test(value.database_audit_id) ||
|
|
721
|
+
value.database_audit_id !== rowProof.database_audit_id ||
|
|
722
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
723
|
+
!POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) ||
|
|
724
|
+
sha256Json({
|
|
725
|
+
action_id: value.action_id,
|
|
726
|
+
process_id: value.process_id,
|
|
727
|
+
process_version: value.process_version,
|
|
728
|
+
exchange_index: value.exchange_index,
|
|
729
|
+
data_set_internal_id: value.data_set_internal_id,
|
|
730
|
+
flow_id: value.flow_id,
|
|
731
|
+
flow_version: value.flow_version,
|
|
732
|
+
direction: value.direction,
|
|
733
|
+
before_exchange_sha256: value.before_exchange_sha256,
|
|
734
|
+
before_mean_amount: value.before_mean_amount,
|
|
735
|
+
before_resulting_amount: value.before_resulting_amount,
|
|
736
|
+
after_mean_amount: value.after_mean_amount,
|
|
737
|
+
after_resulting_amount: value.after_resulting_amount,
|
|
738
|
+
after_exchange_sha256: value.after_exchange_sha256,
|
|
739
|
+
}) !== sha256Json(rewrite) ||
|
|
740
|
+
exchangeKeys.has(key)) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
exchangeKeys.add(key);
|
|
744
|
+
}
|
|
745
|
+
return options.batch.exchange_rewrites.every((rewrite) => exchangeKeys.has(aliasExchangeProgressKey({ batch_id: options.batch.batch_id, ...rewrite })));
|
|
746
|
+
}
|
|
747
|
+
function aliasPlanDerivedLogsComplete(options) {
|
|
748
|
+
return orderedAliasBatches(options.plan).every((batch) => {
|
|
749
|
+
const batchSuccess = options.batchProgress.successes.get(batch.batch_id);
|
|
750
|
+
return Boolean(batchSuccess &&
|
|
751
|
+
aliasBatchDerivedLogsComplete({
|
|
752
|
+
plan: options.plan,
|
|
753
|
+
batch,
|
|
754
|
+
planSuccess: options.planSuccess,
|
|
755
|
+
batchSuccess,
|
|
756
|
+
progress: options.progress,
|
|
757
|
+
exchangeProgressPath: options.exchangeProgressPath,
|
|
758
|
+
}));
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function appendAliasSuccessLogs(options) {
|
|
762
|
+
const batchRpc = options.execution.rpc.batches.get(options.batch.dimension);
|
|
763
|
+
for (const actionId of options.batch.action_ids) {
|
|
764
|
+
const action = options.plan.actions.find((entry) => entry.action_id === actionId);
|
|
765
|
+
const proof = batchRpc.audits.get(actionId);
|
|
766
|
+
const existingSuccess = options.progress.successes.get(actionId);
|
|
767
|
+
if (existingSuccess) {
|
|
768
|
+
if (existingSuccess.target_mode !== 'owner_draft' ||
|
|
769
|
+
existingSuccess.batch_id !== options.batch.batch_id ||
|
|
770
|
+
existingSuccess.batch_request_sha256 !== batchRpc.batch_request_sha256 ||
|
|
771
|
+
existingSuccess.database_audit_id !== proof.audit_id ||
|
|
772
|
+
existingSuccess.summary_audit_id !== batchRpc.summary_audit_id ||
|
|
773
|
+
existingSuccess.plan_request_sha256 !== options.execution.rpc.plan_request_sha256 ||
|
|
774
|
+
existingSuccess.plan_summary_audit_id !== options.execution.rpc.summary_audit_id ||
|
|
775
|
+
existingSuccess.after_sha256 !== options.execution.after_by_action.get(actionId)) {
|
|
776
|
+
throw new CliError('Existing alias row progress does not match replay audit proof.', {
|
|
777
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PROGRESS_INVALID',
|
|
778
|
+
exitCode: 1,
|
|
779
|
+
details: { action_id: actionId },
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const entry = {
|
|
785
|
+
schema_version: 1,
|
|
786
|
+
plan_sha256: options.plan.plan_sha256,
|
|
787
|
+
operation_id: options.plan.operation_id,
|
|
788
|
+
action_id: action.action_id,
|
|
789
|
+
action: action.action,
|
|
790
|
+
table: action.table,
|
|
791
|
+
id: action.id,
|
|
792
|
+
version: action.version,
|
|
793
|
+
reason_code: action.reason_code,
|
|
794
|
+
audit_context: {
|
|
795
|
+
plan_sha256: options.plan.plan_sha256,
|
|
796
|
+
operation_id: options.plan.operation_id,
|
|
797
|
+
action_id: action.action_id,
|
|
798
|
+
reason_code: action.reason_code,
|
|
799
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
800
|
+
target_mode: 'owner_draft',
|
|
801
|
+
},
|
|
802
|
+
actor: { user_id: options.context.account.user_id, email: options.context.account.email },
|
|
803
|
+
started_at_utc: options.startedAt,
|
|
804
|
+
ended_at_utc: options.endedAt,
|
|
805
|
+
before_sha256: action.before.row_sha256,
|
|
806
|
+
after_sha256: options.execution.after_by_action.get(actionId),
|
|
807
|
+
remote_result_sha256: sha256Json({
|
|
808
|
+
plan_response: options.execution.rpc.raw,
|
|
809
|
+
batch_response: batchRpc.raw,
|
|
810
|
+
audit: proof,
|
|
811
|
+
}),
|
|
812
|
+
result: 'success',
|
|
813
|
+
error: null,
|
|
814
|
+
rollback: action.rollback,
|
|
815
|
+
batch_id: options.batch.batch_id,
|
|
816
|
+
target_mode: 'owner_draft',
|
|
817
|
+
batch_request_sha256: batchRpc.batch_request_sha256,
|
|
818
|
+
database_audit_id: proof.audit_id,
|
|
819
|
+
summary_audit_id: batchRpc.summary_audit_id,
|
|
820
|
+
plan_request_sha256: options.execution.rpc.plan_request_sha256,
|
|
821
|
+
plan_summary_audit_id: options.execution.rpc.summary_audit_id,
|
|
822
|
+
};
|
|
823
|
+
appendStableJsonLine(options.progressPath, entry);
|
|
824
|
+
options.progress.entries.push(entry);
|
|
825
|
+
options.progress.successes.set(actionId, entry);
|
|
826
|
+
options.progress.latestFailures.delete(actionId);
|
|
827
|
+
}
|
|
828
|
+
const existing = readJsonLinesIfPresent(options.exchangeProgressPath);
|
|
829
|
+
const existingKeys = new Set();
|
|
830
|
+
const expectedRewrites = new Map(options.plan.alias_batches.flatMap((batch) => batch.exchange_rewrites.map((rewrite) => [
|
|
831
|
+
aliasExchangeProgressKey({ batch_id: batch.batch_id, ...rewrite }),
|
|
832
|
+
{ batch, rewrite },
|
|
833
|
+
])));
|
|
834
|
+
for (const value of existing) {
|
|
835
|
+
const key = isJsonObject(value) &&
|
|
836
|
+
typeof value.batch_id === 'string' &&
|
|
837
|
+
typeof value.action_id === 'string' &&
|
|
838
|
+
typeof value.exchange_index === 'number' &&
|
|
839
|
+
typeof value.data_set_internal_id === 'string'
|
|
840
|
+
? aliasExchangeProgressKey({
|
|
841
|
+
batch_id: value.batch_id,
|
|
842
|
+
action_id: value.action_id,
|
|
843
|
+
exchange_index: value.exchange_index,
|
|
844
|
+
data_set_internal_id: value.data_set_internal_id,
|
|
845
|
+
})
|
|
846
|
+
: '';
|
|
847
|
+
const expected = expectedRewrites.get(key);
|
|
848
|
+
const rowProof = expected ? options.progress.successes.get(expected.rewrite.action_id) : null;
|
|
849
|
+
if (!isJsonObject(value) ||
|
|
850
|
+
!expected ||
|
|
851
|
+
!rowProof ||
|
|
852
|
+
value.schema_version !== 1 ||
|
|
853
|
+
value.plan_sha256 !== options.plan.plan_sha256 ||
|
|
854
|
+
value.operation_id !== options.plan.operation_id ||
|
|
855
|
+
value.target_mode !== 'owner_draft' ||
|
|
856
|
+
value.factor !== expected.batch.factor ||
|
|
857
|
+
value.result !== 'success' ||
|
|
858
|
+
!isJsonObject(value.actor) ||
|
|
859
|
+
value.actor.user_id !== options.plan.account.user_id ||
|
|
860
|
+
value.actor.email !== options.plan.account.email ||
|
|
861
|
+
typeof value.logged_at_utc !== 'string' ||
|
|
862
|
+
typeof value.batch_request_sha256 !== 'string' ||
|
|
863
|
+
!/^[a-f0-9]{64}$/u.test(value.batch_request_sha256) ||
|
|
864
|
+
value.batch_request_sha256 !== rowProof.batch_request_sha256 ||
|
|
865
|
+
typeof value.database_audit_id !== 'string' ||
|
|
866
|
+
POSITIVE_INTEGER_TEXT.test(value.database_audit_id) === false ||
|
|
867
|
+
value.database_audit_id !== rowProof.database_audit_id ||
|
|
868
|
+
typeof value.summary_audit_id !== 'string' ||
|
|
869
|
+
POSITIVE_INTEGER_TEXT.test(value.summary_audit_id) === false ||
|
|
870
|
+
value.summary_audit_id !== rowProof.summary_audit_id ||
|
|
871
|
+
typeof value.plan_request_sha256 !== 'string' ||
|
|
872
|
+
!/^[a-f0-9]{64}$/u.test(value.plan_request_sha256) ||
|
|
873
|
+
value.plan_request_sha256 !== rowProof.plan_request_sha256 ||
|
|
874
|
+
typeof value.plan_summary_audit_id !== 'string' ||
|
|
875
|
+
!POSITIVE_INTEGER_TEXT.test(value.plan_summary_audit_id) ||
|
|
876
|
+
value.plan_summary_audit_id !== rowProof.plan_summary_audit_id ||
|
|
877
|
+
sha256Json({
|
|
878
|
+
action_id: value.action_id,
|
|
879
|
+
process_id: value.process_id,
|
|
880
|
+
process_version: value.process_version,
|
|
881
|
+
exchange_index: value.exchange_index,
|
|
882
|
+
data_set_internal_id: value.data_set_internal_id,
|
|
883
|
+
flow_id: value.flow_id,
|
|
884
|
+
flow_version: value.flow_version,
|
|
885
|
+
direction: value.direction,
|
|
886
|
+
before_exchange_sha256: value.before_exchange_sha256,
|
|
887
|
+
before_mean_amount: value.before_mean_amount,
|
|
888
|
+
before_resulting_amount: value.before_resulting_amount,
|
|
889
|
+
after_mean_amount: value.after_mean_amount,
|
|
890
|
+
after_resulting_amount: value.after_resulting_amount,
|
|
891
|
+
after_exchange_sha256: value.after_exchange_sha256,
|
|
892
|
+
}) !== sha256Json(expected.rewrite) ||
|
|
893
|
+
existingKeys.has(key)) {
|
|
894
|
+
throw new CliError('Alias exchange progress contains an invalid or foreign entry.', {
|
|
895
|
+
code: 'DATASET_MAINTENANCE_ALIAS_EXCHANGE_PROGRESS_INVALID',
|
|
896
|
+
exitCode: 1,
|
|
897
|
+
details: value,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
existingKeys.add(key);
|
|
901
|
+
}
|
|
902
|
+
for (const rewrite of options.batch.exchange_rewrites) {
|
|
903
|
+
const key = aliasExchangeProgressKey({ batch_id: options.batch.batch_id, ...rewrite });
|
|
904
|
+
if (existingKeys.has(key))
|
|
905
|
+
continue;
|
|
906
|
+
const proof = batchRpc.audits.get(rewrite.action_id);
|
|
907
|
+
appendStableJsonLine(options.exchangeProgressPath, {
|
|
908
|
+
schema_version: 1,
|
|
909
|
+
plan_sha256: options.plan.plan_sha256,
|
|
910
|
+
operation_id: options.plan.operation_id,
|
|
911
|
+
batch_id: options.batch.batch_id,
|
|
912
|
+
target_mode: 'owner_draft',
|
|
913
|
+
batch_request_sha256: batchRpc.batch_request_sha256,
|
|
914
|
+
factor: options.batch.factor,
|
|
915
|
+
actor: { user_id: options.context.account.user_id, email: options.context.account.email },
|
|
916
|
+
logged_at_utc: options.endedAt,
|
|
917
|
+
database_audit_id: proof.audit_id,
|
|
918
|
+
summary_audit_id: batchRpc.summary_audit_id,
|
|
919
|
+
plan_request_sha256: options.execution.rpc.plan_request_sha256,
|
|
920
|
+
plan_summary_audit_id: options.execution.rpc.summary_audit_id,
|
|
921
|
+
result: 'success',
|
|
922
|
+
...rewrite,
|
|
923
|
+
});
|
|
924
|
+
existingKeys.add(key);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function appendAliasProofProgress(options) {
|
|
928
|
+
const batchProofs = [];
|
|
929
|
+
for (const batch of orderedAliasBatches(options.plan)) {
|
|
930
|
+
const rpc = options.execution.rpc.batches.get(batch.dimension);
|
|
931
|
+
const existing = options.batchProgress.successes.get(batch.batch_id);
|
|
932
|
+
if (existing &&
|
|
933
|
+
(existing.batch_request_sha256 !== rpc.batch_request_sha256 ||
|
|
934
|
+
existing.summary_audit_id !== rpc.summary_audit_id ||
|
|
935
|
+
existing.plan_request_sha256 !== options.execution.rpc.plan_request_sha256 ||
|
|
936
|
+
existing.plan_summary_audit_id !== options.execution.rpc.summary_audit_id)) {
|
|
937
|
+
throw new CliError('Existing alias batch proof does not match whole-plan replay.', {
|
|
938
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PROGRESS_INVALID',
|
|
939
|
+
exitCode: 1,
|
|
940
|
+
details: { batch_id: batch.batch_id },
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const proof = {
|
|
944
|
+
batch_id: batch.batch_id,
|
|
945
|
+
dimension: batch.dimension,
|
|
946
|
+
batch_request_sha256: rpc.batch_request_sha256,
|
|
947
|
+
summary_audit_id: rpc.summary_audit_id,
|
|
948
|
+
};
|
|
949
|
+
batchProofs.push(proof);
|
|
950
|
+
if (!existing) {
|
|
951
|
+
const entry = {
|
|
952
|
+
schema_version: 1,
|
|
953
|
+
plan_sha256: options.plan.plan_sha256,
|
|
954
|
+
operation_id: options.plan.operation_id,
|
|
955
|
+
batch_id: batch.batch_id,
|
|
956
|
+
target_mode: 'owner_draft',
|
|
957
|
+
dimension: batch.dimension,
|
|
958
|
+
factor: batch.factor,
|
|
959
|
+
actor: {
|
|
960
|
+
user_id: options.context.account.user_id,
|
|
961
|
+
email: options.context.account.email,
|
|
962
|
+
},
|
|
963
|
+
started_at_utc: options.startedAt,
|
|
964
|
+
ended_at_utc: options.endedAt,
|
|
965
|
+
batch_request_sha256: rpc.batch_request_sha256,
|
|
966
|
+
idempotent_replay: rpc.idempotent_replay,
|
|
967
|
+
row_count: batch.summary.rows,
|
|
968
|
+
exchange_count: rpc.exchange_count,
|
|
969
|
+
summary_audit_id: rpc.summary_audit_id,
|
|
970
|
+
plan_request_sha256: options.execution.rpc.plan_request_sha256,
|
|
971
|
+
plan_summary_audit_id: options.execution.rpc.summary_audit_id,
|
|
972
|
+
result: 'success',
|
|
973
|
+
error: null,
|
|
974
|
+
};
|
|
975
|
+
appendStableJsonLine(options.batchProgressPath, entry);
|
|
976
|
+
options.batchProgress.entries.push(entry);
|
|
977
|
+
options.batchProgress.successes.set(batch.batch_id, entry);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const entry = {
|
|
981
|
+
schema_version: 1,
|
|
982
|
+
plan_sha256: options.plan.plan_sha256,
|
|
983
|
+
operation_id: options.plan.operation_id,
|
|
984
|
+
target_mode: 'owner_draft',
|
|
985
|
+
actor: { user_id: options.context.account.user_id, email: options.context.account.email },
|
|
986
|
+
started_at_utc: options.startedAt,
|
|
987
|
+
ended_at_utc: options.endedAt,
|
|
988
|
+
plan_request_sha256: options.execution.rpc.plan_request_sha256,
|
|
989
|
+
idempotent_replay: options.execution.rpc.idempotent_replay,
|
|
990
|
+
batch_count: 2,
|
|
991
|
+
row_count: 52,
|
|
992
|
+
exchange_count: 59,
|
|
993
|
+
summary_audit_id: options.execution.rpc.summary_audit_id,
|
|
994
|
+
batches: batchProofs,
|
|
995
|
+
result: 'success',
|
|
996
|
+
error: null,
|
|
997
|
+
};
|
|
998
|
+
if (options.planProgress.success) {
|
|
999
|
+
if (options.planProgress.success.plan_request_sha256 !== entry.plan_request_sha256 ||
|
|
1000
|
+
options.planProgress.success.summary_audit_id !== entry.summary_audit_id ||
|
|
1001
|
+
sha256Json(options.planProgress.success.batches) !== sha256Json(entry.batches)) {
|
|
1002
|
+
throw new CliError('Existing alias plan proof does not match whole-plan replay.', {
|
|
1003
|
+
code: 'DATASET_MAINTENANCE_ALIAS_PLAN_PROGRESS_INVALID',
|
|
1004
|
+
exitCode: 1,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return options.planProgress.success;
|
|
1008
|
+
}
|
|
1009
|
+
appendStableJsonLine(options.planProgressPath, entry);
|
|
1010
|
+
options.planProgress.entries.push(entry);
|
|
1011
|
+
options.planProgress.success = entry;
|
|
1012
|
+
options.planProgress.latestFailure = null;
|
|
1013
|
+
return entry;
|
|
1014
|
+
}
|
|
1015
|
+
function appendAliasPlanFailure(options) {
|
|
1016
|
+
const entry = {
|
|
1017
|
+
schema_version: 1,
|
|
1018
|
+
plan_sha256: options.plan.plan_sha256,
|
|
1019
|
+
operation_id: options.plan.operation_id,
|
|
1020
|
+
target_mode: 'owner_draft',
|
|
1021
|
+
actor: { user_id: options.context.account.user_id, email: options.context.account.email },
|
|
1022
|
+
started_at_utc: options.startedAt,
|
|
1023
|
+
ended_at_utc: options.endedAt,
|
|
1024
|
+
plan_request_sha256: null,
|
|
1025
|
+
idempotent_replay: null,
|
|
1026
|
+
batch_count: 2,
|
|
1027
|
+
row_count: 52,
|
|
1028
|
+
exchange_count: 59,
|
|
1029
|
+
summary_audit_id: null,
|
|
1030
|
+
batches: [],
|
|
1031
|
+
result: 'failed',
|
|
1032
|
+
error: errorMessage(options.error),
|
|
1033
|
+
};
|
|
1034
|
+
appendStableJsonLine(options.progressPath, entry);
|
|
1035
|
+
options.planProgress.entries.push(entry);
|
|
1036
|
+
if (!options.planProgress.success) {
|
|
1037
|
+
options.planProgress.latestFailure = entry;
|
|
1038
|
+
}
|
|
1039
|
+
return entry;
|
|
1040
|
+
}
|
|
1041
|
+
async function executeAction(options) {
|
|
1042
|
+
if (!options.action.before) {
|
|
1043
|
+
throw new CliError(`Action lacks a before snapshot: ${options.action.action_id}`, {
|
|
1044
|
+
code: 'DATASET_MAINTENANCE_PLAN_INVALID',
|
|
1045
|
+
exitCode: 2,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const justInTime = await fetchMaintenanceExactRows({
|
|
1049
|
+
context: options.context,
|
|
1050
|
+
table: options.action.table,
|
|
1051
|
+
id: options.action.id,
|
|
1052
|
+
version: options.action.version,
|
|
1053
|
+
});
|
|
1054
|
+
const pendingRow = justInTime.rows.length === 1 ? justInTime.rows[0] : null;
|
|
1055
|
+
const pendingSnapshot = pendingRow ? snapshotRemoteRow(pendingRow) : null;
|
|
1056
|
+
const exactDraft = Boolean(pendingRow &&
|
|
1057
|
+
pendingRow.state_code === 0 &&
|
|
1058
|
+
pendingSnapshot?.row_sha256 === options.action.before.row_sha256);
|
|
1059
|
+
if (!pendingRow || pendingRow.user_id !== options.action.expected_user_id || !exactDraft) {
|
|
1060
|
+
throw new CliError(`Action row drifted immediately before write: ${options.action.action_id}`, {
|
|
1061
|
+
code: 'DATASET_MAINTENANCE_ACTION_JUST_IN_TIME_DRIFT',
|
|
1062
|
+
exitCode: 1,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
const audit = {
|
|
1066
|
+
plan_sha256: options.plan.plan_sha256,
|
|
1067
|
+
operation_id: options.plan.operation_id,
|
|
1068
|
+
action_id: options.action.action_id,
|
|
1069
|
+
reason_code: options.action.reason_code,
|
|
1070
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1071
|
+
};
|
|
1072
|
+
if (options.action.action === 'save_draft') {
|
|
1073
|
+
const remoteResult = await saveDraftMaintenanceRow({
|
|
1074
|
+
context: options.context,
|
|
1075
|
+
table: options.action.table,
|
|
1076
|
+
id: options.action.id,
|
|
1077
|
+
version: options.action.version,
|
|
1078
|
+
payload: loadDesiredPayload(options.planDir, options.action),
|
|
1079
|
+
modelId: options.action.before?.model_id ?? null,
|
|
1080
|
+
ruleVerification: options.action.before?.rule_verification ?? null,
|
|
1081
|
+
audit,
|
|
1082
|
+
});
|
|
1083
|
+
const readback = await fetchMaintenanceExactRows({
|
|
1084
|
+
context: options.context,
|
|
1085
|
+
table: options.action.table,
|
|
1086
|
+
id: options.action.id,
|
|
1087
|
+
version: options.action.version,
|
|
1088
|
+
});
|
|
1089
|
+
const row = readback.rows[0];
|
|
1090
|
+
if (readback.rows.length !== 1 || !row) {
|
|
1091
|
+
throw new CliError(`save_draft readback failed for ${options.action.action_id}.`, {
|
|
1092
|
+
code: 'DATASET_MAINTENANCE_ACTION_READBACK_FAILED',
|
|
1093
|
+
exitCode: 1,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
const expectedPayload = options.action.desired_payload?.sha256;
|
|
1097
|
+
const readbackSnapshot = snapshotRemoteRow(row);
|
|
1098
|
+
if (readbackSnapshot.payload_sha256 !== expectedPayload ||
|
|
1099
|
+
row.user_id !== options.action.expected_user_id ||
|
|
1100
|
+
row.state_code !== 0) {
|
|
1101
|
+
throw new CliError(`save_draft readback mismatch for ${options.action.action_id}.`, {
|
|
1102
|
+
code: 'DATASET_MAINTENANCE_ACTION_READBACK_FAILED',
|
|
1103
|
+
exitCode: 1,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
afterSha256: readbackSnapshot.row_sha256,
|
|
1108
|
+
remoteResultSha256: sha256Json(remoteResult),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (options.action.action === 'update_json_ordered') {
|
|
1112
|
+
throw new CliError('Atomic alias actions must execute through the whole-plan RPC.', {
|
|
1113
|
+
code: 'DATASET_MAINTENANCE_ALIAS_SEQUENTIAL_WRITE_FORBIDDEN',
|
|
1114
|
+
exitCode: 1,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
const remoteResult = await deleteMaintenanceRow({
|
|
1118
|
+
context: options.context,
|
|
1119
|
+
table: options.action.table,
|
|
1120
|
+
id: options.action.id,
|
|
1121
|
+
version: options.action.version,
|
|
1122
|
+
audit,
|
|
1123
|
+
});
|
|
1124
|
+
const readback = await fetchMaintenanceExactRows({
|
|
1125
|
+
context: options.context,
|
|
1126
|
+
table: options.action.table,
|
|
1127
|
+
id: options.action.id,
|
|
1128
|
+
version: options.action.version,
|
|
1129
|
+
});
|
|
1130
|
+
if (readback.rows.length !== 0) {
|
|
1131
|
+
throw new CliError(`delete readback failed for ${options.action.action_id}.`, {
|
|
1132
|
+
code: 'DATASET_MAINTENANCE_ACTION_READBACK_FAILED',
|
|
1133
|
+
exitCode: 1,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
|
|
1137
|
+
}
|
|
1138
|
+
function nextAttemptPath(planDir) {
|
|
1139
|
+
let attempt = 1;
|
|
1140
|
+
while (existsSync(path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`))) {
|
|
1141
|
+
attempt += 1;
|
|
1142
|
+
}
|
|
1143
|
+
return path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`);
|
|
1144
|
+
}
|
|
1145
|
+
export async function runDatasetMaintenanceApply(options) {
|
|
1146
|
+
if (!options.commit) {
|
|
1147
|
+
throw new CliError('Dataset maintenance apply requires commit=true.', {
|
|
1148
|
+
code: 'DATASET_MAINTENANCE_COMMIT_REQUIRED',
|
|
1149
|
+
exitCode: 2,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
const planPath = path.resolve(options.planPath);
|
|
1153
|
+
const planDir = path.dirname(planPath);
|
|
1154
|
+
const plan = parseMaintenancePlan(readJsonFile(planPath, 'Maintenance plan'));
|
|
1155
|
+
if (options.approvePlan !== plan.plan_sha256) {
|
|
1156
|
+
throw new CliError('approvePlan must exactly match the canonical maintenance plan hash.', {
|
|
1157
|
+
code: 'DATASET_MAINTENANCE_PLAN_APPROVAL_REQUIRED',
|
|
1158
|
+
exitCode: 2,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
if (plan.status !== 'ready' || plan.blockers.length > 0) {
|
|
1162
|
+
throw new CliError('Blocked maintenance plan cannot be applied.', {
|
|
1163
|
+
code: 'DATASET_MAINTENANCE_PLAN_BLOCKED',
|
|
1164
|
+
exitCode: 1,
|
|
1165
|
+
details: plan.blockers,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (plan.operation === 'redo-import' &&
|
|
1169
|
+
!plan.source_import_run_id &&
|
|
1170
|
+
plan.source_lineage === null) {
|
|
1171
|
+
throw new CliError('redo-import apply requires frozen redo source/import lineage.', {
|
|
1172
|
+
code: 'DATASET_MAINTENANCE_REDO_NOT_READY',
|
|
1173
|
+
exitCode: 1,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
const progressPath = path.join(planDir, 'apply-progress.jsonl');
|
|
1177
|
+
const aliasPlanProgressPath = path.join(planDir, 'alias-plan-progress.jsonl');
|
|
1178
|
+
const aliasBatchProgressPath = path.join(planDir, 'alias-batch-progress.jsonl');
|
|
1179
|
+
const aliasExchangeProgressPath = path.join(planDir, 'alias-exchange-progress.jsonl');
|
|
1180
|
+
return withStateFileLock(progressPath, { reason: `dataset_maintenance_apply_${plan.operation_id}` }, async () => {
|
|
1181
|
+
const context = await resolveMaintenanceRemoteContext({
|
|
1182
|
+
env: options.env,
|
|
1183
|
+
fetchImpl: options.fetchImpl,
|
|
1184
|
+
timeoutMs: options.timeoutMs,
|
|
1185
|
+
now: options.now,
|
|
1186
|
+
});
|
|
1187
|
+
if (context.account.user_id !== plan.account.user_id ||
|
|
1188
|
+
context.account.email !== plan.account.email) {
|
|
1189
|
+
throw new CliError('Current authenticated account does not match the maintenance plan.', {
|
|
1190
|
+
code: 'DATASET_MAINTENANCE_ACCOUNT_MISMATCH',
|
|
1191
|
+
exitCode: 1,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (options.confirm !== context.account.email) {
|
|
1195
|
+
throw new CliError('confirm must exactly match the current authenticated account email.', {
|
|
1196
|
+
code: 'DATASET_MAINTENANCE_CONFIRMATION_REQUIRED',
|
|
1197
|
+
exitCode: 2,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
const progress = parseProgress(plan, progressPath);
|
|
1201
|
+
let resumedSuccesses = progress.successes.size;
|
|
1202
|
+
const aliasPlanProgress = plan.operation === 'merge-support-aliases'
|
|
1203
|
+
? parseAliasPlanProgress(plan, aliasPlanProgressPath)
|
|
1204
|
+
: { entries: [], success: null, latestFailure: null };
|
|
1205
|
+
const aliasBatchProgress = plan.operation === 'merge-support-aliases'
|
|
1206
|
+
? parseAliasBatchProgress(plan, aliasBatchProgressPath)
|
|
1207
|
+
: { entries: [], successes: new Map() };
|
|
1208
|
+
const current = await fetchMaintenanceAccountRows({
|
|
1209
|
+
context,
|
|
1210
|
+
userId: plan.account.user_id,
|
|
1211
|
+
});
|
|
1212
|
+
assertApplyPreconditions({
|
|
1213
|
+
plan,
|
|
1214
|
+
planDir,
|
|
1215
|
+
currentRows: current.rows,
|
|
1216
|
+
progress,
|
|
1217
|
+
aliasPlanProgress,
|
|
1218
|
+
});
|
|
1219
|
+
if (plan.operation === 'merge-support-aliases') {
|
|
1220
|
+
await assertAliasSupportSnapshots({ plan, context });
|
|
1221
|
+
}
|
|
1222
|
+
const approvalPath = path.join(planDir, 'approval-record.json');
|
|
1223
|
+
validateApprovalRecord({ path: approvalPath, plan, context });
|
|
1224
|
+
if (!existsSync(approvalPath)) {
|
|
1225
|
+
writeImmutableJson(approvalPath, {
|
|
1226
|
+
schema_version: 1,
|
|
1227
|
+
approved_at_utc: clock(options),
|
|
1228
|
+
plan_path: planPath,
|
|
1229
|
+
plan_sha256: plan.plan_sha256,
|
|
1230
|
+
task_id: plan.task_id,
|
|
1231
|
+
operation: plan.operation,
|
|
1232
|
+
target_mode: plan.target_mode,
|
|
1233
|
+
operation_id: plan.operation_id,
|
|
1234
|
+
account: {
|
|
1235
|
+
user_id: context.account.user_id,
|
|
1236
|
+
email: context.account.email,
|
|
1237
|
+
},
|
|
1238
|
+
confirmed_email: options.confirm,
|
|
1239
|
+
row_counts: plan.summary,
|
|
1240
|
+
snapshot_completeness: current.completeness,
|
|
1241
|
+
redo_rows_ready: plan.operation === 'redo-import'
|
|
1242
|
+
? Boolean(plan.source_import_run_id || plan.source_lineage !== null)
|
|
1243
|
+
: null,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
if (plan.operation === 'merge-support-aliases') {
|
|
1247
|
+
const alreadyComplete = Boolean(aliasPlanProgress.success &&
|
|
1248
|
+
aliasPlanDerivedLogsComplete({
|
|
1249
|
+
plan,
|
|
1250
|
+
planSuccess: aliasPlanProgress.success,
|
|
1251
|
+
batchProgress: aliasBatchProgress,
|
|
1252
|
+
progress,
|
|
1253
|
+
exchangeProgressPath: aliasExchangeProgressPath,
|
|
1254
|
+
}));
|
|
1255
|
+
resumedSuccesses = alreadyComplete ? plan.actions.length : 0;
|
|
1256
|
+
let planSuccess = alreadyComplete ? aliasPlanProgress.success : null;
|
|
1257
|
+
let planFailure = null;
|
|
1258
|
+
if (!alreadyComplete) {
|
|
1259
|
+
const startedAt = clock(options);
|
|
1260
|
+
try {
|
|
1261
|
+
const execution = await executeAliasPlan({ plan, planDir, context });
|
|
1262
|
+
const endedAt = clock(options);
|
|
1263
|
+
for (const batch of orderedAliasBatches(plan)) {
|
|
1264
|
+
appendAliasSuccessLogs({
|
|
1265
|
+
plan,
|
|
1266
|
+
batch,
|
|
1267
|
+
execution,
|
|
1268
|
+
progress,
|
|
1269
|
+
progressPath,
|
|
1270
|
+
exchangeProgressPath: aliasExchangeProgressPath,
|
|
1271
|
+
context,
|
|
1272
|
+
startedAt,
|
|
1273
|
+
endedAt,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
planSuccess = appendAliasProofProgress({
|
|
1277
|
+
plan,
|
|
1278
|
+
execution,
|
|
1279
|
+
planProgress: aliasPlanProgress,
|
|
1280
|
+
batchProgress: aliasBatchProgress,
|
|
1281
|
+
planProgressPath: aliasPlanProgressPath,
|
|
1282
|
+
batchProgressPath: aliasBatchProgressPath,
|
|
1283
|
+
context,
|
|
1284
|
+
startedAt,
|
|
1285
|
+
endedAt,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
planFailure = appendAliasPlanFailure({
|
|
1290
|
+
plan,
|
|
1291
|
+
planProgress: aliasPlanProgress,
|
|
1292
|
+
progressPath: aliasPlanProgressPath,
|
|
1293
|
+
context,
|
|
1294
|
+
startedAt,
|
|
1295
|
+
endedAt: clock(options),
|
|
1296
|
+
error,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const fullyProven = Boolean(planSuccess &&
|
|
1301
|
+
aliasPlanDerivedLogsComplete({
|
|
1302
|
+
plan,
|
|
1303
|
+
planSuccess,
|
|
1304
|
+
batchProgress: aliasBatchProgress,
|
|
1305
|
+
progress,
|
|
1306
|
+
exchangeProgressPath: aliasExchangeProgressPath,
|
|
1307
|
+
}));
|
|
1308
|
+
const failureError = planFailure?.error ?? 'Whole-plan proof is incomplete.';
|
|
1309
|
+
const actions = plan.actions.map((action) => {
|
|
1310
|
+
return {
|
|
1311
|
+
action_id: action.action_id,
|
|
1312
|
+
action: action.action,
|
|
1313
|
+
table: action.table,
|
|
1314
|
+
id: action.id,
|
|
1315
|
+
version: action.version,
|
|
1316
|
+
status: fullyProven ? 'success' : 'failed',
|
|
1317
|
+
error: fullyProven ? null : failureError,
|
|
1318
|
+
};
|
|
1319
|
+
});
|
|
1320
|
+
const successCount = fullyProven ? actions.length : 0;
|
|
1321
|
+
const failureCount = fullyProven ? 0 : actions.length;
|
|
1322
|
+
const attemptPath = nextAttemptPath(planDir);
|
|
1323
|
+
const report = {
|
|
1324
|
+
schema_version: 1,
|
|
1325
|
+
generated_at_utc: clock(options),
|
|
1326
|
+
status: successCount === actions.length ? 'completed' : 'completed_with_failures',
|
|
1327
|
+
task_id: plan.task_id,
|
|
1328
|
+
operation: plan.operation,
|
|
1329
|
+
operation_id: plan.operation_id,
|
|
1330
|
+
target_mode: plan.target_mode,
|
|
1331
|
+
plan_sha256: plan.plan_sha256,
|
|
1332
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1333
|
+
summary: {
|
|
1334
|
+
actions: actions.length,
|
|
1335
|
+
success: successCount,
|
|
1336
|
+
failed: failureCount,
|
|
1337
|
+
pending: actions.length - successCount - failureCount,
|
|
1338
|
+
resumed_successes: resumedSuccesses,
|
|
1339
|
+
},
|
|
1340
|
+
actions,
|
|
1341
|
+
artifacts: {
|
|
1342
|
+
approval_record: approvalPath,
|
|
1343
|
+
apply_progress: progressPath,
|
|
1344
|
+
commit_report: path.join(planDir, 'commit-report.json'),
|
|
1345
|
+
attempt_report: attemptPath,
|
|
1346
|
+
alias_plan_progress: aliasPlanProgressPath,
|
|
1347
|
+
alias_batch_progress: aliasBatchProgressPath,
|
|
1348
|
+
alias_exchange_progress: aliasExchangeProgressPath,
|
|
1349
|
+
},
|
|
1350
|
+
database_audit: {
|
|
1351
|
+
rpc_transaction_log: 'public.command_audit_log',
|
|
1352
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1353
|
+
correlation_fields: [
|
|
1354
|
+
'plan_sha256',
|
|
1355
|
+
'operation_id',
|
|
1356
|
+
'target_visibility',
|
|
1357
|
+
'plan_request_sha256',
|
|
1358
|
+
'batch_id',
|
|
1359
|
+
'action_id',
|
|
1360
|
+
'batch_request_sha256',
|
|
1361
|
+
],
|
|
1362
|
+
},
|
|
1363
|
+
...(fullyProven && planSuccess
|
|
1364
|
+
? {
|
|
1365
|
+
alias_plan_proof: {
|
|
1366
|
+
plan_request_sha256: planSuccess.plan_request_sha256,
|
|
1367
|
+
summary_audit_id: planSuccess.summary_audit_id,
|
|
1368
|
+
batch_count: 2,
|
|
1369
|
+
row_count: 52,
|
|
1370
|
+
exchange_count: 59,
|
|
1371
|
+
idempotent_replay: planSuccess.idempotent_replay,
|
|
1372
|
+
},
|
|
1373
|
+
}
|
|
1374
|
+
: {}),
|
|
1375
|
+
};
|
|
1376
|
+
writeImmutableJson(attemptPath, report);
|
|
1377
|
+
writeJsonArtifact(report.artifacts.commit_report, report);
|
|
1378
|
+
return report;
|
|
1379
|
+
}
|
|
1380
|
+
const ordered = [...plan.actions].sort((left, right) => {
|
|
1381
|
+
const rank = {
|
|
1382
|
+
save_draft: 0,
|
|
1383
|
+
update_json_ordered: 0,
|
|
1384
|
+
delete: 1,
|
|
1385
|
+
};
|
|
1386
|
+
const actionOrder = rank[left.action] - rank[right.action];
|
|
1387
|
+
return actionOrder || left.ordinal - right.ordinal;
|
|
1388
|
+
});
|
|
1389
|
+
for (const action of ordered) {
|
|
1390
|
+
if (progress.successes.has(action.action_id)) {
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const startedAt = clock(options);
|
|
1394
|
+
try {
|
|
1395
|
+
const actionResult = await executeAction({ action, plan, planDir, context });
|
|
1396
|
+
const entry = {
|
|
1397
|
+
schema_version: 1,
|
|
1398
|
+
plan_sha256: plan.plan_sha256,
|
|
1399
|
+
operation_id: plan.operation_id,
|
|
1400
|
+
action_id: action.action_id,
|
|
1401
|
+
action: action.action,
|
|
1402
|
+
table: action.table,
|
|
1403
|
+
id: action.id,
|
|
1404
|
+
version: action.version,
|
|
1405
|
+
reason_code: action.reason_code,
|
|
1406
|
+
audit_context: {
|
|
1407
|
+
plan_sha256: plan.plan_sha256,
|
|
1408
|
+
operation_id: plan.operation_id,
|
|
1409
|
+
action_id: action.action_id,
|
|
1410
|
+
reason_code: action.reason_code,
|
|
1411
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1412
|
+
},
|
|
1413
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1414
|
+
started_at_utc: startedAt,
|
|
1415
|
+
ended_at_utc: clock(options),
|
|
1416
|
+
before_sha256: action.before.row_sha256,
|
|
1417
|
+
after_sha256: actionResult.afterSha256,
|
|
1418
|
+
remote_result_sha256: actionResult.remoteResultSha256,
|
|
1419
|
+
result: 'success',
|
|
1420
|
+
error: null,
|
|
1421
|
+
rollback: action.rollback,
|
|
1422
|
+
};
|
|
1423
|
+
appendStableJsonLine(progressPath, entry);
|
|
1424
|
+
progress.entries.push(entry);
|
|
1425
|
+
progress.successes.set(action.action_id, entry);
|
|
1426
|
+
progress.latestFailures.delete(action.action_id);
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
const entry = {
|
|
1430
|
+
schema_version: 1,
|
|
1431
|
+
plan_sha256: plan.plan_sha256,
|
|
1432
|
+
operation_id: plan.operation_id,
|
|
1433
|
+
action_id: action.action_id,
|
|
1434
|
+
action: action.action,
|
|
1435
|
+
table: action.table,
|
|
1436
|
+
id: action.id,
|
|
1437
|
+
version: action.version,
|
|
1438
|
+
reason_code: action.reason_code,
|
|
1439
|
+
audit_context: {
|
|
1440
|
+
plan_sha256: plan.plan_sha256,
|
|
1441
|
+
operation_id: plan.operation_id,
|
|
1442
|
+
action_id: action.action_id,
|
|
1443
|
+
reason_code: action.reason_code,
|
|
1444
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1445
|
+
},
|
|
1446
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1447
|
+
started_at_utc: startedAt,
|
|
1448
|
+
ended_at_utc: clock(options),
|
|
1449
|
+
before_sha256: action.before.row_sha256,
|
|
1450
|
+
after_sha256: null,
|
|
1451
|
+
remote_result_sha256: null,
|
|
1452
|
+
result: 'failed',
|
|
1453
|
+
error: errorMessage(error),
|
|
1454
|
+
rollback: action.rollback,
|
|
1455
|
+
};
|
|
1456
|
+
appendStableJsonLine(progressPath, entry);
|
|
1457
|
+
progress.entries.push(entry);
|
|
1458
|
+
progress.latestFailures.set(action.action_id, entry);
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const actions = plan.actions.map((action) => {
|
|
1463
|
+
const success = progress.successes.get(action.action_id);
|
|
1464
|
+
const failure = progress.latestFailures.get(action.action_id);
|
|
1465
|
+
return {
|
|
1466
|
+
action_id: action.action_id,
|
|
1467
|
+
action: action.action,
|
|
1468
|
+
table: action.table,
|
|
1469
|
+
id: action.id,
|
|
1470
|
+
version: action.version,
|
|
1471
|
+
status: success
|
|
1472
|
+
? 'success'
|
|
1473
|
+
: failure
|
|
1474
|
+
? 'failed'
|
|
1475
|
+
: 'pending',
|
|
1476
|
+
error: failure?.error ?? null,
|
|
1477
|
+
};
|
|
1478
|
+
});
|
|
1479
|
+
const successCount = actions.filter((action) => action.status === 'success').length;
|
|
1480
|
+
const failureCount = actions.filter((action) => action.status === 'failed').length;
|
|
1481
|
+
const attemptPath = nextAttemptPath(planDir);
|
|
1482
|
+
const report = {
|
|
1483
|
+
schema_version: 1,
|
|
1484
|
+
generated_at_utc: clock(options),
|
|
1485
|
+
status: successCount === actions.length ? 'completed' : 'completed_with_failures',
|
|
1486
|
+
task_id: plan.task_id,
|
|
1487
|
+
operation: plan.operation,
|
|
1488
|
+
operation_id: plan.operation_id,
|
|
1489
|
+
target_mode: plan.target_mode,
|
|
1490
|
+
plan_sha256: plan.plan_sha256,
|
|
1491
|
+
actor: { user_id: context.account.user_id, email: context.account.email },
|
|
1492
|
+
summary: {
|
|
1493
|
+
actions: actions.length,
|
|
1494
|
+
success: successCount,
|
|
1495
|
+
failed: failureCount,
|
|
1496
|
+
pending: actions.length - successCount - failureCount,
|
|
1497
|
+
resumed_successes: resumedSuccesses,
|
|
1498
|
+
},
|
|
1499
|
+
actions,
|
|
1500
|
+
artifacts: {
|
|
1501
|
+
approval_record: approvalPath,
|
|
1502
|
+
apply_progress: progressPath,
|
|
1503
|
+
commit_report: path.join(planDir, 'commit-report.json'),
|
|
1504
|
+
attempt_report: attemptPath,
|
|
1505
|
+
},
|
|
1506
|
+
database_audit: {
|
|
1507
|
+
rpc_transaction_log: 'public.command_audit_log',
|
|
1508
|
+
source: 'tiangong-lca dataset maintenance apply',
|
|
1509
|
+
correlation_fields: ['plan_sha256', 'operation_id', 'action_id', 'reason_code'],
|
|
1510
|
+
},
|
|
1511
|
+
};
|
|
1512
|
+
writeImmutableJson(attemptPath, report);
|
|
1513
|
+
writeJsonArtifact(report.artifacts.commit_report, report);
|
|
1514
|
+
return report;
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
export const __testInternals = {
|
|
1518
|
+
aliasBatchDerivedLogsComplete,
|
|
1519
|
+
aliasPlanDerivedLogsComplete,
|
|
1520
|
+
aliasExchangeProgressKey,
|
|
1521
|
+
appendAliasSuccessLogs,
|
|
1522
|
+
assertApplyPreconditions,
|
|
1523
|
+
assertAliasSupportSnapshots,
|
|
1524
|
+
buildAliasBatchRequest,
|
|
1525
|
+
buildAliasPlanRequest,
|
|
1526
|
+
clock,
|
|
1527
|
+
errorMessage,
|
|
1528
|
+
executeAliasPlan,
|
|
1529
|
+
executeAction,
|
|
1530
|
+
finalProjectedRows,
|
|
1531
|
+
loadDesiredPayload,
|
|
1532
|
+
nextAttemptPath,
|
|
1533
|
+
parseAliasBatchProgress,
|
|
1534
|
+
parseAliasPlanProgress,
|
|
1535
|
+
parseProgress,
|
|
1536
|
+
validateAliasRpcResult,
|
|
1537
|
+
validateAliasPlanRpcResult,
|
|
1538
|
+
validateApprovalRecord,
|
|
1539
|
+
};
|
|
1540
|
+
//# sourceMappingURL=dataset-maintenance-apply.js.map
|