Skip to main content

canic_backup/preflight/
mod.rs

1use crate::{
2    journal::{DownloadOperationMetrics, JournalResumeReport},
3    manifest::{FleetBackupManifest, manifest_validation_summary},
4    persistence::{
5        BackupInspectionReport, BackupIntegrityReport, BackupLayout, BackupProvenanceReport,
6        PersistenceError,
7    },
8    restore::{RestoreMapping, RestorePlan, RestorePlanError, RestorePlanner, RestoreStatus},
9};
10use serde_json::json;
11use std::{
12    fs,
13    path::{Path, PathBuf},
14};
15use thiserror::Error as ThisError;
16
17///
18/// BackupPreflightConfig
19///
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct BackupPreflightConfig {
23    pub backup_dir: PathBuf,
24    pub out_dir: PathBuf,
25    pub mapping: Option<PathBuf>,
26}
27
28///
29/// BackupPreflightReport
30///
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[expect(
34    clippy::struct_excessive_bools,
35    reason = "preflight reports intentionally mirror machine-readable JSON status flags"
36)]
37pub struct BackupPreflightReport {
38    pub status: String,
39    pub backup_id: String,
40    pub backup_dir: String,
41    pub source_environment: String,
42    pub source_root_canister: String,
43    pub topology_hash: String,
44    pub mapping_path: Option<String>,
45    pub journal_complete: bool,
46    pub journal_operation_metrics: DownloadOperationMetrics,
47    pub inspection_status: String,
48    pub provenance_status: String,
49    pub backup_id_status: String,
50    pub topology_receipts_status: String,
51    pub topology_mismatch_count: usize,
52    pub integrity_verified: bool,
53    pub manifest_design_v1_ready: bool,
54    pub manifest_members: usize,
55    pub backup_unit_count: usize,
56    pub restore_plan_members: usize,
57    pub restore_mapping_supplied: bool,
58    pub restore_all_sources_mapped: bool,
59    pub restore_fixed_members: usize,
60    pub restore_relocatable_members: usize,
61    pub restore_in_place_members: usize,
62    pub restore_mapped_members: usize,
63    pub restore_remapped_members: usize,
64    pub restore_ready: bool,
65    pub restore_readiness_reasons: Vec<String>,
66    pub restore_all_members_have_module_hash: bool,
67    pub restore_all_members_have_wasm_hash: bool,
68    pub restore_all_members_have_code_version: bool,
69    pub restore_all_members_have_checksum: bool,
70    pub restore_members_with_module_hash: usize,
71    pub restore_members_with_wasm_hash: usize,
72    pub restore_members_with_code_version: usize,
73    pub restore_members_with_checksum: usize,
74    pub restore_verification_required: bool,
75    pub restore_all_members_have_checks: bool,
76    pub restore_fleet_checks: usize,
77    pub restore_member_check_groups: usize,
78    pub restore_member_checks: usize,
79    pub restore_members_with_checks: usize,
80    pub restore_total_checks: usize,
81    pub restore_planned_snapshot_uploads: usize,
82    pub restore_planned_snapshot_loads: usize,
83    pub restore_planned_code_reinstalls: usize,
84    pub restore_planned_verification_checks: usize,
85    pub restore_planned_operations: usize,
86    pub restore_planned_phases: usize,
87    pub restore_phase_count: usize,
88    pub restore_dependency_free_members: usize,
89    pub restore_in_group_parent_edges: usize,
90    pub restore_cross_group_parent_edges: usize,
91    pub manifest_validation_path: String,
92    pub backup_status_path: String,
93    pub backup_inspection_path: String,
94    pub backup_provenance_path: String,
95    pub backup_integrity_path: String,
96    pub restore_plan_path: String,
97    pub restore_status_path: String,
98    pub preflight_summary_path: String,
99}
100
101///
102/// BackupPreflightError
103///
104
105#[derive(Debug, ThisError)]
106pub enum BackupPreflightError {
107    #[error(
108        "backup journal {backup_id} is incomplete: {pending_artifacts}/{total_artifacts} artifacts still require resume work"
109    )]
110    IncompleteJournal {
111        backup_id: String,
112        total_artifacts: usize,
113        pending_artifacts: usize,
114    },
115
116    #[error(transparent)]
117    Io(#[from] std::io::Error),
118
119    #[error(transparent)]
120    Json(#[from] serde_json::Error),
121
122    #[error(transparent)]
123    Persistence(#[from] PersistenceError),
124
125    #[error(transparent)]
126    RestorePlan(#[from] RestorePlanError),
127}
128
129///
130/// PreflightArtifactPaths
131///
132
133struct PreflightArtifactPaths {
134    manifest_validation: PathBuf,
135    backup_status: PathBuf,
136    backup_inspection: PathBuf,
137    backup_provenance: PathBuf,
138    backup_integrity: PathBuf,
139    restore_plan: PathBuf,
140    restore_status: PathBuf,
141    preflight_summary: PathBuf,
142}
143
144///
145/// PreflightReportInput
146///
147
148struct PreflightReportInput<'a> {
149    config: &'a BackupPreflightConfig,
150    manifest: &'a FleetBackupManifest,
151    status: &'a JournalResumeReport,
152    inspection: &'a BackupInspectionReport,
153    provenance: &'a BackupProvenanceReport,
154    integrity: &'a BackupIntegrityReport,
155    restore_plan: &'a RestorePlan,
156    paths: &'a PreflightArtifactPaths,
157}
158
159///
160/// PreflightArtifactInput
161///
162
163struct PreflightArtifactInput<'a> {
164    paths: &'a PreflightArtifactPaths,
165    manifest: &'a FleetBackupManifest,
166    status: &'a JournalResumeReport,
167    inspection: &'a BackupInspectionReport,
168    provenance: &'a BackupProvenanceReport,
169    integrity: &'a BackupIntegrityReport,
170    restore_plan: &'a RestorePlan,
171    restore_status: &'a RestoreStatus,
172}
173
174/// Run all no-mutation backup checks and write the standard preflight bundle.
175pub fn run_backup_preflight(
176    config: &BackupPreflightConfig,
177) -> Result<BackupPreflightReport, BackupPreflightError> {
178    fs::create_dir_all(&config.out_dir)?;
179
180    let layout = BackupLayout::new(config.backup_dir.clone());
181    let manifest = layout.read_manifest()?;
182    let status = layout.read_journal()?.resume_report();
183    ensure_complete_status(&status)?;
184    let inspection = layout.inspect()?;
185    let provenance = layout.provenance()?;
186    let integrity = layout.verify_integrity()?;
187    let mapping = config.mapping.as_ref().map(read_mapping).transpose()?;
188    let restore_plan = RestorePlanner::plan(&manifest, mapping.as_ref())?;
189    let restore_status = RestoreStatus::from_plan(&restore_plan);
190    let paths = preflight_artifact_paths(&config.out_dir);
191
192    write_preflight_artifacts(PreflightArtifactInput {
193        paths: &paths,
194        manifest: &manifest,
195        status: &status,
196        inspection: &inspection,
197        provenance: &provenance,
198        integrity: &integrity,
199        restore_plan: &restore_plan,
200        restore_status: &restore_status,
201    })?;
202    let report = build_preflight_report(PreflightReportInput {
203        config,
204        manifest: &manifest,
205        status: &status,
206        inspection: &inspection,
207        provenance: &provenance,
208        integrity: &integrity,
209        restore_plan: &restore_plan,
210        paths: &paths,
211    });
212    write_json_value_file(&paths.preflight_summary, &preflight_summary_value(&report))?;
213    Ok(report)
214}
215
216// Ensure a journal status report has no remaining resume work.
217fn ensure_complete_status(report: &JournalResumeReport) -> Result<(), BackupPreflightError> {
218    if report.is_complete {
219        return Ok(());
220    }
221
222    Err(BackupPreflightError::IncompleteJournal {
223        backup_id: report.backup_id.clone(),
224        total_artifacts: report.total_artifacts,
225        pending_artifacts: report.pending_artifacts,
226    })
227}
228
229// Build the standard preflight artifact path set under one output directory.
230fn preflight_artifact_paths(out_dir: &Path) -> PreflightArtifactPaths {
231    PreflightArtifactPaths {
232        manifest_validation: out_dir.join("manifest-validation.json"),
233        backup_status: out_dir.join("backup-status.json"),
234        backup_inspection: out_dir.join("backup-inspection.json"),
235        backup_provenance: out_dir.join("backup-provenance.json"),
236        backup_integrity: out_dir.join("backup-integrity.json"),
237        restore_plan: out_dir.join("restore-plan.json"),
238        restore_status: out_dir.join("restore-status.json"),
239        preflight_summary: out_dir.join("preflight-summary.json"),
240    }
241}
242
243// Write the standard preflight artifacts before emitting the compact summary.
244fn write_preflight_artifacts(
245    input: PreflightArtifactInput<'_>,
246) -> Result<(), BackupPreflightError> {
247    write_json_value_file(
248        &input.paths.manifest_validation,
249        &manifest_validation_summary(input.manifest),
250    )?;
251    fs::write(
252        &input.paths.backup_status,
253        serde_json::to_vec_pretty(&input.status)?,
254    )?;
255    fs::write(
256        &input.paths.backup_inspection,
257        serde_json::to_vec_pretty(&input.inspection)?,
258    )?;
259    fs::write(
260        &input.paths.backup_provenance,
261        serde_json::to_vec_pretty(&input.provenance)?,
262    )?;
263    fs::write(
264        &input.paths.backup_integrity,
265        serde_json::to_vec_pretty(&input.integrity)?,
266    )?;
267    fs::write(
268        &input.paths.restore_plan,
269        serde_json::to_vec_pretty(&input.restore_plan)?,
270    )?;
271    fs::write(
272        &input.paths.restore_status,
273        serde_json::to_vec_pretty(&input.restore_status)?,
274    )?;
275    Ok(())
276}
277
278// Build the in-memory preflight report mirrored by preflight-summary.json.
279fn build_preflight_report(input: PreflightReportInput<'_>) -> BackupPreflightReport {
280    let identity = &input.restore_plan.identity_summary;
281    let snapshot = &input.restore_plan.snapshot_summary;
282    let verification = &input.restore_plan.verification_summary;
283    let operation = &input.restore_plan.operation_summary;
284    let ordering = &input.restore_plan.ordering_summary;
285
286    BackupPreflightReport {
287        status: "ready".to_string(),
288        backup_id: input.manifest.backup_id.clone(),
289        backup_dir: input.config.backup_dir.display().to_string(),
290        source_environment: input.manifest.source.environment.clone(),
291        source_root_canister: input.manifest.source.root_canister.clone(),
292        topology_hash: input.manifest.fleet.topology_hash.clone(),
293        mapping_path: input
294            .config
295            .mapping
296            .as_ref()
297            .map(|path| path.display().to_string()),
298        journal_complete: input.status.is_complete,
299        journal_operation_metrics: input.status.operation_metrics.clone(),
300        inspection_status: readiness_status(input.inspection.ready_for_verify).to_string(),
301        provenance_status: consistency_status(
302            input.provenance.backup_id_matches && input.provenance.topology_receipts_match,
303        )
304        .to_string(),
305        backup_id_status: match_status(input.provenance.backup_id_matches).to_string(),
306        topology_receipts_status: match_status(input.provenance.topology_receipts_match)
307            .to_string(),
308        topology_mismatch_count: input.provenance.topology_receipt_mismatches.len(),
309        integrity_verified: input.integrity.verified,
310        manifest_design_v1_ready: input.manifest.design_conformance_report().design_v1_ready,
311        manifest_members: input.manifest.fleet.members.len(),
312        backup_unit_count: input.provenance.backup_unit_count,
313        restore_plan_members: input.restore_plan.member_count,
314        restore_mapping_supplied: identity.mapping_supplied,
315        restore_all_sources_mapped: identity.all_sources_mapped,
316        restore_fixed_members: identity.fixed_members,
317        restore_relocatable_members: identity.relocatable_members,
318        restore_in_place_members: identity.in_place_members,
319        restore_mapped_members: identity.mapped_members,
320        restore_remapped_members: identity.remapped_members,
321        restore_ready: input.restore_plan.readiness_summary.ready,
322        restore_readiness_reasons: input.restore_plan.readiness_summary.reasons.clone(),
323        restore_all_members_have_module_hash: snapshot.all_members_have_module_hash,
324        restore_all_members_have_wasm_hash: snapshot.all_members_have_wasm_hash,
325        restore_all_members_have_code_version: snapshot.all_members_have_code_version,
326        restore_all_members_have_checksum: snapshot.all_members_have_checksum,
327        restore_members_with_module_hash: snapshot.members_with_module_hash,
328        restore_members_with_wasm_hash: snapshot.members_with_wasm_hash,
329        restore_members_with_code_version: snapshot.members_with_code_version,
330        restore_members_with_checksum: snapshot.members_with_checksum,
331        restore_verification_required: verification.verification_required,
332        restore_all_members_have_checks: verification.all_members_have_checks,
333        restore_fleet_checks: verification.fleet_checks,
334        restore_member_check_groups: verification.member_check_groups,
335        restore_member_checks: verification.member_checks,
336        restore_members_with_checks: verification.members_with_checks,
337        restore_total_checks: verification.total_checks,
338        restore_planned_snapshot_uploads: operation
339            .effective_planned_snapshot_uploads(input.restore_plan.member_count),
340        restore_planned_snapshot_loads: operation.planned_snapshot_loads,
341        restore_planned_code_reinstalls: operation.planned_code_reinstalls,
342        restore_planned_verification_checks: operation.planned_verification_checks,
343        restore_planned_operations: operation
344            .effective_planned_operations(input.restore_plan.member_count),
345        restore_planned_phases: operation.planned_phases,
346        restore_phase_count: ordering.phase_count,
347        restore_dependency_free_members: ordering.dependency_free_members,
348        restore_in_group_parent_edges: ordering.in_group_parent_edges,
349        restore_cross_group_parent_edges: ordering.cross_group_parent_edges,
350        manifest_validation_path: input.paths.manifest_validation.display().to_string(),
351        backup_status_path: input.paths.backup_status.display().to_string(),
352        backup_inspection_path: input.paths.backup_inspection.display().to_string(),
353        backup_provenance_path: input.paths.backup_provenance.display().to_string(),
354        backup_integrity_path: input.paths.backup_integrity.display().to_string(),
355        restore_plan_path: input.paths.restore_plan.display().to_string(),
356        restore_status_path: input.paths.restore_status.display().to_string(),
357        preflight_summary_path: input.paths.preflight_summary.display().to_string(),
358    }
359}
360
361// Build the compact preflight summary emitted after all checks pass.
362fn preflight_summary_value(report: &BackupPreflightReport) -> serde_json::Value {
363    let mut summary = serde_json::Map::new();
364    insert_preflight_source_summary(&mut summary, report);
365    insert_preflight_restore_summary(&mut summary, report);
366    insert_preflight_report_paths(&mut summary, report);
367    serde_json::Value::Object(summary)
368}
369
370// Insert one named JSON value into the compact preflight summary.
371fn insert_summary_value(
372    summary: &mut serde_json::Map<String, serde_json::Value>,
373    key: &'static str,
374    value: serde_json::Value,
375) {
376    summary.insert(key.to_string(), value);
377}
378
379// Insert backup source and validation status fields into the summary.
380fn insert_preflight_source_summary(
381    summary: &mut serde_json::Map<String, serde_json::Value>,
382    report: &BackupPreflightReport,
383) {
384    insert_summary_value(summary, "status", json!(report.status));
385    insert_summary_value(summary, "backup_id", json!(report.backup_id));
386    insert_summary_value(summary, "backup_dir", json!(report.backup_dir));
387    insert_summary_value(
388        summary,
389        "source_environment",
390        json!(report.source_environment),
391    );
392    insert_summary_value(
393        summary,
394        "source_root_canister",
395        json!(report.source_root_canister),
396    );
397    insert_summary_value(summary, "topology_hash", json!(report.topology_hash));
398    insert_summary_value(summary, "mapping_path", json!(report.mapping_path));
399    insert_summary_value(summary, "journal_complete", json!(report.journal_complete));
400    insert_summary_value(
401        summary,
402        "journal_operation_metrics",
403        json!(report.journal_operation_metrics),
404    );
405    insert_summary_value(
406        summary,
407        "inspection_status",
408        json!(report.inspection_status),
409    );
410    insert_summary_value(
411        summary,
412        "provenance_status",
413        json!(report.provenance_status),
414    );
415    insert_summary_value(summary, "backup_id_status", json!(report.backup_id_status));
416    insert_summary_value(
417        summary,
418        "topology_receipts_status",
419        json!(report.topology_receipts_status),
420    );
421    insert_summary_value(
422        summary,
423        "topology_mismatch_count",
424        json!(report.topology_mismatch_count),
425    );
426    insert_summary_value(
427        summary,
428        "integrity_verified",
429        json!(report.integrity_verified),
430    );
431    insert_summary_value(
432        summary,
433        "manifest_design_v1_ready",
434        json!(report.manifest_design_v1_ready),
435    );
436    insert_summary_value(summary, "manifest_members", json!(report.manifest_members));
437    insert_summary_value(
438        summary,
439        "backup_unit_count",
440        json!(report.backup_unit_count),
441    );
442}
443
444// Insert restore planning summary fields into the compact preflight summary.
445fn insert_preflight_restore_summary(
446    summary: &mut serde_json::Map<String, serde_json::Value>,
447    report: &BackupPreflightReport,
448) {
449    insert_summary_value(
450        summary,
451        "restore_plan_members",
452        json!(report.restore_plan_members),
453    );
454    insert_summary_value(
455        summary,
456        "restore_mapping_supplied",
457        json!(report.restore_mapping_supplied),
458    );
459    insert_summary_value(
460        summary,
461        "restore_all_sources_mapped",
462        json!(report.restore_all_sources_mapped),
463    );
464    insert_preflight_restore_identity_summary(summary, report);
465    insert_preflight_restore_readiness_summary(summary, report);
466    insert_preflight_restore_snapshot_summary(summary, report);
467    insert_preflight_restore_verification_summary(summary, report);
468    insert_preflight_restore_operation_summary(summary, report);
469    insert_preflight_restore_ordering_summary(summary, report);
470}
471
472// Insert restore identity summary fields into the compact preflight summary.
473fn insert_preflight_restore_identity_summary(
474    summary: &mut serde_json::Map<String, serde_json::Value>,
475    report: &BackupPreflightReport,
476) {
477    insert_summary_value(
478        summary,
479        "restore_fixed_members",
480        json!(report.restore_fixed_members),
481    );
482    insert_summary_value(
483        summary,
484        "restore_relocatable_members",
485        json!(report.restore_relocatable_members),
486    );
487    insert_summary_value(
488        summary,
489        "restore_in_place_members",
490        json!(report.restore_in_place_members),
491    );
492    insert_summary_value(
493        summary,
494        "restore_mapped_members",
495        json!(report.restore_mapped_members),
496    );
497    insert_summary_value(
498        summary,
499        "restore_remapped_members",
500        json!(report.restore_remapped_members),
501    );
502}
503
504// Insert restore readiness summary fields into the compact preflight summary.
505fn insert_preflight_restore_readiness_summary(
506    summary: &mut serde_json::Map<String, serde_json::Value>,
507    report: &BackupPreflightReport,
508) {
509    insert_summary_value(summary, "restore_ready", json!(report.restore_ready));
510    insert_summary_value(
511        summary,
512        "restore_readiness_reasons",
513        json!(report.restore_readiness_reasons),
514    );
515}
516
517// Insert restore snapshot summary fields into the compact preflight summary.
518fn insert_preflight_restore_snapshot_summary(
519    summary: &mut serde_json::Map<String, serde_json::Value>,
520    report: &BackupPreflightReport,
521) {
522    insert_summary_value(
523        summary,
524        "restore_all_members_have_module_hash",
525        json!(report.restore_all_members_have_module_hash),
526    );
527    insert_summary_value(
528        summary,
529        "restore_all_members_have_wasm_hash",
530        json!(report.restore_all_members_have_wasm_hash),
531    );
532    insert_summary_value(
533        summary,
534        "restore_all_members_have_code_version",
535        json!(report.restore_all_members_have_code_version),
536    );
537    insert_summary_value(
538        summary,
539        "restore_all_members_have_checksum",
540        json!(report.restore_all_members_have_checksum),
541    );
542    insert_summary_value(
543        summary,
544        "restore_members_with_module_hash",
545        json!(report.restore_members_with_module_hash),
546    );
547    insert_summary_value(
548        summary,
549        "restore_members_with_wasm_hash",
550        json!(report.restore_members_with_wasm_hash),
551    );
552    insert_summary_value(
553        summary,
554        "restore_members_with_code_version",
555        json!(report.restore_members_with_code_version),
556    );
557    insert_summary_value(
558        summary,
559        "restore_members_with_checksum",
560        json!(report.restore_members_with_checksum),
561    );
562}
563
564// Insert restore verification summary fields into the compact preflight summary.
565fn insert_preflight_restore_verification_summary(
566    summary: &mut serde_json::Map<String, serde_json::Value>,
567    report: &BackupPreflightReport,
568) {
569    insert_summary_value(
570        summary,
571        "restore_verification_required",
572        json!(report.restore_verification_required),
573    );
574    insert_summary_value(
575        summary,
576        "restore_all_members_have_checks",
577        json!(report.restore_all_members_have_checks),
578    );
579    insert_summary_value(
580        summary,
581        "restore_fleet_checks",
582        json!(report.restore_fleet_checks),
583    );
584    insert_summary_value(
585        summary,
586        "restore_member_check_groups",
587        json!(report.restore_member_check_groups),
588    );
589    insert_summary_value(
590        summary,
591        "restore_member_checks",
592        json!(report.restore_member_checks),
593    );
594    insert_summary_value(
595        summary,
596        "restore_members_with_checks",
597        json!(report.restore_members_with_checks),
598    );
599    insert_summary_value(
600        summary,
601        "restore_total_checks",
602        json!(report.restore_total_checks),
603    );
604}
605
606// Insert restore operation summary fields into the compact preflight summary.
607fn insert_preflight_restore_operation_summary(
608    summary: &mut serde_json::Map<String, serde_json::Value>,
609    report: &BackupPreflightReport,
610) {
611    insert_summary_value(
612        summary,
613        "restore_planned_snapshot_uploads",
614        json!(report.restore_planned_snapshot_uploads),
615    );
616    insert_summary_value(
617        summary,
618        "restore_planned_snapshot_loads",
619        json!(report.restore_planned_snapshot_loads),
620    );
621    insert_summary_value(
622        summary,
623        "restore_planned_code_reinstalls",
624        json!(report.restore_planned_code_reinstalls),
625    );
626    insert_summary_value(
627        summary,
628        "restore_planned_verification_checks",
629        json!(report.restore_planned_verification_checks),
630    );
631    insert_summary_value(
632        summary,
633        "restore_planned_operations",
634        json!(report.restore_planned_operations),
635    );
636    insert_summary_value(
637        summary,
638        "restore_planned_phases",
639        json!(report.restore_planned_phases),
640    );
641}
642
643// Insert restore ordering summary fields into the compact preflight summary.
644fn insert_preflight_restore_ordering_summary(
645    summary: &mut serde_json::Map<String, serde_json::Value>,
646    report: &BackupPreflightReport,
647) {
648    insert_summary_value(
649        summary,
650        "restore_phase_count",
651        json!(report.restore_phase_count),
652    );
653    insert_summary_value(
654        summary,
655        "restore_dependency_free_members",
656        json!(report.restore_dependency_free_members),
657    );
658    insert_summary_value(
659        summary,
660        "restore_in_group_parent_edges",
661        json!(report.restore_in_group_parent_edges),
662    );
663    insert_summary_value(
664        summary,
665        "restore_cross_group_parent_edges",
666        json!(report.restore_cross_group_parent_edges),
667    );
668}
669
670// Insert generated report paths into the compact preflight summary.
671fn insert_preflight_report_paths(
672    summary: &mut serde_json::Map<String, serde_json::Value>,
673    report: &BackupPreflightReport,
674) {
675    insert_summary_value(
676        summary,
677        "manifest_validation_path",
678        json!(report.manifest_validation_path),
679    );
680    insert_summary_value(
681        summary,
682        "backup_status_path",
683        json!(report.backup_status_path),
684    );
685    insert_summary_value(
686        summary,
687        "backup_inspection_path",
688        json!(report.backup_inspection_path),
689    );
690    insert_summary_value(
691        summary,
692        "backup_provenance_path",
693        json!(report.backup_provenance_path),
694    );
695    insert_summary_value(
696        summary,
697        "backup_integrity_path",
698        json!(report.backup_integrity_path),
699    );
700    insert_summary_value(
701        summary,
702        "restore_plan_path",
703        json!(report.restore_plan_path),
704    );
705    insert_summary_value(
706        summary,
707        "restore_status_path",
708        json!(report.restore_status_path),
709    );
710    insert_summary_value(
711        summary,
712        "preflight_summary_path",
713        json!(report.preflight_summary_path),
714    );
715}
716
717// Return the stable summary status for inspection readiness.
718const fn readiness_status(ready: bool) -> &'static str {
719    if ready { "ready" } else { "not-ready" }
720}
721
722// Return the stable summary status for provenance consistency.
723const fn consistency_status(consistent: bool) -> &'static str {
724    if consistent {
725        "consistent"
726    } else {
727        "inconsistent"
728    }
729}
730
731// Return the stable summary status for equality checks.
732const fn match_status(matches: bool) -> &'static str {
733    if matches { "matched" } else { "mismatched" }
734}
735
736// Read and decode an optional source-to-target restore mapping from disk.
737fn read_mapping(path: &PathBuf) -> Result<RestoreMapping, BackupPreflightError> {
738    let data = fs::read_to_string(path)?;
739    serde_json::from_str(&data).map_err(BackupPreflightError::from)
740}
741
742// Write one pretty JSON value artifact.
743fn write_json_value_file(
744    path: &PathBuf,
745    value: &serde_json::Value,
746) -> Result<(), BackupPreflightError> {
747    fs::write(path, serde_json::to_vec_pretty(value)?)?;
748    Ok(())
749}