@tiangong-lca/cli 0.0.23 → 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.
@@ -4,9 +4,10 @@ import { writeJsonArtifact } from './artifacts.js';
4
4
  import { CliError } from './errors.js';
5
5
  import { withStateFileLock } from './state-lock.js';
6
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
9
  import { isSnapshotCompletenessCompatible } from './dataset-maintenance-pagination.js';
9
- import { applyMaintenanceAliasPlan, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
10
+ import { applyMaintenanceAliasPlan, applyMaintenanceDerivativeRebuild, deleteMaintenanceRow, fetchMaintenanceAccountRows, fetchMaintenanceDerivativeSnapshot, fetchMaintenanceExactRows, resolveMaintenanceRemoteContext, saveDraftMaintenanceRow, } from './dataset-maintenance-remote.js';
10
11
  const POSITIVE_INTEGER_TEXT = /^[1-9]\d*$/u;
11
12
  function clock(options) {
12
13
  return (options.now ?? new Date()).toISOString();
@@ -101,6 +102,98 @@ function parseProgress(plan, progressPath) {
101
102
  }
102
103
  return { entries, successes, latestFailures };
103
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
+ }
104
197
  function parseAliasBatchProgress(plan, progressPath) {
105
198
  const batches = new Map(plan.alias_batches.map((batch) => [batch.batch_id, batch]));
106
199
  const entries = readJsonLinesIfPresent(progressPath).map((value) => {
@@ -1039,6 +1132,12 @@ function appendAliasPlanFailure(options) {
1039
1132
  return entry;
1040
1133
  }
1041
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
+ }
1042
1141
  if (!options.action.before) {
1043
1142
  throw new CliError(`Action lacks a before snapshot: ${options.action.action_id}`, {
1044
1143
  code: 'DATASET_MAINTENANCE_PLAN_INVALID',
@@ -1114,6 +1213,12 @@ async function executeAction(options) {
1114
1213
  exitCode: 1,
1115
1214
  });
1116
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
+ }
1117
1222
  const remoteResult = await deleteMaintenanceRow({
1118
1223
  context: options.context,
1119
1224
  table: options.action.table,
@@ -1135,6 +1240,60 @@ async function executeAction(options) {
1135
1240
  }
1136
1241
  return { afterSha256: null, remoteResultSha256: sha256Json(remoteResult) };
1137
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
+ }
1138
1297
  function nextAttemptPath(planDir) {
1139
1298
  let attempt = 1;
1140
1299
  while (existsSync(path.join(planDir, `commit-report.attempt-${String(attempt).padStart(4, '0')}.json`))) {
@@ -1173,7 +1332,9 @@ export async function runDatasetMaintenanceApply(options) {
1173
1332
  exitCode: 1,
1174
1333
  });
1175
1334
  }
1176
- 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');
1177
1338
  const aliasPlanProgressPath = path.join(planDir, 'alias-plan-progress.jsonl');
1178
1339
  const aliasBatchProgressPath = path.join(planDir, 'alias-batch-progress.jsonl');
1179
1340
  const aliasExchangeProgressPath = path.join(planDir, 'alias-exchange-progress.jsonl');
@@ -1197,7 +1358,12 @@ export async function runDatasetMaintenanceApply(options) {
1197
1358
  exitCode: 2,
1198
1359
  });
1199
1360
  }
1200
- 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);
1201
1367
  let resumedSuccesses = progress.successes.size;
1202
1368
  const aliasPlanProgress = plan.operation === 'merge-support-aliases'
1203
1369
  ? parseAliasPlanProgress(plan, aliasPlanProgressPath)
@@ -1220,8 +1386,9 @@ export async function runDatasetMaintenanceApply(options) {
1220
1386
  await assertAliasSupportSnapshots({ plan, context });
1221
1387
  }
1222
1388
  const approvalPath = path.join(planDir, 'approval-record.json');
1389
+ const approvalAlreadyExisted = existsSync(approvalPath);
1223
1390
  validateApprovalRecord({ path: approvalPath, plan, context });
1224
- if (!existsSync(approvalPath)) {
1391
+ if (!approvalAlreadyExisted) {
1225
1392
  writeImmutableJson(approvalPath, {
1226
1393
  schema_version: 1,
1227
1394
  approved_at_utc: clock(options),
@@ -1243,6 +1410,100 @@ export async function runDatasetMaintenanceApply(options) {
1243
1410
  : null,
1244
1411
  });
1245
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
+ }
1246
1507
  if (plan.operation === 'merge-support-aliases') {
1247
1508
  const alreadyComplete = Boolean(aliasPlanProgress.success &&
1248
1509
  aliasPlanDerivedLogsComplete({
@@ -1381,6 +1642,7 @@ export async function runDatasetMaintenanceApply(options) {
1381
1642
  const rank = {
1382
1643
  save_draft: 0,
1383
1644
  update_json_ordered: 0,
1645
+ rebuild_derivatives: 0,
1384
1646
  delete: 1,
1385
1647
  };
1386
1648
  const actionOrder = rank[left.action] - rank[right.action];
@@ -1525,11 +1787,14 @@ export const __testInternals = {
1525
1787
  buildAliasPlanRequest,
1526
1788
  clock,
1527
1789
  errorMessage,
1790
+ executeDerivativeAdmission,
1528
1791
  executeAliasPlan,
1529
1792
  executeAction,
1530
1793
  finalProjectedRows,
1531
1794
  loadDesiredPayload,
1532
1795
  nextAttemptPath,
1796
+ parseDerivativeSubmitProgress,
1797
+ validateDerivativeAdmissionAttempt,
1533
1798
  parseAliasBatchProgress,
1534
1799
  parseAliasPlanProgress,
1535
1800
  parseProgress,