use super::session::ResidentMark;
use cyberbrain_core::blocks::{MAX_BLOCK_TOKENS, approx_tokens, blocks_of};
use cyberbrain_core::{Block, Fingerprint, Note, Ring, Slash, Store, slash};
use std::collections::BTreeMap;
use std::path::PathBuf;
pub struct ResidentNote {
pub note: Note,
pub blocks: Vec<Block>,
pub fingerprint: Fingerprint,
}
impl ResidentNote {
pub fn key(&self) -> String {
format!("{}/{}", self.note.front.ring, self.note.front.name)
}
pub fn mark(&self) -> ResidentMark {
ResidentMark {
ring: self.note.front.ring.as_u8(),
path: slash(&self.note.path),
hash: self.fingerprint.hash_hex(),
size: self.fingerprint.size,
updated: self.note.front.updated.to_string(),
}
}
}
#[derive(Default)]
pub struct Resident {
pub notes: Vec<ResidentNote>,
pub unreadable: Vec<(PathBuf, String)>,
pub other_files: usize,
pub missing_dirs: Vec<Ring>,
pub tokens: usize,
}
pub fn read(store: &Store) -> Resident {
let mut r = Resident::default();
for ring in [Ring::Invariant, Ring::Protocol] {
let dir = store.ring_dir(ring);
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
r.missing_dirs.push(ring);
continue;
}
Err(e) => {
r.unreadable.push((dir, e.to_string()));
continue;
}
};
let mut paths: Vec<PathBuf> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
continue;
}
if entry.file_type().is_ok_and(|t| t.is_dir()) {
r.other_files += 1;
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("md") {
r.other_files += 1;
continue;
}
paths.push(path);
}
paths.sort();
for path in paths {
let fingerprint = match Fingerprint::of(&path) {
Ok(f) => f,
Err(e) => {
r.unreadable.push((path, e.to_string()));
continue;
}
};
match store.read_path(&path) {
Ok(note) => {
if note.front.ring != ring {
r.unreadable.push((
path,
format!(
"frontmatter says ring {} but the file is in {ring}",
note.front.ring
),
));
continue;
}
r.tokens += approx_tokens(¬e.body) as usize;
let (blocks, _oversized) = blocks_of(¬e, MAX_BLOCK_TOKENS);
r.notes.push(ResidentNote {
note,
blocks,
fingerprint,
});
}
Err(e) => r.unreadable.push((path, e.to_string())),
}
}
}
r
}
pub fn marks(r: &Resident) -> BTreeMap<String, ResidentMark> {
r.notes.iter().map(|n| (n.key(), n.mark())).collect()
}
#[derive(Default)]
pub struct Changes<'a> {
pub changed: Vec<&'a ResidentNote>,
pub added: Vec<&'a ResidentNote>,
pub removed: Vec<String>,
}
impl Changes<'_> {
pub fn is_empty(&self) -> bool {
self.changed.is_empty() && self.added.is_empty() && self.removed.is_empty()
}
}
pub fn diff<'a>(before: &BTreeMap<String, ResidentMark>, now: &'a Resident) -> Changes<'a> {
let mut c = Changes::default();
let mut seen = std::collections::BTreeSet::new();
for n in &now.notes {
let key = n.key();
seen.insert(key.clone());
match before.get(&key) {
None => c.added.push(n),
Some(m) if m.hash != n.fingerprint.hash_hex() || m.size != n.fingerprint.size => {
c.changed.push(n)
}
Some(_) => {}
}
}
for key in before.keys() {
if !seen.contains(key) {
c.removed.push(key.clone());
}
}
c
}
pub fn ring_title(ring: Ring) -> &'static str {
match ring {
Ring::Invariant => "Ring 0: operator invariants (override everything below)",
Ring::Protocol => "Ring 1: operating protocol and handoff state (override rings 2+)",
Ring::Knowledge => "Ring 2: curated project knowledge",
Ring::Session => "Ring 3: session records",
Ring::External => "Ring 4: external, unverified",
}
}
pub fn render_note(n: &ResidentNote, out: &mut String) {
let f = &n.note.front;
out.push_str(&format!("### {} `{}`", f.ring, f.name));
let mut meta = vec![format!("kind: {}", kind_name(f.kind))];
meta.push(format!("updated: {}", f.updated.strftime("%Y-%m-%d")));
if !f.tags.is_empty() {
meta.push(format!("tags: {}", f.tags.join(", ")));
}
out.push_str(&format!(" ({})\n", meta.join(", ")));
if n.blocks.is_empty() {
out.push_str("(empty body)\n\n");
return;
}
for b in &n.blocks {
out.push_str(&format!("[{}]\n{}\n\n", b.citation, b.text.trim_end()));
}
}
pub fn render_rings(r: &Resident, out: &mut String) {
for ring in [Ring::Invariant, Ring::Protocol] {
out.push_str(&format!("## {}\n\n", ring_title(ring)));
let notes: Vec<&ResidentNote> = r
.notes
.iter()
.filter(|n| n.note.front.ring == ring)
.collect();
if r.missing_dirs.contains(&ring) {
out.push_str(&format!(
"(the directory notes/{} does not exist; `cyberbrain init` creates it)\n\n",
ring.dir()
));
} else if notes.is_empty() {
out.push_str(&format!("(ring {} is empty)\n\n", ring.as_u8()));
}
for n in notes {
render_note(n, out);
}
}
if !r.unreadable.is_empty() {
out.push_str("## Resident notes that could NOT be injected\n\n");
out.push_str(
"These files sit in ring 0 or 1 and failed to read or parse. Whatever they say \
is not in your context; tell the operator.\n\n",
);
for (p, why) in &r.unreadable {
out.push_str(&format!("- {}: {why}\n", Slash(p)));
}
out.push('\n');
}
}
pub fn kind_name(k: cyberbrain_core::NoteKind) -> String {
match serde_json::to_value(k) {
Ok(serde_json::Value::String(s)) => s,
_ => format!("{k:?}").to_lowercase(),
}
}