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}