@tiangong-lca/cli 0.0.23 → 0.0.25

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,8 +17,8 @@ checkPaths:
17
17
  - bin/**
18
18
  - src/cli.ts
19
19
  - src/main.ts
20
- lastReviewedAt: 2026-07-13
21
- lastReviewedCommit: 4c79df4623e3cf296bc8d1baeea688d78351570a
20
+ lastReviewedAt: 2026-07-15
21
+ lastReviewedCommit: ca0cdd7549cad9003d08fb338223ba74682955ae
22
22
  ---
23
23
 
24
24
  # TianGong LCA CLI
@@ -29,6 +29,10 @@ Review note, 2026-07-12: `dataset maintenance plan/apply/verify` provides curren
29
29
 
30
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
31
 
32
+ Review note, 2026-07-14: maintenance now includes the protected derivative-only `rebuild-derivatives` operation. V1 plans exactly one current-owner state-0 process with `action=rebuild_derivatives`, `target_mode=owner_draft`, and components `extracted_md` plus `embedding_ft`. Apply only proves guarded-RPC admission (`accepted`/`queued`); independent verify reports `pending`, `passed`, or `failed`.
33
+
34
+ Review note, 2026-07-15: `dataset maintenance run-protected` adds a production-only path for one sealed private alias execution and its exact 50-target derivative closure. The protected executor is server-dispatched and fenced by the authenticated owner plus exact actor/user_id/state_code=0 and plan-closure checks; RLS remains a defense on public and independent-read surfaces. It performs one server preflight, writes an immutable local attempt marker before one admission POST, and requires status-only recovery after any marker or ambiguous response. It has no dev, legacy-alias, publication, or state-code fallback.
35
+
32
36
  ## Run
33
37
 
34
38
  One-off published run:
@@ -278,8 +282,10 @@ tiangong-lca dataset evidence-search plan --query "中国2026年电力结构数
278
282
  tiangong-lca dataset evidence-search run --input ./evidence-search.request.json --results ./search-results.json --out-dir /abs/path/to/evidence-search --json
279
283
  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
280
284
  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
285
+ tiangong-lca dataset maintenance plan --scope ./derivative-rebuild-scope.json --operation rebuild-derivatives --out-dir /abs/path/to/derivative-rebuild --json
281
286
  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
287
  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
288
+ tiangong-lca dataset maintenance run-protected --plan /abs/path/to/maintenance-plan.json --freeze /abs/path/to/protected-execution-seal.json --approval /abs/path/to/protected-approval.json --out-dir /abs/path/to/protected-run --status-only --json
283
289
  tiangong-lca lifecyclemodel auto-build --input ./examples/lifecyclemodel-auto-build.request.json --out-dir /abs/path/to/lifecyclemodel-run --json
284
290
  tiangong-lca lifecyclemodel validate-build --run-dir /abs/path/to/lifecyclemodel-run --json
285
291
  tiangong-lca lifecyclemodel publish-build --run-dir /abs/path/to/lifecyclemodel-run --json
@@ -324,12 +330,12 @@ For `dataset references rewrite`, `--commit` executes the state-aware save-draft
324
330
 
325
331
  ## Dataset Maintenance
326
332
 
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.
333
+ `dataset maintenance plan/apply/run-protected/verify` is the row-level cleanup surface for bad imports, the fixed BAFU private alias rewrite, and protected derivative rebuilds. Ordinary planning, apply, and independent verification use the authenticated account and RLS. The protected executor is server-dispatched and additionally enforces the sealed actor, user_id, state_code=0, exact target set, and closure hashes on every write.
328
334
 
329
335
  ```bash
330
336
  tiangong-lca dataset maintenance plan \
331
337
  --scope ./maintenance-scope.json \
332
- --operation merge-support-aliases \
338
+ --operation repair-references \
333
339
  --out-dir ./dataset-maintenance \
334
340
  --page-size 1000 \
335
341
  --timeout-ms 10000 \
@@ -349,8 +355,39 @@ tiangong-lca dataset maintenance verify \
349
355
  --page-size 1000 \
350
356
  --timeout-ms 10000 \
351
357
  --json
358
+
359
+ tiangong-lca dataset maintenance run-protected \
360
+ --plan ./protected-step2/maintenance-plan.json \
361
+ --freeze ./protected-step2/protected-execution-seal.json \
362
+ --approval ./protected-step2/protected-approval.json \
363
+ --out-dir ./protected-step2/run \
364
+ --commit \
365
+ --approve-execution <approved-execution-sha256> \
366
+ --confirm <current-account-email> \
367
+ --wait-seconds 60 \
368
+ --poll-ms 10000 \
369
+ --page-size 1000 \
370
+ --timeout-ms 10000 \
371
+ --json
372
+
373
+ tiangong-lca dataset maintenance run-protected \
374
+ --plan ./protected-step2/maintenance-plan.json \
375
+ --freeze ./protected-step2/protected-execution-seal.json \
376
+ --approval ./protected-step2/protected-approval.json \
377
+ --out-dir ./protected-step2/run \
378
+ --status-only \
379
+ --wait-seconds 60 \
380
+ --json
352
381
  ```
353
382
 
383
+ `run-protected` is a separate one-shot path for an already reviewed and sealed production execution; it does not replace ordinary planning. Both modes require the exact plan, freeze/seal, approval artifact, and private output directory. Commit mode additionally requires `--commit`, the exact approved execution identity through `--approve-execution`, and the authenticated account email through `--confirm`. `--status-only` is mutually exclusive with `--commit` and performs no preflight or admission.
384
+
385
+ Before requesting preflight, the command validates the sealed production project, full current-user RLS before-state, support closure, and exact derivative baseline. The server then returns the three expected gate digests and a token valid for at most 180 seconds; the CLI captures and compares the live gate receipts before admission. The server-dispatched write remains fenced to the authenticated actor's exact `user_id`, `state_code=0` rows and sealed plan/closure; independent readback still uses RLS. The CLI writes an immutable local submission marker and sends at most one admission POST. A marker, admission timeout, connection loss, or ambiguous admission response permanently switches that local run to status-only recovery; status-read failures may be polled only within the configured wait window and never cause a second admission or fallback to dev or the legacy whole-plan RPC. The default status polling interval is 10 seconds.
386
+
387
+ Success requires the terminal database proof and independent RLS readback to agree on the approved execution, exact row/exchange/audit closure, and exactly 50 derivative targets split into 23 flows and 27 processes. `pending`, `failed`, and `indeterminate` all return a non-zero exit status. The protected operation keeps all affected rows private to their owner, changes no `state_code`, and does not publish data.
388
+
389
+ For the derivative-only profile, use the same three commands with `--operation rebuild-derivatives`. Its scope must contain exactly one `processes` action with `action: "rebuild_derivatives"`, `target_mode: "owner_draft"`, expected current owner, expected `state_code: 0`, and the exact component set `extracted_md` plus `embedding_ft`.
390
+
354
391
  `--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
392
 
356
393
  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.
@@ -358,18 +395,21 @@ This completeness proof means the CLI traversed the filtered result while that t
358
395
  The scope is intentionally narrow:
359
396
 
360
397
  - 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.
398
+ - `--operation` accepts `delete`, `retire`, `redo-import`, `repair-references`, `merge-support-aliases`, or `rebuild-derivatives`; it records the operator's maintenance intent and does not broaden the eligible row actions.
362
399
  - Only current-user `contacts`, `sources`, `flows`, and `processes` can become `save_draft` or `delete` actions.
363
400
  - `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
401
  - 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
402
  - 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.
403
+ - `rebuild-derivatives` accepts exactly one exact-version `processes` row and is bidirectionally bound to `action=rebuild_derivatives`, `target_mode=owner_draft`, and components `extracted_md` plus `embedding_ft`. It cannot target a public/shared row, a foreign owner, a non-draft row, another table, multiple actions, or a partial/different component set. It rebuilds derivatives only; the primary process payload, owner/state, and `modified_at` remain unchanged.
366
404
  - 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
405
 
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.
406
+ `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. A derivative rebuild plan additionally obtains a database-produced snapshot for only the exact target action and binds its primary and derivative preconditions into the plan; large markdown/vector fields are not added to the account-wide scan. The plan SHA-256 is the approval identity; do not edit or recompute the plan after review.
407
+
408
+ `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 original V1 alias adapter retains its frozen request and artifact contract for compatibility, but it is not an authorized execution or recovery fallback for a sealed production `merge-support-aliases` plan. That plan must use `run-protected`, whose database contract replaces replay with one durable attempt/admission identity.
369
409
 
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.
410
+ For `rebuild-derivatives`, apply submits the frozen single-action plan only to the authenticated guarded RPC. The database admission envelope must report `queued`; the CLI records that durable admission as an `accepted` action with queued proof. It does not mean markdown or embedding generation has completed. Replay must return the same durable request/proof rather than enqueueing a second rebuild. There is no fallback to a direct Edge call, `admin embedding-run`, a raw queue, SQL, service-role credentials, or raw REST mutation.
371
411
 
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`.
412
+ `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`. For derivative rebuild plans, verify reads the durable request plus a fresh action-scoped process snapshot and reports only `pending`, `passed`, or `failed`. Database statuses `queued`, `dispatching`, `markdown_pending`, and `embedding_pending` map to `pending`; `completed` maps to `passed` only when both requested derivatives are current and every frozen primary-field precondition remains unchanged; `stale` and `failed` map to `failed`. The raw status proof, including `phase` and `fence_active`, is preserved in the report. A failed rebuild does not by itself prove that the primary-row write fence has been released.
373
413
 
374
414
  `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
415
 
package/dist/src/cli.js CHANGED
@@ -56,6 +56,7 @@ import { runDatasetClassificationApply, runDatasetClassificationAudit, runDatase
56
56
  import { runDatasetMaintenanceClearAccount, } from './lib/dataset-maintenance-clear-account.js';
57
57
  import { runDatasetMaintenancePlan } from './lib/dataset-maintenance-plan.js';
58
58
  import { runDatasetMaintenanceApply } from './lib/dataset-maintenance-apply.js';
59
+ import { runDatasetMaintenanceProtected } from './lib/dataset-maintenance-protected-run.js';
59
60
  import { runDatasetMaintenanceVerify } from './lib/dataset-maintenance-verify.js';
60
61
  import { runDatasetSourceUploadAttachments, } from './lib/dataset-source-upload-attachments.js';
61
62
  function renderMainHelp(dotEnvStatus) {
@@ -131,6 +132,7 @@ Examples:
131
132
  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
132
133
  tiangong-lca dataset maintenance clear-account --out-dir /abs/path/to/account-clear --json
133
134
  tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir /abs/path/to/dataset-maintenance
135
+ tiangong-lca dataset maintenance run-protected --plan ./maintenance-plan.json --freeze ./protected-execution-seal.json --approval ./protected-approval.json --out-dir /abs/path/to/protected-run --status-only
134
136
  tiangong-lca lifecyclemodel auto-build --input ./lifecyclemodel-auto-build.request.json --out-dir /abs/path/to/lifecyclemodel-run
135
137
  tiangong-lca lifecyclemodel validate-build --run-dir /abs/path/to/lifecyclemodel-run
136
138
  tiangong-lca lifecyclemodel publish-build --run-dir /abs/path/to/lifecyclemodel-run
@@ -283,6 +285,7 @@ Implemented Subcommands:
283
285
  maintenance clear-account Dry-run or clear current authenticated account-owned dataset rows through RLS
284
286
  maintenance plan Build an immutable, RLS-visible row-level maintenance plan
285
287
  maintenance apply Execute an explicitly approved maintenance plan through current-user RLS
288
+ maintenance run-protected Run or inspect one sealed, one-shot production maintenance execution
286
289
  maintenance verify Read back affected rows and references against the immutable plan
287
290
 
288
291
  Examples:
@@ -310,21 +313,25 @@ Examples:
310
313
  tiangong-lca dataset references refresh-remote --input ./rows.jsonl --out ./rows.refreshed.jsonl --out-dir ./dataset-reference-refresh --help
311
314
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json --help
312
315
  tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation merge-support-aliases --out-dir ./dataset-maintenance --help
316
+ tiangong-lca dataset maintenance run-protected --help
313
317
  `.trim();
314
318
  }
315
319
  function renderDatasetMaintenanceHelp() {
316
320
  return `Usage:
317
- tiangong-lca dataset maintenance <clear-account|plan|apply|verify> [options]
321
+ tiangong-lca dataset maintenance <clear-account|plan|apply|run-protected|verify> [options]
318
322
 
319
323
  Actions:
320
324
  clear-account Dry-run or delete current authenticated account-owned lifecyclemodels, processes, flows, sources, and contacts.
321
325
  plan Build an immutable maintenance plan from a scope manifest, visible remote snapshot, dependency impact report, and intended operation.
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.
323
- verify Re-fetch affected rows and references, then prove that deleted, updated, skipped, protected, and redone rows match the plan.
326
+ apply Execute or durably admit an ordinary approved plan through current-user RLS and platform dataset command paths.
327
+ run-protected Run or inspect one sealed production execution with one-shot admission and terminal derivative proof.
328
+ verify Re-fetch rows, references, or derivative request state and report passed, pending, or failed independently of apply.
324
329
 
325
330
  Safety:
326
331
  plan and verify are read-only.
327
332
  apply is commit-only and requires --commit, the exact plan SHA-256 via --approve-plan, and the current account email via --confirm.
333
+ A sealed production merge-support-aliases execution must use run-protected; apply is not its fallback.
334
+ run-protected is production-only: authenticated owner context plus server-side actor/user_id/state_code=0 and exact-plan closure fences protect writes; RLS remains a defense on public and independent-read surfaces. It never falls back to dev, a legacy alias RPC, or a second admission POST.
328
335
 
329
336
  Required Artifact Contract:
330
337
  - maintenance-plan.json
@@ -334,12 +341,15 @@ Required Artifact Contract:
334
341
  - dry-run-report.json
335
342
  - commit-report.json
336
343
  - readback-verify-report.json
344
+ run-protected uses a separate immutable protected-execution seal, submission marker, append-only status ledger, and terminal report.
337
345
 
338
346
  Examples:
339
347
  tiangong-lca dataset maintenance clear-account --out-dir ./account-clear --json
340
348
  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 merge-support-aliases --out-dir ./dataset-maintenance
349
+ tiangong-lca dataset maintenance plan --scope ./maintenance-scope.json --operation repair-references --out-dir ./dataset-maintenance
350
+ tiangong-lca dataset maintenance plan --scope ./derivative-rebuild-scope.json --operation rebuild-derivatives --out-dir ./derivative-rebuild
342
351
  tiangong-lca dataset maintenance apply --plan ./dataset-maintenance/maintenance-plan.json --commit --approve-plan <sha256> --confirm user@example.com
352
+ tiangong-lca dataset maintenance run-protected --plan ./maintenance-plan.json --freeze ./protected-execution-seal.json --approval ./protected-approval.json --out-dir ./protected-run --status-only
343
353
  tiangong-lca dataset maintenance verify --plan ./dataset-maintenance/maintenance-plan.json --out-dir ./dataset-maintenance/verify
344
354
  `.trim();
345
355
  }
@@ -348,7 +358,7 @@ function renderDatasetMaintenancePlanHelp() {
348
358
  tiangong-lca dataset maintenance plan --scope <file> --operation <operation> --out-dir <dir> [options]
349
359
 
350
360
  Operations:
351
- delete | retire | redo-import | repair-references | merge-support-aliases
361
+ delete | retire | redo-import | repair-references | merge-support-aliases | rebuild-derivatives
352
362
 
353
363
  Options:
354
364
  --scope <file> Maintenance scope manifest
@@ -360,7 +370,8 @@ Options:
360
370
  -h, --help
361
371
 
362
372
  Outputs written under --out-dir include the immutable maintenance-plan.json, visible snapshot,
363
- protected-row ledger, reference-impact report, and dry-run report.
373
+ protected-row ledger, reference-impact report, and dry-run report. rebuild-derivatives additionally
374
+ freezes one action-scoped database snapshot without expanding the account scan with vector data.
364
375
  `.trim();
365
376
  }
366
377
  function renderDatasetMaintenanceApplyHelp() {
@@ -370,6 +381,8 @@ function renderDatasetMaintenanceApplyHelp() {
370
381
  Behavior:
371
382
  Commit-only. The command rejects dry-run mode, a missing --commit flag, a plan hash mismatch,
372
383
  or a confirmation email that does not match the current authenticated account.
384
+ For rebuild-derivatives, success means guarded admission is queued; it never means derivatives completed.
385
+ Sealed production merge-support-aliases executions must use run-protected; this command is not a fallback.
373
386
 
374
387
  Options:
375
388
  --plan <file> Immutable maintenance-plan.json
@@ -380,7 +393,46 @@ Options:
380
393
  --json Print compact JSON
381
394
  -h, --help
382
395
 
383
- Outputs include approval-record.json and commit-report.json alongside an append-only action ledger.
396
+ Outputs include approval-record.json and commit-report.json alongside an append-only action or admission ledger.
397
+ `.trim();
398
+ }
399
+ function renderDatasetMaintenanceRunProtectedHelp() {
400
+ return `Usage:
401
+ tiangong-lca dataset maintenance run-protected --plan <file> --freeze <file> --approval <file> --out-dir <dir> --commit --approve-execution <sha256> --confirm <email> [options]
402
+ tiangong-lca dataset maintenance run-protected --plan <file> --freeze <file> --approval <file> --out-dir <dir> --status-only [options]
403
+
404
+ Behavior:
405
+ Runs one sealed, server-dispatched production maintenance execution fenced to the authenticated actor's
406
+ exact owner-draft target set, or reads durable status through an auth.uid-, actor-, and plan-fenced RPC
407
+ without admitting work; only the independent table readback uses RLS.
408
+ Commit and status-only modes are mutually exclusive. The commit path validates the full private
409
+ before-state, accepts only server-derived gates within the 180-second preflight window, writes an immutable
410
+ local submission marker, and sends at most one admission POST. After a marker or ambiguous response,
411
+ use --status-only; the command never retries admission or falls back to dev or the legacy alias RPC.
412
+
413
+ Required in both modes:
414
+ --plan <file> Immutable maintenance-plan.json
415
+ --freeze <file> Protected production execution seal/freeze artifact
416
+ --approval <file> Exact human-approval binding artifact
417
+ --out-dir <dir> Private protected-run artifact directory
418
+
419
+ Commit-only guards:
420
+ --commit Submit the sealed execution exactly once
421
+ --approve-execution <sha256> Exact approved execution identity SHA-256
422
+ --confirm <email> Current authenticated account email
423
+
424
+ Recovery:
425
+ --status-only Read/poll and independently verify the sealed request; never preflight or admit
426
+
427
+ Options:
428
+ --wait-seconds <n> Maximum terminal-status wait in seconds; 0 performs one status read
429
+ --poll-ms <n> Positive polling interval in milliseconds; default 10000
430
+ --page-size <n> Requested RLS readback page size, 1-5000
431
+ --timeout-ms <n> Positive request timeout in milliseconds
432
+ --json Print compact JSON
433
+ -h, --help
434
+
435
+ Only a terminal passed proof exits successfully. pending, failed, and indeterminate return non-zero.
384
436
  `.trim();
385
437
  }
386
438
  function renderDatasetMaintenanceVerifyHelp() {
@@ -395,7 +447,8 @@ Options:
395
447
  --json Print compact JSON
396
448
  -h, --help
397
449
 
398
- Outputs include readback-verify-report.json with affected-row and reference checks.
450
+ Outputs include readback-verify-report.json. rebuild-derivatives reports pending, passed, or failed;
451
+ pending and failed both return a non-zero exit status.
399
452
  `.trim();
400
453
  }
401
454
  function renderDatasetMaintenanceClearAccountHelp() {
@@ -3036,6 +3089,19 @@ function parseDatasetMaintenancePositiveInteger(value, flagName) {
3036
3089
  }
3037
3090
  return Number.parseInt(value.trim(), 10);
3038
3091
  }
3092
+ function parseDatasetMaintenanceNonNegativeInteger(value, flagName) {
3093
+ if (typeof value !== 'string') {
3094
+ return undefined;
3095
+ }
3096
+ if (!/^\d+$/u.test(value.trim())) {
3097
+ throw new CliError(`${flagName} must be a non-negative integer.`, {
3098
+ code: 'DATASET_MAINTENANCE_INTEGER_INVALID',
3099
+ exitCode: 2,
3100
+ details: { flag: flagName, value },
3101
+ });
3102
+ }
3103
+ return Number.parseInt(value.trim(), 10);
3104
+ }
3039
3105
  function parseDatasetMaintenancePlanFlags(args) {
3040
3106
  let values;
3041
3107
  try {
@@ -3062,8 +3128,15 @@ function parseDatasetMaintenancePlanFlags(args) {
3062
3128
  }
3063
3129
  const rawOperation = typeof values.operation === 'string' ? values.operation : null;
3064
3130
  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'.", {
3131
+ ![
3132
+ 'delete',
3133
+ 'retire',
3134
+ 'redo-import',
3135
+ 'repair-references',
3136
+ 'merge-support-aliases',
3137
+ 'rebuild-derivatives',
3138
+ ].includes(rawOperation)) {
3139
+ throw new CliError("--operation must be 'delete', 'retire', 'redo-import', 'repair-references', 'merge-support-aliases', or 'rebuild-derivatives'.", {
3067
3140
  code: 'DATASET_MAINTENANCE_OPERATION_INVALID',
3068
3141
  exitCode: 2,
3069
3142
  details: rawOperation,
@@ -3115,6 +3188,54 @@ function parseDatasetMaintenanceApplyFlags(args) {
3115
3188
  dryRun: Boolean(values['dry-run']),
3116
3189
  };
3117
3190
  }
3191
+ function parseDatasetMaintenanceProtectedFlags(args) {
3192
+ let values;
3193
+ try {
3194
+ ({ values } = parseArgs({
3195
+ args,
3196
+ allowPositionals: false,
3197
+ strict: true,
3198
+ options: {
3199
+ help: { type: 'boolean', short: 'h' },
3200
+ json: { type: 'boolean' },
3201
+ plan: { type: 'string' },
3202
+ freeze: { type: 'string' },
3203
+ approval: { type: 'string' },
3204
+ 'out-dir': { type: 'string' },
3205
+ commit: { type: 'boolean' },
3206
+ 'status-only': { type: 'boolean' },
3207
+ 'approve-execution': { type: 'string' },
3208
+ confirm: { type: 'string' },
3209
+ 'wait-seconds': { type: 'string' },
3210
+ 'poll-ms': { type: 'string' },
3211
+ 'page-size': { type: 'string' },
3212
+ 'timeout-ms': { type: 'string' },
3213
+ },
3214
+ }));
3215
+ }
3216
+ catch (error) {
3217
+ throw new CliError(String(error), {
3218
+ code: 'INVALID_ARGS',
3219
+ exitCode: 2,
3220
+ });
3221
+ }
3222
+ return {
3223
+ help: Boolean(values.help),
3224
+ json: Boolean(values.json),
3225
+ planPath: typeof values.plan === 'string' ? values.plan : '',
3226
+ freezePath: typeof values.freeze === 'string' ? values.freeze : '',
3227
+ approvalPath: typeof values.approval === 'string' ? values.approval : '',
3228
+ outDir: typeof values['out-dir'] === 'string' ? values['out-dir'] : '',
3229
+ commit: Boolean(values.commit),
3230
+ statusOnly: Boolean(values['status-only']),
3231
+ approveExecution: typeof values['approve-execution'] === 'string' ? values['approve-execution'] : undefined,
3232
+ confirm: typeof values.confirm === 'string' ? values.confirm : undefined,
3233
+ waitSeconds: parseDatasetMaintenanceNonNegativeInteger(values['wait-seconds'], '--wait-seconds'),
3234
+ pollMs: parseDatasetMaintenancePositiveInteger(values['poll-ms'], '--poll-ms'),
3235
+ pageSize: parseDatasetMaintenancePositiveInteger(values['page-size'], '--page-size'),
3236
+ timeoutMs: parseDatasetMaintenancePositiveInteger(values['timeout-ms'], '--timeout-ms'),
3237
+ };
3238
+ }
3118
3239
  function parseDatasetMaintenanceVerifyFlags(args) {
3119
3240
  let values;
3120
3241
  try {
@@ -4894,6 +5015,7 @@ export async function executeCli(argv, deps) {
4894
5015
  const datasetMaintenanceClearAccountImpl = deps.runDatasetMaintenanceClearAccountImpl ?? runDatasetMaintenanceClearAccount;
4895
5016
  const datasetMaintenancePlanImpl = deps.runDatasetMaintenancePlanImpl ?? runDatasetMaintenancePlan;
4896
5017
  const datasetMaintenanceApplyImpl = deps.runDatasetMaintenanceApplyImpl ?? runDatasetMaintenanceApply;
5018
+ const datasetMaintenanceProtectedImpl = deps.runDatasetMaintenanceProtectedImpl ?? runDatasetMaintenanceProtected;
4897
5019
  const datasetMaintenanceVerifyImpl = deps.runDatasetMaintenanceVerifyImpl ?? runDatasetMaintenanceVerify;
4898
5020
  const datasetSourceUploadAttachmentsImpl = deps.runDatasetSourceUploadAttachmentsImpl ?? runDatasetSourceUploadAttachments;
4899
5021
  if (flags.version) {
@@ -5606,6 +5728,85 @@ export async function executeCli(argv, deps) {
5606
5728
  stderr: '',
5607
5729
  };
5608
5730
  }
5731
+ if (action === 'run-protected') {
5732
+ const datasetFlags = parseDatasetMaintenanceProtectedFlags(commandArgs.slice(1));
5733
+ if (datasetFlags.help) {
5734
+ return {
5735
+ exitCode: 0,
5736
+ stdout: `${renderDatasetMaintenanceRunProtectedHelp()}\n`,
5737
+ stderr: '',
5738
+ };
5739
+ }
5740
+ if (!datasetFlags.planPath) {
5741
+ throw new CliError('dataset maintenance run-protected requires --plan.', {
5742
+ code: 'DATASET_MAINTENANCE_PROTECTED_PLAN_REQUIRED',
5743
+ exitCode: 2,
5744
+ });
5745
+ }
5746
+ if (!datasetFlags.freezePath) {
5747
+ throw new CliError('dataset maintenance run-protected requires --freeze.', {
5748
+ code: 'DATASET_MAINTENANCE_PROTECTED_FREEZE_REQUIRED',
5749
+ exitCode: 2,
5750
+ });
5751
+ }
5752
+ if (!datasetFlags.approvalPath) {
5753
+ throw new CliError('dataset maintenance run-protected requires --approval.', {
5754
+ code: 'DATASET_MAINTENANCE_PROTECTED_APPROVAL_FILE_REQUIRED',
5755
+ exitCode: 2,
5756
+ });
5757
+ }
5758
+ if (!datasetFlags.outDir) {
5759
+ throw new CliError('dataset maintenance run-protected requires --out-dir.', {
5760
+ code: 'DATASET_MAINTENANCE_PROTECTED_OUT_DIR_REQUIRED',
5761
+ exitCode: 2,
5762
+ });
5763
+ }
5764
+ if (datasetFlags.commit && datasetFlags.statusOnly) {
5765
+ throw new CliError('Cannot pass both --commit and --status-only.', {
5766
+ code: 'DATASET_MAINTENANCE_PROTECTED_MODE_CONFLICT',
5767
+ exitCode: 2,
5768
+ });
5769
+ }
5770
+ if (!datasetFlags.commit && !datasetFlags.statusOnly) {
5771
+ throw new CliError('dataset maintenance run-protected requires either --commit or --status-only.', {
5772
+ code: 'DATASET_MAINTENANCE_PROTECTED_MODE_REQUIRED',
5773
+ exitCode: 2,
5774
+ });
5775
+ }
5776
+ if (datasetFlags.commit && !datasetFlags.approveExecution) {
5777
+ throw new CliError('dataset maintenance run-protected requires --approve-execution <sha256> with --commit.', {
5778
+ code: 'DATASET_MAINTENANCE_PROTECTED_APPROVAL_REQUIRED',
5779
+ exitCode: 2,
5780
+ });
5781
+ }
5782
+ if (datasetFlags.commit && !datasetFlags.confirm) {
5783
+ throw new CliError('dataset maintenance run-protected requires --confirm <email> with --commit.', {
5784
+ code: 'DATASET_MAINTENANCE_PROTECTED_CONFIRM_REQUIRED',
5785
+ exitCode: 2,
5786
+ });
5787
+ }
5788
+ const report = await datasetMaintenanceProtectedImpl({
5789
+ planPath: datasetFlags.planPath,
5790
+ freezePath: datasetFlags.freezePath,
5791
+ approvalPath: datasetFlags.approvalPath,
5792
+ outDir: datasetFlags.outDir,
5793
+ commit: datasetFlags.commit,
5794
+ statusOnly: datasetFlags.statusOnly,
5795
+ approveExecution: datasetFlags.approveExecution,
5796
+ confirm: datasetFlags.confirm,
5797
+ waitSeconds: datasetFlags.waitSeconds,
5798
+ pollMs: datasetFlags.pollMs,
5799
+ pageSize: datasetFlags.pageSize,
5800
+ timeoutMs: datasetFlags.timeoutMs,
5801
+ env: deps.env,
5802
+ fetchImpl: deps.fetchImpl,
5803
+ });
5804
+ return {
5805
+ exitCode: report.status === 'passed' ? 0 : 1,
5806
+ stdout: stringifyJson(report, datasetFlags.json),
5807
+ stderr: '',
5808
+ };
5809
+ }
5609
5810
  if (action === 'verify') {
5610
5811
  const datasetFlags = parseDatasetMaintenanceVerifyFlags(commandArgs.slice(1));
5611
5812
  if (datasetFlags.help) {
@@ -5630,12 +5831,12 @@ export async function executeCli(argv, deps) {
5630
5831
  fetchImpl: deps.fetchImpl,
5631
5832
  });
5632
5833
  return {
5633
- exitCode: report.status === 'failed' ? 1 : 0,
5834
+ exitCode: report.status === 'passed' ? 0 : 1,
5634
5835
  stdout: stringifyJson(report, datasetFlags.json),
5635
5836
  stderr: '',
5636
5837
  };
5637
5838
  }
5638
- throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', or 'verify'.", {
5839
+ throw new CliError("dataset maintenance action must be 'clear-account', 'plan', 'apply', 'run-protected', or 'verify'.", {
5639
5840
  code: 'DATASET_MAINTENANCE_ACTION_INVALID',
5640
5841
  exitCode: 2,
5641
5842
  });