axiolid_contracts/
plan.rs

1//! Reproducible operation plans.
2//!
3//! # Why a plan and not just options
4//!
5//! [`ExecutionOptions`] says how to run one operation. It does not say what
6//! was run, what it produced, or whether re-running it would produce the same
7//! thing. A graph evaluated twice under the same options gave no guarantee of
8//! the same result, and nothing recorded which inputs and budgets produced a
9//! given output.
10//!
11//! A [`Plan`] is that missing artifact: the options, plus the recorded
12//! provenance of every step, plus the outcome. Re-executing a plan against the
13//! same inputs must produce the same result, and the plan itself is the
14//! evidence of what was run.
15//!
16//! # Determinism is requested, not assumed
17//!
18//! [`Determinism`] has always been declarable, but nothing read it: a caller
19//! could ask for [`Determinism::Bitwise`] and receive best-effort output with
20//! no indication the request was ignored. A plan closes that hole. Executing a
21//! plan admits the requested level against what the executing provider
22//! actually guarantees, and refuses when the request cannot be met.
23//!
24//! Refusing is the point. Silently accepting a determinism request a provider
25//! cannot honour is exactly the class of quiet wrongness this kernel exists to
26//! avoid: the caller believes it can hash the result and compare it across
27//! machines, and it cannot.
28//!
29//! # Scope
30//!
31//! A plan is an in-process artifact. It is deliberately not serialised: a wire
32//! format is a compatibility promise, and freezing one before the public API
33//! stabilises would commit to a shape the kernel has not finished learning.
34//! Reproducibility is proved by re-executing the same plan, not by persisting
35//! it. Serialisation can be added without changing this contract.
36
37use crate::capability::{BackendId, Operation};
38use crate::error::{GeomError, GeomResult};
39use crate::execution::{Determinism, ExecutionOptions, ScratchRequirement};
40
41/// One recorded step: what ran, where, and under what guarantee.
42///
43/// Provenance survives across operations by accumulating these in order. A
44/// step records the guarantee the provider actually delivered, not the one the
45/// caller asked for, so a plan that ran at a weaker level than requested is
46/// visible after the fact rather than indistinguishable from one that did not.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PlanStep {
49    /// Operation performed.
50    pub operation: Operation,
51    /// Backend that performed it.
52    pub backend: BackendId,
53    /// Determinism the backend actually guaranteed for this step.
54    pub guaranteed: Determinism,
55    /// Stable description of the input family this step consumed.
56    pub input: String,
57}
58
59impl PlanStep {
60    /// Record a step.
61    pub fn new(
62        operation: Operation,
63        backend: BackendId,
64        guaranteed: Determinism,
65        input: &str,
66    ) -> Self {
67        Self {
68            operation,
69            backend,
70            guaranteed,
71            input: input.to_owned(),
72        }
73    }
74}
75
76/// A reproducible operation plan.
77///
78/// Carries the options every step runs under and the provenance of the steps
79/// taken so far. Re-executing the same plan against the same inputs must
80/// produce the same result; the recorded steps are the evidence of what ran.
81#[derive(Debug, Clone, PartialEq)]
82pub struct Plan {
83    options: ExecutionOptions,
84    steps: Vec<PlanStep>,
85}
86
87impl Plan {
88    /// Start a plan from the options every step will run under.
89    #[must_use]
90    pub fn new(options: ExecutionOptions) -> Self {
91        Self {
92            options,
93            steps: Vec::new(),
94        }
95    }
96
97    /// Options every step of this plan runs under.
98    #[must_use]
99    pub fn options(&self) -> &ExecutionOptions {
100        &self.options
101    }
102
103    /// Steps recorded so far, in execution order.
104    #[must_use]
105    pub fn steps(&self) -> &[PlanStep] {
106        &self.steps
107    }
108}
109
110impl Plan {
111    /// Admit and record a step, or refuse it.
112    ///
113    /// Two admissions, both fail-closed:
114    ///
115    /// - The provider's `guaranteed` determinism must be at least the level
116    ///   the plan requested. A provider that cannot meet the request is
117    ///   refused rather than silently downgraded, because the caller has no
118    ///   other way to learn the guarantee it relied on was not delivered.
119    /// - The step's scratch requirement must fit the plan's memory budget.
120    ///   Exhaustion is a typed `BudgetExceeded`, never a silent truncation to
121    ///   a smaller result that still looks plausible.
122    pub fn admit(
123        &mut self,
124        step: PlanStep,
125        scratch: ScratchRequirement,
126        elements: usize,
127    ) -> GeomResult<()> {
128        if !step.guaranteed.satisfies(self.options.determinism()) {
129            return Err(GeomError::BackendContractViolation {
130                backend: step.backend,
131                detail: format!(
132                    "plan requires {:?} determinism, backend guarantees only {:?}",
133                    self.options.determinism(),
134                    step.guaranteed
135                ),
136            });
137        }
138        if !scratch.fits_budget(&self.options, elements) {
139            return Err(GeomError::BudgetExceeded {
140                resource: "plan step scratch memory",
141            });
142        }
143        self.steps.push(step);
144        Ok(())
145    }
146}