- 1
//! The trash (docs/plans/data-architecture-plan.md, M0): a session moved to - 2
//! the trash is hidden everywhere, from the session lists, every search a - 3
//! person or the model can run, its transcript and export, and digests, - 4
//! and can be restored. Erasure, which removes it for good, is later work - 5
//! (M7). This module is the one place any of those readers asks. - 6
//! - 7
//! The set is keyed by session id in the shared data home, like the archive, - 8
//! so a single mutation and its bulk form share one boundary check - 9
//! (AGENTS.md invariant 22) in the server. - 10
- 11
use std::collections::{HashMap, HashSet}; - 12
use std::path::{Path, PathBuf}; - 13
use std::sync::{Mutex, OnceLock}; - 14
- 15
fn path(shared_home: &Path) -> PathBuf { - 16
shared_home.join("deleted.json") - 17
} - 18
- 19
fn read(shared_home: &Path) -> HashMap<String, bool> { - 20
std::fs::read_to_string(path(shared_home)) - 21
.ok() - 22
.and_then(|text| serde_json::from_str(&text).ok()) - 23
.unwrap_or_default() - 24
} - 25
- 26
/// Every session id in the trash. - 27
pub fn trashed(shared_home: &Path) -> HashSet<String> { - 28
read(shared_home) - 29
.into_iter() - 30
.filter_map(|(id, trashed)| trashed.then_some(id)) - 31
.collect() - 32
} - 33
- 34
pub fn is_trashed(shared_home: &Path, session_id: &str) -> bool { - 35
read(shared_home).get(session_id).copied().unwrap_or(false) - 36
} - 37
- 38
/// What a cross-session search must skip: the trash and, when there is one, - 39
/// the session doing the searching. - 40
pub fn search_exclusions(shared_home: &Path, current: Option<&str>) -> HashSet<String> { - 41
let mut excluded = trashed(shared_home); - 42
excluded.extend(current.map(str::to_string)); - 43
excluded - 44
} - 45
- 46
/// Moves sessions into or out of the trash, atomically and one writer at a - 47
/// time within the process. - 48
pub fn set(shared_home: &Path, session_ids: &[String], trashed: bool) -> std::io::Result<()> { - 49
static WRITE: OnceLock<Mutex<()>> = OnceLock::new(); - 50
let _guard = WRITE - 51
.get_or_init(|| Mutex::new(())) - 52
.lock() - 53
.unwrap_or_else(std::sync::PoisonError::into_inner); - 54
let mut map = read(shared_home); - 55
for id in session_ids { - 56
if trashed { - 57
map.insert(id.clone(), true); - 58
} else { - 59
map.remove(id); - 60
} - 61
} - 62
std::fs::create_dir_all(shared_home)?; - 63
let temp = path(shared_home).with_extension("json.tmp"); - 64
std::fs::write( - 65
&temp, - 66
serde_json::to_string(&map).map_err(std::io::Error::other)?, - 67
)?; - 68
std::fs::rename(temp, path(shared_home)) - 69
} - 70
- 71
#[cfg(test)] - 72
#[allow(clippy::unwrap_used)] - 73
mod tests { - 74
use super::*; - 75
- 76
#[test] - 77
fn a_session_moves_into_and_out_of_the_trash() { - 78
let home = tempfile::tempdir().unwrap(); - 79
assert!(!is_trashed(home.path(), "s1")); - 80
set(home.path(), &["s1".into(), "s2".into()], true).unwrap(); - 81
assert!(is_trashed(home.path(), "s1")); - 82
assert_eq!(trashed(home.path()).len(), 2); - 83
set(home.path(), &["s1".into()], false).unwrap(); - 84
assert!(!is_trashed(home.path(), "s1")); - 85
let excluded = search_exclusions(home.path(), Some("current")); - 86
assert!(excluded.contains("s2") && excluded.contains("current")); - 87
assert!(!excluded.contains("s1")); - 88
} - 89
} - 90
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.