@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.
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { inspectMaintenanceSupportPayload } from './dataset-maintenance-support-validation.js';
5
+ import { isDatasetMaintenanceSnapshotCompleteness, } from './dataset-maintenance-pagination.js';
5
6
  import { CliError } from './errors.js';
6
7
  export const MAINTENANCE_MUTABLE_TABLES = ['contacts', 'sources', 'flows', 'processes'];
7
8
  export const MAINTENANCE_SUPPORT_TABLES = ['unitgroups', 'flowproperties'];
@@ -10,6 +11,7 @@ export const MAINTENANCE_SCAN_TABLES = [
10
11
  'lifecyclemodels',
11
12
  ...MAINTENANCE_SUPPORT_TABLES,
12
13
  ];
14
+ export const DERIVATIVE_REBUILD_COMPONENTS = ['extracted_md', 'embedding_ft'];
13
15
  function token(value) {
14
16
  if (typeof value !== 'string') {
15
17
  return null;
@@ -158,16 +160,19 @@ function parseAction(value, index, accountUserId, operation) {
158
160
  }
159
161
  const action = requireToken(value.action, `actions[${index}].action`);
160
162
  const table = requireToken(value.table, `actions[${index}].table`);
161
- if (!['save_draft', 'delete', 'update_json_ordered'].includes(action)) {
163
+ if (!['save_draft', 'delete', 'update_json_ordered', 'rebuild_derivatives'].includes(action)) {
162
164
  throw new CliError(`Unsupported maintenance action: ${action}`, {
163
165
  code: 'DATASET_MAINTENANCE_ACTION_UNSUPPORTED',
164
166
  exitCode: 2,
165
167
  });
166
168
  }
167
169
  const aliasAction = action === 'update_json_ordered';
168
- const allowedTable = aliasAction
169
- ? ['flowproperties', 'flows', 'processes'].includes(table)
170
- : MAINTENANCE_MUTABLE_TABLES.includes(table);
170
+ const derivativeAction = action === 'rebuild_derivatives';
171
+ const allowedTable = derivativeAction
172
+ ? table === 'processes'
173
+ : aliasAction
174
+ ? ['flowproperties', 'flows', 'processes'].includes(table)
175
+ : MAINTENANCE_MUTABLE_TABLES.includes(table);
171
176
  if (!allowedTable) {
172
177
  throw new CliError(`Maintenance cannot mutate protected or unsupported dataset table: ${table}`, {
173
178
  code: 'DATASET_MAINTENANCE_TABLE_PROTECTED',
@@ -175,7 +180,9 @@ function parseAction(value, index, accountUserId, operation) {
175
180
  });
176
181
  }
177
182
  if ((operation === 'merge-support-aliases' && !aliasAction) ||
178
- (operation !== 'merge-support-aliases' && aliasAction)) {
183
+ (operation === 'rebuild-derivatives' && !derivativeAction) ||
184
+ (!['merge-support-aliases', 'rebuild-derivatives'].includes(operation) &&
185
+ (aliasAction || derivativeAction))) {
179
186
  throw new CliError(`Maintenance operation ${operation} cannot contain ${action} actions.`, {
180
187
  code: 'DATASET_MAINTENANCE_OPERATION_ACTION_MISMATCH',
181
188
  exitCode: 2,
@@ -239,6 +246,23 @@ function parseAction(value, index, accountUserId, operation) {
239
246
  exitCode: 2,
240
247
  });
241
248
  }
249
+ const components = Array.isArray(value.components)
250
+ ? value.components.map((component, componentIndex) => requireToken(component, `actions[${index}].components[${componentIndex}]`))
251
+ : [];
252
+ if (derivativeAction &&
253
+ (components.length !== DERIVATIVE_REBUILD_COMPONENTS.length ||
254
+ components.some((component, componentIndex) => component !== DERIVATIVE_REBUILD_COMPONENTS[componentIndex]))) {
255
+ throw new CliError(`Derivative rebuild action ${index} requires components in exact order: ${DERIVATIVE_REBUILD_COMPONENTS.join(', ')}.`, {
256
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_COMPONENTS_INVALID',
257
+ exitCode: 2,
258
+ });
259
+ }
260
+ if (!derivativeAction && 'components' in value) {
261
+ throw new CliError(`Maintenance non-derivative action ${index} cannot include components.`, {
262
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_COMPONENTS_INVALID',
263
+ exitCode: 2,
264
+ });
265
+ }
242
266
  const exchangeKeys = new Set(exchangeInstances.map((entry) => `${entry.exchange_index}\u0000${entry.data_set_internal_id}\u0000${entry.flow_id}`));
243
267
  if (exchangeKeys.size !== exchangeInstances.length) {
244
268
  throw new CliError(`Alias action ${index} contains duplicate exchange instances.`, {
@@ -268,6 +292,7 @@ function parseAction(value, index, accountUserId, operation) {
268
292
  ...(exchangeInstances.length ? { exchange_instances: exchangeInstances } : {}),
269
293
  ...(desiredPayloadPath ? { desired_payload_path: desiredPayloadPath } : {}),
270
294
  ...(expectedBeforeSha256 ? { expected_before_sha256: expectedBeforeSha256 } : {}),
295
+ ...(derivativeAction ? { components: [...DERIVATIVE_REBUILD_COMPONENTS] } : {}),
271
296
  };
272
297
  }
273
298
  export function parseMaintenanceScope(value, expectedOperation) {
@@ -278,7 +303,14 @@ export function parseMaintenanceScope(value, expectedOperation) {
278
303
  });
279
304
  }
280
305
  const operation = requireToken(value.operation, 'operation');
281
- if (!['delete', 'retire', 'redo-import', 'repair-references', 'merge-support-aliases'].includes(operation)) {
306
+ if (![
307
+ 'delete',
308
+ 'retire',
309
+ 'redo-import',
310
+ 'repair-references',
311
+ 'merge-support-aliases',
312
+ 'rebuild-derivatives',
313
+ ].includes(operation)) {
282
314
  throw new CliError(`Unsupported maintenance operation: ${operation}`, {
283
315
  code: 'DATASET_MAINTENANCE_OPERATION_UNSUPPORTED',
284
316
  exitCode: 2,
@@ -299,9 +331,10 @@ export function parseMaintenanceScope(value, expectedOperation) {
299
331
  }
300
332
  const actions = value.actions.map((entry, index) => parseAction(entry, index, userId, operation));
301
333
  const targetMode = token(value.target_mode);
302
- if ((operation === 'merge-support-aliases' && targetMode !== 'owner_draft') ||
303
- (operation !== 'merge-support-aliases' && targetMode !== null)) {
304
- throw new CliError('merge-support-aliases requires target_mode=owner_draft; other operations forbid target_mode.', {
334
+ if ((['merge-support-aliases', 'rebuild-derivatives'].includes(operation) &&
335
+ targetMode !== 'owner_draft') ||
336
+ (!['merge-support-aliases', 'rebuild-derivatives'].includes(operation) && targetMode !== null)) {
337
+ throw new CliError('merge-support-aliases and rebuild-derivatives require target_mode=owner_draft; other operations forbid target_mode.', {
305
338
  code: 'DATASET_MAINTENANCE_TARGET_MODE_INVALID',
306
339
  exitCode: 2,
307
340
  details: { operation, target_mode: targetMode },
@@ -317,6 +350,12 @@ export function parseMaintenanceScope(value, expectedOperation) {
317
350
  exitCode: 2,
318
351
  });
319
352
  }
353
+ if (operation === 'rebuild-derivatives' && actions.length !== 1) {
354
+ throw new CliError('rebuild-derivatives requires exactly one action.', {
355
+ code: 'DATASET_MAINTENANCE_DERIVATIVE_ACTION_COUNT_INVALID',
356
+ exitCode: 2,
357
+ });
358
+ }
320
359
  if (aliasBatches.length) {
321
360
  const batchIds = new Set(aliasBatches.map((batch) => batch.batch_id));
322
361
  const dimensions = new Set(aliasBatches.map((batch) => batch.dimension));
@@ -473,6 +512,32 @@ export function computePlanSha256(plan) {
473
512
  const body = { ...plan, plan_sha256: '' };
474
513
  return sha256Json(body);
475
514
  }
515
+ function validDerivativeSnapshot(value, action) {
516
+ if (!isJsonObject(value))
517
+ return false;
518
+ const requiredHashes = [
519
+ value.json_sha256,
520
+ value.json_ordered_sha256,
521
+ value.extracted_text_sha256,
522
+ value.snapshot_sha256,
523
+ ];
524
+ const nullableHashes = [value.extracted_md_sha256, value.embedding_ft_sha256];
525
+ return Boolean(value.schema_version === 'dataset-derivative-snapshot.v1' &&
526
+ value.table === 'processes' &&
527
+ value.id === action.id &&
528
+ value.version === action.version &&
529
+ value.user_id === action.expected_user_id &&
530
+ value.state_code === 0 &&
531
+ value.modified_at === action.before?.modified_at &&
532
+ typeof value.modified_at === 'string' &&
533
+ Number.isFinite(Date.parse(value.modified_at)) &&
534
+ requiredHashes.every((hash) => typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash)) &&
535
+ nullableHashes.every((hash) => hash === null || (typeof hash === 'string' && /^[a-f0-9]{64}$/u.test(hash))) &&
536
+ value.json_sha256 === value.json_ordered_sha256 &&
537
+ (value.embedding_ft_at === null ||
538
+ (typeof value.embedding_ft_at === 'string' &&
539
+ Number.isFinite(Date.parse(value.embedding_ft_at)))));
540
+ }
476
541
  export function parseMaintenancePlan(value) {
477
542
  if (!isJsonObject(value) ||
478
543
  value.schema_version !== 1 ||
@@ -487,6 +552,13 @@ export function parseMaintenancePlan(value) {
487
552
  });
488
553
  }
489
554
  const plan = value;
555
+ if (plan.snapshot_completeness !== undefined &&
556
+ !isDatasetMaintenanceSnapshotCompleteness(plan.snapshot_completeness, MAINTENANCE_SCAN_TABLES)) {
557
+ throw new CliError('Maintenance plan snapshot completeness proof is invalid.', {
558
+ code: 'DATASET_MAINTENANCE_PLAN_INVALID',
559
+ exitCode: 2,
560
+ });
561
+ }
490
562
  const expected = computePlanSha256(plan);
491
563
  if (plan.plan_sha256 !== expected) {
492
564
  throw new CliError('Maintenance plan hash does not match its canonical contents.', {
@@ -585,13 +657,23 @@ export function parseMaintenancePlan(value) {
585
657
  ? 'save_before_snapshot'
586
658
  : action.action === 'delete'
587
659
  ? 'restore_deleted_before_snapshot'
588
- : 'restore_atomic_alias_before_snapshot';
589
- if (action.rollback.strategy !== expectedRollbackStrategy ||
590
- action.rollback.before_payload_sha256 !== before.payload_sha256 ||
591
- !isJsonObject(action.rollback.before_payload) ||
592
- sha256Json(action.rollback.before_payload) !== before.payload_sha256 ||
593
- action.rollback.model_id !== before.model_id ||
594
- action.rollback.rule_verification !== before.rule_verification) {
660
+ : action.action === 'update_json_ordered'
661
+ ? 'restore_atomic_alias_before_snapshot'
662
+ : 'none_derivative_only';
663
+ const derivativeRollback = action.action === 'rebuild_derivatives';
664
+ const rollbackMatches = derivativeRollback
665
+ ? action.rollback.strategy === expectedRollbackStrategy &&
666
+ action.rollback.before_payload_sha256 === null &&
667
+ action.rollback.before_payload === null &&
668
+ action.rollback.model_id === null &&
669
+ action.rollback.rule_verification === null
670
+ : action.rollback.strategy === expectedRollbackStrategy &&
671
+ action.rollback.before_payload_sha256 === before.payload_sha256 &&
672
+ isJsonObject(action.rollback.before_payload) &&
673
+ sha256Json(action.rollback.before_payload) === before.payload_sha256 &&
674
+ action.rollback.model_id === before.model_id &&
675
+ action.rollback.rule_verification === before.rule_verification;
676
+ if (!rollbackMatches) {
595
677
  throw new CliError('Ready maintenance plan action rollback snapshot is invalid.', {
596
678
  code: 'DATASET_MAINTENANCE_PLAN_INVALID',
597
679
  exitCode: 2,
@@ -621,6 +703,8 @@ export function parseMaintenancePlan(value) {
621
703
  plan.summary.delete === plan.actions.filter((action) => action.action === 'delete').length &&
622
704
  (plan.summary.update_json_ordered ?? 0) ===
623
705
  plan.actions.filter((action) => action.action === 'update_json_ordered').length &&
706
+ (plan.summary.rebuild_derivatives ?? 0) ===
707
+ plan.actions.filter((action) => action.action === 'rebuild_derivatives').length &&
624
708
  (plan.summary.atomic_batches ?? 0) === (plan.alias_batches?.length ?? 0) &&
625
709
  (plan.summary.scaled_exchanges ?? 0) ===
626
710
  (plan.alias_batches?.reduce((sum, batch) => sum + batch.summary.exchanges, 0) ?? 0) &&
@@ -831,6 +915,32 @@ export function parseMaintenancePlan(value) {
831
915
  });
832
916
  }
833
917
  }
918
+ if (plan.operation === 'rebuild-derivatives') {
919
+ const action = plan.actions[0];
920
+ const validDerivativePlan = Boolean(plan.target_mode === 'owner_draft' &&
921
+ plan.actions.length === 1 &&
922
+ !plan.alias_batches &&
923
+ plan.artifacts.derivative_baseline === 'derivative-baseline.json' &&
924
+ action?.action === 'rebuild_derivatives' &&
925
+ action.table === 'processes' &&
926
+ sha256Json(action.components) === sha256Json(DERIVATIVE_REBUILD_COMPONENTS) &&
927
+ action.desired_payload === null &&
928
+ action.rollback.strategy === 'none_derivative_only' &&
929
+ (action.status === 'blocked' || validDerivativeSnapshot(action.derivative_before, action)));
930
+ if (!validDerivativePlan) {
931
+ throw new CliError('Maintenance derivative rebuild plan contract is inconsistent.', {
932
+ code: 'DATASET_MAINTENANCE_PLAN_INVALID',
933
+ exitCode: 2,
934
+ });
935
+ }
936
+ }
937
+ else if (plan.actions.some((action) => action.derivative_before !== undefined) ||
938
+ plan.artifacts.derivative_baseline !== undefined) {
939
+ throw new CliError('Non-derivative maintenance plan contains derivative-only fields.', {
940
+ code: 'DATASET_MAINTENANCE_PLAN_INVALID',
941
+ exitCode: 2,
942
+ });
943
+ }
834
944
  return plan;
835
945
  }
836
946
  //# sourceMappingURL=dataset-maintenance-contract.js.map