diff --git a/poc-memory/src/graph.rs b/poc-memory/src/graph.rs index 163453b..d6a311d 100644 --- a/poc-memory/src/graph.rs +++ b/poc-memory/src/graph.rs @@ -524,27 +524,12 @@ fn metrics_log_path() -> std::path::PathBuf { /// Load previous metrics snapshots pub fn load_metrics_history() -> Vec { - let path = metrics_log_path(); - let content = match std::fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - content.lines() - .filter_map(|line| serde_json::from_str(line).ok()) - .collect() + crate::util::jsonl_load(&metrics_log_path()) } /// Append a metrics snapshot to the log pub fn save_metrics_snapshot(snap: &MetricsSnapshot) { - let path = metrics_log_path(); - if let Ok(json) = serde_json::to_string(snap) { - use std::io::Write; - if let Ok(mut f) = std::fs::OpenOptions::new() - .create(true).append(true).open(&path) - { - let _ = writeln!(f, "{}", json); - } - } + let _ = crate::util::jsonl_append(&metrics_log_path(), snap); } /// Compute current graph metrics as a snapshot (no side effects). diff --git a/poc-memory/src/util.rs b/poc-memory/src/util.rs index 8c475f7..4b65a96 100644 --- a/poc-memory/src/util.rs +++ b/poc-memory/src/util.rs @@ -2,8 +2,12 @@ use crate::store; +use serde::de::DeserializeOwned; +use serde::Serialize; + use std::fs; -use std::path::PathBuf; +use std::io::Write; +use std::path::{Path, PathBuf}; /// Ensure a subdirectory of the memory dir exists and return its path. pub fn memory_subdir(name: &str) -> Result { @@ -29,6 +33,30 @@ pub fn first_n_chars(s: &str, n: usize) -> String { s.chars().take(n).collect() } +// ── JSONL helpers ─────────────────────────────────────────────────── + +/// Read a JSONL file, deserializing each line. Silently skips bad lines. +pub fn jsonl_load(path: &Path) -> Vec { + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + content.lines() + .filter_map(|line| serde_json::from_str(line).ok()) + .collect() +} + +/// Append one record as a JSON line to a file (create if missing). +pub fn jsonl_append(path: &Path, item: &T) -> Result<(), String> { + let json = serde_json::to_string(item) + .map_err(|e| format!("serialize: {}", e))?; + let mut f = fs::OpenOptions::new() + .create(true).append(true).open(path) + .map_err(|e| format!("open {}: {}", path.display(), e))?; + writeln!(f, "{}", json) + .map_err(|e| format!("write {}: {}", path.display(), e)) +} + /// Parse a timestamp string to unix epoch seconds. /// Handles: "2026-03-05T19:56:00", "2026-03-05T19:56", "2026-03-05 19:56:00", "2026-03-05 19:56" pub fn parse_timestamp_to_epoch(ts: &str) -> Option {