Skip to main content

gam_problem/
topology_certificates.rs

1//! The unified certificate contract (task #16).
2//!
3//! Across the program a dozen independent analyses each emit a "certificate":
4//! the outer-optimum first-order self-audit ([`CriterionCertificate`]), the
5//! sensitivity-coreset error budget ([`CoresetCertificate`]), the log-det
6//! enclosure ([`LogdetEnclosure`]), the Kantorovich encode atlas
7//! ([`EncodeResult`]), the exact-orbit residual-gauge report, the dictionary
8//! incoherence / global-optimality report, the structure-search collapse
9//! events, and the topology evidence certification. Each grew its own struct,
10//! its own verdict enum, and its own scattered payload key.
11//!
12//! This module gives them ONE shared contract so a fit returns a single
13//! inspectable certificate ledger — the program's signature artifact. The
14//! contract is the [`Certificate`] trait: every certificate states
15//!
16//! 1. **the CLAIM** it certifies — a stable machine id plus a human sentence
17//!    ([`Certificate::claim`]);
18//! 2. **the EVIDENCE** quantities behind the claim ([`Certificate::evidence`]),
19//!    as named scalars/flags/text;
20//! 3. **a conservative VERDICT** ([`Certificate::verdict`]) drawn from
21//!    [`Verdict`], in which *certified-but-wrong is structurally impossible*:
22//!    the verdict can only STRENGTHEN as evidence accrues, the weakest state is
23//!    the default, and there are explicit [`Verdict::Insufficient`] /
24//!    [`Verdict::Unavailable`] states so a missing or below-margin certificate
25//!    never silently reads as "certified".
26//!
27//! Migration rule (task #16): the existing certificate types KEEP their math
28//! unchanged; they merely implement [`Certificate`]. Their bespoke methods
29//! (`passes`, `certify_margin`, `decide_within_margin`, `is_certified`, …) stay
30//! as-is and the trait's [`Certificate::verdict`] is defined in terms of them,
31//! so there is exactly one source of truth for each verdict.
32
33use std::collections::BTreeMap;
34
35/// The conservative verdict ladder shared by every certificate.
36///
37/// The ordering is a soundness lattice, weakest → strongest:
38/// `Unavailable < Insufficient < Certified`. A verdict may only move UP this
39/// ladder as evidence accrues; it can never claim more than the evidence
40/// supports. The weakest state ([`Verdict::Unavailable`]) is the default, so a
41/// certificate that was never computed, or whose inputs were degenerate, reads
42/// as "no claim" — never as a silent pass. This is what makes
43/// "certified-but-wrong" structurally impossible: `Certified` is reachable only
44/// when the owning certificate's own (unchanged) decision rule says the
45/// evidence strictly clears its required margin.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub enum Verdict {
48    /// The certificate could not be evaluated: inputs were missing, degenerate,
49    /// or non-finite. No claim is made. This is the default — the absence of a
50    /// certificate is this state, not `Certified`.
51    Unavailable,
52    /// The certificate was evaluated and the evidence is present, but it does
53    /// NOT clear the margin required to certify the claim. The consumer must
54    /// escalate (refine, gather more evidence, or fall back to the exact path);
55    /// it must NOT treat this as a pass.
56    Insufficient,
57    /// The evidence strictly clears the claim's required margin. The claim holds
58    /// — and, by construction of each owning decision rule, the conservative
59    /// (worst-case) bound was used, so this verdict cannot be falsely positive.
60    Certified,
61}
62
63impl Verdict {
64    /// Whether the claim is certified. The ONLY `true` case is
65    /// [`Verdict::Certified`]; `Insufficient` and `Unavailable` are both `false`
66    /// so no caller can read a missing or below-margin certificate as a pass.
67    pub fn is_certified(self) -> bool {
68        matches!(self, Verdict::Certified)
69    }
70
71    /// Stable machine label for payloads.
72    pub fn label(self) -> &'static str {
73        match self {
74            Verdict::Unavailable => "unavailable",
75            Verdict::Insufficient => "insufficient",
76            Verdict::Certified => "certified",
77        }
78    }
79
80    /// Combine two verdicts about the SAME claim conservatively: the result is
81    /// the WEAKER of the two (the meet on the soundness lattice). Aggregating a
82    /// batch of per-item verdicts this way guarantees the summary can never be
83    /// stronger than its weakest member — one uncertified row makes the batch
84    /// uncertified.
85    pub fn meet(self, other: Verdict) -> Verdict {
86        self.min(other)
87    }
88}
89
90/// The claim a certificate makes: a stable machine id and a human sentence.
91///
92/// `id` is a kebab-case key stable across runs (used as the payload sub-key and
93/// for programmatic lookup); `statement` is the one-line human description of
94/// exactly what is being certified.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Claim {
97    pub id: &'static str,
98    pub statement: String,
99}
100
101impl Claim {
102    pub fn new(id: &'static str, statement: impl Into<String>) -> Self {
103        Self {
104            id,
105            statement: statement.into(),
106        }
107    }
108}
109
110/// A single named evidence quantity behind a claim. Evidence is reported as
111/// typed values so the ledger is machine-inspectable, not just a string blob.
112#[derive(Debug, Clone, PartialEq)]
113pub enum EvidenceValue {
114    Scalar(f64),
115    Integer(i64),
116    Flag(bool),
117    Text(String),
118    /// A short list of scalars (e.g. per-atom statistics). Kept small; large
119    /// arrays belong in the typed diagnostics, not the certificate ledger.
120    Vector(Vec<f64>),
121}
122
123impl From<f64> for EvidenceValue {
124    fn from(v: f64) -> Self {
125        EvidenceValue::Scalar(v)
126    }
127}
128impl From<usize> for EvidenceValue {
129    fn from(v: usize) -> Self {
130        EvidenceValue::Integer(v as i64)
131    }
132}
133impl From<i64> for EvidenceValue {
134    fn from(v: i64) -> Self {
135        EvidenceValue::Integer(v)
136    }
137}
138impl From<bool> for EvidenceValue {
139    fn from(v: bool) -> Self {
140        EvidenceValue::Flag(v)
141    }
142}
143impl From<String> for EvidenceValue {
144    fn from(v: String) -> Self {
145        EvidenceValue::Text(v)
146    }
147}
148impl From<&str> for EvidenceValue {
149    fn from(v: &str) -> Self {
150        EvidenceValue::Text(v.to_string())
151    }
152}
153impl From<Vec<f64>> for EvidenceValue {
154    fn from(v: Vec<f64>) -> Self {
155        EvidenceValue::Vector(v)
156    }
157}
158
159/// The ordered set of evidence quantities behind a claim. Ordering is stable
160/// (`BTreeMap`) so payloads and snapshots are deterministic.
161pub type Evidence = BTreeMap<&'static str, EvidenceValue>;
162
163/// The shared contract every certificate in the program implements (task #16).
164///
165/// Implementors do NOT change their math — they expose their existing claim,
166/// evidence, and (unchanged) decision rule through this uniform shape. The
167/// default [`Certificate::ledger_entry`] folds the three into one inspectable
168/// record so the fit can assemble a single certificate ledger.
169pub trait Certificate {
170    /// What this certificate certifies — stable id + human sentence.
171    fn claim(&self) -> Claim;
172
173    /// The named evidence quantities behind the claim.
174    fn evidence(&self) -> Evidence;
175
176    /// The conservative verdict. MUST be derived from the certificate's own
177    /// (unchanged) decision rule, and MUST return [`Verdict::Unavailable`] /
178    /// [`Verdict::Insufficient`] rather than a silent pass when the evidence is
179    /// missing or below margin.
180    fn verdict(&self) -> Verdict;
181
182    /// Fold claim + evidence + verdict into one ledger record.
183    fn ledger_entry(&self) -> LedgerEntry {
184        LedgerEntry {
185            claim: self.claim(),
186            evidence: self.evidence(),
187            verdict: self.verdict(),
188        }
189    }
190}
191
192/// One certificate's contribution to the ledger: its claim, evidence, and
193/// conservative verdict, frozen at the time the fit recorded it.
194#[derive(Debug, Clone, PartialEq)]
195pub struct LedgerEntry {
196    pub claim: Claim,
197    pub evidence: Evidence,
198    pub verdict: Verdict,
199}
200
201/// The fit's certificate ledger: every certificate the fit produced, keyed by
202/// claim id, in stable order. This is the single inspectable artifact that
203/// replaces the scattered per-feature payload keys.
204///
205/// The ledger never fabricates a verdict: a claim that was not evaluated simply
206/// is absent (queried as [`Verdict::Unavailable`] via [`Self::verdict_of`]).
207#[derive(Debug, Clone, Default, PartialEq)]
208pub struct CertificateLedger {
209    entries: BTreeMap<&'static str, LedgerEntry>,
210}
211
212impl CertificateLedger {
213    pub fn new() -> Self {
214        Self::default()
215    }
216
217    /// Record one certificate. If two certificates share a claim id, they are
218    /// combined conservatively: the retained verdict is the WEAKER of the two
219    /// (so duplicate evidence can never upgrade a claim past its weakest
220    /// witness), and the evidence of the weaker verdict is kept.
221    pub fn record<C: Certificate>(&mut self, certificate: &C) {
222        self.record_entry(certificate.ledger_entry());
223    }
224
225    /// Record a pre-built entry (for certificates whose owning type lives behind
226    /// a boundary that only hands back the folded record).
227    pub fn record_entry(&mut self, entry: LedgerEntry) {
228        match self.entries.get(entry.claim.id) {
229            Some(existing) if existing.verdict <= entry.verdict => {
230                // Existing is weaker-or-equal: keep the conservative one.
231            }
232            _ => {
233                self.entries.insert(entry.claim.id, entry);
234            }
235        }
236    }
237
238    /// The verdict for a claim id, or [`Verdict::Unavailable`] if the fit never
239    /// recorded it — the absence of a certificate is "no claim", never a pass.
240    pub fn verdict_of(&self, claim_id: &str) -> Verdict {
241        self.entries
242            .get(claim_id)
243            .map(|e| e.verdict)
244            .unwrap_or(Verdict::Unavailable)
245    }
246
247    /// All recorded entries in stable (claim-id) order.
248    pub fn entries(&self) -> impl Iterator<Item = &LedgerEntry> {
249        self.entries.values()
250    }
251
252    pub fn is_empty(&self) -> bool {
253        self.entries.is_empty()
254    }
255
256    pub fn len(&self) -> usize {
257        self.entries.len()
258    }
259
260    /// The conservative roll-up across the whole ledger: the WEAKEST verdict of
261    /// any recorded claim (the meet over the soundness lattice). An empty ledger
262    /// rolls up to [`Verdict::Unavailable`]. This is the single number that
263    /// answers "did everything this fit could certify, certify?" — and it cannot
264    /// be stronger than its weakest member.
265    pub fn overall(&self) -> Verdict {
266        self.entries
267            .values()
268            .map(|e| e.verdict)
269            .fold(Verdict::Certified, Verdict::meet)
270            // An empty ledger has nothing to certify → Unavailable, not the
271            // vacuous-Certified fold seed.
272            .min(if self.entries.is_empty() {
273                Verdict::Unavailable
274            } else {
275                Verdict::Certified
276            })
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    struct FakeCert {
285        id: &'static str,
286        verdict: Verdict,
287    }
288    impl Certificate for FakeCert {
289        fn claim(&self) -> Claim {
290            Claim::new(self.id, "fake claim")
291        }
292        fn evidence(&self) -> Evidence {
293            let mut e = Evidence::new();
294            e.insert("x", 1.0.into());
295            e
296        }
297        fn verdict(&self) -> Verdict {
298            self.verdict
299        }
300    }
301
302    #[test]
303    fn verdict_ladder_orders_weakest_to_strongest() {
304        assert!(Verdict::Unavailable < Verdict::Insufficient);
305        assert!(Verdict::Insufficient < Verdict::Certified);
306        assert!(!Verdict::Insufficient.is_certified());
307        assert!(!Verdict::Unavailable.is_certified());
308        assert!(Verdict::Certified.is_certified());
309    }
310
311    #[test]
312    fn meet_is_conservative() {
313        assert_eq!(
314            Verdict::Certified.meet(Verdict::Insufficient),
315            Verdict::Insufficient
316        );
317        assert_eq!(
318            Verdict::Insufficient.meet(Verdict::Unavailable),
319            Verdict::Unavailable
320        );
321        assert_eq!(
322            Verdict::Certified.meet(Verdict::Certified),
323            Verdict::Certified
324        );
325    }
326
327    #[test]
328    fn absent_claim_reads_as_unavailable_never_pass() {
329        let ledger = CertificateLedger::new();
330        assert_eq!(ledger.verdict_of("nonexistent"), Verdict::Unavailable);
331        assert!(!ledger.verdict_of("nonexistent").is_certified());
332        // Empty ledger rolls up to Unavailable, not a vacuous pass.
333        assert_eq!(ledger.overall(), Verdict::Unavailable);
334    }
335
336    #[test]
337    fn overall_is_weakest_member() {
338        let mut ledger = CertificateLedger::new();
339        ledger.record(&FakeCert {
340            id: "a",
341            verdict: Verdict::Certified,
342        });
343        ledger.record(&FakeCert {
344            id: "b",
345            verdict: Verdict::Insufficient,
346        });
347        assert_eq!(ledger.overall(), Verdict::Insufficient);
348        assert!(!ledger.overall().is_certified());
349    }
350
351    #[test]
352    fn duplicate_record_keeps_weaker_verdict() {
353        let mut ledger = CertificateLedger::new();
354        ledger.record(&FakeCert {
355            id: "a",
356            verdict: Verdict::Certified,
357        });
358        ledger.record(&FakeCert {
359            id: "a",
360            verdict: Verdict::Insufficient,
361        });
362        assert_eq!(ledger.verdict_of("a"), Verdict::Insufficient);
363        assert_eq!(ledger.len(), 1);
364    }
365}