1use serde::{Deserialize, Serialize};
16
17use crate::composite::{
18 rank_declared, AgentSubmission, CompositeScore, MandateDeclarations, ScoreConfig,
19};
20use crate::process::{check_lifecycle, ProcessEvent};
21
22pub const LIFECYCLE_CERTIFIED_V1: &str = "lifecycle-certified/v1";
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum RankMode {
29 LifecycleCertifiedV1,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum RankModeError {
37 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 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 pub fn identifier(self) -> &'static str {
68 match self {
69 Self::LifecycleCertifiedV1 => LIFECYCLE_CERTIFIED_V1,
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(tag = "property", rename_all = "snake_case")]
80pub enum CertificationGap {
81 HostIneligible,
84 LifecycleEvidenceAbsent { run: usize },
88 LifecycleOrderingBlocked { run: usize, block_violations: usize },
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Certification {
95 pub mode: String,
97 pub certified: bool,
99 pub lifecycle_warnings: usize,
102 pub withheld: Vec<CertificationGap>,
104}
105
106impl Certification {
107 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
131pub 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
166pub 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 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 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 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 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}