Skip to main content

sharpebench_core/
suite_controls.rs

1//! Suite controls: what each control was for, and what it actually showed.
2//!
3//! A control is not an entrant. It is an assertion about the *apparatus*, and
4//! different controls assert different things:
5//!
6//! - a cash or no-op policy establishes that the protocol round-trips and the
7//!   accounting closes,
8//! - a deliberately invalid order establishes that the refusal path refuses,
9//! - a buy-and-hold policy is an economic comparator, and establishes only that
10//!   a comparable return series was produced.
11//!
12//! Collapsing those into one verdict surface loses the question each was asked.
13//! A pipeline that works perfectly while running an unprofitable policy should
14//! report **control held**, because the control was about the pipeline; reading
15//! that as evidence of trading skill is exactly the confusion this module is
16//! built to prevent. Symmetrically, a broken accounting path or a refusal path
17//! that let an invalid order through must fail *its own* control and no other,
18//! so the failure names the component rather than the suite.
19//!
20//! So every control carries three things that travel together into the
21//! evidence: its **identity**, the **property it was intended to establish**,
22//! and the **observed outcome** it actually produced. A verdict is withheld with
23//! a typed [`ControlShortfall`] rather than reported as a favourable value, and
24//! a control with nothing to refuse does not pass vacuously.
25//!
26//! Controls never enter a ranking. A privileged row that is handed a perfect
27//! score because it is the apparatus is a pipeline-validation bypass, not an
28//! attainable ceiling, and publishing one on a competitive board misstates what
29//! the board measures. [`ControlVerdict`] therefore carries no score field at
30//! all, and [`bind_to_suite`] refuses a suite in which a control identity also
31//! appears as a ranked entrant.
32//!
33//! Pure and deterministic: no I/O, no clock, no ambient randomness.
34
35use 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
43/// How far a closed accounting identity may sit from zero and still be closed.
44/// A residual is a sum of signed cash movements that should cancel exactly; this
45/// admits float reduction error and nothing else.
46pub const ACCOUNTING_RESIDUAL_TOLERANCE: f64 = 1e-12;
47
48/// What a control was intended to establish. Each answers a different question,
49/// and a control answers only its own.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum ControlProperty {
53    /// The protocol round-trips and the accounting identity closes. A cash or
54    /// no-op policy: it takes no market risk, so anything it moves is apparatus.
55    ProtocolAndAccounting,
56    /// The refusal path refuses. A deliberately invalid order is submitted and
57    /// must be rejected, every time it is submitted.
58    RefusalOfInvalidOrder,
59    /// A comparable return series was produced. A buy-and-hold policy: an
60    /// economic reference point, never a claim that the apparatus is skilled.
61    EconomicComparator,
62}
63
64impl ControlProperty {
65    /// The property's identifier, as published.
66    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/// What a control run actually produced. One variant per property: an
82/// observation of the wrong shape cannot be offered as evidence for a property
83/// it does not describe.
84#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
85#[serde(tag = "observed", rename_all = "snake_case")]
86pub enum ControlObservation {
87    /// Decisions the harness asked for, decisions that came back, and the
88    /// residual of the accounting identity over the whole control run.
89    ProtocolAndAccounting {
90        decisions_expected: usize,
91        decisions_round_tripped: usize,
92        accounting_residual: f64,
93    },
94    /// Invalid orders deliberately submitted, and refusals observed.
95    RefusalOfInvalidOrder {
96        invalid_orders_submitted: usize,
97        refusals_observed: usize,
98    },
99    /// Return periods the comparator was expected to produce, periods it did
100    /// produce, and its realized mean return. The mean is reported because a
101    /// reader wants it; it is not a condition of the control.
102    EconomicComparator {
103        periods_expected: usize,
104        periods_observed: usize,
105        mean_return: f64,
106    },
107}
108
109impl ControlObservation {
110    /// The property this observation is capable of speaking to.
111    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/// One control as declared and run: identity, intended property, observed
121/// outcome.
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct ControlRun {
124    pub control_id: String,
125    /// What this control was intended to establish, declared independently of
126    /// what it observed.
127    pub property: ControlProperty,
128    pub observation: ControlObservation,
129}
130
131/// Why a control did not establish its property. Each variant names the
132/// component that fell short, never the suite.
133#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
134#[serde(tag = "shortfall", rename_all = "snake_case")]
135pub enum ControlShortfall {
136    /// Decisions went out and did not all come back: the protocol did not
137    /// round-trip.
138    ProtocolIncomplete {
139        expected: usize,
140        round_tripped: usize,
141    },
142    /// The accounting identity did not close within
143    /// [`ACCOUNTING_RESIDUAL_TOLERANCE`].
144    AccountingDidNotClose { residual: f64 },
145    /// No invalid order was submitted, so the refusal path was never exercised.
146    /// A control that ran nothing establishes nothing.
147    RefusalNotExercised,
148    /// An invalid order was accepted. The refusal path is the component at
149    /// fault.
150    InvalidOrderAccepted { submitted: usize, refused: usize },
151    /// The comparator did not produce the return series it was asked for, so it
152    /// is not comparable to anything. Independent of the series' sign.
153    ComparatorSeriesIncomplete { expected: usize, observed: usize },
154    /// The comparator's realized mean return is not a finite number, so no
155    /// usable comparable series was established. Also independent of sign: a
156    /// negative mean is a number and this is not one.
157    ComparatorReturnNotFinite { mean_return: f64 },
158}
159
160/// The verdict on one control.
161///
162/// It has no score field and never gains one: a control is apparatus evidence,
163/// and a number here would be read as a competitive result.
164#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct ControlVerdict {
166    pub control_id: String,
167    /// The property the control was intended to establish.
168    pub property: ControlProperty,
169    /// The outcome it observed.
170    pub observation: ControlObservation,
171    /// True exactly when `shortfalls` is empty.
172    pub held: bool,
173    /// Every way the observation fell short of the intended property.
174    pub shortfalls: Vec<ControlShortfall>,
175    /// One line naming identity, intent and outcome together, for a reader who
176    /// is looking at the row rather than parsing it.
177    pub detail: String,
178}
179
180/// The controls of one suite, kept apart from its entrant rankings.
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182pub struct SuiteControlEvidence {
183    /// One verdict per declared control, in declared order.
184    pub controls: Vec<ControlVerdict>,
185    /// Every control established its own property.
186    pub all_held: bool,
187}
188
189impl SuiteControlEvidence {
190    /// The verdict for one control identity.
191    pub fn control(&self, control_id: &str) -> Option<&ControlVerdict> {
192        self.controls.iter().find(|c| c.control_id == control_id)
193    }
194
195    /// Whether every control declared for `property` held. `None` when the suite
196    /// declared no control for that property, which is not the same as passing.
197    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    /// The `run_provenance` digest over these controls, with the statement of
208    /// what it covers.
209    ///
210    /// The suite digest is taken over the ordered per-control preimages, so
211    /// reordering the controls, dropping one, or changing any covered field of
212    /// any one of them moves it. The excluded `detail` line moves nothing.
213    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    /// The identities of the controls that did not hold, in declared order.
251    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/// Why a suite's controls could not be evaluated.
261#[derive(Clone, Debug, PartialEq, Eq)]
262pub enum ControlError {
263    /// The suite declared no controls at all. An empty battery is not a passing
264    /// one, so it is refused rather than reported as `all_held`.
265    NoControls,
266    /// The same control identity was declared twice.
267    DuplicateControl { control_id: String },
268    /// The observation describes a different property from the declared one, so
269    /// it cannot be evidence for the declared one.
270    PropertyMismatch {
271        control_id: String,
272        declared: ControlProperty,
273        observed: ControlProperty,
274    },
275    /// A control identity also appears as a ranked entrant. Apparatus evidence
276    /// and competitive results are different records and must not share a row.
277    ControlRankedAsEntrant { control_id: String },
278    /// [`SUITE_CONTROL_INVENTORY`] and [`control_preimage`] disagree about which
279    /// fields the `run_provenance` digest covers, so the digest would bind
280    /// something other than what the record declares. Refused rather than
281    /// published under a coverage statement it does not satisfy.
282    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            // Not `!(abs <= tol)`: a NaN residual is not a closed identity, and
338            // every comparison against it is false either way round.
339            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        // The comparator establishes that a comparable series exists. Its sign
361        // and its size are reported and deliberately not conditions: an
362        // unprofitable comparator is a working apparatus running a policy that
363        // lost money, which is a fact about the market, not a broken control.
364        // Finiteness is a different question from sign: a negative or zero mean
365        // is a number a reader can compare against, and a NaN or an infinity is
366        // the arithmetic saying it produced none, which establishes no
367        // comparable series at all. The accounting control above refuses a
368        // non-finite residual for the same reason.
369        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
422/// Evaluate a suite's controls, each against its own declared property.
423///
424/// Refuses an empty battery, a repeated identity, and an observation that
425/// describes a different property from the declared one.
426pub 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
460/// Evaluate a suite's controls and bind them to the suite they belong to,
461/// beside its board rather than inside it.
462///
463/// Refuses a suite in which a control identity is also a ranked entrant. That
464/// row would be a privileged entrant whose result is the apparatus validating
465/// itself, and it would be read off the board as an attainable score.
466pub 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
484/// Coverage declaration for [`ControlVerdict`], the record that binds a
485/// control's identity, its intended property and its observed outcome into the
486/// suite's evidence.
487///
488/// Bound by [`DigestId::RunProvenance`]: a control describes how the suite ran,
489/// not how an agent scored, and it must not move when the field's composition
490/// changes.
491pub 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/// A field the binding deliberately does not cover, with the inventory's stated
511/// reason carried along so a reader does not have to go and find it.
512#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
513pub struct UnboundField {
514    pub field: String,
515    pub reason: String,
516}
517
518/// The digest of one control's covered fields.
519#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
520pub struct ControlDigest {
521    pub control_id: String,
522    pub sha256: String,
523}
524
525/// The digest binding a suite's controls, published with the statement of what
526/// it covers.
527///
528/// [`SUITE_CONTROL_INVENTORY`] declares which [`ControlVerdict`] fields the
529/// `run_provenance` digest covers, and declaring is not binding: a reader
530/// holding the evidence JSON could still not tell whether the row in front of
531/// them is the row that ran. This record closes that gap. It carries the digest
532/// itself beside the fields it was computed over and the fields it was not, so
533/// the statement of coverage and the coverage travel together.
534///
535/// `used_by_gate` is recorded here for the reason [`crate::SharpeDiagnostics`]
536/// records it: this is provenance, and a reader of the JSON alone must not
537/// mistake it for a board column. Nothing here reaches the gate, eligibility or
538/// the rank.
539///
540/// Serialize only, for the reason [`crate::SuiteEvidence`] is: a deserializable
541/// `used_by_gate` would be a field an input document could set to true, and a
542/// deserializable digest would be one an input document could assert rather than
543/// one this crate computed.
544#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
545pub struct ControlBinding {
546    /// Always `false`.
547    pub used_by_gate: bool,
548    /// The document the inventory describes, by type name.
549    pub document: String,
550    /// The digest identity, as the inventory names it.
551    pub digest: DigestId,
552    /// The fields whose values entered the preimage, in inventory order.
553    pub covered_fields: Vec<String>,
554    /// The fields that entered nothing, each with the inventory's reason.
555    pub unbound_fields: Vec<UnboundField>,
556    /// SHA-256 over the ordered per-control preimages.
557    pub sha256: String,
558    /// One digest per control, in declared order, so a reader can tell which
559    /// row moved rather than only that one did.
560    pub per_control: Vec<ControlDigest>,
561}
562
563/// Shortest round-trip rendering, with the three non-finite values named rather
564/// than collapsed. `serde_json` writes every one of them as `null`, so a digest
565/// taken over JSON could not tell a NaN residual from an infinite one, and both
566/// are outcomes a control reports.
567fn 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
635/// The bytes [`SUITE_CONTROL_INVENTORY`] says are bound, for one verdict.
636///
637/// The destructure is load-bearing: a field added to [`ControlVerdict`] fails to
638/// compile here, so a new field cannot join the published record while the
639/// digest silently keeps covering the old set.
640pub fn control_preimage(verdict: &ControlVerdict) -> Result<Vec<u8>, PreimageError> {
641    let ControlVerdict {
642        control_id,
643        property,
644        observation,
645        held,
646        shortfalls,
647        // Declared excluded, and deliberately not supplied below: the inventory
648        // refuses a value for a field it does not bind, so offering the prose
649        // here would be an error rather than a silent inclusion.
650        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    /// The distinction the module exists for: the apparatus worked, the policy
711    /// lost money, and those are different findings.
712    #[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        // And the record says what it does and does not mean.
724        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    /// Sign is not a condition and finiteness is. A mean that is not a number
811    /// establishes no comparable series, and the arm that only counted periods
812    /// reported one as held.
813    #[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        // The control on the control: a comparator that lost money is a working
838        // apparatus, and must keep holding, or the check above would be passing
839        // by refusing every comparator it is shown.
840        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        // Zero decisions out and zero back is a perfect round-trip rate over an
853        // empty run. The same vacuity the refusal control refuses.
854        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        // The tolerance is inclusive: it admits float reduction error at the
878        // bound, and the shortfall begins past it.
879        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        // A passing accounting observation offered as evidence that the refusal
893        // path refuses.
894        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        // The same controls bind cleanly beside a board of actual entrants.
956        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    /// The passing control for the whole binding group. Without it, a `binding`
1010    /// that returned an empty digest, or refused everything, would satisfy every
1011    /// "this moves the digest" assertion below by emitting nothing.
1012    #[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        // Deterministic: the same evidence binds to the same bytes.
1059        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    /// The point of binding rather than only reporting: an outcome that moves
1067    /// moves the digest, so a reader can tell the row in front of them is the
1068    /// row that ran.
1069    #[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        // The refusal path let one invalid order through: `held`, `shortfalls`
1077        // and `observation` all move, and so must the digest.
1078        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        // A residual inside the tolerance still holds its control, and is still
1090        // a different observation from a residual of exactly zero.
1091        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        // Order is part of the suite digest: the same two controls declared the
1098        // other way round are a different declaration.
1099        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        // Dropping a control cannot leave the digest where it was.
1106        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    /// The three non-finite values are outcomes a control reports, and JSON
1114    /// writes all three as `null`. A digest that could not tell them apart would
1115    /// bind less than the record shows.
1116    #[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        // And none of them is the digest of a control that closed.
1132        assert_ne!(nan, digest(0.0));
1133    }
1134
1135    /// The excluded field is excluded: a wording edit to the prose line must not
1136    /// break a digest over unchanged evidence, which is the reason the inventory
1137    /// states for excluding it.
1138    #[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    /// What actually enters the hash, read rather than inferred from the digest
1149    /// moving. A renderer that dropped a covered field would still produce a
1150    /// digest that moves for the other four.
1151    #[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}