1use crate::flag::Flag;
10use std::fs::{File, OpenOptions};
11use std::io::{self, BufRead, BufReader, Write};
12use std::path::{Path, PathBuf};
13
14pub struct FlagStore {
15 path: PathBuf,
16}
17
18impl FlagStore {
19 pub fn open(path: impl AsRef<Path>) -> Self {
20 FlagStore { path: path.as_ref().to_path_buf() }
21 }
22
23 pub fn remember(&self, flag: &Flag) -> io::Result<()> {
26 if let Some(parent) = self.path.parent() {
27 if !parent.as_os_str().is_empty() {
28 std::fs::create_dir_all(parent)?;
29 }
30 }
31 let mut f = OpenOptions::new().create(true).append(true).open(&self.path)?;
32 let line = serde_json::to_string(flag).map_err(io::Error::other)?;
33 writeln!(f, "{line}")
34 }
35
36 pub fn recall_all(&self) -> io::Result<Vec<Flag>> {
38 if !self.path.exists() {
39 return Ok(Vec::new());
40 }
41 let f = File::open(&self.path)?;
42 let mut out = Vec::new();
43 for line in BufReader::new(f).lines() {
44 let line = line?;
45 if line.trim().is_empty() {
46 continue;
47 }
48 if let Ok(flag) = serde_json::from_str::<Flag>(&line) {
49 out.push(flag);
50 }
51 }
52 Ok(out)
53 }
54
55 pub fn reconfirm_count(&self, note_prefix: &str) -> io::Result<u32> {
58 Ok(self
59 .recall_all()?
60 .iter()
61 .filter(|f| f.trit == 1 && f.note.starts_with(note_prefix))
62 .count() as u32)
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::flag::Source;
70 use crate::trit::Trit;
71
72 #[test]
73 fn remember_creates_missing_parent_directory() {
74 let dir = tempfile::tempdir().unwrap();
78 let nested = dir.path().join("does").join("not").join("exist").join("flags.jsonl");
79 let store = FlagStore::open(&nested);
80 let f = Flag::new(Trit::Affirm, 0.9, "regress", Source::Agent, "t");
81 assert!(store.remember(&f).is_ok());
82 assert_eq!(store.recall_all().unwrap().len(), 1);
83 }
84
85 #[test]
86 fn remember_and_recall_roundtrip() {
87 let dir = tempfile::tempdir().unwrap();
88 let store = FlagStore::open(dir.path().join("flags.jsonl"));
89 let f1 = Flag::new(Trit::Affirm, 0.9, "test:one", Source::Agent, "2026-07-02T20:00:00Z");
90 store.remember(&f1).unwrap();
91 let all = store.recall_all().unwrap();
92 assert_eq!(all.len(), 1);
93 assert_eq!(all[0].trit, 1);
94 }
95
96 #[test]
97 fn reconfirm_count_counts_affirms_only() {
98 let dir = tempfile::tempdir().unwrap();
99 let store = FlagStore::open(dir.path().join("flags.jsonl"));
100 store.remember(&Flag::new(Trit::Affirm, 0.9, "x:1", Source::Agent, "t1")).unwrap();
101 store.remember(&Flag::new(Trit::Affirm, 0.8, "x:2", Source::Human, "t2")).unwrap();
102 store.remember(&Flag::new(Trit::Reject, 0.5, "x:3", Source::Agent, "t3")).unwrap();
103 assert_eq!(store.reconfirm_count("x:").unwrap(), 2);
104 }
105}