Skip to main content

ic_backup/ops/persistence/execution_workflow/progress/
mod.rs

1//! Read-only original workflow inspection through existing plan and journal admission.
2
3use super::{
4    ArtifactChecksumRecord, BINDING_FILE, BackupLayoutGuard, ExecutionProgressPersistenceError,
5    ExecutionStageBindingRecord, ExecutionWorkflowPersistenceError, MAX_EXECUTION_STAGE_BYTES,
6    ancestry::{acquire_stage, read_admitted_binding},
7    read_execution_progress, read_execution_stage_occupancy, read_execution_workflow, read_json,
8};
9use crate::view::execution_workflow::{
10    ExecutionStageProgressState, ExecutionStageProgressView, ExecutionWorkflowProgressView,
11};
12use thiserror::Error;
13
14/// Inspect all original stage journals without calling a provider or changing evidence.
15///
16/// Hold the workflow layout, but no stage/attempt guards. Each occupied stage must
17/// admit its exact binding, ancestors and complete original journal set. Missing
18/// journals or partial stage preparation are typed failures, never unused allowance.
19/// Pending mutation/observation identities remain visible. Applied rows are not
20/// checkpoint, transfer, application or terminal proof. No remote call, write,
21/// repair, retry, artifact read or learned-evidence inference occurs.
22/// # Errors
23/// Rejects unsafe/changed originals, missing journals, contention and invalid accounting.
24pub fn read_execution_workflow_progress(
25    layout: &BackupLayoutGuard,
26    expected: &ArtifactChecksumRecord,
27) -> Result<ExecutionWorkflowProgressView, ExecutionWorkflowProgressError> {
28    let workflow = read_execution_workflow(layout, expected)?;
29    let occupied = read_execution_stage_occupancy(layout, expected)?;
30    let mut stages = Vec::with_capacity(workflow.stages().len());
31    for node in workflow.allocation().graph().ordered_nodes() {
32        let sequence = node.operation_sequence();
33        let state = if occupied.contains(&sequence) {
34            let child = acquire_stage(layout, sequence)?;
35            let binding: ExecutionStageBindingRecord =
36                read_json(&child.root().join(BINDING_FILE), MAX_EXECUTION_STAGE_BYTES)
37                    .map_err(ExecutionWorkflowPersistenceError::from)?;
38            let (binding, plan) =
39                read_admitted_binding(layout, &child, &workflow, sequence, &binding.digest())?;
40            let progress = read_execution_progress(&child, &plan.digest())?;
41            read_admitted_binding(layout, &child, &workflow, sequence, &binding.digest())?;
42            ExecutionStageProgressState::Prepared {
43                binding: binding.digest(),
44                progress: Box::new(progress),
45            }
46        } else {
47            ExecutionStageProgressState::Unprepared
48        };
49        stages.push(ExecutionStageProgressView {
50            stage_sequence: sequence,
51            state,
52        });
53    }
54    if read_execution_stage_occupancy(layout, expected)? != occupied {
55        return Err(ExecutionWorkflowPersistenceError::DigestMismatch.into());
56    }
57    Ok(ExecutionWorkflowProgressView {
58        workflow: expected.clone(),
59        stages,
60    })
61}
62
63/// Local workflow inspection refusal; no original evidence or spending is changed.
64#[derive(Debug, Error)]
65pub enum ExecutionWorkflowProgressError {
66    /// Original workflow, stage, ancestors or filesystem custody failed admission.
67    #[error(transparent)]
68    Stage(#[from] ExecutionWorkflowPersistenceError),
69    /// Complete original journals, accounting or causal progress failed admission.
70    #[error(transparent)]
71    Journals(#[from] ExecutionProgressPersistenceError),
72}