@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 CHANGED
@@ -17,14 +17,18 @@ checkPaths:
17
17
  - bin/**
18
18
  - src/cli.ts
19
19
  - src/main.ts
20
- lastReviewedAt: 2026-06-29
21
- lastReviewedCommit: 695e6d6fe718cb92d499f3ce8be2dc24c3f6ce29
20
+ lastReviewedAt: 2026-07-13
21
+ lastReviewedCommit: 4c79df4623e3cf296bc8d1baeea688d78351570a
22
22
  ---
23
23
 
24
24
  # TianGong LCA CLI
25
25
 
26
26
  Package: `@tiangong-lca/cli` Executable: `tiangong-lca` Node: `24.x`
27
27
 
28
+ Review note, 2026-07-12: `dataset maintenance plan/apply/verify` provides current-user RLS-scoped exact-row maintenance with immutable plans, explicit approval, per-action logs, platform audit correlation, and independent readback. `merge-support-aliases` now runs only in `target_mode=owner_draft`: source/target support and all changed rows stay private `state_code=0`; publication is a separate future workflow.
29
+
30
+ Review note, 2026-07-13: maintenance scans now prove exact-count pagination even when PostgREST returns fewer rows than the requested `--page-size`. An incomplete or inconsistent scan fails before artifacts, approval, or mutation; under stable filtered membership/order the proof represents a complete ordered multi-request traversal, not one transaction-level/MVCC snapshot.
31
+
28
32
  ## Run
29
33
 
30
34
  One-off published run:
@@ -273,7 +277,9 @@ tiangong-lca dataset curation-queue verify --queue-dir /abs/path/to/curation-que
273
277
  tiangong-lca dataset evidence-search plan --query "中国2026年电力结构数据" --out-dir /abs/path/to/evidence-search --json
274
278
  tiangong-lca dataset evidence-search run --input ./evidence-search.request.json --results ./search-results.json --out-dir /abs/path/to/evidence-search --json
275
279
  tiangong-lca dataset references rewrite --input ./rows.jsonl --from flow:<old-id>@<old-version> --to flow:<new-id>@<new-version> --out-dir /abs/path/to/dataset-rewrite --json
276
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir /abs/path/to/dataset-maintenance
280
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir /abs/path/to/dataset-maintenance --page-size 1000 --timeout-ms 10000 --json
281
+ tiangong-lca dataset maintenance apply --plan /abs/path/to/dataset-maintenance/maintenance-plan.json --commit --approve-plan <sha256> --confirm <current-account-email> --timeout-ms 10000 --json
282
+ tiangong-lca dataset maintenance verify --plan /abs/path/to/dataset-maintenance/maintenance-plan.json --out-dir /abs/path/to/dataset-maintenance/verify --page-size 1000 --timeout-ms 10000 --json
277
283
  tiangong-lca lifecyclemodel auto-build --input ./examples/lifecyclemodel-auto-build.request.json --out-dir /abs/path/to/lifecyclemodel-run --json
278
284
  tiangong-lca lifecyclemodel validate-build --run-dir /abs/path/to/lifecyclemodel-run --json
279
285
  tiangong-lca lifecyclemodel publish-build --run-dir /abs/path/to/lifecyclemodel-run --json
@@ -316,7 +322,58 @@ For `dataset curation-queue build/next/verify`, the CLI owns entity-level Foundr
316
322
 
317
323
  For `dataset references rewrite`, `--commit` executes the state-aware save-draft path for patched process and lifecyclemodel rows; without `--commit`, the command only writes local rewrite artifacts.
318
324
 
319
- For `dataset maintenance plan/apply/verify`, the planned command family owns RLS-scoped delete/redo workflows for bad imports. The contract requires a frozen scope manifest, current-user visible snapshot, protected rows list, reference impact report, dry-run report, explicit commit report, and readback verification. Foundry and skills may orchestrate it, but they must not add private Supabase delete logic.
325
+ ## Dataset Maintenance
326
+
327
+ `dataset maintenance plan/apply/verify` is the row-level cleanup surface for bad imports and the fixed BAFU private alias rewrite. It runs as the currently authenticated user and relies on RLS for visibility and ownership enforcement.
328
+
329
+ ```bash
330
+ tiangong-lca dataset maintenance plan \
331
+ --scope ./maintenance-scope.json \
332
+ --operation merge-support-aliases \
333
+ --out-dir ./dataset-maintenance \
334
+ --page-size 1000 \
335
+ --timeout-ms 10000 \
336
+ --json
337
+
338
+ tiangong-lca dataset maintenance apply \
339
+ --plan ./dataset-maintenance/maintenance-plan.json \
340
+ --commit \
341
+ --approve-plan <sha256> \
342
+ --confirm <current-account-email> \
343
+ --timeout-ms 10000 \
344
+ --json
345
+
346
+ tiangong-lca dataset maintenance verify \
347
+ --plan ./dataset-maintenance/maintenance-plan.json \
348
+ --out-dir ./dataset-maintenance/verify \
349
+ --page-size 1000 \
350
+ --timeout-ms 10000 \
351
+ --json
352
+ ```
353
+
354
+ `--page-size` accepts `1-5000` and is only the requested maximum. PostgREST may enforce a lower server-side cap. The CLI requests `Prefer: count=exact`, validates the exact total and returned range from each `Content-Range`, advances the next offset by the number of rows actually returned, and requires strict `id`/`version` ordering without missing or duplicate identities. Each accepted scan records per-table requested/effective page size, page count, rows fetched, exact total, and aggregate entity counts.
355
+
356
+ This completeness proof means the CLI traversed the filtered result while that table's membership and ordering keys remained stable. Because the tables are read through multiple HTTP requests, it is not a transaction-level or MVCC snapshot of one instant; same-cardinality delete/insert churn can evade total and ordering checks. Plan hashes and apply-time drift checks provide the later mutation guard, and operators must avoid concurrent maintenance of the same account while planning or clearing it.
357
+
358
+ The scope is intentionally narrow:
359
+
360
+ - Each requested row must name its table, exact `id`, exact `version`, expected current owner, and draft `state_code=0` state.
361
+ - `--operation` accepts `delete`, `retire`, `redo-import`, `repair-references`, or `merge-support-aliases`; it records the operator's maintenance intent and does not broaden the eligible row actions.
362
+ - Only current-user `contacts`, `sources`, `flows`, and `processes` can become `save_draft` or `delete` actions.
363
+ - `merge-support-aliases` requires top-level `target_mode: "owner_draft"` and accepts only two named batches, `time` and `length_time`. The scope must bind reviewed current-owner draft source and target FP/UG exact ids/versions to 52 `update_json_ordered` actions: 25 time rows (1 FP, 10 flows, 14 processes) and 27 length-time rows (1 FP, 13 flows, 13 processes). Process actions freeze every selected exchange index, internal id, flow id/version, direction, before hash, and both amount strings.
364
+ - The alias factors are exact decimal strings: `0.00011415525114155251` for time and `1000` for length-time. Planning requires exactly 20 and 39 selected exchanges, preserves exactly 309 other exchanges in the affected processes, and proves the fixed source-zero/target-reference postconditions. The transformation changes references and the selected `meanAmount`/`resultingAmount`; it does not delete the source FP/UG rows.
365
+ - Source alias support, target FP/UG, and every changed flow/process must all belong to the authenticated account at `state_code=0`. Public/shared, foreign-owner, mixed-visibility, non-draft, lifecyclemodel, and unsupported action/table rows remain protected.
366
+ - The CLI classifies and executes an operator-authored scope; it does not decide whether rows are semantically duplicates, canonical replacements, or safe business-level cleanup targets.
367
+
368
+ `plan` accepts the account scan only after its exact-count proof is complete, then writes the frozen `maintenance-scope.json`, `rls-visible-snapshot.json`, `protected-rows.jsonl`, `reference-impact-report.json`, `maintenance-plan.json`, and `dry-run-report.json`. The snapshot, dry-run report, and newly generated plan carry the aggregate completeness proof, so it is bound into the plan SHA-256. Alias plans additionally write `exchange-rewrite-plan.jsonl`, freeze current-owner state-0 target FP, target UG, and source UG snapshots for each batch, derive schema-valid desired payloads with matching embedded UUID/version, and include the exact closure, `modified_at`, hashes, conversion evidence, and postconditions in the approved plan. The plan SHA-256 is the approval identity; do not edit or recompute the plan after review.
369
+
370
+ `apply` is write-disabled unless all three commit guards are present: `--commit`, `--approve-plan <sha256>`, and `--confirm <current-account-email>`. Before approval is persisted or any write runs, it requires a fresh complete exact-count account scan and re-checks the whole plan for drift; the current completeness proof is recorded in `approval-record.json`. Ordinary draft updates/deletes use their platform paths. The ordered `time` plus `length_time` alias request is sent once to `cmd_dataset_alias_plan_guarded` with `target_visibility=owner_draft`; the CLI has neither a per-dimension fallback nor a 52-write sequential fallback. The RPC locks and validates the complete 52-row/59-exchange closure before both dimensions commit, so a second-dimension failure rolls back the first. It checks actor ownership, state 0, exact payload/timestamp locks and embedded UUID/version, rejects missing or phantom flow/exchange references, and returns one plan summary audit id plus both batch and per-row audit proofs. The CLI writes `alias-plan-progress.jsonl` together with plan-bound per-batch, per-row, and per-exchange ledgers. A lost response or incomplete derived ledger is repaired only by replaying the same whole plan and matching every returned plan and batch proof.
371
+
372
+ `verify` requires another complete exact-count account readback rather than trusting the apply report, records its completeness proof, and writes `readback-verify-report.json` in its own output directory. For alias plans it also requires both successful batch records, all 52 correlated row records, all 59 unique exchange records, unchanged support snapshots, and exact desired row payloads. It validates the RPC-returned audit ids against the local proof chain; it does not independently query `public.command_audit_log`.
373
+
374
+ `dataset maintenance clear-account` uses the same exact-count rule for its initial five-table snapshot, per-table commit checks, and a final fresh scan of all five tables. It reports `cleared_account` only when that final aggregate proof exists with `row_count=0`; if the final proof fails after deletions begin, it still writes a `completed_with_failures` audit report. If the initial scan cannot prove completeness, it writes no snapshot or approval artifact and performs zero deletes.
375
+
376
+ Foundry and skills may prepare the scope, invoke these commands, and retain their artifacts. They must not replace the CLI with direct SQL, service-role access, raw REST mutation, or private Supabase delete/update code.
320
377
 
321
378
  ## More Docs
322
379
 
package/dist/src/cli.js CHANGED
@@ -54,6 +54,9 @@ import { runDatasetPatchApply, } from './lib/dataset-patch.js';
54
54
  import { runDatasetSaveDraft, } from './lib/dataset-save-draft-run.js';
55
55
  import { runDatasetClassificationApply, runDatasetClassificationAudit, runDatasetClassificationChildren, runDatasetClassificationPath, } from './lib/dataset-classification.js';
56
56
  import { runDatasetMaintenanceClearAccount, } from './lib/dataset-maintenance-clear-account.js';
57
+ import { runDatasetMaintenancePlan } from './lib/dataset-maintenance-plan.js';
58
+ import { runDatasetMaintenanceApply } from './lib/dataset-maintenance-apply.js';
59
+ import { runDatasetMaintenanceVerify } from './lib/dataset-maintenance-verify.js';
57
60
  import { runDatasetSourceUploadAttachments, } from './lib/dataset-source-upload-attachments.js';
58
61
  function renderMainHelp(dotEnvStatus) {
59
62
  return `TianGong LCA CLI
@@ -74,7 +77,7 @@ Implemented Commands:
74
77
  doctor show environment diagnostics
75
78
  search flow | process | lifecyclemodel
76
79
  process get | list | identity-preflight | build-plan | scope-statistics | dedup-review | auto-build | resume-build | publish-build | complete-required-fields | save-draft | batch-build | refresh-references | verify-rows
77
- dataset contract get | context-pack | classification children/path/audit/apply | curation-queue build/next/verify | import-lca convert | author | patch apply | save-draft | source upload-attachments | validate | verify-remote | bilingual extract/apply/validate | evidence-search plan/run | references rewrite/refresh-remote | maintenance clear-account
80
+ dataset contract get | context-pack | classification children/path/audit/apply | curation-queue build/next/verify | import-lca convert | author | patch apply | save-draft | source upload-attachments | validate | verify-remote | bilingual extract/apply/validate | evidence-search plan/run | references rewrite/refresh-remote | maintenance clear-account/plan/apply/verify
78
81
  flow get | list | identity-preflight | build-plan | fetch-rows | materialize-decisions | remediate | publish-version | publish-reviewed-data | build-alias-map | scan-process-flow-refs | plan-process-flow-repairs | apply-process-flow-repairs | regen-product | validate-processes
79
82
  lifecyclemodel auto-build | validate-build | publish-build | save-draft | graph | build-resulting-process | publish-resulting-process | orchestrate
80
83
  qa process | flow | lifecyclemodel
@@ -85,7 +88,6 @@ Implemented Commands:
85
88
  Planned Surface (not implemented yet):
86
89
  auth whoami | doctor-auth
87
90
  job get | wait | logs
88
- dataset maintenance plan | apply | verify
89
91
 
90
92
  Planned commands currently print an explicit "not implemented yet" message and exit with code 2.
91
93
 
@@ -128,7 +130,7 @@ Examples:
128
130
  tiangong-lca dataset evidence-search run --input ./evidence-search.request.json --results ./search-results.json --out-dir ./evidence-search
129
131
  tiangong-lca dataset references rewrite --input ./rows.jsonl --from flow:<old-id>@<old-version> --to flow:<new-id>@<new-version> --out-dir /abs/path/to/dataset-rewrite
130
132
  tiangong-lca dataset maintenance clear-account --out-dir /abs/path/to/account-clear --json
131
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir /abs/path/to/dataset-maintenance
133
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir /abs/path/to/dataset-maintenance
132
134
  tiangong-lca lifecyclemodel auto-build --input ./lifecyclemodel-auto-build.request.json --out-dir /abs/path/to/lifecyclemodel-run
133
135
  tiangong-lca lifecyclemodel validate-build --run-dir /abs/path/to/lifecyclemodel-run
134
136
  tiangong-lca lifecyclemodel publish-build --run-dir /abs/path/to/lifecyclemodel-run
@@ -279,9 +281,9 @@ Implemented Subcommands:
279
281
  references rewrite Rewrite flow references in local process and lifecyclemodel rows
280
282
  references refresh-remote Refresh local TIDAS reference versions to latest reachable remote rows
281
283
  maintenance clear-account Dry-run or clear current authenticated account-owned dataset rows through RLS
282
-
283
- Planned Subcommands:
284
- maintenance plan/apply/verify Plan, execute, and verify RLS-scoped dataset delete/redo maintenance
284
+ maintenance plan Build an immutable, RLS-visible row-level maintenance plan
285
+ maintenance apply Execute an explicitly approved maintenance plan through current-user RLS
286
+ maintenance verify Read back affected rows and references against the immutable plan
285
287
 
286
288
  Examples:
287
289
  tiangong-lca dataset contract get --type process --include schema,methodology,ruleset --out-dir ./contract --help
@@ -307,25 +309,23 @@ Examples:
307
309
  tiangong-lca dataset references rewrite --input ./rows.jsonl --from flow:<old-id>@<old-version> --to flow:<new-id>@<new-version> --out-dir ./dataset-rewrite --help
308
310
  tiangong-lca dataset references refresh-remote --input ./rows.jsonl --out ./rows.refreshed.jsonl --out-dir ./dataset-reference-refresh --help
309
311
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json --help
310
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir ./dataset-maintenance --help
312
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir ./dataset-maintenance --help
311
313
  `.trim();
312
314
  }
313
315
  function renderDatasetMaintenanceHelp() {
314
316
  return `Usage:
315
317
  tiangong-lca dataset maintenance <clear-account|plan|apply|verify> [options]
316
318
 
317
- Status:
318
- clear-account is implemented for current authenticated account cleanup.
319
- plan/apply/verify remain reserved for row-level RLS-scoped cleanup and redo workflows and currently exit with code 2 for executable actions.
320
-
321
- Implemented Actions:
319
+ Actions:
322
320
  clear-account Dry-run or delete current authenticated account-owned lifecyclemodels, processes, flows, sources, and contacts.
323
-
324
- Planned Actions:
325
321
  plan Build an immutable maintenance plan from a scope manifest, visible remote snapshot, dependency impact report, and intended operation.
326
322
  apply Execute an approved plan through current-user RLS and platform dataset command paths; never bypass RLS or delete rows outside the visible scope.
327
323
  verify Re-fetch affected rows and references, then prove that deleted, updated, skipped, protected, and redone rows match the plan.
328
324
 
325
+ Safety:
326
+ plan and verify are read-only.
327
+ apply is commit-only and requires --commit, the exact plan SHA-256 via --approve-plan, and the current account email via --confirm.
328
+
329
329
  Required Artifact Contract:
330
330
  - maintenance-plan.json
331
331
  - rls-visible-snapshot.json
@@ -338,11 +338,66 @@ Required Artifact Contract:
338
338
  Examples:
339
339
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json
340
340
  tiangong-lca dataset maintenance clear-account --commit --confirm user@example.com --out-dir ./account-clear
341
- tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation redo-import --out-dir ./dataset-maintenance
342
- tiangong-lca dataset maintenance apply --plan ./dataset-maintenance/maintenance-plan.json --commit
341
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir ./dataset-maintenance
342
+ tiangong-lca dataset maintenance apply --plan ./dataset-maintenance/maintenance-plan.json --commit --approve-plan <sha256> --confirm user@example.com
343
343
  tiangong-lca dataset maintenance verify --plan ./dataset-maintenance/maintenance-plan.json --out-dir ./dataset-maintenance/verify
344
344
  `.trim();
345
345
  }
346
+ function renderDatasetMaintenancePlanHelp() {
347
+ return `Usage:
348
+ tiangong-lca dataset maintenance plan --scope <file> --operation <operation> --out-dir <dir> [options]
349
+
350
+ Operations:
351
+ delete | retire | redo-import | repair-references | merge-support-aliases
352
+
353
+ Options:
354
+ --scope <file> Maintenance scope manifest
355
+ --operation <value> Intended row-level maintenance operation
356
+ --out-dir <dir> Artifact directory
357
+ --page-size <n> Requested snapshot page size, 1-5000 (default: 1000); server caps are followed using exact counts
358
+ --timeout-ms <n> Request timeout in milliseconds
359
+ --json Print compact JSON
360
+ -h, --help
361
+
362
+ Outputs written under --out-dir include the immutable maintenance-plan.json, visible snapshot,
363
+ protected-row ledger, reference-impact report, and dry-run report.
364
+ `.trim();
365
+ }
366
+ function renderDatasetMaintenanceApplyHelp() {
367
+ return `Usage:
368
+ tiangong-lca dataset maintenance apply --plan <file> --commit --approve-plan <sha256> --confirm <email> [options]
369
+
370
+ Behavior:
371
+ Commit-only. The command rejects dry-run mode, a missing --commit flag, a plan hash mismatch,
372
+ or a confirmation email that does not match the current authenticated account.
373
+
374
+ Options:
375
+ --plan <file> Immutable maintenance-plan.json
376
+ --commit Execute the approved plan
377
+ --approve-plan <sha256> Exact SHA-256 recorded for the plan
378
+ --confirm <email> Current authenticated account email
379
+ --timeout-ms <n> Request timeout in milliseconds
380
+ --json Print compact JSON
381
+ -h, --help
382
+
383
+ Outputs include approval-record.json and commit-report.json alongside an append-only action ledger.
384
+ `.trim();
385
+ }
386
+ function renderDatasetMaintenanceVerifyHelp() {
387
+ return `Usage:
388
+ tiangong-lca dataset maintenance verify --plan <file> [options]
389
+
390
+ Options:
391
+ --plan <file> Immutable maintenance-plan.json
392
+ --out-dir <dir> Optional verification artifact directory
393
+ --page-size <n> Requested readback page size, 1-5000 (default: 1000); server caps are followed using exact counts
394
+ --timeout-ms <n> Request timeout in milliseconds
395
+ --json Print compact JSON
396
+ -h, --help
397
+
398
+ Outputs include readback-verify-report.json with affected-row and reference checks.
399
+ `.trim();
400
+ }
346
401
  function renderDatasetMaintenanceClearAccountHelp() {
347
402
  return `Usage:
348
403
  tiangong-lca dataset maintenance clear-account [options]
@@ -355,7 +410,7 @@ Behavior:
355
410
  Options:
356
411
  --state-code <n> Optional repeatable state_code filter. Omit to clear all state codes for the current account.
357
412
  --out-dir <dir> Artifact directory (default: ./dataset-maintenance/clear-account)
358
- --page-size <n> Snapshot page size, 1-5000 (default: 1000)
413
+ --page-size <n> Requested snapshot page size, 1-5000 (default: 1000); server caps are followed using exact counts
359
414
  --timeout-ms <n> Request timeout in milliseconds (default: 10000)
360
415
  --commit Execute deletion. Without this flag the command only writes a dry-run report.
361
416
  --dry-run Explicit dry-run mode
@@ -369,6 +424,8 @@ Outputs written under --out-dir:
369
424
  - approval-record.json (commit only)
370
425
  - commit-report.json (commit only)
371
426
  - readback-verify-report.json (commit only)
427
+
428
+ Every snapshot/readback must prove complete exact-count pagination before an artifact or delete is accepted.
372
429
  `.trim();
373
430
  }
374
431
  function renderDatasetContractHelp() {
@@ -2966,6 +3023,130 @@ function parseDatasetMaintenanceClearAccountFlags(args) {
2966
3023
  confirm: typeof values.confirm === 'string' ? values.confirm : null,
2967
3024
  };
2968
3025
  }
3026
+ function parseDatasetMaintenancePositiveInteger(value, flagName) {
3027
+ if (typeof value !== 'string') {
3028
+ return undefined;
3029
+ }
3030
+ if (!/^\d+$/u.test(value.trim()) || Number.parseInt(value.trim(), 10) <= 0) {
3031
+ throw new CliError(`${flagName} must be a positive integer.`, {
3032
+ code: 'DATASET_MAINTENANCE_INTEGER_INVALID',
3033
+ exitCode: 2,
3034
+ details: { flag: flagName, value },
3035
+ });
3036
+ }
3037
+ return Number.parseInt(value.trim(), 10);
3038
+ }
3039
+ function parseDatasetMaintenancePlanFlags(args) {
3040
+ let values;
3041
+ try {
3042
+ ({ values } = parseArgs({
3043
+ args,
3044
+ allowPositionals: false,
3045
+ strict: true,
3046
+ options: {
3047
+ help: { type: 'boolean', short: 'h' },
3048
+ json: { type: 'boolean' },
3049
+ scope: { type: 'string' },
3050
+ operation: { type: 'string' },
3051
+ 'out-dir': { type: 'string' },
3052
+ 'page-size': { type: 'string' },
3053
+ 'timeout-ms': { type: 'string' },
3054
+ },
3055
+ }));
3056
+ }
3057
+ catch (error) {
3058
+ throw new CliError(String(error), {
3059
+ code: 'INVALID_ARGS',
3060
+ exitCode: 2,
3061
+ });
3062
+ }
3063
+ const rawOperation = typeof values.operation === 'string' ? values.operation : null;
3064
+ if (rawOperation !== null &&
3065
+ !['delete', 'retire', 'redo-import', 'repair-references', 'merge-support-aliases'].includes(rawOperation)) {
3066
+ throw new CliError("--operation must be 'delete', 'retire', 'redo-import', 'repair-references', or 'merge-support-aliases'.", {
3067
+ code: 'DATASET_MAINTENANCE_OPERATION_INVALID',
3068
+ exitCode: 2,
3069
+ details: rawOperation,
3070
+ });
3071
+ }
3072
+ return {
3073
+ help: Boolean(values.help),
3074
+ json: Boolean(values.json),
3075
+ scopePath: typeof values.scope === 'string' ? values.scope : '',
3076
+ operation: rawOperation,
3077
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : '',
3078
+ pageSize: parseDatasetMaintenancePositiveInteger(values['page-size'], '--page-size'),
3079
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3080
+ };
3081
+ }
3082
+ function parseDatasetMaintenanceApplyFlags(args) {
3083
+ let values;
3084
+ try {
3085
+ ({ values } = parseArgs({
3086
+ args,
3087
+ allowPositionals: false,
3088
+ strict: true,
3089
+ options: {
3090
+ help: { type: 'boolean', short: 'h' },
3091
+ json: { type: 'boolean' },
3092
+ plan: { type: 'string' },
3093
+ commit: { type: 'boolean' },
3094
+ 'approve-plan': { type: 'string' },
3095
+ confirm: { type: 'string' },
3096
+ 'timeout-ms': { type: 'string' },
3097
+ 'dry-run': { type: 'boolean' },
3098
+ },
3099
+ }));
3100
+ }
3101
+ catch (error) {
3102
+ throw new CliError(String(error), {
3103
+ code: 'INVALID_ARGS',
3104
+ exitCode: 2,
3105
+ });
3106
+ }
3107
+ return {
3108
+ help: Boolean(values.help),
3109
+ json: Boolean(values.json),
3110
+ planPath: typeof values.plan === 'string' ? values.plan : '',
3111
+ commit: Boolean(values.commit),
3112
+ approvePlan: typeof values['approve-plan'] === 'string' ? values['approve-plan'] : '',
3113
+ confirm: typeof values.confirm === 'string' ? values.confirm : '',
3114
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3115
+ dryRun: Boolean(values['dry-run']),
3116
+ };
3117
+ }
3118
+ function parseDatasetMaintenanceVerifyFlags(args) {
3119
+ let values;
3120
+ try {
3121
+ ({ values } = parseArgs({
3122
+ args,
3123
+ allowPositionals: false,
3124
+ strict: true,
3125
+ options: {
3126
+ help: { type: 'boolean', short: 'h' },
3127
+ json: { type: 'boolean' },
3128
+ plan: { type: 'string' },
3129
+ 'out-dir': { type: 'string' },
3130
+ 'page-size': { type: 'string' },
3131
+ 'timeout-ms': { type: 'string' },
3132
+ },
3133
+ }));
3134
+ }
3135
+ catch (error) {
3136
+ throw new CliError(String(error), {
3137
+ code: 'INVALID_ARGS',
3138
+ exitCode: 2,
3139
+ });
3140
+ }
3141
+ return {
3142
+ help: Boolean(values.help),
3143
+ json: Boolean(values.json),
3144
+ planPath: typeof values.plan === 'string' ? values.plan : '',
3145
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : undefined,
3146
+ pageSize: parseDatasetMaintenancePositiveInteger(values['page-size'], '--page-size'),
3147
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3148
+ };
3149
+ }
2969
3150
  function parseIdentityPreflightFlags(args) {
2970
3151
  let values;
2971
3152
  try {
@@ -4711,6 +4892,9 @@ export async function executeCli(argv, deps) {
4711
4892
  const datasetClassificationAuditImpl = deps.runDatasetClassificationAuditImpl ?? runDatasetClassificationAudit;
4712
4893
  const datasetClassificationApplyImpl = deps.runDatasetClassificationApplyImpl ?? runDatasetClassificationApply;
4713
4894
  const datasetMaintenanceClearAccountImpl = deps.runDatasetMaintenanceClearAccountImpl ?? runDatasetMaintenanceClearAccount;
4895
+ const datasetMaintenancePlanImpl = deps.runDatasetMaintenancePlanImpl ?? runDatasetMaintenancePlan;
4896
+ const datasetMaintenanceApplyImpl = deps.runDatasetMaintenanceApplyImpl ?? runDatasetMaintenanceApply;
4897
+ const datasetMaintenanceVerifyImpl = deps.runDatasetMaintenanceVerifyImpl ?? runDatasetMaintenanceVerify;
4714
4898
  const datasetSourceUploadAttachmentsImpl = deps.runDatasetSourceUploadAttachmentsImpl ?? runDatasetSourceUploadAttachments;
4715
4899
  if (flags.version) {
4716
4900
  return { exitCode: 0, stdout: `${loadCliPackageVersion(import.meta.url)}\n`, stderr: '' };
@@ -5320,13 +5504,141 @@ export async function executeCli(argv, deps) {
5320
5504
  stderr: '',
5321
5505
  };
5322
5506
  }
5323
- if (!['plan', 'apply', 'verify'].includes(action)) {
5324
- throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', or 'verify'.", {
5325
- code: 'DATASET_MAINTENANCE_ACTION_INVALID',
5326
- exitCode: 2,
5507
+ if (action === 'plan') {
5508
+ const datasetFlags = parseDatasetMaintenancePlanFlags(commandArgs.slice(1));
5509
+ if (datasetFlags.help) {
5510
+ return {
5511
+ exitCode: 0,
5512
+ stdout: `${renderDatasetMaintenancePlanHelp()}\n`,
5513
+ stderr: '',
5514
+ };
5515
+ }
5516
+ if (!datasetFlags.scopePath) {
5517
+ throw new CliError('dataset maintenance plan requires --scope.', {
5518
+ code: 'DATASET_MAINTENANCE_SCOPE_REQUIRED',
5519
+ exitCode: 2,
5520
+ });
5521
+ }
5522
+ if (!datasetFlags.operation) {
5523
+ throw new CliError('dataset maintenance plan requires --operation.', {
5524
+ code: 'DATASET_MAINTENANCE_OPERATION_REQUIRED',
5525
+ exitCode: 2,
5526
+ });
5527
+ }
5528
+ if (!datasetFlags.outDir) {
5529
+ throw new CliError('dataset maintenance plan requires --out-dir.', {
5530
+ code: 'DATASET_MAINTENANCE_OUT_DIR_REQUIRED',
5531
+ exitCode: 2,
5532
+ });
5533
+ }
5534
+ const report = await datasetMaintenancePlanImpl({
5535
+ scopePath: datasetFlags.scopePath,
5536
+ operation: datasetFlags.operation,
5537
+ outDir: datasetFlags.outDir,
5538
+ pageSize: datasetFlags.pageSize,
5539
+ timeoutMs: datasetFlags.timeoutMs,
5540
+ env: deps.env,
5541
+ fetchImpl: deps.fetchImpl,
5327
5542
  });
5543
+ return {
5544
+ exitCode: report.status === 'blocked' ? 1 : 0,
5545
+ stdout: stringifyJson(report, datasetFlags.json),
5546
+ stderr: '',
5547
+ };
5328
5548
  }
5329
- return plannedCommand('dataset', `maintenance ${action}`);
5549
+ if (action === 'apply') {
5550
+ const datasetFlags = parseDatasetMaintenanceApplyFlags(commandArgs.slice(1));
5551
+ if (datasetFlags.help) {
5552
+ return {
5553
+ exitCode: 0,
5554
+ stdout: `${renderDatasetMaintenanceApplyHelp()}\n`,
5555
+ stderr: '',
5556
+ };
5557
+ }
5558
+ if (!datasetFlags.planPath) {
5559
+ throw new CliError('dataset maintenance apply requires --plan.', {
5560
+ code: 'DATASET_MAINTENANCE_PLAN_REQUIRED',
5561
+ exitCode: 2,
5562
+ });
5563
+ }
5564
+ if (datasetFlags.commit && datasetFlags.dryRun) {
5565
+ throw new CliError('Cannot pass both --commit and --dry-run.', {
5566
+ code: 'DATASET_MAINTENANCE_APPLY_MODE_CONFLICT',
5567
+ exitCode: 2,
5568
+ });
5569
+ }
5570
+ if (datasetFlags.dryRun) {
5571
+ throw new CliError('dataset maintenance apply is commit-only; use dataset maintenance plan for a dry run.', {
5572
+ code: 'DATASET_MAINTENANCE_APPLY_COMMIT_ONLY',
5573
+ exitCode: 2,
5574
+ });
5575
+ }
5576
+ if (!datasetFlags.commit) {
5577
+ throw new CliError('dataset maintenance apply requires --commit.', {
5578
+ code: 'DATASET_MAINTENANCE_APPLY_COMMIT_REQUIRED',
5579
+ exitCode: 2,
5580
+ });
5581
+ }
5582
+ if (!datasetFlags.approvePlan) {
5583
+ throw new CliError('dataset maintenance apply requires --approve-plan <sha256>.', {
5584
+ code: 'DATASET_MAINTENANCE_APPROVAL_REQUIRED',
5585
+ exitCode: 2,
5586
+ });
5587
+ }
5588
+ if (!datasetFlags.confirm) {
5589
+ throw new CliError('dataset maintenance apply requires --confirm <email>.', {
5590
+ code: 'DATASET_MAINTENANCE_CONFIRM_REQUIRED',
5591
+ exitCode: 2,
5592
+ });
5593
+ }
5594
+ const report = await datasetMaintenanceApplyImpl({
5595
+ planPath: datasetFlags.planPath,
5596
+ commit: datasetFlags.commit,
5597
+ approvePlan: datasetFlags.approvePlan,
5598
+ confirm: datasetFlags.confirm,
5599
+ timeoutMs: datasetFlags.timeoutMs,
5600
+ env: deps.env,
5601
+ fetchImpl: deps.fetchImpl,
5602
+ });
5603
+ return {
5604
+ exitCode: report.status === 'completed_with_failures' ? 1 : 0,
5605
+ stdout: stringifyJson(report, datasetFlags.json),
5606
+ stderr: '',
5607
+ };
5608
+ }
5609
+ if (action === 'verify') {
5610
+ const datasetFlags = parseDatasetMaintenanceVerifyFlags(commandArgs.slice(1));
5611
+ if (datasetFlags.help) {
5612
+ return {
5613
+ exitCode: 0,
5614
+ stdout: `${renderDatasetMaintenanceVerifyHelp()}\n`,
5615
+ stderr: '',
5616
+ };
5617
+ }
5618
+ if (!datasetFlags.planPath) {
5619
+ throw new CliError('dataset maintenance verify requires --plan.', {
5620
+ code: 'DATASET_MAINTENANCE_PLAN_REQUIRED',
5621
+ exitCode: 2,
5622
+ });
5623
+ }
5624
+ const report = await datasetMaintenanceVerifyImpl({
5625
+ planPath: datasetFlags.planPath,
5626
+ outDir: datasetFlags.outDir,
5627
+ pageSize: datasetFlags.pageSize,
5628
+ timeoutMs: datasetFlags.timeoutMs,
5629
+ env: deps.env,
5630
+ fetchImpl: deps.fetchImpl,
5631
+ });
5632
+ return {
5633
+ exitCode: report.status === 'failed' ? 1 : 0,
5634
+ stdout: stringifyJson(report, datasetFlags.json),
5635
+ stderr: '',
5636
+ };
5637
+ }
5638
+ throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', or 'verify'.", {
5639
+ code: 'DATASET_MAINTENANCE_ACTION_INVALID',
5640
+ exitCode: 2,
5641
+ });
5330
5642
  }
5331
5643
  if (command === 'lifecyclemodel' && !subcommand) {
5332
5644
  return { exitCode: 0, stdout: `${renderLifecyclemodelHelp()}\n`, stderr: '' };