Skip to main content

fxrs_core/
read_evidence.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::RwLock;
4
5/// SHA-256 digest used by the original implementation for file snapshots.
6pub type ContentHash = [u8; 32];
7
8/// Evidence captured when a model-visible file read succeeds.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub struct ReadEvidence {
11    pub modified_ns: i128,
12    pub content_hash: ContentHash,
13    pub model_view_covers_full_file: bool,
14    pub snapshot_covers_full_file: bool,
15}
16
17/// Session-scoped read evidence port.
18///
19/// Hosts may replace the in-memory implementation when reads happen in a
20/// remote workspace or need to be projected into a durable session log.
21pub trait ReadEvidenceStore: Send + Sync {
22    fn record(&self, path: PathBuf, evidence: ReadEvidence);
23    fn lookup(&self, path: &Path) -> Option<ReadEvidence>;
24    fn remove(&self, path: &Path);
25}
26
27/// Minimal in-process store used by the native CLI and tests.
28#[derive(Debug, Default)]
29pub struct MemoryReadEvidenceStore {
30    entries: RwLock<HashMap<PathBuf, ReadEvidence>>,
31}
32
33impl ReadEvidenceStore for MemoryReadEvidenceStore {
34    fn record(&self, path: PathBuf, evidence: ReadEvidence) {
35        self.entries
36            .write()
37            .unwrap_or_else(std::sync::PoisonError::into_inner)
38            .insert(path, evidence);
39    }
40
41    fn lookup(&self, path: &Path) -> Option<ReadEvidence> {
42        self.entries
43            .read()
44            .unwrap_or_else(std::sync::PoisonError::into_inner)
45            .get(path)
46            .copied()
47    }
48
49    fn remove(&self, path: &Path) {
50        self.entries
51            .write()
52            .unwrap_or_else(std::sync::PoisonError::into_inner)
53            .remove(path);
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn records_overwrites_and_removes_evidence() {
63        let store = MemoryReadEvidenceStore::default();
64        let path = PathBuf::from("/tmp/file.txt");
65        let first = ReadEvidence {
66            modified_ns: 1,
67            content_hash: [1; 32],
68            model_view_covers_full_file: true,
69            snapshot_covers_full_file: true,
70        };
71        let second = ReadEvidence {
72            modified_ns: 2,
73            content_hash: [2; 32],
74            model_view_covers_full_file: false,
75            snapshot_covers_full_file: true,
76        };
77
78        store.record(path.clone(), first);
79        assert_eq!(store.lookup(&path), Some(first));
80        store.record(path.clone(), second);
81        assert_eq!(store.lookup(&path), Some(second));
82        store.remove(&path);
83        assert_eq!(store.lookup(&path), None);
84    }
85}