Skip to main content

solverforge_solver/stats/
candidate_trace_qualified.rs

1//! Explicit, fail-closed provenance for a qualified diagnostic run.
2//!
3//! Normal candidate tracing accepts optional caller attestations. A benchmark
4//! that claims loaded-binary work equivalence needs more: every immutable input
5//! digest, the exact core tree, the loaded build, and a non-empty external
6//! producer. This value represents that deliberate stronger request without
7//! changing ordinary tracing semantics or deriving any value from callbacks or
8//! working state.
9
10use std::fmt;
11
12use super::{
13    CandidateTraceExternalDigest, CandidateTraceInputAttestation, CandidateTraceInputProvenance,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum CandidateTraceQualificationError {
18    QualificationNotRequested,
19    EmptyExternalProducer,
20    MissingCoreTreeDigest,
21    MissingBuildDigest,
22}
23
24impl fmt::Display for CandidateTraceQualificationError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::QualificationNotRequested => {
28                write!(f, "qualified candidate-trace provenance was not requested")
29            }
30            Self::EmptyExternalProducer => {
31                write!(f, "qualified candidate-trace producer must not be empty")
32            }
33            Self::MissingCoreTreeDigest => write!(
34                f,
35                "qualified candidate-trace provenance requires a core-tree digest"
36            ),
37            Self::MissingBuildDigest => write!(
38                f,
39                "qualified candidate-trace provenance requires a loaded-build digest"
40            ),
41        }
42    }
43}
44
45impl std::error::Error for CandidateTraceQualificationError {}
46
47/// Whether a trace explicitly requested qualified run provenance.
48///
49/// This is intentionally separate from optional input provenance: ordinary
50/// diagnostics may include digests but must not be treated as fail-closed
51/// qualification unless the qualified run entrypoint installed this value.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CandidateTraceQualificationStatus {
54    NotRequested,
55    Qualified,
56}
57
58/// Immutable attestation required before a caller may request a qualified
59/// diagnostic run.
60///
61/// The constructor takes all five source/build digests directly. It does not
62/// read environment variables, serialize a solution, or invoke model code;
63/// a harness obtains its digests before calling SolverForge and passes this
64/// value through the bridge unchanged.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct QualifiedCandidateTraceRunProvenance {
67    input: CandidateTraceInputProvenance,
68}
69
70impl QualifiedCandidateTraceRunProvenance {
71    #[allow(clippy::too_many_arguments)]
72    pub fn externally_attested(
73        schema_digest: CandidateTraceExternalDigest,
74        instance_digest: CandidateTraceExternalDigest,
75        initial_state_digest: CandidateTraceExternalDigest,
76        core_tree_digest: CandidateTraceExternalDigest,
77        build_digest: CandidateTraceExternalDigest,
78        producer: impl Into<String>,
79    ) -> Result<Self, CandidateTraceQualificationError> {
80        let producer = producer.into();
81        if producer.trim().is_empty() {
82            return Err(CandidateTraceQualificationError::EmptyExternalProducer);
83        }
84        Ok(Self {
85            input: CandidateTraceInputProvenance {
86                schema_digest,
87                instance_digest,
88                initial_state_digest,
89                core_tree_digest: Some(core_tree_digest),
90                build_digest: Some(build_digest),
91                attestation: CandidateTraceInputAttestation::external(producer),
92            },
93        })
94    }
95
96    /// Upgrades an existing externally supplied provenance only when it has
97    /// the full immutable attestation a qualified run requires.
98    pub fn try_from_input(
99        input: CandidateTraceInputProvenance,
100    ) -> Result<Self, CandidateTraceQualificationError> {
101        let producer = input.attestation.external_producer();
102        if producer.trim().is_empty() {
103            return Err(CandidateTraceQualificationError::EmptyExternalProducer);
104        }
105        if input.core_tree_digest.is_none() {
106            return Err(CandidateTraceQualificationError::MissingCoreTreeDigest);
107        }
108        if input.build_digest.is_none() {
109            return Err(CandidateTraceQualificationError::MissingBuildDigest);
110        }
111        Ok(Self { input })
112    }
113
114    pub fn input_provenance(&self) -> &CandidateTraceInputProvenance {
115        &self.input
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::stats::{
123        CandidateTraceExecutionPolicy, CandidateTraceHeader, CandidateTracePhasePlan,
124        CandidateTraceQualificationStatus, CandidateTraceTelemetry,
125    };
126
127    fn digest(byte: u8) -> CandidateTraceExternalDigest {
128        CandidateTraceExternalDigest::sha256([byte; 32])
129    }
130
131    fn qualified() -> QualifiedCandidateTraceRunProvenance {
132        QualifiedCandidateTraceRunProvenance::externally_attested(
133            digest(1),
134            digest(2),
135            digest(3),
136            digest(4),
137            digest(5),
138            "solverforge-bench",
139        )
140        .expect("all immutable digests and a producer qualify the run")
141    }
142
143    #[test]
144    fn qualification_rejects_an_empty_producer_before_execution() {
145        assert_eq!(
146            QualifiedCandidateTraceRunProvenance::externally_attested(
147                digest(1),
148                digest(2),
149                digest(3),
150                digest(4),
151                digest(5),
152                "  ",
153            ),
154            Err(CandidateTraceQualificationError::EmptyExternalProducer),
155        );
156    }
157
158    #[test]
159    fn optional_provenance_cannot_silently_upgrade_to_qualified() {
160        let input = CandidateTraceInputProvenance::externally_attested(
161            digest(1),
162            digest(2),
163            digest(3),
164            "solverforge-bench",
165        );
166        assert_eq!(
167            QualifiedCandidateTraceRunProvenance::try_from_input(input),
168            Err(CandidateTraceQualificationError::MissingCoreTreeDigest),
169        );
170    }
171
172    #[test]
173    fn qualified_header_exposes_exact_attestation_and_normal_header_stays_unqualified() {
174        let qualified = qualified();
175        let header = CandidateTraceHeader::new_qualified(
176            "[candidate_trace]\nmax_entries = 1\n".to_string(),
177            CandidateTraceExecutionPolicy::known("test", std::iter::empty::<(String, String)>()),
178            CandidateTracePhasePlan::known(
179                "test",
180                std::iter::empty::<(String, String)>(),
181                Vec::new(),
182            ),
183            qualified.clone(),
184        );
185        let trace = CandidateTraceTelemetry::new(header, 1);
186        assert_eq!(trace.require_qualified_run_provenance(), Ok(&qualified));
187        assert_eq!(
188            trace.provenance_status().qualification,
189            CandidateTraceQualificationStatus::Qualified,
190        );
191
192        let normal = CandidateTraceTelemetry::new(
193            CandidateTraceHeader::new(
194                "[candidate_trace]\nmax_entries = 1\n".to_string(),
195                CandidateTraceExecutionPolicy::known(
196                    "test",
197                    std::iter::empty::<(String, String)>(),
198                ),
199                CandidateTracePhasePlan::known(
200                    "test",
201                    std::iter::empty::<(String, String)>(),
202                    Vec::new(),
203                ),
204                Some(qualified.input_provenance().clone()),
205            ),
206            1,
207        );
208        assert_eq!(
209            normal.require_qualified_run_provenance(),
210            Err(CandidateTraceQualificationError::QualificationNotRequested),
211        );
212        assert_eq!(
213            normal.provenance_status().qualification,
214            CandidateTraceQualificationStatus::NotRequested,
215        );
216    }
217
218    #[test]
219    fn finalizing_a_plan_preserves_qualified_run_attestation() {
220        let qualified = qualified();
221        let mut trace = CandidateTraceTelemetry::new(
222            CandidateTraceHeader::new_qualified(
223                "[candidate_trace]\nmax_entries = 1\n".to_string(),
224                CandidateTraceExecutionPolicy::known(
225                    "test",
226                    std::iter::empty::<(String, String)>(),
227                ),
228                CandidateTracePhasePlan::opaque("test.pending"),
229                qualified.clone(),
230            ),
231            1,
232        );
233
234        let terminal_plan = CandidateTracePhasePlan::known(
235            "test.terminal",
236            std::iter::empty::<(String, String)>(),
237            Vec::new(),
238        );
239        trace.finalize_resolved_phase_plan(terminal_plan.clone());
240
241        assert_eq!(trace.header.resolved_phase_plan, terminal_plan);
242        assert!(trace.header.resolved_phase_plan_complete);
243        assert_eq!(trace.require_qualified_run_provenance(), Ok(&qualified));
244        assert_eq!(
245            trace.provenance_status().qualification,
246            CandidateTraceQualificationStatus::Qualified,
247        );
248    }
249}