144 lines
4.2 KiB
Rust
144 lines
4.2 KiB
Rust
//! The tiering listing: identity entries and notes with recall-hit counts (M5d spec, 4.4).
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::io::ErrorKind;
|
|
|
|
use bxcore::{MemoryFile, Timestamp};
|
|
use serde::Deserialize;
|
|
|
|
use crate::files::Store;
|
|
use crate::notes::{self, Kind, Note};
|
|
|
|
#[derive(Deserialize)]
|
|
struct Logged {
|
|
time: String,
|
|
hits: Vec<Hit>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Hit {
|
|
kind: String,
|
|
session: String,
|
|
record: i64,
|
|
}
|
|
|
|
/// How many recall lines named one note, and the latest of those lines' times.
|
|
#[derive(Default)]
|
|
struct Tally {
|
|
n: u64,
|
|
last: String,
|
|
}
|
|
|
|
fn fail(path: &std::path::Path, e: std::io::Error) -> String {
|
|
format!("{}: {e}", path.display())
|
|
}
|
|
|
|
fn class_of(note: &Note) -> &'static str {
|
|
match ¬e.kind {
|
|
Kind::Noted { class, .. } => notes::class_name(*class),
|
|
Kind::Moved(_) | Kind::Dropped(_) => "private",
|
|
}
|
|
}
|
|
|
|
fn trust_of(note: &Note) -> &'static str {
|
|
match ¬e.kind {
|
|
Kind::Noted { untrusted, .. } => {
|
|
if *untrusted {
|
|
"untrusted"
|
|
} else {
|
|
"trusted"
|
|
}
|
|
}
|
|
Kind::Moved(_) | Kind::Dropped(_) => "trusted",
|
|
}
|
|
}
|
|
|
|
fn read_notes(store: &Store) -> Result<String, String> {
|
|
let path = store.path("notes.md");
|
|
match std::fs::read_to_string(&path) {
|
|
Ok(text) => Ok(text),
|
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(String::new()),
|
|
Err(e) => Err(fail(&path, e)),
|
|
}
|
|
}
|
|
|
|
/// The recall hits per `(session, record)`: how many lines named it and the latest of their times
|
|
/// (M5d spec, section 4.4). A missing `recalls/` is an empty map; a line that does not parse is
|
|
/// skipped, and a line counts once for each note it names.
|
|
fn tally(store: &Store) -> Result<HashMap<(String, i64), Tally>, String> {
|
|
let dir = store.path("recalls");
|
|
let files = match std::fs::read_dir(&dir) {
|
|
Ok(files) => files,
|
|
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(HashMap::new()),
|
|
Err(e) => return Err(fail(&dir, e)),
|
|
};
|
|
let mut hits: HashMap<(String, i64), Tally> = HashMap::new();
|
|
for file in files {
|
|
let file = file.map_err(|e| fail(&dir, e))?;
|
|
if !file.file_type().map_err(|e| fail(&dir, e))?.is_file() {
|
|
continue;
|
|
}
|
|
let path = file.path();
|
|
let text = match std::fs::read_to_string(&path) {
|
|
Ok(text) => text,
|
|
Err(e) if e.kind() == ErrorKind::NotFound => continue,
|
|
Err(e) => return Err(fail(&path, e)),
|
|
};
|
|
for line in text.lines() {
|
|
let logged: Logged = match serde_json::from_str(line) {
|
|
Ok(logged) => logged,
|
|
Err(_) => continue,
|
|
};
|
|
let mut seen: HashSet<(String, i64)> = HashSet::new();
|
|
for hit in &logged.hits {
|
|
if hit.kind == "noted" {
|
|
seen.insert((hit.session.clone(), hit.record));
|
|
}
|
|
}
|
|
for key in seen {
|
|
let tally = hits.entry(key).or_default();
|
|
tally.n += 1;
|
|
if logged.time > tally.last {
|
|
tally.last = logged.time.clone();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(hits)
|
|
}
|
|
|
|
/// One listing for `bxctl memory tiers` and for the tiering job's user text.
|
|
pub fn list(store: &Store, _now: Timestamp) -> Result<String, String> {
|
|
let mut out = String::new();
|
|
|
|
for file in [MemoryFile::Memory, MemoryFile::User] {
|
|
for entry in store.entries(file)? {
|
|
out.push_str(&format!(
|
|
"identity {} hits 0 last -\n {}\n",
|
|
file.name(),
|
|
entry
|
|
));
|
|
}
|
|
}
|
|
|
|
let hits = tally(store)?;
|
|
for (i, note) in notes::parse(&read_notes(store)?).into_iter().enumerate() {
|
|
let key = (note.session.clone(), i as i64);
|
|
let (n, last) = match hits.get(&key) {
|
|
Some(t) => (t.n, t.last.clone()),
|
|
None => (0, "-".to_string()),
|
|
};
|
|
out.push_str(&format!(
|
|
"note session {} hits {} last {} {} {}\n {}\n",
|
|
note.session,
|
|
n,
|
|
last,
|
|
class_of(¬e),
|
|
trust_of(¬e),
|
|
note.text
|
|
));
|
|
}
|
|
|
|
Ok(out)
|
|
}
|