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 (`OuterCriterionCertificate`), 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 /// All recorded entries in stable (claim-id) order.
239 pub fn entries(&self) -> impl Iterator<Item = &LedgerEntry> {
240 self.entries.values()
241 }
242
243 pub fn is_empty(&self) -> bool {
244 self.entries.is_empty()
245 }
246
247 pub fn len(&self) -> usize {
248 self.entries.len()
249 }
250
251 /// The conservative roll-up across the whole ledger: the WEAKEST verdict of
252 /// any recorded claim (the meet over the soundness lattice). An empty ledger
253 /// rolls up to [`Verdict::Unavailable`]. This is the single number that
254 /// answers "did everything this fit could certify, certify?" — and it cannot
255 /// be stronger than its weakest member.
256 pub fn overall(&self) -> Verdict {
257 self.entries
258 .values()
259 .map(|e| e.verdict)
260 .fold(Verdict::Certified, Verdict::meet)
261 // An empty ledger has nothing to certify → Unavailable, not the
262 // vacuous-Certified fold seed.
263 .min(if self.entries.is_empty() {
264 Verdict::Unavailable
265 } else {
266 Verdict::Certified
267 })
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 struct FakeCert {
276 id: &'static str,
277 verdict: Verdict,
278 }
279 impl Certificate for FakeCert {
280 fn claim(&self) -> Claim {
281 Claim::new(self.id, "fake claim")
282 }
283 fn evidence(&self) -> Evidence {
284 let mut e = Evidence::new();
285 e.insert("x", 1.0.into());
286 e
287 }
288 fn verdict(&self) -> Verdict {
289 self.verdict
290 }
291 }
292
293 #[test]
294 fn verdict_ladder_orders_weakest_to_strongest() {
295 assert!(Verdict::Unavailable < Verdict::Insufficient);
296 assert!(Verdict::Insufficient < Verdict::Certified);
297 assert!(!Verdict::Insufficient.is_certified());
298 assert!(!Verdict::Unavailable.is_certified());
299 assert!(Verdict::Certified.is_certified());
300 }
301
302 #[test]
303 fn meet_is_conservative() {
304 assert_eq!(
305 Verdict::Certified.meet(Verdict::Insufficient),
306 Verdict::Insufficient
307 );
308 assert_eq!(
309 Verdict::Insufficient.meet(Verdict::Unavailable),
310 Verdict::Unavailable
311 );
312 assert_eq!(
313 Verdict::Certified.meet(Verdict::Certified),
314 Verdict::Certified
315 );
316 }
317
318 #[test]
319 fn overall_is_weakest_member() {
320 let mut ledger = CertificateLedger::new();
321 ledger.record(&FakeCert {
322 id: "a",
323 verdict: Verdict::Certified,
324 });
325 ledger.record(&FakeCert {
326 id: "b",
327 verdict: Verdict::Insufficient,
328 });
329 assert_eq!(ledger.overall(), Verdict::Insufficient);
330 assert!(!ledger.overall().is_certified());
331 }
332
333}