use crate::db::GraphDb;
use crate::repograph::facts::label_of;
use crate::repograph::rules::{about_rule, ABOUT_LABELS};
use core_storage::fs::Fs;
use core_storage::{GraphError, Result, Value};
use std::collections::BTreeSet;
const MIN_TEXT_CHARS: usize = 1;
const MAX_TEXT_CHARS: usize = 4000;
pub const NOTE_KINDS: [&str; 3] = ["note", "decision", "todo"];
pub struct RememberInput<'a> {
pub text: &'a str,
pub about: &'a [String],
pub kind: &'a str,
pub ts: i64,
}
pub fn remember<F: Fs>(w: &mut GraphDb<F>, input: &RememberInput<'_>) -> Result<String> {
let text = input.text.trim();
let len = text.chars().count();
if !(MIN_TEXT_CHARS..=MAX_TEXT_CHARS).contains(&len) {
return Err(GraphError::IngestError {
detail: format!(
"remember: text must be {MIN_TEXT_CHARS}..={MAX_TEXT_CHARS} characters \
after trimming, got {len}"
),
});
}
if !NOTE_KINDS.contains(&input.kind) {
return Err(GraphError::IngestError {
detail: format!(
"remember: kind must be one of {}, got {:?}",
NOTE_KINDS.join(", "),
input.kind
),
});
}
let mut about: Vec<String> = input.about.to_vec();
about.sort();
about.dedup();
if let Some(missing) = about.iter().find(|key| !w.has_node(key)) {
return Err(GraphError::KeyNotFound {
key: missing.clone(),
});
}
if !w
.fulltext_pairs()
.contains(&("Note".to_string(), "text".to_string()))
{
w.enable_fulltext("Note", "text")?;
}
ensure_about_rules(w, &about)?;
let key = note_key(input.ts, text);
if !w.has_node(&key) {
let mut props: Vec<(String, Value)> = vec![
("id".into(), Value::Str(key.clone())),
("text".into(), Value::Str(text.to_string())),
("kind".into(), Value::Str(input.kind.to_string())),
("ts".into(), Value::Int(input.ts)),
("source".into(), Value::Str("agent".to_string())),
];
if !about.is_empty() {
props.push((
"about".into(),
Value::List(about.into_iter().map(Value::Str).collect()),
));
}
w.insert_node("Note", &key, props)?;
}
Ok(key)
}
fn ensure_about_rules<F: Fs>(w: &mut GraphDb<F>, about_keys: &[String]) -> Result<()> {
let mut labels: BTreeSet<String> = about_keys
.iter()
.filter_map(|key| label_of(w, key))
.filter(|label| ABOUT_LABELS.contains(&label.as_str()))
.collect();
if labels.is_empty() {
return Ok(());
}
let existing: BTreeSet<String> = w.rules().into_iter().map(|r| r.name).collect();
labels.retain(|label| {
let name = format!("about_{}", label.to_lowercase());
!existing.contains(&name)
});
for label in labels {
w.create_rule(about_rule(&label))?;
}
Ok(())
}
fn note_key(ts: i64, text: &str) -> String {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h = FNV_OFFSET;
for b in ts.to_string().bytes().chain(text.bytes()) {
h ^= u64::from(b);
h = h.wrapping_mul(FNV_PRIME);
}
format!("note:{h:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_key_is_a_function_of_ts_and_text_alone() {
assert_eq!(note_key(1, "a"), note_key(1, "a"));
assert_ne!(note_key(1, "a"), note_key(2, "a"));
assert_ne!(note_key(1, "a"), note_key(1, "b"));
assert!(note_key(1, "a").strip_prefix("note:").unwrap().len() == 16);
}
}