use scc_core::ContextLedger;
use scc_store::Store;
use std::collections::BTreeSet;
const LEDGER_KEY_PREFIX: &str = "ledger:";
pub struct ContextLedgerStore<'a> {
store: &'a Store,
epoch: String,
}
impl<'a> ContextLedgerStore<'a> {
pub fn new(store: &'a Store) -> Self {
let epoch = store.cache_epoch().unwrap_or_else(|_| "no-epoch".into());
ContextLedgerStore { store, epoch }
}
pub fn epoch(&self) -> &str {
&self.epoch
}
fn key(&self) -> String {
format!("{LEDGER_KEY_PREFIX}{}", self.epoch)
}
fn empty(&self) -> ContextLedger {
ContextLedger {
model_epoch: self.epoch.clone(),
..Default::default()
}
}
pub fn load(&self) -> ContextLedger {
match self.store.cache_get(&self.key(), &self.epoch) {
Ok(Some(json)) => serde_json::from_str(&json).unwrap_or_else(|_| self.empty()),
_ => self.empty(),
}
}
pub fn save(&self, ledger: &ContextLedger) {
if let Ok(json) = serde_json::to_string(ledger) {
let _ = self.store.cache_put(&self.key(), &json, &self.epoch);
}
}
pub fn visible_ids(
&self,
) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
let led = self.load();
(
led.visible_symbols,
led.visible_files,
led.visible_components,
led.visible_flows,
)
}
pub fn record_visible(
&self,
symbols: &BTreeSet<String>,
files: &BTreeSet<String>,
components: &BTreeSet<String>,
flows: &BTreeSet<String>,
) {
let mut led = self.load();
led.visible_symbols.extend(symbols.iter().cloned());
led.visible_files.extend(files.iter().cloned());
led.visible_components.extend(components.iter().cloned());
led.visible_flows.extend(flows.iter().cloned());
self.save(&led);
}
}
pub fn novelty_penalty(visible: &ContextLedger, symbol_id: &str, changed: bool) -> f64 {
let already_visible = visible.visible_symbols.contains(symbol_id)
|| visible.visible_entities.contains(symbol_id);
let critical = visible.visible_components.contains(symbol_id)
|| visible.visible_flows.contains(symbol_id);
if already_visible && !changed && !critical {
0.1
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_store() -> (tempfile::TempDir, Store) {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
(dir, store)
}
#[test]
fn novelty_penalty_suppresses_seen_unchanged_only() {
let mut led = ContextLedger::default();
led.visible_symbols.insert("repo://r/symbol/a.py/seen".into());
assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/seen", false), 0.1);
led.visible_entities.insert("repo://r/symbol/b.py/ent".into());
assert_eq!(novelty_penalty(&led, "repo://r/symbol/b.py/ent", false), 0.1);
assert_eq!(novelty_penalty(&led, "repo://r/symbol/c.py/new", false), 1.0);
assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/seen", true), 1.0);
}
#[test]
fn novelty_penalty_never_suppresses_critical_anchors() {
let mut led = ContextLedger::default();
led.visible_symbols.insert("repo://r/symbol/a.py/arch".into());
led.visible_components.insert("repo://r/symbol/a.py/arch".into());
led.visible_flows.insert("repo://r/symbol/b.py/flow".into());
assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/arch", false), 1.0);
assert_eq!(novelty_penalty(&led, "repo://r/symbol/b.py/flow", false), 1.0);
}
#[test]
fn ledger_roundtrip_via_store_cache() {
let (_dir, store) = test_store();
let ls = ContextLedgerStore::new(&store);
let led = ls.load();
assert_eq!(led.model_epoch, ls.epoch());
assert!(led.visible_symbols.is_empty());
let mut led = led;
led.visible_symbols.insert("repo://r/symbol/a.py/x".into());
led.visible_files.insert("a.py".into());
led.visible_components.insert("repo://r/component/c".into());
led.visible_flows.insert("repo://r/flow/f".into());
led.last_task = Some("fix checkout".into());
ls.save(&led);
let ls2 = ContextLedgerStore::new(&store);
let loaded = ls2.load();
assert_eq!(loaded.visible_symbols, led.visible_symbols);
assert_eq!(loaded.visible_files, led.visible_files);
assert_eq!(loaded.visible_components, led.visible_components);
assert_eq!(loaded.visible_flows, led.visible_flows);
assert_eq!(loaded.last_task, led.last_task);
let (syms, files, comps, flows) = ls2.visible_ids();
assert_eq!(syms, led.visible_symbols);
assert_eq!(files, led.visible_files);
assert_eq!(comps, led.visible_components);
assert_eq!(flows, led.visible_flows);
}
#[test]
fn record_visible_persists_rendered_ids_only() {
let (_dir, store) = test_store();
let ls = ContextLedgerStore::new(&store);
let mut syms = BTreeSet::new();
syms.insert("repo://r/symbol/a.py/rendered".into());
let mut comps = BTreeSet::new();
comps.insert("repo://r/component/c".into());
let mut flows = BTreeSet::new();
flows.insert("repo://r/flow/f".into());
ls.record_visible(&syms, &BTreeSet::new(), &comps, &flows);
let led = ls.load();
assert!(led.visible_symbols.contains("repo://r/symbol/a.py/rendered"));
assert!(led.visible_components.contains("repo://r/component/c"));
assert!(led.visible_flows.contains("repo://r/flow/f"));
assert_eq!(led.visible_symbols.len(), 1);
assert!(led.visible_files.is_empty());
let mut more = BTreeSet::new();
more.insert("repo://r/symbol/b.py/new".into());
ls.record_visible(&more, &BTreeSet::new(), &BTreeSet::new(), &BTreeSet::new());
let led = ls.load();
assert!(led.visible_symbols.contains("repo://r/symbol/a.py/rendered"));
assert!(led.visible_symbols.contains("repo://r/symbol/b.py/new"));
assert_eq!(led.visible_symbols.len(), 2);
}
#[test]
fn ledger_is_epoch_scoped() {
let (_dir, store) = test_store();
let e0 = store.cache_epoch().unwrap();
let ls = ContextLedgerStore::new(&store);
let mut led = ls.load();
led.visible_symbols.insert("repo://r/symbol/a.py/x".into());
ls.save(&led);
assert!(ls.load().visible_symbols.contains("repo://r/symbol/a.py/x"));
store.bump_epoch(scc_store::ModelEpochKind::Source).unwrap();
let ls2 = ContextLedgerStore::new(&store);
assert_ne!(ls2.epoch(), e0);
assert!(ls2.load().visible_symbols.is_empty());
}
}