use crate::inference::structure_evidence::StructureLedger;
use crate::terms::smooth::TermCollectionSpec;
use gam_runtime::warm_start::store::{EntryKind, StoreOptions, WarmStartStore};
use gam_runtime::warm_start::{Fingerprint, Fingerprinter};
use std::time::Duration;
pub const STRUCTURE_CERTIFICATE_ALPHA: f64 = 0.05;
const LEDGER_DISK_BUDGET_BYTES: u64 = 16 * 1024 * 1024;
const LEDGER_DISK_TTL_SECS: u64 = 14 * 24 * 60 * 60;
const LEDGER_RUN_ID: &str = "structure-ledger";
pub struct LedgerStore {
key: Fingerprint,
store: Option<WarmStartStore>,
}
impl LedgerStore {
pub fn new(spec: &TermCollectionSpec) -> Self {
let mut fp = Fingerprinter::new();
fp.write_str("sae-structure-ledger-key-v1");
spec.write_structural_shape_hash(&mut fp);
Self::from_fingerprint(fp.finalize())
}
pub fn from_structural_hash(structural_hash: u64) -> Self {
let mut fp = Fingerprinter::new();
fp.write_str("sae-structure-ledger-key-v1");
fp.write_u64(structural_hash);
Self::from_fingerprint(fp.finalize())
}
fn from_fingerprint(key: Fingerprint) -> Self {
let root = std::env::temp_dir()
.join("gam")
.join("sae_structure_ledger")
.join("v1");
let store = WarmStartStore::open(
root,
StoreOptions {
size_budget_bytes: LEDGER_DISK_BUDGET_BYTES,
ttl: Duration::from_secs(LEDGER_DISK_TTL_SECS),
},
)
.ok();
Self { key, store }
}
pub fn load(&self) -> Result<StructureLedger, String> {
let Some(store) = self.store.as_ref() else {
return Ok(StructureLedger::new());
};
match store.lookup_latest(&self.key) {
Ok(Some(entry)) => deserialize_ledger(&entry.payload),
Ok(None) => Ok(StructureLedger::new()),
Err(e) => Err(format!("LedgerStore::load: store lookup failed: {e:?}")),
}
}
pub fn save(&self, ledger: &StructureLedger) -> Result<(), String> {
let Some(store) = self.store.as_ref() else {
return Ok(());
};
let payload = serialize_ledger(ledger)?;
store
.save_overwrite(
&self.key,
LEDGER_RUN_ID,
&payload,
None,
Some(ledger.claims().len() as u64),
EntryKind::Final,
)
.map_err(|e| format!("LedgerStore::save: store write failed: {e:?}"))
}
}
pub fn serialize_ledger(ledger: &StructureLedger) -> Result<Vec<u8>, String> {
serde_json::to_vec(ledger).map_err(|e| format!("ledger serialization failed: {e}"))
}
pub fn deserialize_ledger(bytes: &[u8]) -> Result<StructureLedger, String> {
serde_json::from_slice(bytes).map_err(|e| {
format!(
"ledger payload exists but failed to decode ({e}); refusing to silently reset \
accumulated evidence — delete the entry explicitly if a fresh start is intended"
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inference::structure_evidence::ClaimKind;
#[test]
fn ledger_payload_round_trips_with_evidence() {
let mut ledger = StructureLedger::new();
let idx = ledger.register(ClaimKind::AtomExists { atom: 7 });
ledger.absorb_log(idx, 1.5).unwrap();
ledger.absorb_log(idx, 0.25).unwrap();
let edge = ledger.register(ClaimKind::BindingEdge { a: 1, b: 3 });
ledger.absorb_log(edge, -0.4).unwrap();
let bytes = serialize_ledger(&ledger).expect("encode");
let back = deserialize_ledger(&bytes).expect("decode");
assert_eq!(back.claims().len(), 2);
assert_eq!(back.claims()[idx].kind, ClaimKind::AtomExists { atom: 7 });
assert_eq!(back.claims()[idx].evidence.steps(), 2);
assert!((back.claims()[idx].evidence.log_evidence() - 1.75).abs() < 1e-12);
assert!((back.claims()[edge].evidence.log_evidence() + 0.4).abs() < 1e-12);
}
#[test]
fn corrupt_payload_is_an_error_not_a_reset() {
let err = deserialize_ledger(b"{not json").expect_err("must refuse");
assert!(err.contains("refusing to silently reset"));
}
#[test]
fn distinct_topologies_get_distinct_keys() {
let a = LedgerStore::from_structural_hash(0xDEAD_BEEF);
let b = LedgerStore::from_structural_hash(0xFEED_FACE);
assert_ne!(a.key, b.key);
}
}