Skip to main content

ic_backup/policy/execution_progress/
mod.rs

1//! Pure original-plan progress from exact retained journals; no scheduling or dispatch.
2
3use crate::model::{
4    artifacts::ArtifactChecksumRecord,
5    attempt_journal::{AttemptJournalRecord, AttemptJournalView},
6    operation_plan::MAX_PLAN_OPERATIONS,
7    operation_plan::OperationPlanRecord,
8};
9use serde::Serialize;
10use std::collections::BTreeMap;
11use thiserror::Error;
12
13/// Passive complete journal set supplied by the local evidence-owning caller.
14#[derive(Clone, Debug)]
15pub struct ExecutionProgressRequest<'a> {
16    /// Original immutable declaration; its hash binds every journal's intent.
17    pub plan: &'a OperationPlanRecord,
18    /// Exactly one validated retained journal per declared operation, in any order.
19    pub journals: &'a [&'a AttemptJournalRecord],
20}
21
22/// Derived local attempt condition, without fresh permission or backend qualification.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum OperationProgressState {
26    /// At least one declared prerequisite lacks retained Applied evidence.
27    AwaitingDependencies,
28    /// Prerequisites are Applied and original mutation allowance remains.
29    MutationAvailable,
30    /// No unresolved mutation or Applied receipt, and mutation allowance is exhausted.
31    MutationExhausted,
32    /// A mutation is unresolved and original reconciliation allowance remains.
33    MutationUnresolved,
34    /// An observation reservation remains unresolved, even if its allowance is exhausted.
35    ObservationUnresolved,
36    /// A mutation remains unresolved with no pending observation or remaining observation allowance.
37    ReconciliationExhausted,
38    /// This operation has retained integration-qualified Applied evidence.
39    Applied,
40}
41
42/// Read-only local progress for one exact operation in graph planning order.
43#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
44pub struct OperationProgressView {
45    /// Original opaque operation identity, never an array index.
46    pub operation_sequence: u64,
47    /// Derived local condition; none of these values authorize dispatch.
48    pub state: OperationProgressState,
49    /// Exact retained accounting, authority digest and unresolved attempt identities.
50    pub attempts: AttemptJournalView,
51}
52
53/// Sums over original assigned allowances only; unused plan headroom is excluded.
54#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
55pub struct ExecutionAttemptTotalsView {
56    /// Mutation reservations consumed across all original journals, without refunds.
57    pub mutations_used: u32,
58    /// Observation reservations consumed across all original journals, without refunds.
59    pub observations_used: u32,
60    /// Sum of remaining originally assigned mutation allowances.
61    pub mutations_remaining: u32,
62    /// Sum of remaining originally assigned observation allowances.
63    pub observations_remaining: u32,
64}
65
66/// Read-only projection bound to the complete original declaration and retained journals.
67///
68/// Applied evidence is caller-qualified and does not prove full run completion,
69/// current IC state, artifact durability, application safety or reference release.
70#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
71pub struct ExecutionProgressView {
72    /// Full canonical original plan intent.
73    pub intent: ArtifactChecksumRecord,
74    /// Number of operations with retained Applied receipts, not a terminal proof.
75    pub applied_operations: usize,
76    /// Derived totals over the original per-operation ceilings.
77    pub attempts: ExecutionAttemptTotalsView,
78    /// Exact journal conditions in deterministic graph planning order.
79    pub operations: Vec<OperationProgressView>,
80}
81
82impl ExecutionProgressView {
83    pub(crate) const fn is_unattempted(&self) -> bool {
84        self.attempts.mutations_used == 0 && self.attempts.observations_used == 0
85    }
86
87    pub(crate) fn all_applied(&self) -> bool {
88        self.applied_operations == self.operations.len()
89    }
90}
91
92/// Admit exact complete journal coverage and project causal retained local progress.
93///
94/// Missing journals are errors, never fresh zero-consumption declarations. Any
95/// attempted operation requires Applied evidence for every declared prerequisite.
96/// This validates retained causality, not cross-journal dispatch chronology: no
97/// remote observations, authority probes, writes or scheduling occur.
98///
99/// # Errors
100/// Rejects excessive, duplicate, unknown, missing or mismatched journals, premature
101/// attempts and accounting overflow. Unresolved/exhausted states remain visible.
102pub fn progress(
103    request: &ExecutionProgressRequest<'_>,
104) -> Result<ExecutionProgressView, ExecutionProgressError> {
105    admit_progress(request.plan, request.journals.iter().copied())?.project(request.plan)
106}
107
108// One complete original admission serves projections, reservations and settlements.
109pub(crate) struct AdmittedProgress<'a> {
110    intent: ArtifactChecksumRecord,
111    pub(crate) journals: BTreeMap<u64, &'a AttemptJournalRecord>,
112}
113
114impl AdmittedProgress<'_> {
115    pub(crate) fn dependencies_applied(&self, dependencies: &[u64]) -> bool {
116        dependencies
117            .iter()
118            .all(|sequence| self.journals[sequence].applied_receipt().is_some())
119    }
120
121    pub(crate) fn project(
122        &self,
123        plan: &OperationPlanRecord,
124    ) -> Result<ExecutionProgressView, ExecutionProgressError> {
125        let mut totals = ExecutionAttemptTotalsView {
126            mutations_used: 0,
127            observations_used: 0,
128            mutations_remaining: 0,
129            observations_remaining: 0,
130        };
131        let mut operations = Vec::with_capacity(self.journals.len());
132        for node in plan.ordered_operations() {
133            let sequence = node.operation_sequence();
134            let attempts = self.journals[&sequence].view();
135            totals.add(&attempts)?;
136            operations.push(OperationProgressView {
137                operation_sequence: sequence,
138                state: condition(&attempts, !self.dependencies_applied(node.depends_on())),
139                attempts,
140            });
141        }
142        Ok(ExecutionProgressView {
143            intent: self.intent.clone(),
144            applied_operations: operations
145                .iter()
146                .filter(|operation| operation.attempts.applied)
147                .count(),
148            attempts: totals,
149            operations,
150        })
151    }
152}
153
154pub(crate) fn admit_progress<'a>(
155    plan: &OperationPlanRecord,
156    originals: impl ExactSizeIterator<Item = &'a AttemptJournalRecord>,
157) -> Result<AdmittedProgress<'a>, ExecutionProgressError> {
158    let (intent, journals) = admit_journals(plan, originals)?;
159    // Exact original per-operation budgets already fit the bounded plan totals.
160    // Admission needs no projected authority hashes or allocated operation views.
161    for node in plan.ordered_operations() {
162        let sequence = node.operation_sequence();
163        if journals[&sequence].mutations_used() != 0
164            && let Some(prerequisite) = node
165                .depends_on()
166                .iter()
167                .find(|dependency| journals[*dependency].applied_receipt().is_none())
168        {
169            return Err(ExecutionProgressError::PrematureAttempt {
170                operation_sequence: sequence,
171                prerequisite: *prerequisite,
172            });
173        }
174    }
175    Ok(AdmittedProgress { intent, journals })
176}
177// Admit complete original coverage before deriving any causal state or totals.
178fn admit_journals<'a>(
179    plan: &OperationPlanRecord,
180    originals: impl ExactSizeIterator<Item = &'a AttemptJournalRecord>,
181) -> Result<
182    (
183        ArtifactChecksumRecord,
184        BTreeMap<u64, &'a AttemptJournalRecord>,
185    ),
186    ExecutionProgressError,
187> {
188    if originals.len() > MAX_PLAN_OPERATIONS {
189        return Err(ExecutionProgressError::TooManyJournals);
190    }
191    let intent = plan.digest();
192    let mut journals = BTreeMap::new();
193    for journal in originals {
194        let sequence = journal.authority().binding().operation_sequence();
195        let operation = plan
196            .operation(sequence)
197            .map_err(|_| ExecutionProgressError::UnknownOperation(sequence))?;
198        if journals.contains_key(&sequence) {
199            return Err(ExecutionProgressError::DuplicateJournal(sequence));
200        }
201        if !plan.matches_authority(operation, &intent, journal.authority()) {
202            return Err(ExecutionProgressError::AuthorityMismatch(sequence));
203        }
204        journals.insert(sequence, journal);
205    }
206    for operation in plan.operations() {
207        if !journals.contains_key(&operation.operation_sequence()) {
208            return Err(ExecutionProgressError::MissingJournal(
209                operation.operation_sequence(),
210            ));
211        }
212    }
213    Ok((intent, journals))
214}
215
216impl ExecutionAttemptTotalsView {
217    fn add(&mut self, attempts: &AttemptJournalView) -> Result<(), ExecutionProgressError> {
218        self.mutations_used = sum(self.mutations_used, attempts.mutations_used)?;
219        self.observations_used = sum(self.observations_used, attempts.observations_used)?;
220        self.mutations_remaining = sum(self.mutations_remaining, attempts.mutations_remaining)?;
221        self.observations_remaining =
222            sum(self.observations_remaining, attempts.observations_remaining)?;
223        Ok(())
224    }
225}
226fn sum(left: u32, right: u32) -> Result<u32, ExecutionProgressError> {
227    left.checked_add(right)
228        .ok_or(ExecutionProgressError::AccountingOverflow)
229}
230fn condition(attempts: &AttemptJournalView, unmet_dependencies: bool) -> OperationProgressState {
231    if attempts.applied {
232        OperationProgressState::Applied
233    } else if attempts.pending_observation.is_some() {
234        OperationProgressState::ObservationUnresolved
235    } else if attempts.pending_mutation.is_some() {
236        if attempts.observations_remaining == 0 {
237            OperationProgressState::ReconciliationExhausted
238        } else {
239            OperationProgressState::MutationUnresolved
240        }
241    } else if unmet_dependencies {
242        OperationProgressState::AwaitingDependencies
243    } else if attempts.mutations_remaining == 0 {
244        OperationProgressState::MutationExhausted
245    } else {
246        OperationProgressState::MutationAvailable
247    }
248}
249
250/// Typed original-plan journal admission, retained causality or accounting failure.
251#[derive(Debug, Error, Eq, PartialEq)]
252pub enum ExecutionProgressError {
253    /// The journal input exceeds the original bounded operation count.
254    #[error("execution progress exceeds {MAX_PLAN_OPERATIONS} journals")]
255    TooManyJournals,
256    /// Multiple supplied journals claim the same original operation identity.
257    #[error("duplicate journal for operation {0}")]
258    DuplicateJournal(u64),
259    /// A supplied journal's operation is absent from the original plan.
260    #[error("journal operation {0} is absent from the original plan")]
261    UnknownOperation(u64),
262    /// Retained evidence for an original operation was not supplied.
263    #[error("missing original journal for operation {0}")]
264    MissingJournal(u64),
265    /// Intent, context, target, request or original allowance differs from the plan.
266    #[error("original journal authority mismatch for operation {0}")]
267    AuthorityMismatch(u64),
268    /// A reservation exists while a declared prerequisite lacks Applied evidence.
269    #[error(
270        "operation {operation_sequence} was attempted without applied prerequisite {prerequisite}"
271    )]
272    PrematureAttempt {
273        /// Operation with retained consumed mutation allowance.
274        operation_sequence: u64,
275        /// Exact declared prerequisite lacking retained Applied evidence.
276        prerequisite: u64,
277    },
278    /// Derived accounting could not be represented without overflow.
279    #[error("execution attempt accounting overflow")]
280    AccountingOverflow,
281}
282
283#[cfg(test)]
284mod tests;