Skip to main content

sharpebench_core/
evidence_coverage.rs

1//! What the digest actually covers, declared in machine-readable form.
2//!
3//! A signed evidence record is only as strong as the statement of *what it
4//! signs*. The failure mode this module exists to prevent is the quiet one: a
5//! digest is computed over most of a record, one field group is left out, and
6//! nothing anywhere says so. A consumer holding the document cannot tell the
7//! signed fields from the unsigned ones by inspection, so the whole record reads
8//! as attested when part of it is not. Nobody has to be dishonest for this to
9//! happen; a field added later that nobody remembers to include produces exactly
10//! the same result.
11//!
12//! So coverage is declared, not implied:
13//!
14//! - Every field of an evidence document carries a [`Coverage`] entry naming the
15//!   digest that binds it, or naming the reason it is deliberately unbound.
16//! - [`EvidenceInventory::audit`] reports fields present in the document but
17//!   missing from the inventory, and fields declared but no longer present. The
18//!   schema tests in this module run that audit against the real structs, so a
19//!   new field fails the build until somebody decides what it is.
20//! - [`EvidenceInventory::preimage`] is the only supported way to build the bytes
21//!   that get hashed. It emits covered fields, emits [`REDACTED`] in place of the
22//!   value of a secret-bearing field, and refuses a value set that does not match
23//!   the declaration.
24//!
25//! Secrets are redacted **before** hashing rather than omitted, so the field's
26//! presence is still bound while its value never enters the preimage. Verifying
27//! a document therefore never requires holding the secret.
28//!
29//! The preimage is a byte string, not a hash: this crate is pure and carries no
30//! cryptographic dependency. Feed the output to a digest function such as
31//! `sharpebench_attest::content_digest`.
32
33use std::collections::BTreeSet;
34
35use serde::Serialize;
36
37/// The token substituted for a redacted value. Fixed and public, so a verifier
38/// reconstructs the preimage without the secret.
39pub const REDACTED: &str = "<redacted>";
40
41/// Which digest binds a field.
42///
43/// Two digests exist because two different things are being attested. An agent's
44/// own scored evidence is reproducible from its submission alone; the
45/// field-relative figures are not, because they move whenever the field's
46/// composition changes. Binding both under one digest would make a re-run with a
47/// new entrant invalidate every previously published per-agent attestation.
48#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
49#[serde(rename_all = "snake_case")]
50pub enum DigestId {
51    /// Fields determined by the agent's own runs and the score configuration.
52    AgentScore,
53    /// Fields determined by the composition of the field the agent was scored in.
54    FieldContext,
55    /// Fields describing how a run was produced: dataset, seeds, harness.
56    RunProvenance,
57}
58
59/// The coverage status of one evidence field.
60#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
61#[serde(tag = "coverage", rename_all = "snake_case")]
62pub enum Coverage {
63    /// The field's value enters the named digest's preimage verbatim.
64    Covered { digest: DigestId },
65    /// The field is bound by the named digest, but as [`REDACTED`] rather than
66    /// as its value, because the value is secret. `reason` states why.
67    Redacted {
68        digest: DigestId,
69        reason: &'static str,
70    },
71    /// The field enters no digest at all. `reason` states why, and a reason is
72    /// mandatory: an exclusion nobody can justify is a bug, not a policy.
73    Excluded { reason: &'static str },
74}
75
76impl Coverage {
77    /// The digest binding this field, if any.
78    pub fn digest(&self) -> Option<DigestId> {
79        match self {
80            Coverage::Covered { digest } | Coverage::Redacted { digest, .. } => Some(*digest),
81            Coverage::Excluded { .. } => None,
82        }
83    }
84
85    /// Whether the field's value (as opposed to its presence) is bound.
86    pub fn binds_value(&self) -> bool {
87        matches!(self, Coverage::Covered { .. })
88    }
89}
90
91/// A declaration of what is and is not covered, for one evidence document.
92#[derive(Clone, Copy, Debug)]
93pub struct EvidenceInventory {
94    /// The document this inventory describes, by type name.
95    pub document: &'static str,
96    /// Every field of the document, in serialization order.
97    pub fields: &'static [(&'static str, Coverage)],
98}
99
100/// What an inventory audit found. Empty on both counts is the passing state.
101#[derive(Clone, Debug, Default, Serialize, PartialEq)]
102pub struct InventoryAudit {
103    /// Fields present in the document but absent from the inventory. Each of
104    /// these is a field a consumer cannot classify.
105    pub undeclared: Vec<String>,
106    /// Fields declared by the inventory but no longer present in the document.
107    pub stale: Vec<String>,
108}
109
110impl InventoryAudit {
111    /// Whether the inventory matches the document exactly.
112    pub fn is_complete(&self) -> bool {
113        self.undeclared.is_empty() && self.stale.is_empty()
114    }
115}
116
117/// A value set that does not match the inventory.
118#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
119#[serde(tag = "error", rename_all = "snake_case")]
120pub enum PreimageError {
121    /// A value was supplied for a field the inventory does not declare.
122    UndeclaredField { field: String },
123    /// A field bound by the requested digest had no value supplied.
124    MissingField { field: String },
125    /// The same field was supplied more than once.
126    DuplicateField { field: String },
127}
128
129impl EvidenceInventory {
130    /// The coverage declared for `field`, or `None` if it is undeclared.
131    pub fn coverage(&self, field: &str) -> Option<Coverage> {
132        self.fields
133            .iter()
134            .find(|(name, _)| *name == field)
135            .map(|(_, c)| *c)
136    }
137
138    /// Field names bound by `digest`, in serialization order. Includes redacted
139    /// fields, whose presence is bound even though their value is not.
140    pub fn fields_for(&self, digest: DigestId) -> Vec<&'static str> {
141        self.fields
142            .iter()
143            .filter(|(_, c)| c.digest() == Some(digest))
144            .map(|(name, _)| *name)
145            .collect()
146    }
147
148    /// Field names declared as covered by no digest, with their stated reasons.
149    pub fn exclusions(&self) -> Vec<(&'static str, &'static str)> {
150        self.fields
151            .iter()
152            .filter_map(|(name, c)| match c {
153                Coverage::Excluded { reason } => Some((*name, *reason)),
154                Coverage::Covered { .. } | Coverage::Redacted { .. } => None,
155            })
156            .collect()
157    }
158
159    /// Compare the inventory against the field names a document actually has.
160    ///
161    /// This is what keeps the declaration from drifting: run it in a test with
162    /// the serialized field names of the real struct.
163    pub fn audit<S: AsRef<str>>(&self, observed: &[S]) -> InventoryAudit {
164        let declared: BTreeSet<&str> = self.fields.iter().map(|(n, _)| *n).collect();
165        let present: BTreeSet<&str> = observed.iter().map(|s| s.as_ref()).collect();
166        InventoryAudit {
167            undeclared: present
168                .difference(&declared)
169                .map(|s| (*s).to_string())
170                .collect(),
171            stale: declared
172                .difference(&present)
173                .map(|s| (*s).to_string())
174                .collect(),
175        }
176    }
177
178    /// Build the canonical bytes to hash for one digest.
179    ///
180    /// `values` supplies every field of the document as an already-rendered
181    /// string; the caller owns the rendering so that number formatting stays a
182    /// decision of the producer rather than of this module. Fields bound by
183    /// other digests, and excluded fields, are simply not consulted.
184    ///
185    /// The output is `name\x1fvalue\x1e` per field, in inventory order, with the
186    /// document name and digest as the first record. The unit separators cannot
187    /// appear in a field name, so no value can be crafted to look like a field
188    /// boundary.
189    ///
190    /// Fails when a value is supplied for an undeclared field (the inventory is
191    /// out of date), when a bound field has no value (the digest would silently
192    /// cover less than it claims), or when a field is supplied twice.
193    pub fn preimage(
194        &self,
195        digest: DigestId,
196        values: &[(&str, &str)],
197    ) -> Result<Vec<u8>, PreimageError> {
198        let mut seen: BTreeSet<&str> = BTreeSet::new();
199        for (name, _) in values {
200            if self.coverage(name).is_none() {
201                return Err(PreimageError::UndeclaredField {
202                    field: (*name).to_string(),
203                });
204            }
205            if !seen.insert(name) {
206                return Err(PreimageError::DuplicateField {
207                    field: (*name).to_string(),
208                });
209            }
210        }
211
212        let mut out = Vec::new();
213        push_record(&mut out, "sharpebench.document", self.document);
214        push_record(&mut out, "sharpebench.digest", digest_name(digest));
215
216        for (name, coverage) in self.fields {
217            if coverage.digest() != Some(digest) {
218                continue;
219            }
220            let Some((_, value)) = values.iter().find(|(n, _)| n == name) else {
221                return Err(PreimageError::MissingField {
222                    field: (*name).to_string(),
223                });
224            };
225            match coverage {
226                Coverage::Covered { .. } => push_record(&mut out, name, value),
227                Coverage::Redacted { .. } => push_record(&mut out, name, REDACTED),
228                Coverage::Excluded { .. } => unreachable!("filtered by digest match"),
229            }
230        }
231        Ok(out)
232    }
233}
234
235fn push_record(out: &mut Vec<u8>, name: &str, value: &str) {
236    out.extend_from_slice(name.as_bytes());
237    out.push(0x1f);
238    out.extend_from_slice(value.as_bytes());
239    out.push(0x1e);
240}
241
242fn digest_name(d: DigestId) -> &'static str {
243    match d {
244        DigestId::AgentScore => "agent_score",
245        DigestId::FieldContext => "field_context",
246        DigestId::RunProvenance => "run_provenance",
247    }
248}
249
250const COVERED_SCORE: Coverage = Coverage::Covered {
251    digest: DigestId::AgentScore,
252};
253const COVERED_FIELD: Coverage = Coverage::Covered {
254    digest: DigestId::FieldContext,
255};
256
257/// Coverage declaration for [`crate::composite::CompositeScore`], the per-agent
258/// evidence record the leaderboard publishes.
259///
260/// Split by what determines the value. `AgentScore` fields come out of the
261/// agent's own runs and the score configuration; `FieldContext` fields come out
262/// of the composition of the field it was scored against and change when another
263/// entrant is added. Four fields are bound by neither, each with its reason.
264pub const COMPOSITE_SCORE_INVENTORY: EvidenceInventory = EvidenceInventory {
265    document: "sharpebench_core::composite::CompositeScore",
266    fields: &[
267        ("agent_id", COVERED_SCORE),
268        ("deflated_sharpe", COVERED_SCORE),
269        ("psr", COVERED_SCORE),
270        ("passed_k", COVERED_SCORE),
271        ("process_ok", COVERED_SCORE),
272        ("bootstrap_p", COVERED_SCORE),
273        ("bootstrap_error", COVERED_SCORE),
274        ("deflation_error", COVERED_SCORE),
275        ("selection_error", COVERED_SCORE),
276        ("raw_mean_return", COVERED_SCORE),
277        ("rank_eligible", COVERED_SCORE),
278        ("composite", COVERED_SCORE),
279        ("alpha", COVERED_FIELD),
280        ("beta", COVERED_FIELD),
281        ("calibration_brier", COVERED_SCORE),
282        ("calibration_observations", COVERED_SCORE),
283        ("edge_half_life", COVERED_SCORE),
284        ("field_reality_check_p", COVERED_FIELD),
285        ("max_drawdown", COVERED_SCORE),
286        ("mandate_ok", COVERED_SCORE),
287        ("worst_run_drawdown", COVERED_SCORE),
288        ("turnover", COVERED_SCORE),
289        ("pareto_optimal", COVERED_FIELD),
290        ("step_down_significant", COVERED_FIELD),
291        ("confidence_weighted_return", COVERED_SCORE),
292        ("cost", COVERED_SCORE),
293        ("return_per_cost", COVERED_SCORE),
294        ("field_spa_p", COVERED_FIELD),
295        ("field_spa_consistent_p", COVERED_FIELD),
296        ("field_significance_benchmark", COVERED_FIELD),
297        ("field_crowdedness", COVERED_FIELD),
298        ("field_crowdedness_peers", COVERED_FIELD),
299        ("in_sample_trials", COVERED_SCORE),
300        ("effective_n_trials", COVERED_SCORE),
301        ("dsr_percentile", COVERED_FIELD),
302        ("selection_median_dsr", COVERED_SCORE),
303        ("selection_gap", COVERED_SCORE),
304        ("rank_ordinal", COVERED_FIELD),
305        ("rolling_min_sharpe", COVERED_SCORE),
306        ("rolling_frac_positive", COVERED_SCORE),
307        ("rolling_windows", COVERED_SCORE),
308        ("sortino", COVERED_SCORE),
309        ("downside_deviation", COVERED_SCORE),
310        ("dsr_per_cost", COVERED_SCORE),
311        ("process_floored", COVERED_SCORE),
312        ("realized_floored_return", COVERED_SCORE),
313        ("dsr_ci_low", COVERED_SCORE),
314        ("dsr_ci_high", COVERED_SCORE),
315        ("dsr_se", COVERED_SCORE),
316        ("tie_group", COVERED_FIELD),
317        ("dsr_tied", COVERED_FIELD),
318        ("trials_sr_std", COVERED_SCORE),
319        ("trials_sr_std_annualized", COVERED_SCORE),
320        (
321            "trials_sr_std_annualized_equivalent",
322            Coverage::Excluded {
323                reason: "presentation unit of trials_sr_std, which is covered by agent_score; \
324                         a fixed conversion carries no evidence the covered value does not, \
325                         and binding it would let a units edit break a digest over unchanged \
326                         evidence",
327            },
328        ),
329        ("deflation_bar_per_period", COVERED_SCORE),
330        (
331            "deflation_bar_annualized_equivalent",
332            Coverage::Excluded {
333                reason: "presentation unit of deflation_bar_per_period, which is covered by \
334                         agent_score; excluded for the reason given on \
335                         trials_sr_std_annualized_equivalent",
336            },
337        ),
338        ("deflation_null_mean_per_period", COVERED_SCORE),
339        ("pooled_observations", COVERED_SCORE),
340        ("trials_sr_std_source", COVERED_SCORE),
341        // A fixed-shape record like declared_mandate, and set by the field's
342        // composition like pareto_optimal, so it binds under field_context.
343        ("trials_sr_std_most_influential_vote", COVERED_FIELD),
344        ("runs_submitted", COVERED_SCORE),
345        ("runs_scored", COVERED_SCORE),
346        ("process_score", COVERED_SCORE),
347        ("process_warnings", COVERED_SCORE),
348        ("econ_rationality_score", COVERED_SCORE),
349        ("econ_dominance_violations", COVERED_SCORE),
350        (
351            "role_contributions",
352            Coverage::Excluded {
353                reason: "a variable-length nested structure whose element type carries its own \
354                         fields; binding it through this flat inventory would go stale silently \
355                         the moment RoleContribution gains a field, which is the exact failure \
356                         this inventory exists to prevent. It needs its own inventory before it \
357                         can be bound",
358            },
359        ),
360        ("declared_mandate", COVERED_SCORE),
361        ("verdict_applied", COVERED_SCORE),
362        ("declared_passed_k", COVERED_SCORE),
363        ("declared_mandate_eligible", COVERED_FIELD),
364        ("declared_mandate_ordinal", COVERED_FIELD),
365        (
366            "certification",
367            Coverage::Excluded {
368                reason: "a nested record whose withheld list is variable-length and whose \
369                         elements carry their own fields; binding it through this flat \
370                         inventory would go stale silently the moment Certification or \
371                         CertificationGap gains a field, the failure given on \
372                         role_contributions. It is a second, labeled verdict filled only under \
373                         an opt-in rank mode, and needs its own inventory before it can be \
374                         bound",
375            },
376        ),
377    ],
378};
379
380/// How one scored run was produced.
381///
382/// Distinct from the score itself: two identical scores produced from different
383/// datasets, seeds or harness builds are not the same evidence, and a digest
384/// over the score alone cannot tell them apart.
385///
386/// `dataset_canary` is the leak tripwire bound into a held-out set. It is the
387/// one field here that must never be recoverable from a published preimage, and
388/// it is declared [`Coverage::Redacted`] for that reason.
389#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
390pub struct RunProvenance {
391    pub agent_id: String,
392    pub dataset_id: String,
393    /// Content hash of the frozen dataset bytes.
394    pub dataset_content_hash: String,
395    /// The canary GUID embedded in the held-out set. Secret.
396    pub dataset_canary: String,
397    pub window_id: String,
398    pub seed: u64,
399    pub cost_profile: String,
400    pub harness_version: String,
401}
402
403/// Coverage declaration for [`RunProvenance`].
404pub const RUN_PROVENANCE_INVENTORY: EvidenceInventory = EvidenceInventory {
405    document: "sharpebench_core::evidence_coverage::RunProvenance",
406    fields: &[
407        (
408            "agent_id",
409            Coverage::Covered {
410                digest: DigestId::RunProvenance,
411            },
412        ),
413        (
414            "dataset_id",
415            Coverage::Covered {
416                digest: DigestId::RunProvenance,
417            },
418        ),
419        (
420            "dataset_content_hash",
421            Coverage::Covered {
422                digest: DigestId::RunProvenance,
423            },
424        ),
425        (
426            "dataset_canary",
427            Coverage::Redacted {
428                digest: DigestId::RunProvenance,
429                reason: "the canary is a leak tripwire: publishing a preimage that contains it \
430                         hands an agent the token whose appearance in training data is the \
431                         evidence of contamination. Binding its presence is enough, and \
432                         verification must not require holding it",
433            },
434        ),
435        (
436            "window_id",
437            Coverage::Covered {
438                digest: DigestId::RunProvenance,
439            },
440        ),
441        (
442            "seed",
443            Coverage::Covered {
444                digest: DigestId::RunProvenance,
445            },
446        ),
447        (
448            "cost_profile",
449            Coverage::Covered {
450                digest: DigestId::RunProvenance,
451            },
452        ),
453        (
454            "harness_version",
455            Coverage::Covered {
456                digest: DigestId::RunProvenance,
457            },
458        ),
459    ],
460};
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use crate::composite::{
466        rank_declared, AgentSubmission, CompositeScore, DeclaredMandate, MandateDeclarations, Run,
467        ScoreConfig,
468    };
469    use crate::entrant_visibility::declared_struct_fields;
470    use crate::process::Trace;
471
472    /// Top-level field names of a serialized value.
473    fn field_names<T: Serialize>(v: &T) -> Vec<String> {
474        let json = serde_json::to_value(v).expect("value serializes");
475        json.as_object()
476            .expect("evidence documents serialize as objects")
477            .keys()
478            .cloned()
479            .collect()
480    }
481
482    fn probe_submission(id: &str, mean: f64) -> AgentSubmission {
483        let runs: Vec<Run> = (0..5)
484            .map(|_| Run {
485                returns: (0..60)
486                    .map(|i| mean + 0.0005 * (i as f64 * 0.7).sin())
487                    .collect(),
488                trace: Trace::default(),
489                confidences: Vec::new(),
490                outcomes: Vec::new(),
491                cost: 1.0,
492            })
493            .collect();
494        AgentSubmission {
495            agent_id: id.to_string(),
496            runs,
497            in_sample_trials: 0,
498            candidates: Vec::new(),
499        }
500    }
501
502    /// Every field name a `CompositeScore` can carry.
503    ///
504    /// The authority is the declared field list serde hands to
505    /// `deserialize_struct`, which includes fields a `skip_serializing_if`
506    /// hides. The serialized keys of the probes are added on top, so a field
507    /// that serializes under a name the struct does not declare also fails. The
508    /// probes alone missed `selection_error` and `certification`: no probe
509    /// declared candidates or selected a rank mode, so both stayed `None` and
510    /// were never serialized.
511    fn observed_composite_score_fields() -> Vec<String> {
512        let subs = vec![
513            probe_submission("declared-probe", 0.002),
514            probe_submission("undeclared-probe", 0.0018),
515        ];
516        let mut declarations = MandateDeclarations::new();
517        declarations.insert(
518            "declared-probe".to_string(),
519            DeclaredMandate::AbsoluteReturn,
520        );
521        let mut scored = rank_declared(&subs, &declarations, &ScoreConfig::default());
522        scored.extend(rank_declared(
523            &subs,
524            &declarations,
525            &ScoreConfig {
526                n_boot: 0,
527                ..ScoreConfig::default()
528            },
529        ));
530
531        let mut names: BTreeSet<String> = declared_struct_fields::<CompositeScore>()
532            .iter()
533            .map(|name| (*name).to_string())
534            .collect();
535        for s in &scored {
536            names.extend(field_names(s));
537        }
538        names.into_iter().collect()
539    }
540
541    // -----------------------------------------------------------------------
542    // The drift guards. These are the tests that fail on a new field.
543    // -----------------------------------------------------------------------
544
545    #[test]
546    fn every_composite_score_field_is_declared_covered_or_excluded() {
547        let names = observed_composite_score_fields();
548        let audit = COMPOSITE_SCORE_INVENTORY.audit(&names);
549        assert!(
550            audit.is_complete(),
551            "CompositeScore and its coverage inventory have diverged.\n\
552             Fields present but undeclared: {:?}\n\
553             Fields declared but absent: {:?}\n\
554             Add each new field to COMPOSITE_SCORE_INVENTORY as Covered {{ digest }} \
555             or Excluded {{ reason }}.",
556            audit.undeclared,
557            audit.stale
558        );
559    }
560
561    #[test]
562    fn every_run_provenance_field_is_declared_covered_or_excluded() {
563        let p = RunProvenance {
564            agent_id: "a".to_string(),
565            dataset_id: "us-indices-1d".to_string(),
566            dataset_content_hash: "abc".to_string(),
567            dataset_canary: "canary-token".to_string(),
568            window_id: "w3".to_string(),
569            seed: 7,
570            cost_profile: "retail".to_string(),
571            harness_version: "0.11.0".to_string(),
572        };
573        let audit = RUN_PROVENANCE_INVENTORY.audit(&field_names(&p));
574        assert!(audit.is_complete(), "{audit:?}");
575    }
576
577    #[test]
578    fn a_new_undeclared_field_fails_the_audit() {
579        // The guard above passes only because the inventory is current. Prove it
580        // can actually fail: hand it a document with one extra field.
581        let mut names = observed_composite_score_fields();
582        names.push("newly_added_metric".to_string());
583        let audit = COMPOSITE_SCORE_INVENTORY.audit(&names);
584        assert!(!audit.is_complete());
585        assert_eq!(audit.undeclared, vec!["newly_added_metric".to_string()]);
586        assert!(audit.stale.is_empty());
587    }
588
589    /// `selection_error` and `certification` are absent from every serialized
590    /// probe row, and the drift guard still classifies them.
591    #[test]
592    fn fields_a_skipped_none_hides_are_still_classified() {
593        let subs = vec![probe_submission("probe", 0.002)];
594        let serialized: BTreeSet<String> =
595            rank_declared(&subs, &MandateDeclarations::new(), &ScoreConfig::default())
596                .iter()
597                .flat_map(field_names)
598                .collect();
599        for hidden in ["selection_error", "certification"] {
600            assert!(!serialized.contains(hidden), "{hidden} was serialized");
601            assert!(observed_composite_score_fields().contains(&hidden.to_string()));
602        }
603        assert_eq!(
604            COMPOSITE_SCORE_INVENTORY.coverage("selection_error"),
605            Some(COVERED_SCORE),
606            "a diagnostic of the agent's own candidates, like bootstrap_error and \
607             deflation_error"
608        );
609        assert!(matches!(
610            COMPOSITE_SCORE_INVENTORY.coverage("certification"),
611            Some(Coverage::Excluded { .. })
612        ));
613
614        // Covered means the preimage builder really emits it.
615        let values: Vec<(&str, &str)> = COMPOSITE_SCORE_INVENTORY
616            .fields
617            .iter()
618            .map(|(n, _)| (*n, "0"))
619            .collect();
620        let bytes = COMPOSITE_SCORE_INVENTORY
621            .preimage(DigestId::AgentScore, &values)
622            .unwrap();
623        let record = b"selection_error\x1f0\x1e";
624        assert!(bytes.windows(record.len()).any(|w| w == record));
625    }
626
627    #[test]
628    fn a_removed_field_is_reported_as_stale() {
629        let names: Vec<String> = observed_composite_score_fields()
630            .into_iter()
631            .filter(|n| n != "psr")
632            .collect();
633        let audit = COMPOSITE_SCORE_INVENTORY.audit(&names);
634        assert_eq!(audit.stale, vec!["psr".to_string()]);
635    }
636
637    // -----------------------------------------------------------------------
638    // The declaration itself.
639    // -----------------------------------------------------------------------
640
641    #[test]
642    fn every_exclusion_carries_a_reason() {
643        for inventory in [COMPOSITE_SCORE_INVENTORY, RUN_PROVENANCE_INVENTORY] {
644            for (name, reason) in inventory.exclusions() {
645                assert!(
646                    reason.len() > 30,
647                    "{}::{name} is excluded without a usable reason",
648                    inventory.document
649                );
650            }
651        }
652    }
653
654    #[test]
655    fn every_redaction_carries_a_reason() {
656        for inventory in [COMPOSITE_SCORE_INVENTORY, RUN_PROVENANCE_INVENTORY] {
657            for (name, coverage) in inventory.fields {
658                if let Coverage::Redacted { reason, .. } = coverage {
659                    assert!(
660                        reason.len() > 30,
661                        "{}::{name} is redacted without a usable reason",
662                        inventory.document
663                    );
664                }
665            }
666        }
667    }
668
669    #[test]
670    fn the_two_digests_partition_the_covered_fields() {
671        let score = COMPOSITE_SCORE_INVENTORY.fields_for(DigestId::AgentScore);
672        let context = COMPOSITE_SCORE_INVENTORY.fields_for(DigestId::FieldContext);
673        let excluded = COMPOSITE_SCORE_INVENTORY.exclusions();
674        assert_eq!(
675            score.len() + context.len() + excluded.len(),
676            COMPOSITE_SCORE_INVENTORY.fields.len(),
677            "every field lands in exactly one of the three buckets"
678        );
679        for f in &score {
680            assert!(!context.contains(f), "{f} is claimed by both digests");
681        }
682        assert_eq!(excluded.len(), 4, "exactly four deliberate exclusions");
683    }
684
685    #[test]
686    fn no_field_is_declared_twice() {
687        for inventory in [COMPOSITE_SCORE_INVENTORY, RUN_PROVENANCE_INVENTORY] {
688            let names: BTreeSet<&str> = inventory.fields.iter().map(|(n, _)| *n).collect();
689            assert_eq!(
690                names.len(),
691                inventory.fields.len(),
692                "{} declares a field twice",
693                inventory.document
694            );
695        }
696    }
697
698    #[test]
699    fn coverage_lookup_answers_the_consumer_question() {
700        assert_eq!(
701            COMPOSITE_SCORE_INVENTORY.coverage("deflated_sharpe"),
702            Some(COVERED_SCORE)
703        );
704        assert_eq!(
705            COMPOSITE_SCORE_INVENTORY
706                .coverage("alpha")
707                .unwrap()
708                .digest(),
709            Some(DigestId::FieldContext)
710        );
711        assert!(!COMPOSITE_SCORE_INVENTORY
712            .coverage("role_contributions")
713            .unwrap()
714            .binds_value());
715        assert_eq!(COMPOSITE_SCORE_INVENTORY.coverage("not_a_field"), None);
716    }
717
718    // -----------------------------------------------------------------------
719    // Preimage construction and redaction.
720    // -----------------------------------------------------------------------
721
722    fn provenance_values() -> Vec<(&'static str, &'static str)> {
723        vec![
724            ("agent_id", "donchian-20-10"),
725            ("dataset_id", "us-indices-1d"),
726            ("dataset_content_hash", "9f86d081884c7d65"),
727            ("dataset_canary", "SUPER-SECRET-CANARY-42"),
728            ("window_id", "w3"),
729            ("seed", "7"),
730            ("cost_profile", "retail"),
731            ("harness_version", "0.11.0"),
732        ]
733    }
734
735    #[test]
736    fn a_secret_never_reaches_the_preimage() {
737        let bytes = RUN_PROVENANCE_INVENTORY
738            .preimage(DigestId::RunProvenance, &provenance_values())
739            .expect("complete value set");
740        let text = String::from_utf8(bytes).unwrap();
741        assert!(
742            !text.contains("SUPER-SECRET-CANARY-42"),
743            "the canary leaked into the bytes that get hashed"
744        );
745        assert!(
746            text.contains(REDACTED),
747            "the field's presence must still be bound"
748        );
749        assert!(text.contains("dataset_content_hash"));
750    }
751
752    #[test]
753    fn changing_a_secret_does_not_change_the_preimage() {
754        // The consequence of redacting rather than hashing the value: a verifier
755        // who does not hold the canary still reproduces the digest exactly.
756        let base = RUN_PROVENANCE_INVENTORY
757            .preimage(DigestId::RunProvenance, &provenance_values())
758            .unwrap();
759        let mut rotated = provenance_values();
760        rotated[3] = ("dataset_canary", "A-DIFFERENT-CANARY");
761        let after = RUN_PROVENANCE_INVENTORY
762            .preimage(DigestId::RunProvenance, &rotated)
763            .unwrap();
764        assert_eq!(base, after);
765    }
766
767    #[test]
768    fn changing_a_covered_value_changes_the_preimage() {
769        let base = RUN_PROVENANCE_INVENTORY
770            .preimage(DigestId::RunProvenance, &provenance_values())
771            .unwrap();
772        let mut tampered = provenance_values();
773        tampered[2] = ("dataset_content_hash", "0000000000000000");
774        let after = RUN_PROVENANCE_INVENTORY
775            .preimage(DigestId::RunProvenance, &tampered)
776            .unwrap();
777        assert_ne!(base, after);
778    }
779
780    #[test]
781    fn the_preimage_is_deterministic_and_order_independent() {
782        let mut shuffled = provenance_values();
783        shuffled.reverse();
784        assert_eq!(
785            RUN_PROVENANCE_INVENTORY
786                .preimage(DigestId::RunProvenance, &provenance_values())
787                .unwrap(),
788            RUN_PROVENANCE_INVENTORY
789                .preimage(DigestId::RunProvenance, &shuffled)
790                .unwrap(),
791            "inventory order fixes the layout, not caller order"
792        );
793    }
794
795    #[test]
796    fn a_missing_bound_field_is_refused() {
797        let mut incomplete = provenance_values();
798        incomplete.retain(|(n, _)| *n != "seed");
799        assert_eq!(
800            RUN_PROVENANCE_INVENTORY.preimage(DigestId::RunProvenance, &incomplete),
801            Err(PreimageError::MissingField {
802                field: "seed".to_string()
803            })
804        );
805    }
806
807    #[test]
808    fn an_undeclared_or_duplicated_value_is_refused() {
809        let mut extra = provenance_values();
810        extra.push(("newly_added_metric", "1.0"));
811        assert_eq!(
812            RUN_PROVENANCE_INVENTORY.preimage(DigestId::RunProvenance, &extra),
813            Err(PreimageError::UndeclaredField {
814                field: "newly_added_metric".to_string()
815            })
816        );
817
818        let mut dup = provenance_values();
819        dup.push(("seed", "8"));
820        assert_eq!(
821            RUN_PROVENANCE_INVENTORY.preimage(DigestId::RunProvenance, &dup),
822            Err(PreimageError::DuplicateField {
823                field: "seed".to_string()
824            })
825        );
826    }
827
828    #[test]
829    fn an_excluded_field_never_appears_under_any_digest() {
830        let values: Vec<(&str, &str)> = COMPOSITE_SCORE_INVENTORY
831            .fields
832            .iter()
833            .map(|(n, _)| (*n, "0"))
834            .collect();
835        for digest in [DigestId::AgentScore, DigestId::FieldContext] {
836            let bytes = COMPOSITE_SCORE_INVENTORY.preimage(digest, &values).unwrap();
837            let text = String::from_utf8(bytes).unwrap();
838            for (name, _) in COMPOSITE_SCORE_INVENTORY.exclusions() {
839                assert!(
840                    !text.contains(name),
841                    "{name} is excluded but reached the {digest:?} preimage"
842                );
843            }
844        }
845    }
846
847    #[test]
848    fn the_two_digests_produce_different_preimages_over_the_same_values() {
849        let values: Vec<(&str, &str)> = COMPOSITE_SCORE_INVENTORY
850            .fields
851            .iter()
852            .map(|(n, _)| (*n, "0"))
853            .collect();
854        assert_ne!(
855            COMPOSITE_SCORE_INVENTORY
856                .preimage(DigestId::AgentScore, &values)
857                .unwrap(),
858            COMPOSITE_SCORE_INVENTORY
859                .preimage(DigestId::FieldContext, &values)
860                .unwrap(),
861            "the digest tag and the field set both differ"
862        );
863    }
864}