scc_context/
context_ledger.rs1use scc_core::ContextLedger;
8use scc_store::Store;
9use std::collections::BTreeSet;
10
11const LEDGER_KEY_PREFIX: &str = "ledger:";
12
13pub struct ContextLedgerStore<'a> {
17 store: &'a Store,
18 epoch: String,
19}
20
21impl<'a> ContextLedgerStore<'a> {
23 pub fn new(store: &'a Store) -> Self {
26 let epoch = store.cache_epoch().unwrap_or_else(|_| "no-epoch".into());
27 ContextLedgerStore { store, epoch }
28 }
29
30 pub fn epoch(&self) -> &str {
33 &self.epoch
34 }
35
36fn key(&self) -> String {
38 format!("{LEDGER_KEY_PREFIX}{}", self.epoch)
39 }
40
41fn empty(&self) -> ContextLedger {
43 ContextLedger {
44 model_epoch: self.epoch.clone(),
45 ..Default::default()
46 }
47 }
48
49 pub fn load(&self) -> ContextLedger {
53 match self.store.cache_get(&self.key(), &self.epoch) {
54 Ok(Some(json)) => serde_json::from_str(&json).unwrap_or_else(|_| self.empty()),
55 _ => self.empty(),
56 }
57 }
58
59 pub fn save(&self, ledger: &ContextLedger) {
63 if let Ok(json) = serde_json::to_string(ledger) {
64 let _ = self.store.cache_put(&self.key(), &json, &self.epoch);
65 }
66 }
67
68 pub fn visible_ids(
72 &self,
73 ) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
74 let led = self.load();
75 (
76 led.visible_symbols,
77 led.visible_files,
78 led.visible_components,
79 led.visible_flows,
80 )
81 }
82
83 pub fn record_visible(
90 &self,
91 symbols: &BTreeSet<String>,
92 files: &BTreeSet<String>,
93 components: &BTreeSet<String>,
94 flows: &BTreeSet<String>,
95 ) {
96 let mut led = self.load();
97 led.visible_symbols.extend(symbols.iter().cloned());
98 led.visible_files.extend(files.iter().cloned());
99 led.visible_components.extend(components.iter().cloned());
100 led.visible_flows.extend(flows.iter().cloned());
101 self.save(&led);
102 }
103}
104
105pub fn novelty_penalty(visible: &ContextLedger, symbol_id: &str, changed: bool) -> f64 {
121 let already_visible = visible.visible_symbols.contains(symbol_id)
122 || visible.visible_entities.contains(symbol_id);
123 let critical = visible.visible_components.contains(symbol_id)
124 || visible.visible_flows.contains(symbol_id);
125 if already_visible && !changed && !critical {
126 0.1
127 } else {
128 1.0
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136fn test_store() -> (tempfile::TempDir, Store) {
138 let dir = tempfile::TempDir::new().unwrap();
139 let root = dir.path().join("repo");
140 std::fs::create_dir_all(&root).unwrap();
141 let store = Store::open(&dir.path().join("scc.db"), &root).unwrap();
142 (dir, store)
143 }
144
145 #[test]
146fn novelty_penalty_suppresses_seen_unchanged_only() {
148 let mut led = ContextLedger::default();
149 led.visible_symbols.insert("repo://r/symbol/a.py/seen".into());
150
151 assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/seen", false), 0.1);
153 led.visible_entities.insert("repo://r/symbol/b.py/ent".into());
155 assert_eq!(novelty_penalty(&led, "repo://r/symbol/b.py/ent", false), 0.1);
156 assert_eq!(novelty_penalty(&led, "repo://r/symbol/c.py/new", false), 1.0);
158 assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/seen", true), 1.0);
160 }
161
162 #[test]
163fn novelty_penalty_never_suppresses_critical_anchors() {
165 let mut led = ContextLedger::default();
166 led.visible_symbols.insert("repo://r/symbol/a.py/arch".into());
167 led.visible_components.insert("repo://r/symbol/a.py/arch".into());
168 led.visible_flows.insert("repo://r/symbol/b.py/flow".into());
169 assert_eq!(novelty_penalty(&led, "repo://r/symbol/a.py/arch", false), 1.0);
171 assert_eq!(novelty_penalty(&led, "repo://r/symbol/b.py/flow", false), 1.0);
172 }
173
174 #[test]
175fn ledger_roundtrip_via_store_cache() {
177 let (_dir, store) = test_store();
178 let ls = ContextLedgerStore::new(&store);
179 let led = ls.load();
181 assert_eq!(led.model_epoch, ls.epoch());
182 assert!(led.visible_symbols.is_empty());
183
184 let mut led = led;
185 led.visible_symbols.insert("repo://r/symbol/a.py/x".into());
186 led.visible_files.insert("a.py".into());
187 led.visible_components.insert("repo://r/component/c".into());
188 led.visible_flows.insert("repo://r/flow/f".into());
189 led.last_task = Some("fix checkout".into());
190 ls.save(&led);
191
192 let ls2 = ContextLedgerStore::new(&store);
193 let loaded = ls2.load();
194 assert_eq!(loaded.visible_symbols, led.visible_symbols);
195 assert_eq!(loaded.visible_files, led.visible_files);
196 assert_eq!(loaded.visible_components, led.visible_components);
197 assert_eq!(loaded.visible_flows, led.visible_flows);
198 assert_eq!(loaded.last_task, led.last_task);
199
200 let (syms, files, comps, flows) = ls2.visible_ids();
201 assert_eq!(syms, led.visible_symbols);
202 assert_eq!(files, led.visible_files);
203 assert_eq!(comps, led.visible_components);
204 assert_eq!(flows, led.visible_flows);
205 }
206
207 #[test]
208fn record_visible_persists_rendered_ids_only() {
210 let (_dir, store) = test_store();
211 let ls = ContextLedgerStore::new(&store);
212 let mut syms = BTreeSet::new();
213 syms.insert("repo://r/symbol/a.py/rendered".into());
214 let mut comps = BTreeSet::new();
215 comps.insert("repo://r/component/c".into());
216 let mut flows = BTreeSet::new();
217 flows.insert("repo://r/flow/f".into());
218
219 ls.record_visible(&syms, &BTreeSet::new(), &comps, &flows);
220 let led = ls.load();
221 assert!(led.visible_symbols.contains("repo://r/symbol/a.py/rendered"));
222 assert!(led.visible_components.contains("repo://r/component/c"));
223 assert!(led.visible_flows.contains("repo://r/flow/f"));
224 assert_eq!(led.visible_symbols.len(), 1);
226 assert!(led.visible_files.is_empty());
227
228 let mut more = BTreeSet::new();
230 more.insert("repo://r/symbol/b.py/new".into());
231 ls.record_visible(&more, &BTreeSet::new(), &BTreeSet::new(), &BTreeSet::new());
232 let led = ls.load();
233 assert!(led.visible_symbols.contains("repo://r/symbol/a.py/rendered"));
234 assert!(led.visible_symbols.contains("repo://r/symbol/b.py/new"));
235 assert_eq!(led.visible_symbols.len(), 2);
236 }
237
238 #[test]
239fn ledger_is_epoch_scoped() {
241 let (_dir, store) = test_store();
242 let e0 = store.cache_epoch().unwrap();
243 let ls = ContextLedgerStore::new(&store);
244 let mut led = ls.load();
245 led.visible_symbols.insert("repo://r/symbol/a.py/x".into());
246 ls.save(&led);
247 assert!(ls.load().visible_symbols.contains("repo://r/symbol/a.py/x"));
248
249 store.bump_epoch(scc_store::ModelEpochKind::Source).unwrap();
252 let ls2 = ContextLedgerStore::new(&store);
253 assert_ne!(ls2.epoch(), e0);
254 assert!(ls2.load().visible_symbols.is_empty());
255 }
256}