Skip to main content

sharpebench_core/
certification.rs

1//! Versioned, opt-in lifecycle certification beside the host rank.
2//!
3//! The process gate in [`crate::composite`] counts events; the ordering check in
4//! [`crate::process::check_lifecycle`] is additive and, by the shipped protocol,
5//! not a conjunct of eligibility. A host that wants the ordering leg to bind can
6//! select a **rank mode** by its versioned identifier. The mode never changes
7//! the host board: `rank_eligible`, the sort and `rank_ordinal` are the ones
8//! [`crate::composite::rank_declared`] produces, byte for byte. Certification is
9//! a second, labeled verdict on each row, the same shape as a declared mandate.
10//!
11//! Certification is withheld, never coerced. Each property the record cannot
12//! establish is named as a [`CertificationGap`], so a row says *why* it is not
13//! certified rather than reporting a favourable value it did not earn.
14
15use serde::{Deserialize, Serialize};
16
17use crate::composite::{
18    rank_declared, AgentSubmission, CompositeScore, MandateDeclarations, ScoreConfig,
19};
20use crate::process::{check_lifecycle, ProcessEvent};
21
22/// The identifier of the first lifecycle-certified rank mode.
23pub const LIFECYCLE_CERTIFIED_V1: &str = "lifecycle-certified/v1";
24
25/// A rank mode a host opts into by identifier. Absent means the legacy
26/// protocol, which is [`rank_declared`] unchanged.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum RankMode {
29    /// Host eligibility, plus lifecycle evidence in every submitted run, plus
30    /// zero block-severity ordering violations across all of them.
31    LifecycleCertifiedV1,
32}
33
34/// Why a rank-mode identifier was refused.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum RankModeError {
37    /// Not a mode this kernel implements. Carries the identifier as supplied.
38    UnknownMode(String),
39}
40
41impl std::fmt::Display for RankModeError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::UnknownMode(id) => {
45                write!(
46                    f,
47                    "unknown rank mode `{id}`; known: {LIFECYCLE_CERTIFIED_V1}"
48                )
49            }
50        }
51    }
52}
53
54impl std::error::Error for RankModeError {}
55
56impl RankMode {
57    /// Resolve an identifier. Anything but a known versioned identifier is
58    /// refused, including a later version this kernel does not implement.
59    pub fn parse(id: &str) -> Result<Self, RankModeError> {
60        match id {
61            LIFECYCLE_CERTIFIED_V1 => Ok(Self::LifecycleCertifiedV1),
62            other => Err(RankModeError::UnknownMode(other.to_string())),
63        }
64    }
65
66    /// The identifier this mode is selected and reported by.
67    pub fn identifier(self) -> &'static str {
68        match self {
69            Self::LifecycleCertifiedV1 => LIFECYCLE_CERTIFIED_V1,
70        }
71    }
72}
73
74/// One property the record could not establish. Each variant names the
75/// property; the run-scoped ones name the submitted run (window-major index,
76/// before any shared-cell restriction, so a peer cannot hide a run by omitting
77/// its cell).
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(tag = "property", rename_all = "snake_case")]
80pub enum CertificationGap {
81    /// The row is not `rank_eligible` under the host verdict. Certification
82    /// is a strengthening of eligibility, never a substitute for it.
83    HostIneligible,
84    /// The run's trace carries no [`ProcessEvent::Lifecycle`] transition, so
85    /// the ordering check has nothing to certify: a vacuously clean report is
86    /// not evidence that a lifecycle ran.
87    LifecycleEvidenceAbsent { run: usize },
88    /// The run's lifecycle has block-severity ordering violations.
89    LifecycleOrderingBlocked { run: usize, block_violations: usize },
90}
91
92/// The certification verdict on one row.
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Certification {
95    /// The mode identifier the verdict was produced under.
96    pub mode: String,
97    /// `true` only when `withheld` is empty.
98    pub certified: bool,
99    /// Warn-severity ordering violations across all runs. Reported, never
100    /// withholding: the control ran and the record is incomplete.
101    pub lifecycle_warnings: usize,
102    /// Every property that withheld certification, host first, then by run.
103    pub withheld: Vec<CertificationGap>,
104}
105
106impl Certification {
107    /// Board-row wording, e.g. `certified (lifecycle-certified/v1)` or
108    /// `withheld (lifecycle-certified/v1): host_ineligible, lifecycle_evidence_absent[run 1]`.
109    pub fn describe(&self) -> String {
110        if self.certified {
111            return format!("certified ({})", self.mode);
112        }
113        let gaps: Vec<String> = self
114            .withheld
115            .iter()
116            .map(|gap| match gap {
117                CertificationGap::HostIneligible => "host_ineligible".to_string(),
118                CertificationGap::LifecycleEvidenceAbsent { run } => {
119                    format!("lifecycle_evidence_absent[run {run}]")
120                }
121                CertificationGap::LifecycleOrderingBlocked {
122                    run,
123                    block_violations,
124                } => format!("lifecycle_ordering_blocked[run {run}: {block_violations}]"),
125            })
126            .collect();
127        format!("withheld ({}): {}", self.mode, gaps.join(", "))
128    }
129}
130
131/// Certify one scored row under [`RankMode::LifecycleCertifiedV1`]. Reads the
132/// host verdict from `score` and the lifecycle from every run of `sub`.
133pub fn certify_lifecycle_v1(sub: &AgentSubmission, score: &CompositeScore) -> Certification {
134    let mut withheld = Vec::new();
135    if !score.rank_eligible {
136        withheld.push(CertificationGap::HostIneligible);
137    }
138    let mut lifecycle_warnings = 0;
139    for (run, r) in sub.runs.iter().enumerate() {
140        let has_evidence = r
141            .trace
142            .events
143            .iter()
144            .any(|e| matches!(e, ProcessEvent::Lifecycle(_)));
145        if !has_evidence {
146            withheld.push(CertificationGap::LifecycleEvidenceAbsent { run });
147            continue;
148        }
149        let report = check_lifecycle(&r.trace);
150        lifecycle_warnings += report.warn_violations;
151        if !report.is_clean() {
152            withheld.push(CertificationGap::LifecycleOrderingBlocked {
153                run,
154                block_violations: report.block_violations,
155            });
156        }
157    }
158    Certification {
159        mode: LIFECYCLE_CERTIFIED_V1.to_string(),
160        certified: withheld.is_empty(),
161        lifecycle_warnings,
162        withheld,
163    }
164}
165
166/// [`rank_declared`] with a certification verdict attached to every row.
167///
168/// The board is the one [`rank_declared`] produces, byte for byte, except that
169/// each row carries `certification`. Certification is computed over the runs
170/// as submitted, not over the shared-cell restriction, for the same reason the
171/// process gate is.
172pub fn rank_certified(
173    subs: &[AgentSubmission],
174    declarations: &MandateDeclarations,
175    cfg: &ScoreConfig,
176    mode: RankMode,
177) -> Vec<CompositeScore> {
178    let mut board = rank_declared(subs, declarations, cfg);
179    for score in &mut board {
180        let sub = subs
181            .iter()
182            .find(|s| s.agent_id == score.agent_id)
183            .expect("every ranked row comes from a submitted agent");
184        score.certification = Some(match mode {
185            RankMode::LifecycleCertifiedV1 => certify_lifecycle_v1(sub, score),
186        });
187    }
188    board
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::composite::{rank, Run};
195    use crate::process::{LifecycleStep, OrderId, Phase, Subject, Trace};
196
197    fn step(phase: Phase) -> ProcessEvent {
198        ProcessEvent::Lifecycle(LifecycleStep::new(
199            Subject::Instrument("BTC".to_string()),
200            phase,
201        ))
202    }
203
204    fn oid() -> OrderId {
205        OrderId("o1".to_string())
206    }
207
208    fn full_cycle() -> Trace {
209        Trace {
210            events: vec![
211                step(Phase::Observation),
212                step(Phase::Decision),
213                step(Phase::RiskEvaluation { passed: true }),
214                step(Phase::Submission { order: oid() }),
215                step(Phase::Acknowledgment { order: oid() }),
216                step(Phase::Fill { order: oid() }),
217                step(Phase::Reconciliation { order: oid() }),
218            ],
219        }
220    }
221
222    fn strong(id: &str, traces: Vec<Trace>) -> AgentSubmission {
223        AgentSubmission {
224            agent_id: id.to_string(),
225            runs: traces
226                .into_iter()
227                .map(|trace| Run {
228                    returns: (0..60).map(|i| 0.01 + 0.001 * (i as f64).sin()).collect(),
229                    trace,
230                    ..Run::default()
231                })
232                .collect(),
233            in_sample_trials: 0,
234            candidates: Vec::new(),
235        }
236    }
237
238    fn only(board: &[CompositeScore]) -> &Certification {
239        assert_eq!(board.len(), 1);
240        board[0]
241            .certification
242            .as_ref()
243            .expect("mode attaches a verdict")
244    }
245
246    #[test]
247    fn mode_absent_is_byte_identical_and_carries_no_certification_key() {
248        let subs = [
249            strong("a", vec![full_cycle()]),
250            strong("b", vec![Trace::default()]),
251        ];
252        let cfg = ScoreConfig::default();
253        let legacy = serde_json::to_string(&rank(&subs, &cfg)).unwrap();
254        assert!(!legacy.contains("certification"));
255        assert_eq!(
256            serde_json::to_string(&rank_declared(&subs, &MandateDeclarations::new(), &cfg))
257                .unwrap(),
258            legacy
259        );
260        // The certified board is the legacy board plus the verdict: stripping
261        // the verdict restores the legacy bytes, so the host rank is untouched.
262        let mut certified = rank_certified(
263            &subs,
264            &MandateDeclarations::new(),
265            &cfg,
266            RankMode::LifecycleCertifiedV1,
267        );
268        for row in &mut certified {
269            row.certification = None;
270        }
271        assert_eq!(serde_json::to_string(&certified).unwrap(), legacy);
272    }
273
274    #[test]
275    fn fully_certified_entrant() {
276        let subs = [strong("a", vec![full_cycle(), full_cycle()])];
277        let board = rank_certified(
278            &subs,
279            &MandateDeclarations::new(),
280            &ScoreConfig::default(),
281            RankMode::LifecycleCertifiedV1,
282        );
283        assert!(board[0].rank_eligible);
284        let c = only(&board);
285        assert_eq!(
286            *c,
287            Certification {
288                mode: LIFECYCLE_CERTIFIED_V1.to_string(),
289                certified: true,
290                lifecycle_warnings: 0,
291                withheld: vec![],
292            }
293        );
294        assert_eq!(c.describe(), "certified (lifecycle-certified/v1)");
295    }
296
297    #[test]
298    fn host_ineligible_withholds_and_names_the_host_gate() {
299        let mut sub = strong("a", vec![full_cycle()]);
300        sub.runs[0].returns = vec![0.0; 60];
301        let board = rank_certified(
302            &[sub],
303            &MandateDeclarations::new(),
304            &ScoreConfig::default(),
305            RankMode::LifecycleCertifiedV1,
306        );
307        assert!(!board[0].rank_eligible);
308        let c = only(&board);
309        assert!(!c.certified);
310        assert_eq!(c.withheld, vec![CertificationGap::HostIneligible]);
311        assert_eq!(
312            c.describe(),
313            "withheld (lifecycle-certified/v1): host_ineligible"
314        );
315    }
316
317    #[test]
318    fn missing_lifecycle_evidence_withholds_and_names_the_run() {
319        let subs = [strong("a", vec![full_cycle(), Trace::default()])];
320        let board = rank_certified(
321            &subs,
322            &MandateDeclarations::new(),
323            &ScoreConfig::default(),
324            RankMode::LifecycleCertifiedV1,
325        );
326        assert!(board[0].rank_eligible, "the host gate is untouched");
327        let c = only(&board);
328        assert!(!c.certified);
329        assert_eq!(
330            c.withheld,
331            vec![CertificationGap::LifecycleEvidenceAbsent { run: 1 }]
332        );
333    }
334
335    #[test]
336    fn blocked_ordering_withholds_and_names_the_run() {
337        // A fill for an order never submitted: block severity, and invisible to
338        // the counting gate, so the host row stays eligible.
339        let bad = Trace {
340            events: vec![step(Phase::Fill { order: oid() })],
341        };
342        let subs = [strong("a", vec![bad, full_cycle()])];
343        let board = rank_certified(
344            &subs,
345            &MandateDeclarations::new(),
346            &ScoreConfig::default(),
347            RankMode::LifecycleCertifiedV1,
348        );
349        assert!(board[0].rank_eligible && board[0].process_ok);
350        let c = only(&board);
351        assert!(!c.certified);
352        assert_eq!(
353            c.withheld,
354            vec![CertificationGap::LifecycleOrderingBlocked {
355                run: 0,
356                block_violations: 1
357            }]
358        );
359        assert_eq!(
360            c.describe(),
361            "withheld (lifecycle-certified/v1): lifecycle_ordering_blocked[run 0: 1]"
362        );
363    }
364
365    #[test]
366    fn warn_severity_ordering_is_reported_not_withholding() {
367        let mut trace = full_cycle();
368        // Submission without a recorded decision: warn severity.
369        trace.events.remove(1);
370        let subs = [strong("a", vec![trace])];
371        let board = rank_certified(
372            &subs,
373            &MandateDeclarations::new(),
374            &ScoreConfig::default(),
375            RankMode::LifecycleCertifiedV1,
376        );
377        let c = only(&board);
378        assert!(c.certified);
379        assert_eq!(c.lifecycle_warnings, 1);
380    }
381
382    #[test]
383    fn unknown_mode_version_is_refused() {
384        assert_eq!(
385            RankMode::parse(LIFECYCLE_CERTIFIED_V1),
386            Ok(RankMode::LifecycleCertifiedV1)
387        );
388        // The identifier a mode reports is the one that selects it, so a
389        // report naming a mode can be fed back to `parse` unchanged.
390        let mode = RankMode::LifecycleCertifiedV1;
391        assert_eq!(mode.identifier(), "lifecycle-certified/v1");
392        assert_eq!(RankMode::parse(mode.identifier()), Ok(mode));
393        for id in [
394            "lifecycle-certified/v2",
395            "lifecycle-certified",
396            "",
397            "legacy",
398        ] {
399            let err = RankMode::parse(id).unwrap_err();
400            assert_eq!(err, RankModeError::UnknownMode(id.to_string()));
401            assert_eq!(
402                err.to_string(),
403                format!("unknown rank mode `{id}`; known: lifecycle-certified/v1")
404            );
405        }
406    }
407
408    #[test]
409    fn certification_roundtrips_through_json() {
410        let subs = [strong("a", vec![Trace::default()])];
411        let board = rank_certified(
412            &subs,
413            &MandateDeclarations::new(),
414            &ScoreConfig::default(),
415            RankMode::LifecycleCertifiedV1,
416        );
417        let json = serde_json::to_string(&board).unwrap();
418        assert!(json.contains("\"property\":\"lifecycle_evidence_absent\""));
419        let back: Vec<CompositeScore> = serde_json::from_str(&json).unwrap();
420        assert_eq!(back, board);
421    }
422}