//! JSONL file–backed `EditLogRepository`. //! //! Path: `/edits.jsonl` //! //! Append-only log: new entries are appended to the file, never rewritten. //! In-memory cache is capped at 10K entries to prevent unbounded growth. #![allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap )] use std::io::{BufRead, BufReader, Write}; use std::path::Path; use anyhow::{Context, Result}; use crate::domain::edit_log::{EditLog, EditLogEntry, MAX_MEMORY_ENTRIES}; use crate::domain::repository::EditLogRepository; /// Persists `EditLog` as an append-only JSONL file at `/edits.jsonl`. #[derive(Debug, Clone, Default)] pub struct JsonlEditLogRepository; impl JsonlEditLogRepository { /// Create a new repository instance. pub fn new() -> Self { Self } /// Read existing entries from disk into memory, capped at `MAX_MEMORY_ENTRIES`. fn load_from_disk(path: &Path) -> Vec { let Ok(file) = std::fs::File::open(path) else { return Vec::new(); }; let reader = BufReader::new(file); let mut entries: Vec = Vec::new(); for line in reader.lines() { let Ok(line) = line else { continue; }; if let Ok(entry) = serde_json::from_str::(&line) { if entries.len() >= MAX_MEMORY_ENTRIES { entries.remove(0); } entries.push(entry); } } entries } } impl EditLogRepository for JsonlEditLogRepository { fn open(&self, session_dir: &Path) -> Result { let path = session_dir.join("edits.jsonl"); // Ensure parent dir exists if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("failed to create session dir '{}'", parent.display()))?; } let entries = Self::load_from_disk(&path); // Touch the file if it doesn't exist yet if !path.exists() { std::fs::OpenOptions::new() .create(true) .append(true) .open(&path) .with_context(|| format!("failed to create edits.jsonl at '{}'", path.display()))?; } Ok(EditLog { entries }) } fn append(&self, session_dir: &Path, log: &mut EditLog, entry: EditLogEntry) -> Result<()> { let path = session_dir.join("edits.jsonl"); let line = serde_json::to_string(&entry) .context("failed to serialize edit log entry")? + "\n"; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("failed to create session dir '{}'", parent.display()))?; } { let mut file = std::fs::OpenOptions::new() .create(true) .append(true) .open(&path) .with_context(|| format!("failed to open edits.jsonl at '{}'", path.display()))?; file.write_all(line.as_bytes()) .context("failed to write edit log entry")?; file.sync_all() .context("failed to fsync edit log")?; } log.entries.push(entry); // Enforce in-memory cap if log.entries.len() > MAX_MEMORY_ENTRIES { log.entries.remove(0); } Ok(()) } fn entries(&self, log: &EditLog) -> Vec { log.entries.clone() } }