1use std::collections::BTreeSet;
36use std::fmt;
37
38use serde::{Deserialize, Serialize};
39
40use crate::composite::CompositeScore;
41use crate::evidence_coverage::{Coverage, DigestId, EvidenceInventory, PreimageError};
42
43pub const ACCOUNTING_RESIDUAL_TOLERANCE: f64 = 1e-12;
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum ControlProperty {
53 ProtocolAndAccounting,
56 RefusalOfInvalidOrder,
59 EconomicComparator,
62}
63
64impl ControlProperty {
65 pub fn identifier(self) -> &'static str {
67 match self {
68 Self::ProtocolAndAccounting => "protocol_and_accounting",
69 Self::RefusalOfInvalidOrder => "refusal_of_invalid_order",
70 Self::EconomicComparator => "economic_comparator",
71 }
72 }
73}
74
75impl fmt::Display for ControlProperty {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(self.identifier())
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
85#[serde(tag = "observed", rename_all = "snake_case")]
86pub enum ControlObservation {
87 ProtocolAndAccounting {
90 decisions_expected: usize,
91 decisions_round_tripped: usize,
92 accounting_residual: f64,
93 },
94 RefusalOfInvalidOrder {
96 invalid_orders_submitted: usize,
97 refusals_observed: usize,
98 },
99 EconomicComparator {
103 periods_expected: usize,
104 periods_observed: usize,
105 mean_return: f64,
106 },
107}
108
109impl ControlObservation {
110 pub fn property(&self) -> ControlProperty {
112 match self {
113 Self::ProtocolAndAccounting { .. } => ControlProperty::ProtocolAndAccounting,
114 Self::RefusalOfInvalidOrder { .. } => ControlProperty::RefusalOfInvalidOrder,
115 Self::EconomicComparator { .. } => ControlProperty::EconomicComparator,
116 }
117 }
118}
119
120#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct ControlRun {
124 pub control_id: String,
125 pub property: ControlProperty,
128 pub observation: ControlObservation,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
134#[serde(tag = "shortfall", rename_all = "snake_case")]
135pub enum ControlShortfall {
136 ProtocolIncomplete {
139 expected: usize,
140 round_tripped: usize,
141 },
142 AccountingDidNotClose { residual: f64 },
145 RefusalNotExercised,
148 InvalidOrderAccepted { submitted: usize, refused: usize },
151 ComparatorSeriesIncomplete { expected: usize, observed: usize },
154 ComparatorReturnNotFinite { mean_return: f64 },
158}
159
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct ControlVerdict {
166 pub control_id: String,
167 pub property: ControlProperty,
169 pub observation: ControlObservation,
171 pub held: bool,
173 pub shortfalls: Vec<ControlShortfall>,
175 pub detail: String,
178}
179
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182pub struct SuiteControlEvidence {
183 pub controls: Vec<ControlVerdict>,
185 pub all_held: bool,
187}
188
189impl SuiteControlEvidence {
190 pub fn control(&self, control_id: &str) -> Option<&ControlVerdict> {
192 self.controls.iter().find(|c| c.control_id == control_id)
193 }
194
195 pub fn property_held(&self, property: ControlProperty) -> Option<bool> {
198 let mut declared = self
199 .controls
200 .iter()
201 .filter(|c| c.property == property)
202 .peekable();
203 declared.peek()?;
204 Some(declared.all(|c| c.held))
205 }
206
207 pub fn binding(&self) -> Result<ControlBinding, PreimageError> {
214 use sha2::{Digest, Sha256};
215
216 let mut suite = Sha256::new();
217 let mut per_control = Vec::with_capacity(self.controls.len());
218 for verdict in &self.controls {
219 let preimage = control_preimage(verdict)?;
220 suite.update(&preimage);
221 let digest = Sha256::digest(&preimage);
222 per_control.push(ControlDigest {
223 control_id: verdict.control_id.clone(),
224 sha256: crate::lower_hex(&digest),
225 });
226 }
227 let suite = suite.finalize();
228 Ok(ControlBinding {
229 used_by_gate: false,
230 document: SUITE_CONTROL_INVENTORY.document.to_string(),
231 digest: DigestId::RunProvenance,
232 covered_fields: SUITE_CONTROL_INVENTORY
233 .fields_for(DigestId::RunProvenance)
234 .into_iter()
235 .map(str::to_string)
236 .collect(),
237 unbound_fields: SUITE_CONTROL_INVENTORY
238 .exclusions()
239 .into_iter()
240 .map(|(field, reason)| UnboundField {
241 field: field.to_string(),
242 reason: reason.to_string(),
243 })
244 .collect(),
245 sha256: crate::lower_hex(&suite),
246 per_control,
247 })
248 }
249
250 pub fn failed(&self) -> Vec<&str> {
252 self.controls
253 .iter()
254 .filter(|c| !c.held)
255 .map(|c| c.control_id.as_str())
256 .collect()
257 }
258}
259
260#[derive(Clone, Debug, PartialEq, Eq)]
262pub enum ControlError {
263 NoControls,
266 DuplicateControl { control_id: String },
268 PropertyMismatch {
271 control_id: String,
272 declared: ControlProperty,
273 observed: ControlProperty,
274 },
275 ControlRankedAsEntrant { control_id: String },
278 ControlBindingIncomplete { detail: PreimageError },
283}
284
285impl fmt::Display for ControlError {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 match self {
288 Self::NoControls => write!(
289 f,
290 "suite controls: the suite declared no controls; an empty battery \
291 establishes nothing and is not reported as held"
292 ),
293 Self::DuplicateControl { control_id } => write!(
294 f,
295 "suite controls: control `{control_id}` is declared more than once"
296 ),
297 Self::PropertyMismatch {
298 control_id,
299 declared,
300 observed,
301 } => write!(
302 f,
303 "suite controls: control `{control_id}` was declared to establish \
304 `{declared}` but observed `{observed}`, which cannot be evidence for it"
305 ),
306 Self::ControlRankedAsEntrant { control_id } => write!(
307 f,
308 "suite controls: `{control_id}` is a control and also a ranked entrant. \
309 A control is validation of the apparatus, not an attainable score"
310 ),
311 Self::ControlBindingIncomplete { detail } => write!(
312 f,
313 "suite controls: the run_provenance binding does not cover exactly what \
314 SUITE_CONTROL_INVENTORY declares ({detail:?}); a digest that binds a different \
315 field set from the one published beside it is not published"
316 ),
317 }
318 }
319}
320
321impl std::error::Error for ControlError {}
322
323fn shortfalls_of(observation: &ControlObservation) -> Vec<ControlShortfall> {
324 let mut out = Vec::new();
325 match *observation {
326 ControlObservation::ProtocolAndAccounting {
327 decisions_expected,
328 decisions_round_tripped,
329 accounting_residual,
330 } => {
331 if decisions_round_tripped != decisions_expected || decisions_expected == 0 {
332 out.push(ControlShortfall::ProtocolIncomplete {
333 expected: decisions_expected,
334 round_tripped: decisions_round_tripped,
335 });
336 }
337 if !accounting_residual.is_finite()
340 || accounting_residual.abs() > ACCOUNTING_RESIDUAL_TOLERANCE
341 {
342 out.push(ControlShortfall::AccountingDidNotClose {
343 residual: accounting_residual,
344 });
345 }
346 }
347 ControlObservation::RefusalOfInvalidOrder {
348 invalid_orders_submitted,
349 refusals_observed,
350 } => {
351 if invalid_orders_submitted == 0 {
352 out.push(ControlShortfall::RefusalNotExercised);
353 } else if refusals_observed != invalid_orders_submitted {
354 out.push(ControlShortfall::InvalidOrderAccepted {
355 submitted: invalid_orders_submitted,
356 refused: refusals_observed,
357 });
358 }
359 }
360 ControlObservation::EconomicComparator {
370 periods_expected,
371 periods_observed,
372 mean_return,
373 } => {
374 if periods_observed != periods_expected || periods_expected == 0 {
375 out.push(ControlShortfall::ComparatorSeriesIncomplete {
376 expected: periods_expected,
377 observed: periods_observed,
378 });
379 }
380 if !mean_return.is_finite() {
381 out.push(ControlShortfall::ComparatorReturnNotFinite { mean_return });
382 }
383 }
384 }
385 out
386}
387
388fn describe(run: &ControlRun, shortfalls: &[ControlShortfall]) -> String {
389 let outcome = match run.observation {
390 ControlObservation::ProtocolAndAccounting {
391 decisions_expected,
392 decisions_round_tripped,
393 accounting_residual,
394 } => format!(
395 "{decisions_round_tripped}/{decisions_expected} decisions round-tripped, \
396 accounting residual {accounting_residual:e}"
397 ),
398 ControlObservation::RefusalOfInvalidOrder {
399 invalid_orders_submitted,
400 refusals_observed,
401 } => format!("{refusals_observed}/{invalid_orders_submitted} invalid orders refused"),
402 ControlObservation::EconomicComparator {
403 periods_expected,
404 periods_observed,
405 mean_return,
406 } => format!(
407 "{periods_observed}/{periods_expected} periods produced, \
408 mean return {mean_return:.6} (reported, not a condition)"
409 ),
410 };
411 let verdict = if shortfalls.is_empty() {
412 "held".to_string()
413 } else {
414 format!("withheld: {shortfalls:?}")
415 };
416 format!(
417 "`{}` was run to establish `{}`; it observed {outcome}; {verdict}",
418 run.control_id, run.property
419 )
420}
421
422pub fn evaluate_controls(runs: &[ControlRun]) -> Result<SuiteControlEvidence, ControlError> {
427 if runs.is_empty() {
428 return Err(ControlError::NoControls);
429 }
430 let mut seen: BTreeSet<&str> = BTreeSet::new();
431 let mut controls = Vec::with_capacity(runs.len());
432 for run in runs {
433 if !seen.insert(run.control_id.as_str()) {
434 return Err(ControlError::DuplicateControl {
435 control_id: run.control_id.clone(),
436 });
437 }
438 let observed = run.observation.property();
439 if observed != run.property {
440 return Err(ControlError::PropertyMismatch {
441 control_id: run.control_id.clone(),
442 declared: run.property,
443 observed,
444 });
445 }
446 let shortfalls = shortfalls_of(&run.observation);
447 controls.push(ControlVerdict {
448 control_id: run.control_id.clone(),
449 property: run.property,
450 observation: run.observation,
451 held: shortfalls.is_empty(),
452 detail: describe(run, &shortfalls),
453 shortfalls,
454 });
455 }
456 let all_held = controls.iter().all(|c| c.held);
457 Ok(SuiteControlEvidence { controls, all_held })
458}
459
460pub fn bind_to_suite(
467 runs: &[ControlRun],
468 board: &[CompositeScore],
469) -> Result<SuiteControlEvidence, ControlError> {
470 for run in runs {
471 if board.iter().any(|s| s.agent_id == run.control_id) {
472 return Err(ControlError::ControlRankedAsEntrant {
473 control_id: run.control_id.clone(),
474 });
475 }
476 }
477 evaluate_controls(runs)
478}
479
480const COVERED_RUN: Coverage = Coverage::Covered {
481 digest: DigestId::RunProvenance,
482};
483
484pub const SUITE_CONTROL_INVENTORY: EvidenceInventory = EvidenceInventory {
492 document: "sharpebench_core::suite_controls::ControlVerdict",
493 fields: &[
494 ("control_id", COVERED_RUN),
495 ("property", COVERED_RUN),
496 ("observation", COVERED_RUN),
497 ("held", COVERED_RUN),
498 ("shortfalls", COVERED_RUN),
499 (
500 "detail",
501 Coverage::Excluded {
502 reason: "a rendering of control_id, property, observation and shortfalls, all \
503 of which are covered by run_provenance; binding the prose would let a \
504 wording edit break a digest over unchanged evidence",
505 },
506 ),
507 ],
508};
509
510#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
513pub struct UnboundField {
514 pub field: String,
515 pub reason: String,
516}
517
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
520pub struct ControlDigest {
521 pub control_id: String,
522 pub sha256: String,
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
545pub struct ControlBinding {
546 pub used_by_gate: bool,
548 pub document: String,
550 pub digest: DigestId,
552 pub covered_fields: Vec<String>,
554 pub unbound_fields: Vec<UnboundField>,
556 pub sha256: String,
558 pub per_control: Vec<ControlDigest>,
561}
562
563fn render_f64(value: f64) -> String {
568 if value.is_nan() {
569 "nan".to_string()
570 } else if value == f64::INFINITY {
571 "inf".to_string()
572 } else if value == f64::NEG_INFINITY {
573 "-inf".to_string()
574 } else {
575 format!("{value:?}")
576 }
577}
578
579fn render_observation(observation: &ControlObservation) -> String {
580 match *observation {
581 ControlObservation::ProtocolAndAccounting {
582 decisions_expected,
583 decisions_round_tripped,
584 accounting_residual,
585 } => format!(
586 "protocol_and_accounting(decisions_expected={decisions_expected},\
587 decisions_round_tripped={decisions_round_tripped},accounting_residual={})",
588 render_f64(accounting_residual)
589 ),
590 ControlObservation::RefusalOfInvalidOrder {
591 invalid_orders_submitted,
592 refusals_observed,
593 } => format!(
594 "refusal_of_invalid_order(invalid_orders_submitted={invalid_orders_submitted},\
595 refusals_observed={refusals_observed})"
596 ),
597 ControlObservation::EconomicComparator {
598 periods_expected,
599 periods_observed,
600 mean_return,
601 } => format!(
602 "economic_comparator(periods_expected={periods_expected},\
603 periods_observed={periods_observed},mean_return={})",
604 render_f64(mean_return)
605 ),
606 }
607}
608
609fn render_shortfall(shortfall: &ControlShortfall) -> String {
610 match *shortfall {
611 ControlShortfall::ProtocolIncomplete {
612 expected,
613 round_tripped,
614 } => format!("protocol_incomplete(expected={expected},round_tripped={round_tripped})"),
615 ControlShortfall::AccountingDidNotClose { residual } => {
616 format!(
617 "accounting_did_not_close(residual={})",
618 render_f64(residual)
619 )
620 }
621 ControlShortfall::RefusalNotExercised => "refusal_not_exercised()".to_string(),
622 ControlShortfall::InvalidOrderAccepted { submitted, refused } => {
623 format!("invalid_order_accepted(submitted={submitted},refused={refused})")
624 }
625 ControlShortfall::ComparatorSeriesIncomplete { expected, observed } => {
626 format!("comparator_series_incomplete(expected={expected},observed={observed})")
627 }
628 ControlShortfall::ComparatorReturnNotFinite { mean_return } => format!(
629 "comparator_return_not_finite(mean_return={})",
630 render_f64(mean_return)
631 ),
632 }
633}
634
635pub fn control_preimage(verdict: &ControlVerdict) -> Result<Vec<u8>, PreimageError> {
641 let ControlVerdict {
642 control_id,
643 property,
644 observation,
645 held,
646 shortfalls,
647 detail: _,
651 } = verdict;
652 let observation = render_observation(observation);
653 let shortfalls = shortfalls
654 .iter()
655 .map(render_shortfall)
656 .collect::<Vec<_>>()
657 .join(",");
658 SUITE_CONTROL_INVENTORY.preimage(
659 DigestId::RunProvenance,
660 &[
661 ("control_id", control_id.as_str()),
662 ("property", property.identifier()),
663 ("observation", observation.as_str()),
664 ("held", if *held { "true" } else { "false" }),
665 ("shortfalls", shortfalls.as_str()),
666 ],
667 )
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::composite::{rank, AgentSubmission, Run, ScoreConfig};
674
675 fn cash(residual: f64) -> ControlRun {
676 ControlRun {
677 control_id: "cash".to_string(),
678 property: ControlProperty::ProtocolAndAccounting,
679 observation: ControlObservation::ProtocolAndAccounting {
680 decisions_expected: 100,
681 decisions_round_tripped: 100,
682 accounting_residual: residual,
683 },
684 }
685 }
686
687 fn refusal(submitted: usize, refused: usize) -> ControlRun {
688 ControlRun {
689 control_id: "invalid-order".to_string(),
690 property: ControlProperty::RefusalOfInvalidOrder,
691 observation: ControlObservation::RefusalOfInvalidOrder {
692 invalid_orders_submitted: submitted,
693 refusals_observed: refused,
694 },
695 }
696 }
697
698 fn comparator(mean_return: f64) -> ControlRun {
699 ControlRun {
700 control_id: "buy-and-hold".to_string(),
701 property: ControlProperty::EconomicComparator,
702 observation: ControlObservation::EconomicComparator {
703 periods_expected: 250,
704 periods_observed: 250,
705 mean_return,
706 },
707 }
708 }
709
710 #[test]
713 fn an_unprofitable_comparator_still_holds_its_control() {
714 let evidence =
715 evaluate_controls(&[cash(0.0), refusal(3, 3), comparator(-0.004)]).expect("evaluates");
716 let bh = evidence.control("buy-and-hold").expect("declared control");
717 assert!(
718 bh.held,
719 "a complete series from a losing policy still establishes the comparator: {bh:?}"
720 );
721 assert!(bh.shortfalls.is_empty());
722 assert!(evidence.all_held, "{evidence:?}");
723 assert!(bh.detail.contains("economic_comparator"), "{}", bh.detail);
725 assert!(
726 bh.detail.contains("not a condition"),
727 "the row must not read as a skill claim: {}",
728 bh.detail
729 );
730 }
731
732 #[test]
733 fn a_broken_accounting_path_fails_only_its_own_control() {
734 let evidence =
735 evaluate_controls(&[cash(1e-6), refusal(3, 3), comparator(0.002)]).expect("evaluates");
736 assert_eq!(evidence.failed(), vec!["cash"], "{evidence:?}");
737 assert_eq!(
738 evidence.control("cash").expect("declared").shortfalls,
739 vec![ControlShortfall::AccountingDidNotClose { residual: 1e-6 }]
740 );
741 assert_eq!(
742 evidence.property_held(ControlProperty::ProtocolAndAccounting),
743 Some(false)
744 );
745 assert_eq!(
746 evidence.property_held(ControlProperty::RefusalOfInvalidOrder),
747 Some(true),
748 "the refusal path is a different question and answered it"
749 );
750 assert!(!evidence.all_held);
751 }
752
753 #[test]
754 fn a_refusal_path_that_accepted_an_invalid_order_fails_only_its_own_control() {
755 let evidence =
756 evaluate_controls(&[cash(0.0), refusal(3, 2), comparator(0.002)]).expect("evaluates");
757 assert_eq!(evidence.failed(), vec!["invalid-order"], "{evidence:?}");
758 assert_eq!(
759 evidence
760 .control("invalid-order")
761 .expect("declared")
762 .shortfalls,
763 vec![ControlShortfall::InvalidOrderAccepted {
764 submitted: 3,
765 refused: 2,
766 }]
767 );
768 assert_eq!(
769 evidence.property_held(ControlProperty::ProtocolAndAccounting),
770 Some(true),
771 "the accounting path is a different question and answered it"
772 );
773 }
774
775 #[test]
776 fn a_refusal_control_with_nothing_to_refuse_does_not_hold() {
777 let evidence = evaluate_controls(&[refusal(0, 0)]).expect("evaluates");
778 let verdict = evidence.control("invalid-order").expect("declared");
779 assert!(
780 !verdict.held,
781 "a refusal control that refused nothing because nothing was submitted \
782 establishes nothing: {verdict:?}"
783 );
784 assert_eq!(
785 verdict.shortfalls,
786 vec![ControlShortfall::RefusalNotExercised]
787 );
788 }
789
790 #[test]
791 fn an_incomplete_comparator_series_fails_whatever_its_return() {
792 let mut run = comparator(0.05);
793 run.observation = ControlObservation::EconomicComparator {
794 periods_expected: 250,
795 periods_observed: 12,
796 mean_return: 0.05,
797 };
798 let evidence = evaluate_controls(&[run]).expect("evaluates");
799 let verdict = evidence.control("buy-and-hold").expect("declared");
800 assert!(!verdict.held);
801 assert_eq!(
802 verdict.shortfalls,
803 vec![ControlShortfall::ComparatorSeriesIncomplete {
804 expected: 250,
805 observed: 12,
806 }]
807 );
808 }
809
810 #[test]
814 fn a_non_finite_comparator_return_does_not_establish_the_comparator() {
815 for mean in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
816 let evidence = evaluate_controls(&[comparator(mean)]).expect("evaluates");
817 let verdict = evidence.control("buy-and-hold").expect("declared");
818 assert!(
819 !verdict.held,
820 "mean {mean} is not a series anything can be compared against: {verdict:?}"
821 );
822 assert_eq!(
823 verdict.shortfalls.len(),
824 1,
825 "the series itself is complete, so finiteness is the only shortfall: {verdict:?}"
826 );
827 assert!(
828 matches!(
829 verdict.shortfalls[0],
830 ControlShortfall::ComparatorReturnNotFinite { .. }
831 ),
832 "{verdict:?}"
833 );
834 assert!(!evidence.all_held);
835 }
836
837 let losing = evaluate_controls(&[comparator(-0.1)]).expect("evaluates");
841 let verdict = losing.control("buy-and-hold").expect("declared");
842 assert!(
843 verdict.held,
844 "a finite losing mean is a fact about the market, not a broken control: {verdict:?}"
845 );
846 assert!(verdict.shortfalls.is_empty());
847 assert!(losing.all_held);
848 }
849
850 #[test]
851 fn a_protocol_control_that_was_asked_for_nothing_does_not_hold() {
852 let run = ControlRun {
855 control_id: "cash".to_string(),
856 property: ControlProperty::ProtocolAndAccounting,
857 observation: ControlObservation::ProtocolAndAccounting {
858 decisions_expected: 0,
859 decisions_round_tripped: 0,
860 accounting_residual: 0.0,
861 },
862 };
863 let evidence = evaluate_controls(&[run]).expect("evaluates");
864 let verdict = evidence.control("cash").expect("declared");
865 assert!(!verdict.held, "{verdict:?}");
866 assert_eq!(
867 verdict.shortfalls,
868 vec![ControlShortfall::ProtocolIncomplete {
869 expected: 0,
870 round_tripped: 0,
871 }]
872 );
873 }
874
875 #[test]
876 fn a_residual_exactly_at_the_tolerance_still_closes() {
877 let at = evaluate_controls(&[cash(ACCOUNTING_RESIDUAL_TOLERANCE)]).expect("evaluates");
880 assert!(
881 at.control("cash").expect("declared").held,
882 "{:?}",
883 at.control("cash")
884 );
885 let past =
886 evaluate_controls(&[cash(ACCOUNTING_RESIDUAL_TOLERANCE * 2.0)]).expect("evaluates");
887 assert!(!past.control("cash").expect("declared").held);
888 }
889
890 #[test]
891 fn an_observation_of_another_property_is_refused() {
892 let run = ControlRun {
895 control_id: "invalid-order".to_string(),
896 property: ControlProperty::RefusalOfInvalidOrder,
897 observation: ControlObservation::ProtocolAndAccounting {
898 decisions_expected: 100,
899 decisions_round_tripped: 100,
900 accounting_residual: 0.0,
901 },
902 };
903 assert_eq!(
904 evaluate_controls(&[run]),
905 Err(ControlError::PropertyMismatch {
906 control_id: "invalid-order".to_string(),
907 declared: ControlProperty::RefusalOfInvalidOrder,
908 observed: ControlProperty::ProtocolAndAccounting,
909 })
910 );
911 }
912
913 #[test]
914 fn an_empty_battery_is_refused_rather_than_reported_as_held() {
915 assert_eq!(evaluate_controls(&[]), Err(ControlError::NoControls));
916 }
917
918 #[test]
919 fn a_repeated_control_identity_is_refused() {
920 assert_eq!(
921 evaluate_controls(&[cash(0.0), cash(0.0)]),
922 Err(ControlError::DuplicateControl {
923 control_id: "cash".to_string(),
924 })
925 );
926 }
927
928 fn board_with(agent_id: &str) -> Vec<CompositeScore> {
929 rank(
930 &[AgentSubmission {
931 agent_id: agent_id.to_string(),
932 runs: (0..4)
933 .map(|_| Run {
934 returns: (0..60)
935 .map(|i| 0.002 + 0.0005 * (i as f64 * 0.7).sin())
936 .collect(),
937 ..Run::default()
938 })
939 .collect(),
940 ..AgentSubmission::default()
941 }],
942 &ScoreConfig::default(),
943 )
944 }
945
946 #[test]
947 fn a_control_cannot_also_be_a_ranked_entrant() {
948 let board = board_with("buy-and-hold");
949 assert_eq!(
950 bind_to_suite(&[cash(0.0), comparator(0.002)], &board),
951 Err(ControlError::ControlRankedAsEntrant {
952 control_id: "buy-and-hold".to_string(),
953 })
954 );
955 let entrants = board_with("momentum");
957 assert!(bind_to_suite(&[cash(0.0), comparator(0.002)], &entrants).is_ok());
958 }
959
960 #[test]
961 fn a_control_verdict_carries_no_competitive_score() {
962 let evidence = evaluate_controls(&[comparator(0.002)]).expect("evaluates");
963 let json = serde_json::to_value(&evidence.controls[0]).expect("serializes");
964 let keys: BTreeSet<String> = json
965 .as_object()
966 .expect("a verdict serializes as an object")
967 .keys()
968 .cloned()
969 .collect();
970 for forbidden in [
971 "composite",
972 "deflated_sharpe",
973 "psr",
974 "rank_eligible",
975 "rank_ordinal",
976 ] {
977 assert!(
978 !keys.contains(forbidden),
979 "a control verdict must carry no competitive score: found `{forbidden}` in {keys:?}"
980 );
981 }
982 }
983
984 #[test]
985 fn the_control_inventory_matches_the_verdict_it_declares() {
986 let evidence = evaluate_controls(&[cash(1e-6)]).expect("evaluates");
987 let json = serde_json::to_value(&evidence.controls[0]).expect("serializes");
988 let names: Vec<String> = json
989 .as_object()
990 .expect("a verdict serializes as an object")
991 .keys()
992 .cloned()
993 .collect();
994 let audit = SUITE_CONTROL_INVENTORY.audit(&names);
995 assert!(audit.is_complete(), "{audit:?}");
996 assert_eq!(
997 SUITE_CONTROL_INVENTORY.fields_for(DigestId::RunProvenance),
998 vec![
999 "control_id",
1000 "property",
1001 "observation",
1002 "held",
1003 "shortfalls"
1004 ],
1005 "identity, intent and outcome are all bound"
1006 );
1007 }
1008
1009 #[test]
1013 fn a_binding_is_a_digest_over_the_fields_the_inventory_declares() {
1014 let evidence = evaluate_controls(&[cash(0.0), refusal(3, 3)]).expect("evaluates");
1015 let binding = evidence.binding().expect("the controls bind");
1016
1017 assert!(!binding.used_by_gate);
1018 assert_eq!(binding.digest, DigestId::RunProvenance);
1019 assert_eq!(
1020 binding.document,
1021 "sharpebench_core::suite_controls::ControlVerdict"
1022 );
1023 assert_eq!(
1024 binding.covered_fields,
1025 SUITE_CONTROL_INVENTORY.fields_for(DigestId::RunProvenance),
1026 "the published coverage statement is the inventory's, not a second list"
1027 );
1028 assert_eq!(
1029 binding
1030 .unbound_fields
1031 .iter()
1032 .map(|u| u.field.as_str())
1033 .collect::<Vec<_>>(),
1034 vec!["detail"],
1035 "every field that enters nothing is named with its reason"
1036 );
1037 assert!(!binding.unbound_fields[0].reason.is_empty());
1038
1039 assert_eq!(binding.sha256.len(), 64, "{}", binding.sha256);
1040 assert!(binding
1041 .sha256
1042 .bytes()
1043 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)));
1044 assert_eq!(
1045 binding
1046 .per_control
1047 .iter()
1048 .map(|c| c.control_id.as_str())
1049 .collect::<Vec<_>>(),
1050 vec!["cash", "invalid-order"],
1051 "one digest per control, in declared order"
1052 );
1053 assert_ne!(
1054 binding.per_control[0].sha256, binding.per_control[1].sha256,
1055 "two different controls cannot share a digest"
1056 );
1057
1058 let again = evaluate_controls(&[cash(0.0), refusal(3, 3)])
1060 .expect("evaluates")
1061 .binding()
1062 .expect("binds");
1063 assert_eq!(binding.sha256, again.sha256);
1064 }
1065
1066 #[test]
1070 fn a_changed_control_outcome_moves_the_binding() {
1071 let held = evaluate_controls(&[cash(0.0), refusal(3, 3)])
1072 .expect("evaluates")
1073 .binding()
1074 .expect("binds");
1075
1076 let withheld = evaluate_controls(&[cash(0.0), refusal(3, 2)])
1079 .expect("evaluates")
1080 .binding()
1081 .expect("binds");
1082 assert_ne!(held.sha256, withheld.sha256);
1083 assert_eq!(
1084 held.per_control[0].sha256, withheld.per_control[0].sha256,
1085 "the control that did not change keeps its digest, so the reader sees which row moved"
1086 );
1087 assert_ne!(held.per_control[1].sha256, withheld.per_control[1].sha256);
1088
1089 let nudged = evaluate_controls(&[cash(1e-13), refusal(3, 3)])
1092 .expect("evaluates")
1093 .binding()
1094 .expect("binds");
1095 assert!(nudged.per_control[0].sha256 != held.per_control[0].sha256);
1096
1097 let reordered = evaluate_controls(&[refusal(3, 3), cash(0.0)])
1100 .expect("evaluates")
1101 .binding()
1102 .expect("binds");
1103 assert_ne!(held.sha256, reordered.sha256);
1104
1105 let one = evaluate_controls(&[cash(0.0)])
1107 .expect("evaluates")
1108 .binding()
1109 .expect("binds");
1110 assert_ne!(held.sha256, one.sha256);
1111 }
1112
1113 #[test]
1117 fn the_three_non_finite_residuals_bind_to_three_different_digests() {
1118 let digest = |residual: f64| {
1119 evaluate_controls(&[cash(residual)])
1120 .expect("evaluates")
1121 .binding()
1122 .expect("binds")
1123 .sha256
1124 };
1125 let nan = digest(f64::NAN);
1126 let infinite = digest(f64::INFINITY);
1127 let negative_infinite = digest(f64::NEG_INFINITY);
1128 assert_ne!(nan, infinite);
1129 assert_ne!(infinite, negative_infinite);
1130 assert_ne!(nan, negative_infinite);
1131 assert_ne!(nan, digest(0.0));
1133 }
1134
1135 #[test]
1139 fn the_unbound_detail_line_moves_no_digest() {
1140 let mut evidence = evaluate_controls(&[cash(0.0)]).expect("evaluates");
1141 let before = evidence.binding().expect("binds");
1142 evidence.controls[0].detail = "reworded, and binding nothing".to_string();
1143 let after = evidence.binding().expect("binds");
1144 assert_eq!(before.sha256, after.sha256);
1145 assert_eq!(before.per_control, after.per_control);
1146 }
1147
1148 #[test]
1152 fn the_preimage_carries_every_covered_field_and_no_excluded_one() {
1153 let evidence = evaluate_controls(&[refusal(3, 2)]).expect("evaluates");
1154 let verdict = &evidence.controls[0];
1155 let preimage = control_preimage(verdict).expect("the inventory and the renderer agree");
1156 let text = String::from_utf8(preimage).expect("the preimage is text here");
1157
1158 for field in SUITE_CONTROL_INVENTORY.fields_for(DigestId::RunProvenance) {
1159 assert!(
1160 text.contains(field),
1161 "`{field}` is bound but absent: {text:?}"
1162 );
1163 }
1164 assert!(text.contains("invalid-order"), "{text:?}");
1165 assert!(text.contains("refusal_of_invalid_order"), "{text:?}");
1166 assert!(
1167 text.contains("false"),
1168 "the withheld verdict is bound: {text:?}"
1169 );
1170 assert!(
1171 text.contains("invalid_order_accepted(submitted=3,refused=2)"),
1172 "the shortfall's own numbers are bound: {text:?}"
1173 );
1174 assert!(
1175 !text.contains("detail"),
1176 "the excluded prose line must not enter the preimage: {text:?}"
1177 );
1178 assert!(
1179 !text.contains("was run to establish"),
1180 "nor its rendering: {text:?}"
1181 );
1182 }
1183}