use crate::profile::Paths;
use anyhow::{anyhow, bail, Context, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub mod deliver;
pub mod expiry;
pub mod index;
pub mod ingest;
pub mod promote;
pub mod recall;
pub mod tools;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Layer {
Team,
Local,
}
pub const GUEST_LOCAL_NOTES: &str = "/omh/notes/local";
impl Layer {
pub const ALL: [Layer; 2] = [Self::Team, Self::Local];
pub const AGENT_WRITE: Layer = Self::Local;
pub fn dir(&self, paths: &Paths) -> PathBuf {
match self {
Self::Team => paths.repo.join(".omh").join("notes"),
Self::Local => paths.notes().join("local"),
}
}
pub fn is_committed(&self) -> bool {
matches!(self, Self::Team)
}
}
impl std::fmt::Display for Layer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Team => "team",
Self::Local => "local",
})
}
}
impl std::str::FromStr for Layer {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
match s {
"team" => Ok(Self::Team),
"local" => Ok(Self::Local),
other => anyhow::bail!("unknown layer `{other}` (team, local)"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Kind {
Surprise,
Topic,
Stub,
}
impl Kind {
pub const ALL: [Kind; 3] = [Self::Surprise, Self::Topic, Self::Stub];
pub fn required_sections(&self) -> &'static [&'static str] {
match self {
Self::Surprise => &["Expected", "Observed", "Evidence", "Answers"],
Self::Topic => &[],
Self::Stub => &["Answers"],
}
}
pub fn list_sections(&self) -> &'static [&'static str] {
match self {
Self::Surprise => &["Related", "Answers"],
Self::Topic => &["Related"],
Self::Stub => &["Answers"],
}
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Surprise => "surprise",
Self::Topic => "topic",
Self::Stub => "stub",
})
}
}
impl std::str::FromStr for Kind {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
Kind::ALL
.into_iter()
.find(|k| k.to_string() == s)
.ok_or_else(|| {
let known: Vec<String> = Kind::ALL.iter().map(|k| k.to_string()).collect();
anyhow!("unknown note type `{s}` ({})", known.join(", "))
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub key: String,
pub kind: Kind,
pub source: String,
pub recorded: String,
pub invalidated_by: Option<String>,
pub body: String,
pub layer: Layer,
pub path: PathBuf,
}
fn split_frontmatter<'a>(raw: &'a str, path: &Path) -> Result<(&'a str, &'a str)> {
let rest = raw
.strip_prefix("---\n")
.with_context(|| format!("{}: no frontmatter", path.display()))?;
let end = rest
.find("\n---")
.with_context(|| format!("{}: frontmatter is never closed", path.display()))?;
Ok((&rest[..end], rest[end + 4..].trim_start_matches('\n')))
}
fn required<'a>(fields: &BTreeMap<&str, &'a str>, name: &str, path: &Path) -> Result<&'a str> {
match fields.get(name) {
None => bail!("{}: missing `{name}`", path.display()),
Some(&"") => bail!("{}: `{name}` is present but empty", path.display()),
Some(v) => Ok(v),
}
}
pub fn parse(raw: &str, layer: Layer, path: &Path) -> Result<Note> {
let (head, body) = split_frontmatter(raw, path)?;
let mut fields: BTreeMap<&str, &str> = BTreeMap::new();
for line in head.lines().filter(|l| !l.trim().is_empty()) {
let (k, v) = line
.split_once(':')
.with_context(|| format!("{}: `{line}` is not `key: value`", path.display()))?;
fields.insert(k.trim(), v.trim());
}
let recorded = required(&fields, "recorded", path)?;
if !is_calendar_date(recorded) {
bail!(
"{}: `recorded` must be a real calendar date, got `{recorded}`",
path.display()
);
}
let kind: Kind = required(&fields, "type", path)?
.parse()
.map_err(|e| anyhow!("{}: {e}", path.display()))?;
Ok(Note {
key: required(&fields, "key", path)?.to_string(),
kind,
source: required(&fields, "source", path)?.to_string(),
recorded: recorded.to_string(),
invalidated_by: fields
.get("invalidated_by")
.filter(|v| !v.is_empty())
.map(|v| v.to_string()),
body: body.to_string(),
layer,
path: path.to_path_buf(),
})
}
pub fn render(note: &Note) -> String {
let mut out = String::from("---\n");
out.push_str(&format!("key: {}\n", note.key));
out.push_str(&format!("type: {}\n", note.kind));
out.push_str(&format!("source: {}\n", note.source));
out.push_str(&format!("recorded: {}\n", note.recorded));
if let Some(trigger) = ¬e.invalidated_by {
out.push_str(&format!("invalidated_by: {trigger}\n"));
}
out.push_str("---\n\n");
out.push_str(¬e.body);
out
}
pub(crate) fn is_calendar_date(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
return false;
}
if !b
.iter()
.enumerate()
.all(|(i, c)| i == 4 || i == 7 || c.is_ascii_digit())
{
return false;
}
let (year, month, day) = (
s[0..4].parse::<u32>().unwrap_or(0),
s[5..7].parse::<u32>().unwrap_or(0),
s[8..10].parse::<u32>().unwrap_or(0),
);
(1..=12).contains(&month) && day >= 1 && day <= days_in_month(year, month)
}
fn days_in_month(year: u32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(year) => 29,
2 => 28,
_ => 0,
}
}
fn is_leap(year: u32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
pub fn slug(text: &str) -> Result<String> {
let mut out = String::new();
for ch in text.chars() {
if ch.is_alphanumeric() {
out.extend(ch.to_lowercase());
} else if !out.ends_with('-') {
out.push('-');
}
}
let slug = out.trim_matches('-');
if slug.is_empty() {
bail!("`{text}` has nothing a key can be made from");
}
Ok(slug.to_string())
}
pub fn slug_of_observation(observed: &str) -> Result<String> {
let first = observed
.find(['.', '!', '?'])
.map(|i| &observed[..i])
.unwrap_or(observed);
slug(first)
}
pub fn expand_key(template: &str, vars: &[(&str, &str)]) -> Result<String> {
let mut out = String::new();
let mut rest = template;
while let Some(start) = rest.find("{{") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
let end = after
.find("}}")
.with_context(|| format!("`{template}`: a placeholder is never closed"))?;
let name = after[..end].trim();
let (_, value) = vars
.iter()
.find(|(k, _)| *k == name)
.with_context(|| format!("`{template}` uses `{{{{{name}}}}}`, which nothing binds"))?;
out.push_str(value);
rest = &after[end + 2..];
}
out.push_str(rest);
validate_key(&out)?;
Ok(out)
}
pub(crate) fn validate_key(key: &str) -> Result<()> {
let escapes = key.contains('\\')
|| key
.split('/')
.any(|part| part.is_empty() || part.starts_with('.'));
if escapes {
bail!("`{key}` is not a key: a key is slash-separated slugs, never a path");
}
Ok(())
}
pub fn today() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
civil(secs / 86_400)
}
fn civil(days: i64) -> String {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + i64::from(month <= 2);
format!("{year:04}-{month:02}-{day:02}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Rule {
Unreadable,
UnclosedFence,
MissingSection,
ProseInListSection,
KeyDisagreesWithPath,
DuplicateKey,
DanglingLink,
CrossLayerLink,
UnevaluatableTrigger,
Orphan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Refused,
Warning,
}
impl Rule {
pub fn severity(&self) -> Severity {
match self {
Self::Unreadable
| Self::UnclosedFence
| Self::MissingSection
| Self::ProseInListSection
| Self::KeyDisagreesWithPath
| Self::UnevaluatableTrigger => Severity::Refused,
Self::DuplicateKey | Self::DanglingLink | Self::CrossLayerLink | Self::Orphan => {
Severity::Warning
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub key: String,
pub layer: Layer,
pub rule: Rule,
pub detail: String,
}
fn fence_marker(line: &str) -> Option<(char, usize)> {
let trimmed = line.trim_start();
let ch = trimmed.chars().next()?;
if ch != '`' && ch != '~' {
return None;
}
let run = trimmed.chars().take_while(|c| *c == ch).count();
(run >= 3).then_some((ch, run))
}
struct Fenced<'a> {
lines: Vec<(&'a str, bool)>,
unclosed: bool,
}
fn scan_fences(body: &str) -> Fenced<'_> {
let mut open: Option<(char, usize)> = None;
let mut lines = Vec::new();
for line in body.lines() {
let inside = match (open, fence_marker(line)) {
(None, Some(marker)) => {
open = Some(marker);
true
}
(Some((och, orun)), Some((cch, crun))) if cch == och && crun >= orun => {
open = None;
true
}
(Some(_), _) => true,
(None, _) => false,
};
lines.push((line, inside));
}
Fenced {
lines,
unclosed: open.is_some(),
}
}
pub(crate) fn sections(body: &str) -> BTreeMap<&str, Vec<&str>> {
let mut out: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
let mut current: Option<&str> = None;
for (line, quoted) in scan_fences(body).lines {
let heading = if quoted {
None
} else {
line.strip_prefix("## ")
};
if let Some(name) = heading {
current = Some(name.trim());
out.entry(name.trim()).or_default();
} else if let Some(name) = current {
out.entry(name).or_default().push(line);
}
}
out
}
pub fn check(note: &Note) -> Vec<Violation> {
let mut found = Vec::new();
let mut fire = |rule: Rule, detail: String| {
found.push(Violation {
key: note.key.clone(),
layer: note.layer,
rule,
detail,
})
};
if let Some(raw) = note.invalidated_by.as_deref().filter(|v| !v.is_empty()) {
if let Err(e) = expiry::Trigger::parse(raw) {
fire(Rule::UnevaluatableTrigger, format!("{e}"));
}
}
if scan_fences(¬e.body).unclosed {
fire(
Rule::UnclosedFence,
"a code fence is never closed, so everything after it is quoted \
— including the sections and links below it"
.to_string(),
);
return found;
}
let sections = sections(¬e.body);
for name in note.kind.required_sections() {
match sections.get(name) {
None => fire(
Rule::MissingSection,
format!("a `{}` note needs a `## {name}` section", note.kind),
),
Some(lines) if lines.iter().all(|l| l.trim().is_empty()) => {
fire(Rule::MissingSection, format!("`## {name}` is empty"))
}
Some(_) => {}
}
}
for name in note.kind.list_sections() {
let Some(lines) = sections.get(name) else {
continue;
};
if lines.iter().any(|l| {
!l.trim().is_empty() && !l.starts_with('-') && !l.starts_with(char::is_whitespace)
}) {
fire(
Rule::ProseInListSection,
format!("`## {name}` holds bullets and nothing else"),
);
}
}
let stem = note
.path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let leaf = note.key.rsplit('/').next().unwrap_or(¬e.key);
if !stem.is_empty() && stem != leaf {
fire(
Rule::KeyDisagreesWithPath,
format!("key `{}` is stored as `{stem}`", note.key),
);
}
found
}
pub fn links(body: &str) -> Vec<String> {
let mut out = Vec::new();
for (line, quoted) in scan_fences(body).lines {
if quoted {
continue;
}
let mut rest = line;
while let Some(start) = rest.find("[[") {
let after = &rest[start + 2..];
let Some(end) = after.find("]]") else { break };
let target = after[..end].trim();
if !target.is_empty() && !target.contains("[[") {
out.push(target.to_string());
}
rest = &after[end + 2..];
}
}
out
}
pub fn resolve(notes: &[Note], key: &str, from: Layer) -> Vec<Layer> {
let mut found: Vec<Layer> = notes
.iter()
.filter(|n| n.key == key)
.map(|n| n.layer)
.filter(|layer| !from.is_committed() || layer.is_committed())
.collect();
found.sort();
found.dedup();
found
}
pub fn uncommitted_links(notes: &[Note], note: &Note, also_committed: &[String]) -> Vec<String> {
let mut out: Vec<String> = links(¬e.body)
.into_iter()
.filter(|target| {
!also_committed.contains(target)
&& resolve(notes, target, Layer::Team).is_empty()
&& !resolve(notes, target, Layer::Local).is_empty()
})
.collect();
out.sort();
out.dedup();
out
}
pub fn hygiene(notes: &[Note]) -> Vec<Violation> {
let known: std::collections::BTreeSet<&str> = notes.iter().map(|n| n.key.as_str()).collect();
let mut pointed_at: std::collections::BTreeSet<String> = Default::default();
let mut found = Vec::new();
for note in notes {
if note.layer.is_committed() {
for target in uncommitted_links(notes, note, &[]) {
found.push(Violation {
key: note.key.clone(),
layer: note.layer,
rule: Rule::CrossLayerLink,
detail: format!(
"`{}` is committed but links to `{target}`, which is not — \
a fresh clone would not have it",
note.key
),
});
}
}
for target in links(¬e.body) {
if known.contains(target.as_str()) {
pointed_at.insert(target);
} else {
found.push(Violation {
key: note.key.clone(),
layer: note.layer,
rule: Rule::DanglingLink,
detail: format!(
"`{}` links to `{target}`, which is not in the store",
note.key
),
});
}
}
}
let mut by_key: BTreeMap<(Layer, &str), Vec<&Path>> = BTreeMap::new();
for note in notes {
by_key
.entry((note.layer, note.key.as_str()))
.or_default()
.push(¬e.path);
}
for ((layer, key), mut paths) in by_key {
if paths.len() < 2 {
continue;
}
paths.sort();
found.push(Violation {
key: key.to_string(),
layer,
rule: Rule::DuplicateKey,
detail: format!(
"`{key}` is claimed by {} files: {}",
paths.len(),
paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
});
}
for note in notes {
if !pointed_at.contains(¬e.key) {
found.push(Violation {
key: note.key.clone(),
layer: note.layer,
rule: Rule::Orphan,
detail: format!("nothing in the store links to `{}`", note.key),
});
}
}
found
}
fn markdown_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e).with_context(|| format!("reading {}", dir.display())),
};
for entry in entries {
let entry = entry.with_context(|| format!("reading {}", dir.display()))?;
let kind = entry
.file_type()
.with_context(|| format!("reading {}", entry.path().display()))?;
if kind.is_symlink() {
continue;
}
let path = entry.path();
if kind.is_dir() {
markdown_files(&path, out)?;
} else if path.extension().is_some_and(|e| e == "md") {
out.push(path);
}
}
Ok(())
}
fn contained(root: &Path, path: &Path) -> Result<()> {
let anchor = root
.canonicalize()
.with_context(|| format!("resolving {}", root.display()))?;
let mut existing = path.to_path_buf();
while !existing.exists() && existing.pop() {}
let resolved = existing
.canonicalize()
.with_context(|| format!("resolving {}", existing.display()))?;
if !resolved.starts_with(&anchor) {
bail!(
"{} resolves outside the store, so omh will not write there",
path.display()
);
}
Ok(())
}
pub fn notes_in(root: &Path, layer: Layer) -> Result<Vec<Note>> {
let mut files = Vec::new();
markdown_files(root, &mut files)?;
files.sort();
files
.iter()
.map(|path| {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path.display()))?;
parse(&raw, layer, path)
})
.collect()
}
pub fn load_layer(paths: &Paths, layer: Layer) -> Result<Vec<Note>> {
notes_in(&layer.dir(paths), layer)
}
struct LayerRead {
notes: Vec<Note>,
opaque: Vec<PathBuf>,
}
fn read_layer(root: &Path, layer: Layer) -> Result<LayerRead> {
let mut files = Vec::new();
markdown_files(root, &mut files)?;
files.sort();
let mut notes = Vec::new();
let mut opaque = Vec::new();
for path in files {
match std::fs::read_to_string(&path)
.map_err(anyhow::Error::from)
.and_then(|raw| parse(&raw, layer, &path))
{
Ok(note) => notes.push(note),
Err(_) => opaque.push(path),
}
}
Ok(LayerRead { notes, opaque })
}
pub fn load(paths: &Paths) -> Result<Vec<Note>> {
let mut all = Vec::new();
for layer in Layer::ALL {
all.extend(load_layer(paths, layer)?);
}
Ok(all)
}
pub const TEMPLATES: &str = "memory.toml";
pub const SHIPPED_KEYS: &str = "\
# How a note's key is derived. Identity, not a title — the agent never picks
# one, so the same observation cannot be recorded twice under two spellings.
[keys]
surprise = \"surprise/{{slug}}\"
topic = \"{{slug}}\"
stub = \"{{path}}\"
";
pub fn shipped_templates() -> BTreeMap<Kind, String> {
parse_templates(SHIPPED_KEYS).expect("the shipped key templates must parse")
}
fn parse_templates(raw: &str) -> Result<BTreeMap<Kind, String>> {
#[derive(serde::Deserialize)]
struct File {
keys: BTreeMap<String, String>,
}
let file: File = toml::from_str(raw).context("reading key templates")?;
file.keys
.into_iter()
.map(|(name, template)| Ok((name.parse::<Kind>()?, template)))
.collect()
}
pub fn templates(paths: &Paths) -> Result<BTreeMap<Kind, String>> {
let path = paths.repo.join(".omh").join(TEMPLATES);
let stale = paths.repo.join(".omh").join("keys.toml");
if stale.exists() {
let mut msg = format!("{} is where key templates used to live.\n", stale.display());
msg.push_str(&format!("They are read from {} now.\n", path.display()));
msg.push_str("Rename it rather than leaving both: the shipped defaults ");
msg.push_str("would silently re-key every note written from here on, ");
msg.push_str("and every existing key would stop being derivable.");
anyhow::bail!(msg);
}
match std::fs::read_to_string(&path) {
Ok(raw) => parse_templates(&raw).with_context(|| format!("{}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => parse_templates(SHIPPED_KEYS),
Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
}
}
pub fn lint(paths: &Paths) -> Result<Vec<Violation>> {
let mut notes = Vec::new();
let mut found = Vec::new();
for layer in Layer::ALL {
let read = read_layer(&layer.dir(paths), layer)?;
for path in read.opaque {
found.push(Violation {
key: path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default(),
layer,
rule: Rule::Unreadable,
detail: format!("{} is in the store but omh cannot read it", path.display()),
});
}
notes.extend(read.notes);
}
found.extend(notes.iter().flat_map(check));
found.extend(hygiene(¬es));
found.sort_by(|a, b| (a.rule, &a.key).cmp(&(b.rule, &b.key)));
Ok(found)
}
pub fn refused(violations: &[Violation]) -> usize {
violations
.iter()
.filter(|v| v.rule.severity() == Severity::Refused)
.count()
}
pub fn tally(violations: &[Violation]) -> BTreeMap<Rule, usize> {
let mut counts = BTreeMap::new();
for v in violations {
*counts.entry(v.rule).or_insert(0) += 1;
}
counts
}
#[derive(Debug, Clone, Default)]
pub struct Remembered {
pub expected: String,
pub observed: String,
pub evidence: String,
pub answers: Vec<String>,
pub relates_to: Vec<String>,
pub invalidated_by: Option<String>,
pub source: String,
pub recorded: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IfExists {
#[default]
Error,
Skip,
Suffix,
Override,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Wrote {
Created(PathBuf),
Replaced(PathBuf),
Skipped(String),
}
fn normalise_trigger(raw: &str, repo: &Path) -> Result<String> {
Ok(match expiry::Trigger::parse(raw)? {
expiry::Trigger::File { path, hash } => expiry::Trigger::File {
path: expiry::normalise_path(&path, repo),
hash,
}
.render(),
expiry::Trigger::Image { digest } if digest == expiry::IMAGE_NOW => {
let now = crate::image::recipe_digest(&crate::image::base_dockerfile())
.context("recording what the image recipe is right now")?;
expiry::Trigger::Image { digest: now }.render()
}
t @ (expiry::Trigger::Image { .. }
| expiry::Trigger::Base { .. }
| expiry::Trigger::Symbol { .. }) => t.render(),
})
}
fn non_blank(value: &str, name: &str) -> Result<()> {
if value.trim().is_empty() {
bail!("`{name}` is empty; there is nothing here worth recording");
}
Ok(())
}
fn body_of(input: &Remembered) -> String {
let mut body = format!(
"# {}\n\n## Expected\n{}\n\n## Observed\n{}\n\n## Evidence\n{}\n",
input.observed.trim(),
input.expected.trim(),
input.observed.trim(),
input.evidence.trim(),
);
body.push_str("\n## Answers\n\n");
for question in &input.answers {
body.push_str(&format!("- {}\n", question.trim()));
}
if !input.relates_to.is_empty() {
let mut related = input.relates_to.clone();
related.sort();
related.dedup();
body.push_str("\n## Related\n\n");
for key in related {
body.push_str(&format!("- [[{key}]]\n"));
}
}
body
}
pub fn remember(paths: &Paths, input: &Remembered, if_exists: IfExists) -> Result<Wrote> {
remember_in(
&Layer::AGENT_WRITE.dir(paths),
&paths.repo,
&templates(paths)?,
input,
if_exists,
)
}
pub fn remember_in(
root: &Path,
repo: &Path,
templates: &BTreeMap<Kind, String>,
input: &Remembered,
if_exists: IfExists,
) -> Result<Wrote> {
non_blank(&input.expected, "expected")?;
non_blank(&input.observed, "observed")?;
non_blank(&input.evidence, "evidence")?;
non_blank(&input.source, "source")?;
if input.answers.iter().all(|q| q.trim().is_empty()) {
bail!("`answers` is empty; a note nobody can find is a note nobody wrote");
}
let template = templates
.get(&Kind::Surprise)
.context("no key template for `surprise`")?;
let key = expand_key(
template,
&[("slug", &slug_of_observation(&input.observed)?)],
)?;
let layer = Layer::AGENT_WRITE;
let taken = read_layer(root, layer)?;
if let Some(opaque) = taken.opaque.first() {
bail!(
"{} is in the store but omh cannot read it, so it cannot tell whether `{key}` is \
free — fix or remove that note first",
opaque.display()
);
}
let held_at = |k: &str| {
taken
.notes
.iter()
.find(|note| note.key == k)
.map(|note| note.path.clone())
};
let (key, path, replacing) = match if_exists {
IfExists::Suffix => {
let mut candidate = key.clone();
let mut n = 1;
while held_at(&candidate).is_some() || root.join(format!("{candidate}.md")).exists() {
n += 1;
candidate = format!("{key}-{n}");
}
let path = root.join(format!("{candidate}.md"));
(candidate, path, None)
}
_ => match held_at(&key) {
Some(existing) => match if_exists {
IfExists::Skip => return Ok(Wrote::Skipped(key)),
IfExists::Override => {
let path = root.join(format!("{key}.md"));
(key, path, Some(existing))
}
IfExists::Error => {
bail!("`{key}` is already recorded; update that note instead")
}
IfExists::Suffix => unreachable!("handled by the arm above"),
},
None => {
let path = root.join(format!("{key}.md"));
if path.exists() {
bail!(
"{} already holds a note that does not claim `{key}` — `omh memory lint` says which",
path.display()
);
}
(key, path, None)
}
},
};
let note = Note {
key,
kind: Kind::Surprise,
source: input.source.trim().to_string(),
recorded: input.recorded.clone(),
invalidated_by: input
.invalidated_by
.as_deref()
.map(|raw| normalise_trigger(raw, repo))
.transpose()?,
body: body_of(input),
layer,
path: path.clone(),
};
let rendered = render(¬e);
let note = parse(&rendered, layer, &path)?;
let refused = check(¬e);
if let Some(first) = refused.first() {
bail!("`{}` was not written: {}", note.key, first.detail);
}
std::fs::create_dir_all(root).with_context(|| format!("creating {}", root.display()))?;
contained(root, &path)?;
std::fs::create_dir_all(path.parent().unwrap())
.with_context(|| format!("creating {}", root.display()))?;
std::fs::write(&path, &rendered).with_context(|| format!("writing {}", path.display()))?;
match replacing {
Some(stale) => {
if stale != path {
std::fs::remove_file(&stale)
.with_context(|| format!("removing {}", stale.display()))?;
}
Ok(Wrote::Replaced(path))
}
None => Ok(Wrote::Created(path)),
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Removed {
pub path: PathBuf,
pub layer: Layer,
pub inbound: Vec<String>,
}
fn disambiguate<'a>(
paths: &Paths,
many: &[&'a Note],
key: &str,
at: Option<&str>,
) -> Result<&'a Note> {
let shown = |note: &Note| {
note.path
.strip_prefix(note.layer.dir(paths))
.unwrap_or(¬e.path)
.display()
.to_string()
};
if let Some(at) = at {
let picked: Vec<&&Note> = many.iter().filter(|n| n.path.ends_with(at)).collect();
let spans_layers = |notes: &[&&Note]| {
notes
.iter()
.map(|n| n.layer)
.collect::<std::collections::BTreeSet<_>>()
.len()
> 1
};
return match picked.as_slice() {
[one] => Ok(**one),
[] => bail!(
"no note `{key}` at `{at}` — it is in {}",
many.iter().map(|n| shown(n)).collect::<Vec<_>>().join(", ")
),
rest if spans_layers(rest) => bail!(
"`{at}` matches {} notes, in {} — name one with --layer",
rest.len(),
rest.iter()
.map(|n| n.layer.to_string())
.collect::<Vec<_>>()
.join(" and ")
),
rest => bail!(
"`{at}` matches {} of them — give more of the path",
rest.len()
),
};
}
let layers: Vec<String> = many.iter().map(|n| n.layer.to_string()).collect();
if layers
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len()
> 1
{
bail!(
"`{key}` is in {} — name one with --layer",
layers.join(" and ")
);
}
bail!(
"`{key}` is one key over {} files in {} — name one with --at: {}",
many.len(),
layers[0],
many.iter().map(|n| shown(n)).collect::<Vec<_>>().join(", ")
)
}
pub fn remove(paths: &Paths, layer: Option<Layer>, key: &str, at: Option<&str>) -> Result<Removed> {
let notes = load(paths)?;
let matching: Vec<&Note> = notes
.iter()
.filter(|n| n.key == key && layer.is_none_or(|l| n.layer == l))
.collect();
let note = match (matching.as_slice(), at) {
([], _) => bail!("no note `{key}`"),
([one], None) => *one,
(many, _) => disambiguate(paths, many, key, at)?,
};
let mut inbound: Vec<String> = notes
.iter()
.filter(|n| n.key != key && links(&n.body).iter().any(|t| t == key))
.map(|n| n.key.clone())
.collect();
inbound.sort();
inbound.dedup();
std::fs::remove_file(¬e.path)
.with_context(|| format!("removing {}", note.path.display()))?;
Ok(Removed {
path: note.path.clone(),
layer: note.layer,
inbound,
})
}
pub fn render_list(notes: &[Note]) -> String {
let mut sorted: Vec<&Note> = notes.iter().collect();
sorted.sort_by(|a, b| (a.layer, &a.key).cmp(&(b.layer, &b.key)));
let width = sorted.iter().map(|n| n.key.len()).max().unwrap_or(0);
let mut out = String::new();
for note in sorted {
let refs = notes
.iter()
.filter(|n| links(&n.body).contains(¬e.key))
.count();
out.push_str(&format!(
"{:width$} {:<5} {} {} ref{}\n",
note.key,
note.layer.to_string(),
note.recorded,
refs,
if refs == 1 { "" } else { "s" },
));
}
out
}
fn session_of(source: &str) -> Option<&str> {
let rest = source.strip_prefix("session ")?;
Some(rest.split(',').next()?.trim())
}
pub fn from_session<'a>(notes: &'a [Note], session: &str) -> Vec<&'a Note> {
notes
.iter()
.filter(|n| session_of(&n.source) == Some(session))
.collect()
}
pub fn session_nudge(notes: &[Note], session: &str) -> Option<String> {
let n = from_session(notes, session).len();
(n > 0).then(|| {
format!(
"{n} note{} recorded during this session — `omh memory` to review",
if n == 1 { "" } else { "s" }
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn fixture() -> (tempfile::TempDir, Paths) {
let dir = tempfile::tempdir().unwrap();
let paths = Paths {
root: dir.path().join("home"),
repo: dir.path().join("repo"),
};
(dir, paths)
}
fn shipped_rules() -> String {
crate::base::sections()
.into_iter()
.map(|s| s.body)
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn the_layer_remember_writes_to_is_never_committed() {
assert_eq!(Layer::AGENT_WRITE, Layer::Local);
assert!(
!Layer::AGENT_WRITE.is_committed(),
"an unattended writer must not reach the committed layer"
);
assert!(Layer::Team.is_committed());
}
#[test]
fn the_local_store_lives_outside_the_checkout_and_the_team_store_inside_it() {
let (_d, paths) = fixture();
assert!(
Layer::Team.dir(&paths).starts_with(&paths.repo),
"the team layer must be committable: {}",
Layer::Team.dir(&paths).display()
);
assert!(
!Layer::Local.dir(&paths).starts_with(&paths.repo),
"a local note inside the checkout dies with the worktree: {}",
Layer::Local.dir(&paths).display()
);
assert!(
Layer::Local.dir(&paths).starts_with(&paths.root),
"the local store belongs to omh, keyed by repo"
);
}
#[test]
fn two_repos_do_not_share_a_local_store() {
let dir = tempfile::tempdir().unwrap();
let a = Paths {
root: dir.path().join("home"),
repo: dir.path().join("alpha"),
};
let b = Paths {
root: dir.path().join("home"),
repo: dir.path().join("beta"),
};
assert_ne!(Layer::Local.dir(&a), Layer::Local.dir(&b));
}
const SURPRISE: &str = "\
---
key: surprise/mounting-a-credential-file-returns-ebusy
type: surprise
source: session s03, claude
recorded: 2026-08-07
invalidated_by: image:4f2a1c3b5d7e9f0a2b4c6d8e0f1a3b5c7d9e0f1a
---
# Mounting a credential file returns EBUSY
## Expected
A bind mount of the token file to persist the login.
## Observed
The harness rewrites in place; a file mount is one inode, so the write fails.
## Evidence
`EBUSY` from the mount syscall.
## Related
- [[credentials-are-a-named-volume]]
";
fn parsed() -> Note {
parse(SURPRISE, Layer::Local, std::path::Path::new("x.md")).unwrap()
}
#[test]
fn a_note_round_trips_through_its_own_parser() {
let note = parsed();
assert_eq!(
parse(&render(¬e), note.layer, ¬e.path).unwrap(),
note,
"render must produce bytes parse accepts"
);
let mut bare = note.clone();
bare.invalidated_by = None;
assert_eq!(parse(&render(&bare), bare.layer, &bare.path).unwrap(), bare);
assert!(
!render(&bare).contains("invalidated_by"),
"an absent trigger must not render as an empty one:\n{}",
render(&bare)
);
}
#[test]
fn a_note_missing_a_required_field_is_refused_by_name() {
for field in ["key", "type", "source", "recorded"] {
let without: String = SURPRISE
.lines()
.filter(|l| !l.starts_with(&format!("{field}:")))
.collect::<Vec<_>>()
.join("\n");
let err = parse(&without, Layer::Local, std::path::Path::new("x.md"))
.unwrap_err()
.to_string();
assert!(err.contains(field), "dropping `{field}` gave: {err}");
}
}
#[test]
fn a_required_field_present_but_empty_is_refused_too() {
for field in ["key", "type", "source", "recorded"] {
let blank: String = SURPRISE
.lines()
.map(|l| {
if l.starts_with(&format!("{field}:")) {
format!("{field}: ")
} else {
l.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
let err = parse(&blank, Layer::Local, std::path::Path::new("x.md"));
assert!(
err.is_err(),
"a blank `{field}` must be refused, not read as empty"
);
assert!(err.unwrap_err().to_string().contains(field));
}
}
#[test]
fn the_recorded_date_must_be_a_real_calendar_date() {
let with = |d: &str| {
let raw = SURPRISE.replace("recorded: 2026-08-07", &format!("recorded: {d}"));
parse(&raw, Layer::Local, std::path::Path::new("x.md"))
};
for bad in [
"2026-13-45",
"2026-00-10",
"2026-08-32",
"2026-8-7",
"26-08-07",
"abcd-ef-gh",
"2025-02-29",
"2026-08-07x",
] {
assert!(with(bad).is_err(), "`{bad}` is not a date");
}
for good in ["2026-08-07", "2024-02-29", "2000-02-29", "1970-01-01"] {
assert!(with(good).is_ok(), "`{good}` is a date");
}
}
#[test]
fn an_unknown_note_type_is_an_error_not_a_default() {
let raw = SURPRISE.replace("type: surprise", "type: hunch");
let err = parse(&raw, Layer::Local, std::path::Path::new("x.md"))
.unwrap_err()
.to_string();
assert!(err.contains("hunch"), "got: {err}");
for kind in Kind::ALL {
assert!(err.contains(&kind.to_string()), "must name `{kind}`: {err}");
}
}
#[test]
fn a_notes_layer_comes_from_where_it_lives_not_from_what_it_says() {
let lying = SURPRISE.replace("type: surprise", "layer: team\ntype: surprise");
let note = parse(&lying, Layer::Local, std::path::Path::new("x.md")).unwrap();
assert_eq!(note.layer, Layer::Local);
assert!(
!render(¬e).contains("layer:"),
"the layer is not a field a note carries"
);
}
#[test]
fn the_sections_a_surprise_requires_are_the_ones_remember_supplies() {
let supplied: Vec<String> = crate::memory::tools::REQUIRED
.iter()
.map(|a| {
let mut c = a.chars();
c.next()
.map(|f| f.to_uppercase().collect::<String>() + c.as_str())
.unwrap_or_default()
})
.collect();
assert_eq!(Kind::Surprise.required_sections(), supplied.as_slice());
}
#[test]
fn every_note_type_declares_both_its_section_tables() {
for kind in Kind::ALL {
let required = kind.required_sections();
let lists = kind.list_sections();
assert!(
!required.is_empty() || !lists.is_empty(),
"`{kind}` is validated by nothing at all"
);
for name in lists {
assert!(
!name.is_empty(),
"`{kind}` declares an unnamed list section"
);
}
}
}
#[test]
fn every_spelling_of_one_observation_derives_the_same_key() {
let want = "mounting-a-credential-file-returns-ebusy";
for spelling in [
"Mounting a credential FILE returns EBUSY",
"mounting a credential file returns ebusy",
"Mounting a credential file returns EBUSY.",
"Mounting a credential — file — returns EBUSY",
" Mounting a credential file returns EBUSY ",
"Mounting/a/credential/file/returns/EBUSY",
] {
assert_eq!(slug(spelling).unwrap(), want, "from: {spelling:?}");
}
}
#[test]
fn a_key_never_contains_a_path_separator_or_a_leading_dot() {
for hostile in [
"../../.ssh/id_rsa",
"a/b",
"..",
".",
"C:\\Windows",
".hidden",
] {
let Ok(s) = slug(hostile) else { continue };
assert!(!s.contains('/'), "{hostile:?} → {s:?}");
assert!(!s.contains('\\'), "{hostile:?} → {s:?}");
assert!(!s.starts_with('.'), "{hostile:?} → {s:?}");
assert!(s != "." && s != "..", "{hostile:?} → {s:?}");
}
}
#[test]
fn an_empty_or_punctuation_only_input_is_an_error_not_an_empty_key() {
for nothing in ["!!!", " ", "", "---", "..."] {
assert!(slug(nothing).is_err(), "{nothing:?} is not a key");
}
}
#[test]
fn key_derivation_stops_at_a_sentence_boundary_not_a_word_count() {
let key =
slug_of_observation("The mount fails with EBUSY. It is one inode, so the write fails.")
.unwrap();
assert_eq!(key, "the-mount-fails-with-ebusy");
assert!(
!key.contains("inode"),
"the second sentence is not identity"
);
let reworded =
slug_of_observation("The bind mount fails with EBUSY. Something else.").unwrap();
assert_eq!(reworded, "the-bind-mount-fails-with-ebusy");
assert_eq!(
slug_of_observation("no full stop here").unwrap(),
"no-full-stop-here"
);
}
#[test]
fn the_key_template_substitutes_only_the_placeholders_it_knows() {
assert_eq!(
expand_key("surprise/{{slug}}", &[("slug", "ebusy")]).unwrap(),
"surprise/ebusy"
);
let err = expand_key("surprise/{{slugg}}", &[("slug", "ebusy")])
.unwrap_err()
.to_string();
assert!(err.contains("slugg"), "must name the placeholder: {err}");
assert!(
expand_key("surprise/{{slug", &[("slug", "ebusy")]).is_err(),
"an unclosed placeholder is not a literal"
);
}
#[test]
fn a_template_may_introduce_a_namespace_where_a_slug_may_not() {
let key = expand_key(
"surprise/{{slug}}",
&[("slug", &slug("a/b").unwrap() as &str)],
)
.unwrap();
assert_eq!(key, "surprise/a-b");
assert_eq!(key.matches('/').count(), 1);
}
#[test]
fn a_template_that_escapes_the_store_never_becomes_a_key() {
let err = expand_key("../../escaped/{{slug}}", &[("slug", "x")])
.unwrap_err()
.to_string();
assert!(err.contains("not a key"), "got: {err}");
assert!(
expand_key("docs/{{path}}", &[("path", "../../etc/passwd")]).is_err(),
"a bound value must not smuggle in what the template may not spell"
);
}
#[test]
fn todays_date_is_computed_from_the_calendar_not_from_averages() {
for (days, want) in [
(0, "1970-01-01"),
(19_722, "2023-12-31"),
(19_723, "2024-01-01"),
(19_782, "2024-02-29"), (11_016, "2000-02-29"), (365, "1971-01-01"),
] {
assert_eq!(civil(days), want, "day {days}");
}
}
#[test]
#[cfg(unix)]
fn todays_date_agrees_with_the_system_clock() {
let out = std::process::Command::new("date")
.args(["-u", "+%F"])
.output()
.unwrap();
let want = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert_eq!(today(), want);
}
fn note_with(kind: Kind, key: &str, body: &str) -> Note {
Note {
key: key.to_string(),
kind,
source: "session s01, claude".into(),
recorded: "2026-08-07".into(),
invalidated_by: None,
body: body.to_string(),
layer: Layer::Local,
path: PathBuf::from(format!("{}.md", key.rsplit('/').next().unwrap())),
}
}
fn surprise_body() -> String {
"# T\n\n## Expected\na\n\n## Observed\nb\n\n## Evidence\nc\n\n## Answers\n\n- what happens here\n"
.to_string()
}
fn rules(violations: &[Violation]) -> Vec<Rule> {
violations.iter().map(|v| v.rule).collect()
}
#[test]
fn a_note_missing_a_required_section_for_its_type_is_refused() {
assert!(check(¬e_with(Kind::Surprise, "k", &surprise_body())).is_empty());
for missing in Kind::Surprise.required_sections() {
let body = surprise_body().replace(&format!("## {missing}"), "## Something");
let found = check(¬e_with(Kind::Surprise, "k", &body));
assert!(
rules(&found).contains(&Rule::MissingSection),
"dropping `{missing}` must be refused"
);
assert!(
found.iter().any(|v| v.detail.contains(missing)),
"the violation must name `{missing}`: {found:?}"
);
}
let prose = "# T\n\nExpected a mount. Observed an error. Evidence below.\n";
assert!(
rules(&check(¬e_with(Kind::Surprise, "k", prose))).contains(&Rule::MissingSection),
"a section is a heading, not a word that appears somewhere"
);
let hollow = "# T\n\n## Expected\n\n## Observed\nb\n\n## Evidence\nc\n";
assert!(
rules(&check(¬e_with(Kind::Surprise, "k", hollow))).contains(&Rule::MissingSection),
"an empty section is a section that was not filled in"
);
}
#[test]
fn a_list_section_holds_bullets_and_nothing_else() {
let clean = format!("{}\n## Related\n\n- [[a]]\n- [[b]]\n", surprise_body());
assert!(check(¬e_with(Kind::Surprise, "k", &clean)).is_empty());
let prose = format!(
"{}\n## Related\n\nThis re-narrates the note above at length.\n\n- [[a]]\n",
surprise_body()
);
assert!(
rules(&check(¬e_with(Kind::Surprise, "k", &prose)))
.contains(&Rule::ProseInListSection),
"a prose block in a list section is the failure this detects"
);
}
#[test]
fn a_bullet_that_wraps_across_lines_is_still_a_bullet() {
let wrapped = format!(
"{}\n## Related\n\n- [[a]] which needed a longer sentence than fits\n on one line at all\n- [[b]]\n",
surprise_body()
);
assert!(
check(¬e_with(Kind::Surprise, "k", &wrapped)).is_empty(),
"a continuation line is part of its bullet"
);
}
#[test]
fn a_note_whose_key_disagrees_with_its_filename_is_a_violation() {
let mut note = note_with(Kind::Surprise, "k", &surprise_body());
note.path = PathBuf::from("something-else.md");
let found = check(¬e);
assert!(rules(&found).contains(&Rule::KeyDisagreesWithPath));
assert!(
found
.iter()
.any(|v| v.detail.contains("something-else") && v.detail.contains('k')),
"must name both: {found:?}"
);
}
#[test]
fn schema_rules_refuse_and_hygiene_rules_only_warn() {
let broken = note_with(Kind::Surprise, "k", "# T\n\n## Related\n\nprose\n");
let found = check(&broken);
assert!(!found.is_empty(), "this note breaks the schema");
for v in &found {
assert_eq!(
v.rule.severity(),
Severity::Refused,
"`{:?}` came from the schema and must refuse",
v.rule
);
}
}
#[test]
fn a_heading_inside_a_code_fence_does_not_satisfy_the_schema() {
let fenced =
"# T\n\n```markdown\n## Expected\nsample\n## Observed\nx\n## Evidence\ny\n```\n\n## Answers\n\n- what happens here\n";
let note = note_with(Kind::Surprise, "fenced", fenced);
assert_eq!(
rules(&check(¬e)),
vec![
Rule::MissingSection,
Rule::MissingSection,
Rule::MissingSection
],
"a note whose every section is quoted has no sections"
);
}
#[test]
fn a_section_after_a_closed_fence_still_counts() {
let body = "# T\n\n## Expected\n```markdown\n## Observed\nquoted\n```\n\n## Observed\nb\n\n## Evidence\nc\n\n## Answers\n\n- what happens here\n";
assert!(
check(¬e_with(Kind::Surprise, "k", body)).is_empty(),
"got: {:?}",
check(¬e_with(Kind::Surprise, "k", body))
);
}
#[test]
fn an_unclosed_bracket_does_not_swallow_the_next_link() {
let body = "## Evidence\nthe agent typed `[[` in a sample\n\n## Related\n\n- [[a-real-note]]\n- [[another]]\n";
let found = links(body);
assert!(
found.contains(&"a-real-note".to_string()),
"the link after the stray bracket vanished: {found:?}"
);
assert!(found.contains(&"another".to_string()), "got: {found:?}");
}
#[test]
fn a_tilde_fence_hides_a_heading_just_like_a_backtick_one() {
let fenced =
"# T\n\n~~~markdown\n## Expected\nsample\n## Observed\nx\n## Evidence\ny\n~~~\n\n## Answers\n\n- what happens here\n";
assert_eq!(
rules(&check(¬e_with(Kind::Surprise, "tilde", fenced))),
vec![
Rule::MissingSection,
Rule::MissingSection,
Rule::MissingSection
],
);
}
#[test]
fn a_longer_fence_is_not_closed_by_a_shorter_one_inside_it() {
let nested = "# T\n\n````markdown\n```\n## Expected\n## Observed\n## Evidence\n```\n````\n\n## Answers\n\n- what happens here\n";
assert_eq!(
rules(&check(¬e_with(Kind::Surprise, "fourtick", nested))),
vec![
Rule::MissingSection,
Rule::MissingSection,
Rule::MissingSection
],
);
}
#[test]
fn an_indented_fence_still_opens_a_block() {
let indented =
"# T\n\n## Expected\na\n\n ```markdown\n## Observed\nquoted\n ```\n\n## Evidence\nc\n\n## Answers\n\n- what happens here\n";
let found = check(¬e_with(Kind::Surprise, "indented", indented));
assert_eq!(
rules(&found),
vec![Rule::MissingSection],
"`## Observed` sits inside an indented fence: {found:?}"
);
assert!(
found[0].detail.contains("Observed"),
"got: {}",
found[0].detail
);
}
#[test]
fn an_unclosed_fence_is_refused_by_name_not_as_missing_sections() {
let truncated =
"# T\n\n## Expected\na\n\n## Evidence\n```sh\nomh run\n\n## Observed\nb\n\n## Related\n\n- [[somewhere]]\n";
let found = check(¬e_with(Kind::Surprise, "truncated", truncated));
assert_eq!(
rules(&found),
vec![Rule::UnclosedFence],
"the fence is the problem; the sections are right there"
);
assert_eq!(
Rule::UnclosedFence.severity(),
Severity::Refused,
"a note whose links have silently vanished must not be written"
);
assert!(links(truncated).is_empty());
}
#[test]
fn a_target_that_swallows_an_opener_is_not_a_link() {
assert_eq!(links("- [[a [[b]]\n- [[real]]\n"), vec!["real".to_string()]);
}
#[test]
fn a_wiki_link_inside_a_code_fence_is_not_a_link() {
let body = "## Related\n\n```sh\ngrep '[[not-a-note]]' x\n```\n\n- [[real-note]]\n";
assert_eq!(links(body), vec!["real-note".to_string()]);
}
fn find<'a>(notes: &'a [Note], key: &str) -> &'a Note {
notes
.iter()
.find(|n| n.key == key)
.unwrap_or_else(|| panic!("no note `{key}` in the fixture"))
}
fn seed(paths: &Paths, layer: Layer, key: &str, body: &str) {
let path = layer.dir(paths).join(format!("{key}.md"));
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let note = Note {
path: path.clone(),
..note_with(Kind::Surprise, key, body)
};
std::fs::write(&path, render(¬e)).unwrap();
}
fn keys(notes: &[Note]) -> Vec<String> {
let mut out: Vec<String> = notes.iter().map(|n| n.key.clone()).collect();
out.sort();
out
}
#[test]
fn links_are_read_in_order_and_a_bare_bracket_is_not_a_link() {
assert_eq!(links("see [[b]] then [[a]]"), vec!["b", "a"]);
assert_eq!(links("[not a link] and [[ spaced ]]"), vec!["spaced"]);
assert!(links("[[]]").is_empty(), "an empty target is not a link");
}
#[test]
fn a_note_file_that_does_not_parse_is_an_error_not_a_skipped_file() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "good", &surprise_body());
std::fs::write(Layer::Local.dir(&paths).join("bad.md"), "no frontmatter\n").unwrap();
let err = load_layer(&paths, Layer::Local).unwrap_err().to_string();
assert!(
err.contains("bad"),
"must name the file it could not read: {err}"
);
}
#[test]
fn an_absent_store_is_empty_not_an_error() {
let (_d, paths) = fixture();
assert!(load(&paths).unwrap().is_empty());
}
#[test]
fn a_non_markdown_file_in_the_store_is_ignored() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "good", &surprise_body());
std::fs::write(Layer::Local.dir(&paths).join(".DS_Store"), "junk").unwrap();
assert_eq!(keys(&load(&paths).unwrap()), ["good"]);
}
#[test]
fn a_note_in_a_namespace_is_still_in_the_store() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "surprise/ebusy", &surprise_body());
assert_eq!(keys(&load(&paths).unwrap()), ["surprise/ebusy"]);
}
#[test]
fn the_two_layers_do_not_shadow_each_other() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "deploy", &surprise_body());
seed(&paths, Layer::Local, "deploy", &surprise_body());
let all = load(&paths).unwrap();
assert_eq!(all.len(), 2, "one key in two layers is two notes");
let mut layers: Vec<Layer> = all.iter().map(|n| n.layer).collect();
layers.sort();
assert_eq!(layers, [Layer::Team, Layer::Local]);
}
#[test]
fn a_dangling_link_is_found_and_names_both_ends() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "target", &surprise_body());
seed(
&paths,
Layer::Local,
"source",
&format!(
"{}\n## Related\n\n- [[target]]\n- [[nope]]\n",
surprise_body()
),
);
let found = hygiene(&load(&paths).unwrap());
let dangling: Vec<&Violation> = found
.iter()
.filter(|v| v.rule == Rule::DanglingLink)
.collect();
assert_eq!(dangling.len(), 1, "only `nope` dangles: {found:?}");
assert_eq!(dangling[0].key, "source");
assert!(dangling[0].detail.contains("nope"), "{:?}", dangling[0]);
}
#[test]
fn an_orphan_is_a_note_nothing_links_to_not_a_note_with_no_links() {
let (_d, paths) = fixture();
seed(
&paths,
Layer::Local,
"pointer",
&format!("{}\n## Related\n\n- [[leaf]]\n", surprise_body()),
);
seed(&paths, Layer::Local, "leaf", &surprise_body());
let orphans: Vec<String> = hygiene(&load(&paths).unwrap())
.into_iter()
.filter(|v| v.rule == Rule::Orphan)
.map(|v| v.key)
.collect();
assert_eq!(
orphans,
["pointer"],
"`leaf` is pointed at; `pointer` is not"
);
}
#[test]
fn hygiene_only_ever_warns() {
let (_d, paths) = fixture();
seed(
&paths,
Layer::Local,
"source",
&format!("{}\n## Related\n\n- [[nope]]\n", surprise_body()),
);
let found = hygiene(&load(&paths).unwrap());
assert!(!found.is_empty());
for v in &found {
assert_eq!(v.rule.severity(), Severity::Warning, "{v:?}");
}
}
fn observation() -> Remembered {
Remembered {
expected: "A bind mount of the token file to persist the login.".into(),
observed: "The harness rewrites in place. A file mount is one inode.".into(),
evidence: "`EBUSY` from the mount syscall.".into(),
answers: vec!["why does my login not persist".into()],
relates_to: Vec::new(),
invalidated_by: None,
source: "session s03, claude".into(),
recorded: "2026-08-07".into(),
}
}
fn derived_key(paths: &Paths, input: &Remembered) -> String {
expand_key(
templates(paths).unwrap().get(&Kind::Surprise).unwrap(),
&[("slug", &slug_of_observation(&input.observed).unwrap())],
)
.unwrap()
}
fn files_under(dir: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
let mut out = BTreeMap::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&d) else {
continue;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
} else {
let bytes = std::fs::read(&p).unwrap();
out.insert(p, bytes);
}
}
}
out
}
#[test]
fn remember_writes_nothing_outside_the_local_store() {
let (dir, paths) = fixture();
remember(&paths, &observation(), IfExists::Error).unwrap();
let written = files_under(dir.path());
assert!(!written.is_empty(), "something must have been written");
for path in written.keys() {
assert!(
path.starts_with(Layer::Local.dir(&paths)),
"wrote outside the local store: {}",
path.display()
);
}
}
#[test]
fn a_key_is_slash_separated_slugs_and_nothing_else() {
for bad in [
"",
"/etc/passwd",
"..",
"../escaped",
"a/../b",
"a//b",
"a/",
"a\\b",
".ssh/authorized_keys",
"surprise/.",
] {
assert!(
validate_key(bad).is_err(),
"`{bad}` must not be usable as a key"
);
}
for good in ["a", "ns/a", "surprise/the-mount-failed", "docs/a/b/c"] {
assert!(
validate_key(good).is_ok(),
"`{good}` is a key and must stay one"
);
}
}
#[test]
fn a_symlinked_namespace_cannot_carry_a_write_out_of_the_store() {
let (dir, paths) = fixture();
let root = Layer::Local.dir(&paths);
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::create_dir_all(&root).unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(&outside, root.join("surprise")).unwrap();
let err = remember(&paths, &observation(), IfExists::Error).unwrap_err();
assert!(err.to_string().contains("outside the store"), "got: {err}");
assert!(
std::fs::read_dir(&outside).unwrap().next().is_none(),
"a note landed outside the store through a symlink"
);
}
#[test]
fn the_store_does_not_read_through_a_symlink() {
let (dir, paths) = fixture();
let root = Layer::Local.dir(&paths);
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::create_dir_all(&root).unwrap();
let mut stray = note_with(Kind::Surprise, "elsewhere", &surprise_body());
stray.path = outside.join("elsewhere.md");
std::fs::write(&stray.path, render(&stray)).unwrap();
#[cfg(unix)]
{
std::os::unix::fs::symlink(&outside, root.join("linked")).unwrap();
std::os::unix::fs::symlink(&stray.path, root.join("linked.md")).unwrap();
}
assert!(
load_layer(&paths, Layer::Local).unwrap().is_empty(),
"the store answered with a note that is not in it"
);
}
#[test]
fn a_leftover_keys_toml_is_an_error_naming_both_paths() {
let (_d, paths) = fixture();
std::fs::create_dir_all(paths.repo.join(".omh")).unwrap();
std::fs::write(paths.repo.join(".omh/keys.toml"), SHIPPED_KEYS).unwrap();
let err = templates(&paths).unwrap_err().to_string();
assert!(err.contains("keys.toml"), "must name the old path: {err}");
assert!(err.contains("memory.toml"), "and the new one: {err}");
std::fs::write(paths.repo.join(".omh/memory.toml"), SHIPPED_KEYS).unwrap();
let err = templates(&paths).unwrap_err().to_string();
assert!(
err.contains("keys.toml"),
"both present is still an error: {err}"
);
}
#[test]
fn key_templates_are_read_from_memory_toml() {
let (_d, paths) = fixture();
std::fs::create_dir_all(paths.repo.join(".omh")).unwrap();
std::fs::write(
paths.repo.join(".omh/memory.toml"),
"[keys]\nsurprise = \"mine/{{slug}}\"\ntopic = \"{{slug}}\"\nstub = \"docs/{{path}}\"\n",
)
.unwrap();
assert_eq!(templates(&paths).unwrap()[&Kind::Surprise], "mine/{{slug}}");
}
#[test]
fn a_key_template_cannot_write_outside_the_store() {
let (dir, paths) = fixture();
std::fs::create_dir_all(paths.repo.join(".omh")).unwrap();
std::fs::write(
paths.repo.join(".omh/memory.toml"),
"[keys]\nsurprise = \"../../escaped/{{slug}}\"\ntopic = \"{{slug}}\"\nstub = \"docs/{{path}}\"\n",
)
.unwrap();
let err = remember(&paths, &observation(), IfExists::Error).unwrap_err();
assert!(
err.to_string().contains("not a key"),
"the refusal must name the problem, got: {err}"
);
for path in files_under(dir.path()).keys() {
assert!(
path.extension().is_none_or(|e| e != "md")
|| path.starts_with(Layer::Local.dir(&paths)),
"wrote outside the local store: {}",
path.display()
);
}
}
#[test]
fn a_key_already_in_the_layer_is_a_conflict_wherever_it_is_stored() {
let (_d, paths) = fixture();
let taken = derived_key(&paths, &observation());
let elsewhere = Layer::Local.dir(&paths).join("hand-written.md");
std::fs::create_dir_all(elsewhere.parent().unwrap()).unwrap();
let mut note = note_with(Kind::Surprise, &taken, &surprise_body());
note.path = elsewhere.clone();
std::fs::write(&elsewhere, render(¬e)).unwrap();
let err = remember(&paths, &observation(), IfExists::Error).unwrap_err();
assert!(
err.to_string().contains("already recorded"),
"a taken key is a conflict wherever it lives, got: {err}"
);
}
#[test]
fn a_note_omh_cannot_read_stops_the_write_rather_than_risking_a_duplicate() {
let (dir, paths) = fixture();
let root = Layer::Local.dir(&paths);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("unreadable.md"), "this has no frontmatter\n").unwrap();
let err = remember(&paths, &observation(), IfExists::Error).unwrap_err();
assert!(
err.to_string().contains("unreadable.md"),
"the refusal must name the file standing in the way: {err}"
);
let notes = files_under(dir.path())
.into_keys()
.filter(|p| p.extension().is_some_and(|e| e == "md"))
.count();
assert_eq!(
notes, 1,
"nothing new may be written while the store is unverifiable"
);
}
#[test]
fn override_replaces_a_mislocated_note_and_leaves_one_behind() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
let key = derived_key(&paths, &observation());
let stale = root.join("hand-written.md");
std::fs::create_dir_all(root.join("surprise")).unwrap();
let mut note = note_with(Kind::Surprise, &key, &surprise_body());
note.path = stale.clone();
std::fs::write(&stale, render(¬e)).unwrap();
let wrote = remember(&paths, &observation(), IfExists::Override).unwrap();
assert_eq!(
wrote,
Wrote::Replaced(root.join(format!("{key}.md"))),
"a write that destroyed a note must not report as a creation"
);
assert!(!stale.exists(), "the note it replaced is still there");
assert_eq!(
lint(&paths)
.unwrap()
.iter()
.filter(|v| v.rule == Rule::DuplicateKey)
.count(),
0,
"override must leave one note under the key, not two"
);
}
#[test]
fn skip_and_suffix_see_a_key_held_by_a_mislocated_note() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
let key = derived_key(&paths, &observation());
let mut note = note_with(Kind::Surprise, &key, &surprise_body());
note.path = root.join("hand-written.md");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(¬e.path, render(¬e)).unwrap();
assert_eq!(
remember(&paths, &observation(), IfExists::Skip).unwrap(),
Wrote::Skipped(key.clone()),
"the key is taken, so there is nothing to add"
);
let Wrote::Created(path) = remember(&paths, &observation(), IfExists::Suffix).unwrap()
else {
panic!("suffix creates a new note");
};
assert!(
path.ends_with(format!("{key}-2.md")),
"suffix must step over the held key: {}",
path.display()
);
}
#[test]
fn every_input_to_remember_lands_in_its_own_place_on_disk() {
let (_d, paths) = fixture();
let mut input = observation();
input.relates_to = vec!["credentials-are-a-named-volume".into()];
input.invalidated_by = Some("image:4f2a1c3b5d7e9f0a2b4c6d8e0f1a3b5c7d9e0f1a".into());
let Wrote::Created(path) = remember(&paths, &input, IfExists::Error).unwrap() else {
panic!("a fresh key must be created");
};
let note = parse(
&std::fs::read_to_string(&path).unwrap(),
Layer::Local,
&path,
)
.unwrap();
assert_eq!(note.kind, Kind::Surprise);
assert_eq!(note.source, input.source);
assert_eq!(note.recorded, input.recorded);
assert_eq!(note.invalidated_by, input.invalidated_by);
let body = sections(¬e.body);
for (heading, supplied) in [
("Expected", &input.expected),
("Observed", &input.observed),
("Evidence", &input.evidence),
] {
assert_eq!(
body[heading].join("\n").trim(),
supplied.trim(),
"`## {heading}` must hold what was passed as {heading}"
);
}
assert_eq!(links(¬e.body), input.relates_to);
}
#[test]
fn the_key_is_derived_from_what_was_observed() {
let (_d, paths) = fixture();
let mut input = observation();
input.expected = "Zebras would persist the login.".into();
input.observed = "Walruses returned EBUSY.".into();
let Wrote::Created(path) = remember(&paths, &input, IfExists::Error).unwrap() else {
panic!("a fresh key must be created");
};
let shown = path.to_string_lossy().to_string();
assert!(shown.contains("walruses"), "got: {shown}");
assert!(!shown.contains("zebras"), "keyed off the guess: {shown}");
}
#[test]
fn provenance_is_not_the_agents_to_supply() {
let (_d, paths) = fixture();
let mut blank = observation();
blank.source = " ".into();
assert!(
remember(&paths, &blank, IfExists::Error).is_err(),
"a note with no provenance cannot be judged, so it is not written"
);
let note = &load(&paths).unwrap();
assert!(note.is_empty(), "and nothing was written anyway");
}
#[test]
fn an_observation_with_nothing_expected_is_refused() {
let (_d, paths) = fixture();
for blank in ["expected", "observed", "evidence"] {
let mut input = observation();
match blank {
"expected" => input.expected = " ".into(),
"observed" => input.observed = " ".into(),
_ => input.evidence = " ".into(),
}
let err = remember(&paths, &input, IfExists::Error);
assert!(err.is_err(), "a blank `{blank}` is not an observation");
assert!(err.unwrap_err().to_string().contains(blank));
}
}
#[test]
fn writing_an_existing_key_is_an_error_that_says_update_instead() {
let (_d, paths) = fixture();
let first = remember(&paths, &observation(), IfExists::Error).unwrap();
let Wrote::Created(path) = first else {
panic!("expected a write")
};
let before = std::fs::read(&path).unwrap();
let err = remember(&paths, &observation(), IfExists::Error)
.unwrap_err()
.to_string();
assert!(err.contains("update"), "must say what to do instead: {err}");
assert_eq!(std::fs::read(&path).unwrap(), before, "and change nothing");
}
#[test]
fn a_refused_write_leaves_nothing_on_disk() {
let (dir, paths) = fixture();
let mut bad = observation();
bad.recorded = "2026-13-45".into();
assert!(remember(&paths, &bad, IfExists::Error).is_err());
assert!(
files_under(dir.path()).is_empty(),
"a refused write must not leave a file behind"
);
}
#[test]
fn skip_if_exists_is_an_explicit_mode_not_a_fallback() {
let (_d, paths) = fixture();
assert!(
matches!(IfExists::default(), IfExists::Error),
"the default refuses"
);
let Wrote::Created(path) = remember(&paths, &observation(), IfExists::Error).unwrap()
else {
panic!()
};
let before = std::fs::read(&path).unwrap();
let again = remember(&paths, &observation(), IfExists::Skip).unwrap();
assert!(matches!(again, Wrote::Skipped(_)));
assert_eq!(std::fs::read(&path).unwrap(), before);
}
#[test]
fn suffix_never_reuses_a_suffix_already_taken() {
let (_d, paths) = fixture();
let mut written = Vec::new();
for _ in 0..3 {
match remember(&paths, &observation(), IfExists::Suffix).unwrap() {
Wrote::Created(p) => written.push(p),
other => panic!("{other:?}"),
}
}
written.sort();
written.dedup();
assert_eq!(written.len(), 3, "each write is its own file");
assert_eq!(load(&paths).unwrap().len(), 3);
}
#[test]
fn a_teammates_note_on_the_same_topic_does_not_block_the_write() {
let (_d, paths) = fixture();
let key = expand_key(
"surprise/{{slug}}",
&[(
"slug",
&slug_of_observation(&observation().observed).unwrap(),
)],
)
.unwrap();
seed(&paths, Layer::Team, &key, &surprise_body());
assert!(
remember(&paths, &observation(), IfExists::Error).is_ok(),
"the committed layer is a different note, not a collision"
);
assert_eq!(load(&paths).unwrap().len(), 2);
}
#[test]
fn a_hygiene_violation_never_refuses_a_write() {
let (_d, paths) = fixture();
seed(
&paths,
Layer::Local,
"broken",
&format!("{}\n## Related\n\n- [[nowhere]]\n", surprise_body()),
);
assert!(
!hygiene(&load(&paths).unwrap()).is_empty(),
"the store is dirty"
);
assert!(
remember(&paths, &observation(), IfExists::Error).is_ok(),
"a clean note is written into a dirty store"
);
}
#[test]
fn a_multi_valued_key_component_is_pinned_to_one_order() {
let (_d, paths) = fixture();
let mut one = observation();
one.relates_to = vec!["nate".into(), "joanna".into(), "nate".into()];
let Wrote::Created(a) = remember(&paths, &one, IfExists::Error).unwrap() else {
panic!()
};
let first = std::fs::read_to_string(&a).unwrap();
let (_d2, other) = fixture();
let mut two = observation();
two.relates_to = vec!["joanna".into(), "nate".into()];
let Wrote::Created(b) = remember(&other, &two, IfExists::Error).unwrap() else {
panic!()
};
assert_eq!(
first,
std::fs::read_to_string(&b).unwrap(),
"the same neighbours in a different order are the same note"
);
}
#[test]
fn an_at_that_names_nothing_never_falls_through_to_another_note() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "solo", &surprise_body());
let err = remove(&paths, None, "solo", Some("some-other-file.md"))
.unwrap_err()
.to_string();
assert!(err.contains("some-other-file.md"), "got: {err}");
assert!(
Layer::Local.dir(&paths).join("solo.md").exists(),
"a note the caller did not name was removed"
);
}
#[test]
fn an_at_that_cannot_pick_one_note_removes_none_of_them() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
std::fs::create_dir_all(root.join("ns")).unwrap();
for at in ["dup.md", "ns/dup.md"] {
let mut note = note_with(Kind::Surprise, "dup", &surprise_body());
note.path = root.join(at);
std::fs::write(¬e.path, render(¬e)).unwrap();
}
let missed = remove(&paths, Some(Layer::Local), "dup", Some("absent.md"))
.unwrap_err()
.to_string();
assert!(missed.contains("absent.md"), "got: {missed}");
let ambiguous = remove(&paths, Some(Layer::Local), "dup", Some("dup.md"))
.unwrap_err()
.to_string();
assert!(
ambiguous.contains("2"),
"an ambiguous --at must say so: {ambiguous}"
);
assert_eq!(
files_under(&root).len(),
2,
"neither refusal may remove anything"
);
}
#[test]
fn an_at_that_spans_layers_points_at_the_layer_flag() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "shared", &surprise_body());
seed(&paths, Layer::Team, "shared", &surprise_body());
let err = remove(&paths, None, "shared", Some("shared.md"))
.unwrap_err()
.to_string();
assert!(
err.contains("--layer"),
"the only thing that separates these is the layer: {err}"
);
}
#[test]
fn a_key_duplicated_inside_one_layer_is_still_removable() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
std::fs::create_dir_all(root.join("ns")).unwrap();
for at in ["dup.md", "ns/dup.md"] {
let mut note = note_with(Kind::Surprise, "dup", &surprise_body());
note.path = root.join(at);
std::fs::write(¬e.path, render(¬e)).unwrap();
}
let err = remove(&paths, Some(Layer::Local), "dup", None)
.unwrap_err()
.to_string();
assert!(
err.contains("dup.md") && err.contains("ns/dup.md"),
"the error must name the files, since the layer cannot separate them: {err}"
);
assert!(
!err.contains("local and local"),
"duplicates in one layer are not a layer question: {err}"
);
let removed = remove(&paths, Some(Layer::Local), "dup", Some("ns/dup.md"))
.expect("a duplicated key must still be removable");
assert!(
removed.path.ends_with("ns/dup.md"),
"got: {}",
removed.path.display()
);
assert!(
root.join("dup.md").exists(),
"rm must take exactly one note"
);
}
#[test]
fn rm_removes_one_note_and_leaves_every_neighbour_byte_identical() {
let (dir, paths) = fixture();
let pointing = format!("{}\n## Related\n\n- [[b]]\n", surprise_body());
seed(&paths, Layer::Local, "a", &pointing);
seed(&paths, Layer::Team, "c", &pointing);
seed(&paths, Layer::Local, "b", &surprise_body());
let before: BTreeMap<PathBuf, Vec<u8>> = files_under(dir.path())
.into_iter()
.filter(|(p, _)| !p.ends_with("b.md"))
.collect();
remove(&paths, None, "b", None).unwrap();
assert_eq!(
files_under(dir.path()),
before,
"every other note must be untouched, byte for byte"
);
}
#[test]
fn rm_reports_what_linked_to_the_note_it_removed() {
let (_d, paths) = fixture();
let pointing = format!("{}\n## Related\n\n- [[b]]\n", surprise_body());
seed(&paths, Layer::Local, "a", &pointing);
seed(&paths, Layer::Team, "c", &pointing);
seed(&paths, Layer::Local, "b", &surprise_body());
let removed = remove(&paths, None, "b", None).unwrap();
assert_eq!(
removed.inbound,
["a", "c"],
"inbound links cross layers; scoping to one hides half of them"
);
}
#[test]
fn removing_a_key_present_in_both_layers_names_both_rather_than_picking_one() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "deploy", &surprise_body());
seed(&paths, Layer::Local, "deploy", &surprise_body());
let err = remove(&paths, None, "deploy", None)
.unwrap_err()
.to_string();
assert!(err.contains("team") && err.contains("local"), "got: {err}");
assert_eq!(load(&paths).unwrap().len(), 2, "and removed neither");
remove(&paths, Some(Layer::Local), "deploy", None).unwrap();
let left = load(&paths).unwrap();
assert_eq!(left.len(), 1);
assert_eq!(left[0].layer, Layer::Team);
}
#[test]
fn rm_reports_which_layer_the_note_came_out_of() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "shared", &surprise_body());
seed(&paths, Layer::Local, "mine", &surprise_body());
assert!(remove(&paths, None, "shared", None)
.unwrap()
.layer
.is_committed());
assert!(!remove(&paths, None, "mine", None)
.unwrap()
.layer
.is_committed());
}
#[test]
fn rm_on_an_absent_key_says_so_rather_than_succeeding_quietly() {
let (_d, paths) = fixture();
let err = remove(&paths, None, "never-existed", None)
.unwrap_err()
.to_string();
assert!(err.contains("never-existed"), "got: {err}");
}
#[test]
fn omh_memory_lists_every_note_with_its_date_and_its_layer() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "older", &surprise_body());
seed(&paths, Layer::Local, "newer", &surprise_body());
let mut notes = load(&paths).unwrap();
for note in &mut notes {
note.recorded = if note.key == "older" {
"2026-06-12".into()
} else {
"2026-08-07".into()
};
}
let out = render_list(¬es);
for (key, layer, date) in [
("older", "team", "2026-06-12"),
("newer", "local", "2026-08-07"),
] {
let line = out
.lines()
.find(|l| l.contains(key))
.unwrap_or_else(|| panic!("`{key}` is missing from:\n{out}"));
assert!(line.contains(layer), "`{key}` lost its layer: {line}");
assert!(line.contains(date), "`{key}` lost its date: {line}");
}
}
#[test]
fn the_session_removal_nudge_counts_only_that_sessions_notes() {
let mut notes = Vec::new();
for session in ["s1", "s10", "s1x"] {
let mut note = note_with(Kind::Surprise, session, &surprise_body());
note.source = format!("session {session}, claude");
notes.push(note);
}
assert_eq!(from_session(¬es, "s1").len(), 1);
assert_eq!(from_session(¬es, "s10").len(), 1);
assert!(from_session(¬es, "s2").is_empty());
}
#[test]
fn the_nudge_is_silent_when_the_session_recorded_nothing() {
assert!(session_nudge(&[], "s1").is_none());
let mut note = note_with(Kind::Surprise, "k", &surprise_body());
note.source = "session s1, claude".into();
let line = session_nudge(&[note], "s1").expect("one note is worth a line");
assert!(line.contains('1') && line.contains("omh memory"), "{line}");
}
#[test]
fn the_shipped_key_templates_cover_every_note_type() {
let shipped = parse_templates(SHIPPED_KEYS).unwrap();
for kind in Kind::ALL {
let template = shipped
.get(&kind)
.unwrap_or_else(|| panic!("no key template for `{kind}`"));
let key = expand_key(template, &[("slug", "x"), ("path", "docs/x")]).unwrap();
assert!(!key.is_empty() && !key.contains("{{"), "`{kind}` → {key}");
}
}
#[test]
fn the_layer_is_part_of_a_notes_identity() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "deploy", &surprise_body());
seed(&paths, Layer::Local, "deploy", &surprise_body());
let notes = load(&paths).unwrap();
assert_eq!(notes.len(), 2);
assert_eq!(resolve(¬es, "deploy", Layer::Local).len(), 2);
}
#[test]
fn a_local_link_resolves_into_either_layer() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "shared", &surprise_body());
let notes = load(&paths).unwrap();
assert_eq!(resolve(¬es, "shared", Layer::Local), vec![Layer::Team]);
}
#[test]
fn a_committed_note_never_resolves_a_link_into_the_gitignored_layer() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "mine", &surprise_body());
let notes = load(&paths).unwrap();
assert_eq!(resolve(¬es, "mine", Layer::Local), vec![Layer::Local]);
assert!(
resolve(¬es, "mine", Layer::Team).is_empty(),
"a teammate cloning this repo has no local layer to reach"
);
}
#[test]
fn uncommitted_links_names_what_a_clone_would_lose() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "committed", &surprise_body());
seed(&paths, Layer::Local, "private", &surprise_body());
seed(
&paths,
Layer::Local,
"candidate",
&format!(
"{}\n## Related\n\n- [[committed]]\n- [[private]]\n",
surprise_body()
),
);
let notes = load(&paths).unwrap();
assert_eq!(
uncommitted_links(¬es, find(¬es, "candidate"), &[]),
vec!["private".to_string()],
"only the link a clone could not follow"
);
}
#[test]
fn a_pair_that_link_to_each_other_are_promotable_together() {
let (_d, paths) = fixture();
for (key, other) in [("a", "b"), ("b", "a")] {
seed(
&paths,
Layer::Local,
key,
&format!("{}\n## Related\n\n- [[{other}]]\n", surprise_body()),
);
}
let notes = load(&paths).unwrap();
assert_eq!(
uncommitted_links(¬es, find(¬es, "a"), &[]),
vec!["b".to_string()]
);
assert!(
uncommitted_links(¬es, find(¬es, "a"), &["b".to_string()]).is_empty(),
"promoted together, neither dangles"
);
}
#[test]
fn every_committed_note_links_only_to_committed_notes() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "private", &surprise_body());
seed(
&paths,
Layer::Team,
"shared",
&format!("{}\n## Related\n\n- [[private]]\n", surprise_body()),
);
let found = lint(&paths).unwrap();
let crossing: Vec<&Violation> = found
.iter()
.filter(|v| v.rule == Rule::CrossLayerLink)
.collect();
assert_eq!(crossing.len(), 1, "got: {found:?}");
assert_eq!(crossing[0].key, "shared");
assert!(crossing[0].detail.contains("private"), "{:?}", crossing[0]);
}
#[test]
fn a_duplicate_key_never_hides_a_committed_notes_cross_layer_link() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "private", &surprise_body());
seed(&paths, Layer::Team, "dup", &surprise_body());
let other = Layer::Team.dir(&paths).join("ns/dup.md");
std::fs::create_dir_all(other.parent().unwrap()).unwrap();
std::fs::write(
&other,
render(&Note {
path: other.clone(),
..note_with(
Kind::Surprise,
"dup",
&format!("{}\n## Related\n\n- [[private]]\n", surprise_body()),
)
}),
)
.unwrap();
let found = lint(&paths).unwrap();
let crossing: Vec<&Violation> = found
.iter()
.filter(|v| v.rule == Rule::CrossLayerLink)
.collect();
assert_eq!(
crossing.len(),
1,
"the file that links into the gitignored layer, exactly once: {found:?}"
);
assert!(crossing[0].detail.contains("private"), "{:?}", crossing[0]);
}
#[test]
fn a_committed_note_pointing_at_a_committed_note_is_silent() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "target", &surprise_body());
seed(
&paths,
Layer::Team,
"source",
&format!("{}\n## Related\n\n- [[target]]\n", surprise_body()),
);
assert!(
!lint(&paths)
.unwrap()
.iter()
.any(|v| v.rule == Rule::CrossLayerLink),
"a committed link to a committed note is exactly what is wanted"
);
}
#[test]
fn a_local_note_may_point_wherever_it_likes() {
let (_d, paths) = fixture();
seed(&paths, Layer::Team, "shared", &surprise_body());
seed(&paths, Layer::Local, "other", &surprise_body());
seed(
&paths,
Layer::Local,
"mine",
&format!(
"{}\n## Related\n\n- [[shared]]\n- [[other]]\n",
surprise_body()
),
);
assert!(!lint(&paths)
.unwrap()
.iter()
.any(|v| v.rule == Rule::CrossLayerLink));
}
#[test]
fn a_cross_layer_link_warns_rather_than_refusing() {
assert_eq!(Rule::CrossLayerLink.severity(), Severity::Warning);
}
#[test]
fn lint_reports_a_note_it_cannot_read_instead_of_giving_up() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("broken.md"), "no frontmatter here\n").unwrap();
seed(&paths, Layer::Local, "fine", &surprise_body());
let found = lint(&paths).unwrap();
let unreadable: Vec<_> = found
.iter()
.filter(|v| v.rule == Rule::Unreadable)
.collect();
assert_eq!(unreadable.len(), 1, "got: {found:?}");
assert!(
unreadable[0].detail.contains("broken.md"),
"the report must name the file: {}",
unreadable[0].detail
);
assert_eq!(
Rule::Unreadable.severity(),
Severity::Refused,
"a note the store cannot read is not a style warning"
);
assert!(
found.iter().any(|v| v.key == "fine"),
"one bad file must not hide every other violation: {found:?}"
);
}
#[test]
fn two_notes_under_one_key_in_one_layer_are_reported() {
let (_d, paths) = fixture();
let root = Layer::Local.dir(&paths);
std::fs::create_dir_all(root.join("ns")).unwrap();
for at in ["dup.md", "ns/dup.md"] {
let mut note = note_with(Kind::Surprise, "dup", &surprise_body());
note.path = root.join(at);
std::fs::write(¬e.path, render(¬e)).unwrap();
}
let found = lint(&paths).unwrap();
let dupes: Vec<_> = found
.iter()
.filter(|v| v.rule == Rule::DuplicateKey)
.collect();
assert_eq!(dupes.len(), 1, "one key, one report: {found:?}");
assert!(
dupes[0].detail.contains("dup.md") && dupes[0].detail.contains("ns/dup.md"),
"the report must name both files: {}",
dupes[0].detail
);
assert_eq!(Rule::DuplicateKey.severity(), Severity::Warning);
}
#[test]
fn one_key_in_both_layers_is_not_a_duplicate() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "deploy", &surprise_body());
seed(&paths, Layer::Team, "deploy", &surprise_body());
assert!(
!lint(&paths)
.unwrap()
.iter()
.any(|v| v.rule == Rule::DuplicateKey),
"a key in both layers is a disagreement, not a duplicate"
);
}
#[test]
fn only_refusals_decide_whether_lint_fails() {
let warning = Violation {
key: "k".into(),
layer: Layer::Local,
rule: Rule::Orphan,
detail: String::new(),
};
let refusal = Violation {
rule: Rule::MissingSection,
..warning.clone()
};
assert_eq!(
refused(std::slice::from_ref(&warning)),
0,
"a store full of warnings is still a passing store"
);
assert_eq!(refused(&[warning, refusal]), 1);
}
#[test]
fn lint_reports_both_the_schema_and_the_links() {
let (_d, paths) = fixture();
seed(
&paths,
Layer::Local,
"broken",
"# T\n\n## Expected\na\n\n## Related\n\nprose, not bullets\n",
);
let found = lint(&paths).unwrap();
let seen = tally(&found);
assert!(
seen.contains_key(&Rule::MissingSection),
"the schema half must run: {found:?}"
);
assert!(
seen.contains_key(&Rule::ProseInListSection),
"the structural half must run: {found:?}"
);
assert!(
seen.contains_key(&Rule::Orphan),
"the hygiene half must run: {found:?}"
);
}
#[test]
fn the_violation_count_separates_a_good_store_from_a_bad_one() {
let (_d, good) = fixture();
seed(
&good,
Layer::Local,
"a",
&format!("{}\n## Related\n\n- [[b]]\n", surprise_body()),
);
seed(
&good,
Layer::Local,
"b",
&format!("{}\n## Related\n\n- [[a]]\n", surprise_body()),
);
let (_d2, bad) = fixture();
seed(
&bad,
Layer::Local,
"a",
"# T\n\n## Related\n\nre-narration\n",
);
seed(&bad, Layer::Local, "b", "# T\n\n## Related\n\n- [[gone]]\n");
let clean = lint(&good).unwrap();
assert!(
clean.is_empty(),
"a store worth keeping lints clean: {clean:?}"
);
assert!(lint(&bad).unwrap().len() > clean.len());
}
#[test]
fn the_note_template_in_the_staged_rules_actually_parses() {
let rules = shipped_rules();
let start = rules
.find("```markdown\n")
.expect("the rules must show a note template");
let block = &rules[start + "```markdown\n".len()..];
let block = &block[..block.find("```").expect("unterminated code block")];
let filled = block
.replace("<the filename, without .md>", "an-observation")
.replace(
"session $OMH_SESSION, <this harness>",
"session s01, claude",
)
.replace("<YYYY-MM-DD, the day it happened>", "2026-08-07")
.replace("# One line naming the surprise", "# A mount failed")
.replace("## Expected\n", "## Expected\nit would persist\n")
.replace("## Observed\n", "## Observed\nit did not\n")
.replace("## Evidence\n", "## Evidence\n`EBUSY`\n")
.replace(
"- <the question somebody would later ask to find this>",
"- why does my login not persist",
);
let path = PathBuf::from("an-observation.md");
let note = parse(&filled, Layer::Local, &path)
.unwrap_or_else(|e| panic!("the documented shape does not parse: {e}\n\n{filled}"));
assert_eq!(
check(¬e),
vec![],
"the documented shape must satisfy the schema that refuses writes"
);
}
#[test]
fn the_rules_say_which_of_the_two_graphs_answers_which_question() {
let rules = shipped_rules();
let lower = rules.to_lowercase();
assert!(
lower.contains("search_graph"),
"the code graph must be named"
);
assert!(lower.contains("recall"), "memory must be named");
let has_rule = lower.contains("what the code is") && lower.contains("why");
assert!(
has_rule,
"the rules name both graphs but never say how to choose:\n{rules}"
);
}
#[test]
fn recalls_description_says_when_to_reach_for_it_not_only_what_it_holds() {
let text =
crate::memory::index::describe(&crate::memory::index::Index::of(&[])).to_lowercase();
assert!(
text.contains("code"),
"it has to distinguish itself from the code graph: {text}"
);
}
#[test]
fn a_trigger_recorded_in_the_sandbox_is_stored_repo_relative() {
let (_d, paths) = fixture();
let mut input = observation();
input.invalidated_by = Some(format!(
"file:{}/src/main.rs@abc1230",
crate::container_workdir()
));
remember(&paths, &input, IfExists::Error).unwrap();
let note = &load(&paths).unwrap()[0];
assert_eq!(
note.invalidated_by.as_deref(),
Some("file:src/main.rs@abc1230"),
"the sandbox prefix must not survive into the store"
);
}
#[test]
fn an_invalidation_kind_omh_cannot_evaluate_is_refused_by_the_schema() {
let raw = SURPRISE.replace(
"invalidated_by: image:4f2a1c3b5d7e9f0a2b4c6d8e0f1a3b5c7d9e0f1a",
"invalidated_by: vibes:soon",
);
let note = parse(&raw, Layer::Local, std::path::Path::new("x.md"))
.expect("a note that already exists must still be readable");
let found = check(¬e);
let bad: Vec<&Violation> = found
.iter()
.filter(|v| v.rule == Rule::UnevaluatableTrigger)
.collect();
assert_eq!(bad.len(), 1, "got: {found:?}");
assert!(bad[0].detail.contains("vibes"), "{:?}", bad[0]);
assert_eq!(
bad[0].rule.severity(),
Severity::Refused,
"a warning would let it ship"
);
}
#[test]
fn a_note_with_an_unevaluatable_trigger_is_never_written() {
let (_d, paths) = fixture();
let mut input = observation();
input.invalidated_by = Some("whenever:i-feel-like-it".into());
assert!(remember(&paths, &input, IfExists::Error).is_err());
assert!(load(&paths).unwrap().is_empty());
}
#[test]
fn a_trigger_omh_cannot_evaluate_does_not_take_the_store_down() {
let (_d, paths) = fixture();
seed(&paths, Layer::Local, "good", &surprise_body());
let bad = Layer::Local.dir(&paths).join("legacy.md");
std::fs::write(
&bad,
format!(
"---\nkey: legacy\ntype: surprise\nsource: audit\n\
recorded: 2026-08-07\ninvalidated_by: whenever:i-feel-like-it\n---\n\n{}",
surprise_body()
),
)
.unwrap();
let notes = load(&paths).unwrap();
assert_eq!(notes.len(), 2, "both notes still load");
let found = lint(&paths).unwrap();
let bad_trigger: Vec<&Violation> = found
.iter()
.filter(|v| v.rule == Rule::UnevaluatableTrigger)
.collect();
assert_eq!(bad_trigger.len(), 1, "got: {found:?}");
assert_eq!(bad_trigger[0].key, "legacy");
assert_eq!(Rule::UnevaluatableTrigger.severity(), Severity::Refused);
}
#[test]
fn pinning_the_current_image_records_the_digest_omh_would_build() {
let (_d, paths) = fixture();
let mut input = observation();
input.invalidated_by = Some(format!("image:{}", expiry::IMAGE_NOW));
remember(&paths, &input, IfExists::Error).unwrap();
let recorded = load(&paths).unwrap()[0].invalidated_by.clone().unwrap();
let expected = crate::image::recipe_digest(&crate::image::base_dockerfile()).unwrap();
assert_eq!(
recorded,
format!("image:{expected}"),
"the sentinel must not reach the store"
);
assert!(
expiry::Trigger::parse(&recorded).is_ok(),
"and what lands is a pin omh can evaluate"
);
}
#[test]
fn writing_a_note_never_rewrites_its_link_text() {
let (_d, paths) = fixture();
let mut input = observation();
input.relates_to = vec![
"credentials-are-a-named-volume".into(),
"surprise/one-inode".into(),
];
let Wrote::Created(path) = remember(&paths, &input, IfExists::Error).unwrap() else {
panic!()
};
let first = std::fs::read(&path).unwrap();
let note = &load(&paths).unwrap()[0];
std::fs::write(&path, render(note)).unwrap();
assert_eq!(
std::fs::read(&path).unwrap(),
first,
"a round trip must not touch a single byte, link text least of all"
);
let text = String::from_utf8(first).unwrap();
for key in &input.relates_to {
assert!(
text.contains(&format!("[[{key}]]")),
"the link is stored as written: {text}"
);
}
}
#[test]
fn the_staged_rules_name_the_path_the_store_is_mounted_at() {
assert!(
shipped_rules().contains(GUEST_LOCAL_NOTES),
"the rules must point at {GUEST_LOCAL_NOTES}"
);
}
#[test]
fn every_layer_round_trips_through_its_own_name() {
for layer in Layer::ALL {
assert_eq!(Layer::from_str(&layer.to_string()).unwrap(), layer);
}
}
#[test]
fn an_unknown_layer_is_an_error_that_names_the_known_ones() {
let err = Layer::from_str("shared").unwrap_err().to_string();
assert!(err.contains("shared"), "got: {err}");
assert!(err.contains("team") && err.contains("local"), "got: {err}");
}
}