@tiangong-lca/cli 0.0.22 → 0.0.24

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.
@@ -3,9 +3,11 @@ import path from 'node:path';
3
3
  import { writeJsonArtifact } from './artifacts.js';
4
4
  import { CliError } from './errors.js';
5
5
  import { withStateFileLock } from './state-lock.js';
6
- import { appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, resolveMaintenancePlanArtifactPath, sha256Json, snapshotRemoteRow, writeImmutableJson, } from './dataset-maintenance-contract.js';
6
+ import { MAINTENANCE_SCAN_TABLES, appendStableJsonLine, isJsonObject, maintenanceRowKey, parseMaintenancePlan, readJsonFile, readJsonLinesIfPresent, resolveMaintenancePlanArtifactPath, sha256Json, snapshotRemoteRow, writeImmutableJson, } from './dataset-maintenance-contract.js';
7
+ import { buildDerivativePlanRequest, derivativePlanAction, parseDerivativeSnapshotResponse, parseDerivativeSubmitResponse, } from './dataset-maintenance-derivatives.js';
7
8
  import { maintenanceProjectedReferenceFingerprint } from './dataset-maintenance-plan.js';
8
- import { applyMaintenanceAliasPlan, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
9
+ import { isSnapshotCompletenessCompatible } from './dataset-maintenance-pagination.js';
10
+ import { applyMaintenanceAliasPlan, applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
9
11
  const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
10
12
  function clock(options) {
11
13
  return (options.now ?? new Date()).toISOString();
@@ -100,6 +102,98 @@ function parseProgress(plan, progressPath) {
100
102
  }
101
103
  return { entries, successes, latestFailures };
102
104
  }
105
+ function derivativeProofIdentity(proof) {
106
+ return sha256Json({
107
+ schema_version: proof.schema_version,
108
+ plan_sha256: proof.plan_sha256,
109
+ operation_id: proof.operation_id,
110
+ target_visibility: proof.target_visibility,
111
+ plan_request_sha256: proof.plan_request_sha256,
112
+ action_count: proof.action_count,
113
+ accepted_count: proof.accepted_count,
114
+ summary_audit_id: proof.summary_audit_id,
115
+ request_id: proof.request_id,
116
+ action_request_sha256: proof.action_request_sha256,
117
+ database_audit_id: proof.database_audit_id,
118
+ });
119
+ }
120
+ function parseDerivativeSubmitProgress(plan, progressPath) {
121
+ const action = derivativePlanAction(plan);
122
+ const entries = readJsonLinesIfPresent(progressPath).map((value) => {
123
+ const rawProof = isJsonObject(value) && isJsonObject(value.proof) ? value.proof : null;
124
+ let proof;
125
+ try {
126
+ proof = parseDerivativeSubmitResponse(rawProof
127
+ ? {
128
+ ok: true,
129
+ command: 'cmd_dataset_derivative_rebuild_plan_guarded',
130
+ ...rawProof,
131
+ }
132
+ : null, plan);
133
+ }
134
+ catch (error) {
135
+ throw new CliError('Derivative submit progress contains an invalid RPC proof.', {
136
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
137
+ exitCode: 1,
138
+ details: errorMessage(error),
139
+ });
140
+ }
141
+ if (!isJsonObject(value) ||
142
+ value.schema_version !== 1 ||
143
+ value.plan_sha256 !== plan.plan_sha256 ||
144
+ value.operation_id !== plan.operation_id ||
145
+ value.action_id !== action.action_id ||
146
+ value.target_mode !== 'owner_draft' ||
147
+ !isJsonObject(value.actor) ||
148
+ value.actor.user_id !== plan.account.user_id ||
149
+ value.actor.email !== plan.account.email ||
150
+ typeof value.started_at_utc !== 'string' ||
151
+ typeof value.ended_at_utc !== 'string' ||
152
+ value.result !== 'accepted') {
153
+ throw new CliError('Derivative submit progress contains an invalid or foreign entry.', {
154
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
155
+ exitCode: 1,
156
+ details: value,
157
+ });
158
+ }
159
+ return { ...value, proof };
160
+ });
161
+ const identities = new Set(entries.map((entry) => derivativeProofIdentity(entry.proof)));
162
+ if (identities.size > 1) {
163
+ throw new CliError('Derivative submit replays do not identify one durable request.', {
164
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_PROGRESS_INVALID',
165
+ exitCode: 1,
166
+ });
167
+ }
168
+ return { entries, latest: entries.at(-1) ?? null };
169
+ }
170
+ function validateDerivativeAdmissionAttempt(options) {
171
+ if (!existsSync(options.path))
172
+ return null;
173
+ const value = readJsonFile(options.path, 'Derivative admission attempt');
174
+ const action = derivativePlanAction(options.plan);
175
+ if (!isJsonObject(value) ||
176
+ value.schema_version !== 1 ||
177
+ value.plan_sha256 !== options.plan.plan_sha256 ||
178
+ value.operation_id !== options.plan.operation_id ||
179
+ value.action_id !== action.action_id ||
180
+ value.table !== 'processes' ||
181
+ value.id !== action.id ||
182
+ value.version !== action.version ||
183
+ value.expected_snapshot_sha256 !== action.derivative_before?.snapshot_sha256 ||
184
+ !isJsonObject(value.actor) ||
185
+ value.actor.user_id !== options.context.account.user_id ||
186
+ value.actor.email !== options.context.account.email ||
187
+ typeof value.prepared_at_utc !== 'string' ||
188
+ !Number.isFinite(Date.parse(value.prepared_at_utc))) {
189
+ throw new CliError('Derivative admission attempt is invalid or belongs to another plan.', {
190
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_ATTEMPT_INVALID',
191
+ exitCode: 1,
192
+ details: value,
193
+ });
194
+ }
195
+ return value;
196
+ }
103
197
  function parseAliasBatchProgress(plan, progressPath) {
104
198
  const batches = new Map(plan.alias_batches.map((batch) => [batch.batch_id, batch]));
105
199
  const entries = readJsonLinesIfPresent(progressPath).map((value) => {
@@ -390,7 +484,8 @@ function validateApprovalRecord(options) {
390
484
  record.target_mode !== options.plan.target_mode ||
391
485
  !isJsonObject(record.account) ||
392
486
  record.account.user_id !== options.context.account.user_id ||
393
- record.account.email !== options.context.account.email) {
487
+ record.account.email !== options.context.account.email ||
488
+ !isSnapshotCompletenessCompatible(record.snapshot_completeness, options.plan.snapshot_completeness, MAINTENANCE_SCAN_TABLES)) {
394
489
  throw new CliError('Existing approval record does not match this plan and actor.', {
395
490
  code: 'DATASET_MAINTENANCE_APPROVAL_RECORD_MISMATCH',
396
491
  exitCode: 1,
@@ -1037,6 +1132,12 @@ function appendAliasPlanFailure(options) {
1037
1132
  return entry;
1038
1133
  }
1039
1134
  async function executeAction(options) {
1135
+ if (options.action.action === 'rebuild_derivatives') {
1136
+ throw new CliError('Derivative rebuild actions may only execute through the guarded whole-plan RPC.', {
1137
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_SEQUENTIAL_WRITE_FORBIDDEN',
1138
+ exitCode: 1,
1139
+ });
1140
+ }
1040
1141
  if (!options.action.before) {
1041
1142
  throw new CliError(`Action lacks a before snapshot: ${options.action.action_id}`, {
1042
1143
  code: 'DATASET_MAINTENANCE_PLAN_INVALID',
@@ -1112,6 +1213,12 @@ async function executeAction(options) {
1112
1213
  exitCode: 1,
1113
1214
  });
1114
1215
  }
1216
+ if (options.action.action !== 'delete') {
1217
+ throw new CliError(`Unsupported maintenance action: ${options.action.action}`, {
1218
+ code: 'DATASET_MAINTENANCE_ACTION_UNSUPPORTED',
1219
+ exitCode: 2,
1220
+ });
1221
+ }
1115
1222
  const remoteResult = await deleteMaintenanceRow({
1116
1223
  context: options.context,
1117
1224
  table: options.action.table,
@@ -1133,6 +1240,60 @@ async function executeAction(options) {
1133
1240
  }
1134
1241
  return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
1135
1242
  }
1243
+ async function executeDerivativeAdmission(options) {
1244
+ const action = derivativePlanAction(options.plan);
1245
+ const plannedSnapshot = action.derivative_before;
1246
+ const preflight = parseDerivativeSnapshotResponse(await fetchMaintenanceDerivativeSnapshot({
1247
+ context: options.context,
1248
+ id: action.id,
1249
+ version: action.version,
1250
+ }), { id: action.id, version: action.version, userId: action.expected_user_id });
1251
+ if (preflight.modified_at !== plannedSnapshot.modified_at ||
1252
+ preflight.json_sha256 !== plannedSnapshot.json_sha256 ||
1253
+ preflight.json_ordered_sha256 !== plannedSnapshot.json_ordered_sha256 ||
1254
+ preflight.extracted_text_sha256 !== plannedSnapshot.extracted_text_sha256) {
1255
+ throw new CliError('Derivative action primary preconditions drifted after planning.', {
1256
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_PRIMARY_DRIFT',
1257
+ exitCode: 1,
1258
+ details: {
1259
+ expected_snapshot_sha256: plannedSnapshot.snapshot_sha256,
1260
+ actual_snapshot_sha256: preflight.snapshot_sha256,
1261
+ },
1262
+ });
1263
+ }
1264
+ if (!options.replayPossible && preflight.snapshot_sha256 !== plannedSnapshot.snapshot_sha256) {
1265
+ throw new CliError('Derivative action-scoped snapshot drifted before first admission.', {
1266
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_SNAPSHOT_DRIFT',
1267
+ exitCode: 1,
1268
+ details: {
1269
+ expected: plannedSnapshot.snapshot_sha256,
1270
+ actual: preflight.snapshot_sha256,
1271
+ },
1272
+ });
1273
+ }
1274
+ if (!options.replayPossible) {
1275
+ writeImmutableJson(options.attemptPath, {
1276
+ schema_version: 1,
1277
+ plan_sha256: options.plan.plan_sha256,
1278
+ operation_id: options.plan.operation_id,
1279
+ action_id: action.action_id,
1280
+ table: 'processes',
1281
+ id: action.id,
1282
+ version: action.version,
1283
+ expected_snapshot_sha256: plannedSnapshot.snapshot_sha256,
1284
+ actor: {
1285
+ user_id: options.context.account.user_id,
1286
+ email: options.context.account.email,
1287
+ },
1288
+ prepared_at_utc: options.preparedAtUtc,
1289
+ });
1290
+ }
1291
+ const result = await applyMaintenanceDerivativeRebuild({
1292
+ context: options.context,
1293
+ plan: buildDerivativePlanRequest(options.plan),
1294
+ });
1295
+ return parseDerivativeSubmitResponse(result, options.plan);
1296
+ }
1136
1297
  function nextAttemptPath(planDir) {
1137
1298
  let attempt = 1;
1138
1299
  while (existsSync(path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`))) {
@@ -1171,7 +1332,9 @@ export async function runDatasetMaintenanceApply(options) {
1171
1332
  exitCode: 1,
1172
1333
  });
1173
1334
  }
1174
- const progressPath = path.join(planDir, 'apply-progress.jsonl');
1335
+ const progressPath = path.join(planDir, plan.operation === 'rebuild-derivatives'
1336
+ ? 'derivative-submit-progress.jsonl'
1337
+ : 'apply-progress.jsonl');
1175
1338
  const aliasPlanProgressPath = path.join(planDir, 'alias-plan-progress.jsonl');
1176
1339
  const aliasBatchProgressPath = path.join(planDir, 'alias-batch-progress.jsonl');
1177
1340
  const aliasExchangeProgressPath = path.join(planDir, 'alias-exchange-progress.jsonl');
@@ -1195,7 +1358,12 @@ export async function runDatasetMaintenanceApply(options) {
1195
1358
  exitCode: 2,
1196
1359
  });
1197
1360
  }
1198
- const progress = parseProgress(plan, progressPath);
1361
+ const derivativeProgress = plan.operation === 'rebuild-derivatives'
1362
+ ? parseDerivativeSubmitProgress(plan, progressPath)
1363
+ : { entries: [], latest: null };
1364
+ const progress = plan.operation === 'rebuild-derivatives'
1365
+ ? { entries: [], successes: new Map(), latestFailures: new Map() }
1366
+ : parseProgress(plan, progressPath);
1199
1367
  let resumedSuccesses = progress.successes.size;
1200
1368
  const aliasPlanProgress = plan.operation === 'merge-support-aliases'
1201
1369
  ? parseAliasPlanProgress(plan, aliasPlanProgressPath)
@@ -1218,8 +1386,9 @@ export async function runDatasetMaintenanceApply(options) {
1218
1386
  await assertAliasSupportSnapshots({ plan, context });
1219
1387
  }
1220
1388
  const approvalPath = path.join(planDir, 'approval-record.json');
1389
+ const approvalAlreadyExisted = existsSync(approvalPath);
1221
1390
  validateApprovalRecord({ path: approvalPath, plan, context });
1222
- if (!existsSync(approvalPath)) {
1391
+ if (!approvalAlreadyExisted) {
1223
1392
  writeImmutableJson(approvalPath, {
1224
1393
  schema_version: 1,
1225
1394
  approved_at_utc: clock(options),
@@ -1235,11 +1404,106 @@ export async function runDatasetMaintenanceApply(options) {
1235
1404
  },
1236
1405
  confirmed_email: options.confirm,
1237
1406
  row_counts: plan.summary,
1407
+ snapshot_completeness: current.completeness,
1238
1408
  redo_rows_ready: plan.operation === 'redo-import'
1239
1409
  ? Boolean(plan.source_import_run_id || plan.source_lineage !== null)
1240
1410
  : null,
1241
1411
  });
1242
1412
  }
1413
+ if (plan.operation === 'rebuild-derivatives') {
1414
+ const startedAt = clock(options);
1415
+ const derivativeAttemptPath = path.join(planDir, 'derivative-admission-attempt.json');
1416
+ const derivativeAttempt = validateDerivativeAdmissionAttempt({
1417
+ path: derivativeAttemptPath,
1418
+ plan,
1419
+ context,
1420
+ });
1421
+ const proof = await executeDerivativeAdmission({
1422
+ plan,
1423
+ context,
1424
+ replayPossible: derivativeAttempt !== null,
1425
+ attemptPath: derivativeAttemptPath,
1426
+ preparedAtUtc: startedAt,
1427
+ });
1428
+ if (derivativeProgress.latest &&
1429
+ derivativeProofIdentity(derivativeProgress.latest.proof) !==
1430
+ derivativeProofIdentity(proof)) {
1431
+ throw new CliError('Derivative guarded-RPC replay returned a different request proof.', {
1432
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_REPLAY_MISMATCH',
1433
+ exitCode: 1,
1434
+ });
1435
+ }
1436
+ const action = derivativePlanAction(plan);
1437
+ const entry = {
1438
+ schema_version: 1,
1439
+ plan_sha256: plan.plan_sha256,
1440
+ operation_id: plan.operation_id,
1441
+ action_id: action.action_id,
1442
+ target_mode: 'owner_draft',
1443
+ actor: { user_id: context.account.user_id, email: context.account.email },
1444
+ started_at_utc: startedAt,
1445
+ ended_at_utc: clock(options),
1446
+ result: 'accepted',
1447
+ proof,
1448
+ };
1449
+ appendStableJsonLine(progressPath, entry);
1450
+ const attemptPath = nextAttemptPath(planDir);
1451
+ const report = {
1452
+ schema_version: 1,
1453
+ generated_at_utc: clock(options),
1454
+ status: 'accepted',
1455
+ task_id: plan.task_id,
1456
+ operation: plan.operation,
1457
+ operation_id: plan.operation_id,
1458
+ target_mode: plan.target_mode,
1459
+ plan_sha256: plan.plan_sha256,
1460
+ actor: { user_id: context.account.user_id, email: context.account.email },
1461
+ summary: {
1462
+ actions: 1,
1463
+ success: 0,
1464
+ failed: 0,
1465
+ pending: 1,
1466
+ resumed_successes: 0,
1467
+ accepted: 1,
1468
+ },
1469
+ actions: [
1470
+ {
1471
+ action_id: action.action_id,
1472
+ action: action.action,
1473
+ table: action.table,
1474
+ id: action.id,
1475
+ version: action.version,
1476
+ status: 'accepted',
1477
+ error: null,
1478
+ },
1479
+ ],
1480
+ artifacts: {
1481
+ approval_record: approvalPath,
1482
+ apply_progress: progressPath,
1483
+ derivative_submit_progress: progressPath,
1484
+ derivative_admission_attempt: derivativeAttemptPath,
1485
+ commit_report: path.join(planDir, 'commit-report.json'),
1486
+ attempt_report: attemptPath,
1487
+ },
1488
+ database_audit: {
1489
+ rpc_transaction_log: 'public.command_audit_log',
1490
+ source: 'tiangong-lca dataset maintenance apply',
1491
+ correlation_fields: [
1492
+ 'plan_sha256',
1493
+ 'operation_id',
1494
+ 'action_id',
1495
+ 'target_visibility',
1496
+ 'plan_request_sha256',
1497
+ 'action_request_sha256',
1498
+ 'request_id',
1499
+ ],
1500
+ },
1501
+ derivative_admission: { ...proof, admission: 'accepted' },
1502
+ };
1503
+ writeImmutableJson(attemptPath, report);
1504
+ writeJsonArtifact(report.artifacts.commit_report, report);
1505
+ return report;
1506
+ }
1243
1507
  if (plan.operation === 'merge-support-aliases') {
1244
1508
  const alreadyComplete = Boolean(aliasPlanProgress.success &&
1245
1509
  aliasPlanDerivedLogsComplete({
@@ -1378,6 +1642,7 @@ export async function runDatasetMaintenanceApply(options) {
1378
1642
  const rank = {
1379
1643
  save_draft: 0,
1380
1644
  update_json_ordered: 0,
1645
+ rebuild_derivatives: 0,
1381
1646
  delete: 1,
1382
1647
  };
1383
1648
  const actionOrder = rank[left.action] - rank[right.action];
@@ -1522,11 +1787,14 @@ export const __testInternals = {
1522
1787
  buildAliasPlanRequest,
1523
1788
  clock,
1524
1789
  errorMessage,
1790
+ executeDerivativeAdmission,
1525
1791
  executeAliasPlan,
1526
1792
  executeAction,
1527
1793
  finalProjectedRows,
1528
1794
  loadDesiredPayload,
1529
1795
  nextAttemptPath,
1796
+ parseDerivativeSubmitProgress,
1797
+ validateDerivativeAdmissionAttempt,
1530
1798
  parseAliasBatchProgress,
1531
1799
  parseAliasPlanProgress,
1532
1800
  parseProgress,