use crate::base::{Entry, Manifest, Rejected};
use crate::config::{Layer, Setting};
use std::collections::BTreeMap;
pub struct Catalog<'a> {
pub manifest: &'a Manifest,
pub baselines: BTreeMap<String, String>,
pub installed: Vec<Setting>,
pub derived: BTreeMap<String, Derived>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Derived {
pub from: String,
pub command: String,
pub layer: Layer,
}
#[derive(Debug)]
pub enum Verdict<'a> {
Omh {
entry: &'a Entry,
yours: &'a Setting,
},
Differs {
entry: &'a Entry,
ships: String,
yours: &'a Setting,
},
Removed { entry: &'a Entry },
Derived { yours: &'a Setting, from: String },
Yours { yours: &'a Setting },
Rejected { rejection: &'a Rejected },
Unknown { known: Vec<String> },
}
impl<'a> Catalog<'a> {
pub fn why(&'a self, name: &str) -> Verdict<'a> {
let entry = self.manifest.entry(name);
let yours = self.installed.iter().find(|s| s.key == name);
match (entry, yours) {
(Some(entry), Some(yours)) => match self.baselines.get(name) {
Some(ships) if ships != &yours.value => Verdict::Differs {
entry,
ships: ships.clone(),
yours,
},
_ => Verdict::Omh { entry, yours },
},
(Some(entry), None) => Verdict::Removed { entry },
(None, Some(yours)) => match self.derived.get(name) {
Some(d) if d.layer == yours.layer && d.command == yours.value => Verdict::Derived {
yours,
from: d.from.clone(),
},
_ => Verdict::Yours { yours },
},
(None, None) => match self.manifest.rejection(name) {
Some(rejection) => Verdict::Rejected { rejection },
None => Verdict::Unknown {
known: self.known(),
},
},
}
}
fn known(&self) -> Vec<String> {
let mut names: Vec<String> = self
.manifest
.entries
.iter()
.map(|e| e.name.clone())
.chain(self.installed.iter().map(|s| s.key.clone()))
.chain(self.manifest.rejected.iter().map(|r| r.name.clone()))
.collect();
names.sort();
names.dedup();
names
}
}
fn is_stale(measured_on: &str, manifest_version: &str) -> bool {
use crate::base::parse_ym as ym;
match (ym(measured_on), ym(manifest_version)) {
(Some(on), Some(version)) => on < version,
_ => false,
}
}
fn costs(entry: &Entry, version: &str, out: &mut String) {
let claims: Vec<String> = entry
.measured
.iter()
.map(|m| format!("{} {}", m.value, m.what))
.collect();
let width = claims.iter().map(|c| c.chars().count()).max().unwrap_or(0);
let mut label = "costs";
for (m, claim) in entry.measured.iter().zip(&claims) {
let stale = if is_stale(&m.on, version) {
" (stale)"
} else {
""
};
out.push_str(&format!(
" {label:<11} {claim:<width$} measured {}{stale}\n",
m.on
));
out.push_str(&format!(" {:<11} {}\n", "", m.how));
label = "";
}
if entry.measured.iter().any(|m| is_stale(&m.on, version)) {
out.push_str(&format!(
" (stale: taken before base set {version} — re-measure or re-affirm)\n"
));
}
}
fn alternatives(entry: &Entry, out: &mut String) {
let width = entry
.instead_of
.iter()
.map(|a| a.name.chars().count())
.max()
.unwrap_or(0);
let mut label = "instead of";
for a in &entry.instead_of {
out.push_str(&format!(" {label:<11} {:<width$} {}\n", a.name, a.why));
label = "";
}
}
pub fn render_with_source(verdict: &Verdict, version: &str, source: &str) -> String {
let mut out = render(verdict, version);
out.push_str(&format!("\n answered from {source}\n"));
out
}
pub fn render(verdict: &Verdict, version: &str) -> String {
let mut out = String::new();
match verdict {
Verdict::Omh { entry, yours } => {
out.push_str(&format!(
"{} — omh's choice, in the base set since {}\n\n",
entry.name, entry.since
));
out.push_str(&format!(" {:<11} {}\n", "because", entry.because));
costs(entry, version, &mut out);
alternatives(entry, &mut out);
out.push_str(&format!(" {:<11} {}\n", "installed", yours.layer));
out.push_str(&format!(" {:<11} {}\n", "remove", entry.remove));
}
Verdict::Differs {
entry,
ships,
yours,
} => {
out.push_str(&format!(
"{} — omh's choice, and your copy is not what omh ships now\n\n",
entry.name
));
out.push_str(&format!(" {:<11} {ships}\n", "omh ships"));
out.push_str(&format!(
" {:<11} {} in {}\n",
"on disk", yours.value, yours.layer
));
out.push_str(&format!(" {:<11} {}\n", "because", entry.because));
costs(entry, version, &mut out);
out.push_str(&format!(" {:<11} {}\n", "remove", entry.remove));
out.push_str(
"\n Either you changed it, or omh did in a later version:\n \
`init` seeds your profile once and never rewrites it.\n",
);
}
Verdict::Removed { entry } => {
out.push_str(&format!(
"{} — omh's choice, not installed here\n\n",
entry.name
));
out.push_str(&format!(" {:<11} {}\n", "because", entry.because));
costs(entry, version, &mut out);
alternatives(entry, &mut out);
out.push_str(&format!(" {:<11} omh init\n", "restore"));
}
Verdict::Derived { yours, from } => {
out.push_str(&format!(
"{} — written by omh init, from your repo\n\n",
yours.key
));
out.push_str(&format!(" {:<11} {from}\n", "derived from"));
out.push_str(&format!(" {:<11} {}\n", "installed", yours.layer));
out.push_str(
"\n Not a curated choice — it follows from what your repo is,\n \
so there is nothing to argue about. Edit or delete it freely;\n \
init will not write over your version.\n",
);
}
Verdict::Yours { yours } => {
out.push_str(&format!("{} — your choice, not omh's\n\n", yours.key));
out.push_str(&format!(" {:<11} {}\n", "added in", yours.layer));
let shadowed = if yours.shadows.is_empty() {
"nothing".to_string()
} else {
yours
.shadows
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join(", ")
};
out.push_str(&format!(" {:<11} {shadowed}\n", "overrides"));
out.push_str("\n omh has no rationale for this one — it is yours.\n");
}
Verdict::Rejected { rejection } => {
out.push_str(&format!(
"{} — considered {}, not in the base set\n\n",
rejection.name, rejection.considered
));
out.push_str(&format!(" {:<11} {}\n", "because", rejection.because));
}
Verdict::Unknown { known } => {
out.push_str("omh has nothing recorded under that name.\n\n");
out.push_str(" known:\n");
for name in known {
out.push_str(&format!(" {name}\n"));
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Layer;
use std::path::Path;
const BUNDLED: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/base");
fn manifest() -> Manifest {
Manifest::load_dir(Path::new(BUNDLED)).unwrap()
}
fn setting(key: &str, value: &str, layer: Layer) -> Setting {
Setting {
key: key.into(),
value: value.into(),
layer,
shadows: Vec::new(),
}
}
fn catalog<'a>(m: &'a Manifest, installed: Vec<Setting>) -> Catalog<'a> {
let baselines = m
.entries
.iter()
.filter_map(|e| e.command.clone().map(|c| (e.name.clone(), c)))
.collect();
Catalog {
manifest: m,
baselines,
installed,
derived: BTreeMap::new(),
}
}
#[test]
fn an_untouched_base_entry_is_omhs() {
let m = manifest();
let c = catalog(
&m,
vec![setting("codegraph", "codebase-memory-mcp", Layer::Shared)],
);
assert!(matches!(c.why("codegraph"), Verdict::Omh { .. }));
}
#[test]
fn a_changed_base_entry_reports_both_values() {
let m = manifest();
let c = catalog(&m, vec![setting("codegraph", "my-fork", Layer::Local)]);
match c.why("codegraph") {
Verdict::Differs { ships, yours, .. } => {
assert_eq!(ships, "codebase-memory-mcp");
assert_eq!(yours.value, "my-fork");
assert_eq!(yours.layer, Layer::Local);
}
other => panic!("expected Differs, got {other:?}"),
}
}
#[test]
fn a_removed_base_entry_is_still_explained() {
let m = manifest();
let c = catalog(&m, vec![]);
assert!(matches!(c.why("codegraph"), Verdict::Removed { .. }));
}
#[test]
fn your_own_entry_gets_no_rationale() {
let m = manifest();
let c = catalog(&m, vec![setting("linear", "npx", Layer::Local)]);
match c.why("linear") {
Verdict::Yours { yours } => assert_eq!(yours.layer, Layer::Local),
other => panic!("expected Yours, got {other:?}"),
}
}
#[test]
fn a_rejected_candidate_explains_its_rejection() {
let m = manifest();
let c = catalog(&m, vec![]);
match c.why("gitnexus") {
Verdict::Rejected { rejection } => {
assert!(
rejection.because.contains("Noncommercial"),
"{}",
rejection.because
);
}
other => panic!("expected Rejected, got {other:?}"),
}
}
#[test]
fn an_unknown_name_lists_what_is_known_instead_of_guessing() {
let m = manifest();
let c = catalog(&m, vec![setting("linear", "npx", Layer::Local)]);
match c.why("lienar") {
Verdict::Unknown { known } => {
assert!(known.contains(&"codegraph".to_string()), "{known:?}");
assert!(known.contains(&"linear".to_string()), "{known:?}");
assert!(known.contains(&"gitnexus".to_string()), "{known:?}");
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn an_entry_with_no_baseline_is_not_reported_as_modified() {
let m = manifest();
let mut c = catalog(
&m,
vec![setting("graph-read", "anything at all", Layer::Shared)],
);
c.baselines.remove("graph-read");
assert!(matches!(c.why("graph-read"), Verdict::Omh { .. }));
}
#[test]
fn omhs_choice_reports_its_argument_and_its_cost() {
let m = manifest();
let c = catalog(
&m,
vec![setting("codegraph", "codebase-memory-mcp", Layer::Shared)],
);
let out = render(&c.why("codegraph"), "2026.08");
assert!(out.contains("omh's choice"), "{out}");
assert!(
out.contains("re-grepping"),
"the argument is missing:\n{out}"
);
assert!(out.contains("0.46s"), "the cost is missing:\n{out}");
assert!(
out.contains("gitnexus"),
"the alternatives are missing:\n{out}"
);
assert!(
out.contains("omh config mcp rm codegraph"),
"no way out:\n{out}"
);
}
#[test]
fn every_measured_cost_carries_the_date_it_was_taken() {
let m = manifest();
for entry in &m.entries {
let out = render(&Verdict::Removed { entry }, "2026.08");
for measured in &entry.measured {
let line = out
.lines()
.find(|l| l.contains(&measured.value) && l.contains(&measured.what))
.unwrap_or_else(|| panic!("{}: no line for {}", entry.name, measured.what));
assert!(
line.contains(&measured.on),
"{}: cost printed without its date: {line}",
entry.name
);
}
}
}
#[test]
fn a_measurement_older_than_the_entry_is_marked_stale() {
let m: Manifest = toml::from_str(
r#"
version = "2026.08"
[[entry]]
name = "x"
kind = "mcp"
since = "2026.08"
because = "b"
remove = "r"
command = "c"
[[entry.measured]]
what = "per turn"
value = "1s"
how = "h"
on = "2026-01-01"
[[entry.instead_of]]
name = "a"
why = "w"
"#,
)
.unwrap();
let out = render(
&Verdict::Removed {
entry: &m.entries[0],
},
"2026.08",
);
assert!(
out.contains("stale"),
"an outdated measurement must say so:\n{out}"
);
let fresh = render(
&Verdict::Removed {
entry: &m.entries[0],
},
"2026.01",
);
assert!(
!fresh.contains("stale"),
"a current measurement must not be flagged:\n{fresh}"
);
}
#[test]
fn staleness_is_measured_against_the_base_set_not_the_entrys_own_age() {
let m = manifest();
let entry = m.entry("codegraph").unwrap();
let current = render(&Verdict::Removed { entry }, &m.version);
assert!(
!current.contains("stale"),
"shipped numbers are current:\n{current}"
);
let later = render(&Verdict::Removed { entry }, "2027.01");
let first = entry.measured.first().expect("codegraph has measurements");
let cost_line = later
.lines()
.find(|l| l.contains(&first.value) && l.contains("measured"))
.unwrap_or_else(|| panic!("no cost line in:\n{later}"));
assert!(
cost_line.contains("(stale)"),
"re-cutting the base set must flag every carried-over number:\n{cost_line}"
);
}
#[test]
fn the_method_is_shown_for_every_cost_not_only_stale_ones() {
let m = manifest();
let entry = m.entry("codegraph").unwrap();
let out = render(&Verdict::Removed { entry }, &m.version);
for measured in &entry.measured {
assert!(
out.contains(&measured.how),
"cost `{}` printed without how it was taken:\n{out}",
measured.value
);
}
}
fn rust_format() -> Derived {
Derived {
from: "rust, detected from Cargo.toml".into(),
command: "cargo fmt".into(),
layer: Layer::Shared,
}
}
#[test]
fn a_hook_in_a_layer_init_never_writes_to_is_yours() {
let m = manifest();
let mut c = catalog(&m, vec![setting("rust-format", "cargo fmt", Layer::Local)]);
c.derived.insert("rust-format".into(), rust_format());
assert!(
matches!(c.why("rust-format"), Verdict::Yours { .. }),
"init does not write to local, so this is not init's"
);
}
#[test]
fn a_rewritten_stack_hook_is_yours() {
let m = manifest();
let mut c = catalog(
&m,
vec![setting(
"rust-format",
"cargo +nightly fmt --all",
Layer::Shared,
)],
);
c.derived.insert("rust-format".into(), rust_format());
assert!(matches!(c.why("rust-format"), Verdict::Yours { .. }));
}
#[test]
fn a_hook_derived_from_your_stack_is_neither_omhs_opinion_nor_yours() {
let m = manifest();
let mut c = catalog(&m, vec![setting("rust-format", "cargo fmt", Layer::Shared)]);
c.derived.insert("rust-format".into(), rust_format());
match c.why("rust-format") {
Verdict::Derived { from, .. } => assert!(from.contains("Cargo.toml"), "{from}"),
other => panic!("expected Derived, got {other:?}"),
}
let out = render(&c.why("rust-format"), "2026.08");
assert!(!out.contains("base set"), "claims it is curated:\n{out}");
assert!(
!out.contains("your choice"),
"disowns something omh wrote:\n{out}"
);
assert!(
out.contains("Cargo.toml"),
"does not say what it was derived from:\n{out}"
);
}
#[test]
fn your_own_choice_never_borrows_omhs_authority() {
let m = manifest();
let c = catalog(&m, vec![setting("linear", "npx", Layer::Local)]);
let out = render(&c.why("linear"), "2026.08");
assert!(out.contains("your choice"), "{out}");
assert!(!out.contains("base set"), "claims omh installed it:\n{out}");
assert!(!out.contains("because"), "invents a rationale:\n{out}");
assert!(
out.contains("local"),
"provenance is the one thing it can say:\n{out}"
);
}
#[test]
fn a_differing_entry_pairs_each_value_with_its_own_label() {
let m = manifest();
let c = catalog(&m, vec![setting("codegraph", "my-fork", Layer::Local)]);
let out = render(&c.why("codegraph"), "2026.08");
let line = |label: &str| {
out.lines()
.find(|l| l.trim_start().starts_with(label))
.unwrap_or_else(|| panic!("no `{label}` line in:\n{out}"))
};
assert!(line("omh ships").contains("codebase-memory-mcp"), "{out}");
assert!(!line("omh ships").contains("my-fork"), "{out}");
assert!(line("on disk").contains("my-fork"), "{out}");
assert!(!line("on disk").contains("codebase-memory-mcp"), "{out}");
}
#[test]
fn a_difference_never_claims_who_caused_it() {
let m = manifest();
let c = catalog(&m, vec![setting("codegraph", "my-fork", Layer::Local)]);
let out = render(&c.why("codegraph"), "2026.08");
assert!(
!out.contains("modified by you") && !out.contains("you set"),
"omh does not know who changed it:\n{out}"
);
assert!(out.contains("Either you changed it, or omh did"), "{out}");
}
#[test]
fn every_answer_names_the_manifest_that_produced_it() {
let m = manifest();
let c = catalog(
&m,
vec![setting("codegraph", "codebase-memory-mcp", Layer::Shared)],
);
let out = render_with_source(
&c.why("codegraph"),
"2026.08",
"/home/x/.omh/base/2026.08.toml · 2026.08",
);
assert!(out.contains("answered from"), "{out}");
assert!(out.contains("2026.08.toml"), "{out}");
}
#[test]
fn a_rejection_prints_its_reasoning_and_when_it_was_considered() {
let m = manifest();
let c = catalog(&m, vec![]);
let out = render(&c.why("gitnexus"), &m.version);
let r = m.rejection("gitnexus").unwrap();
assert!(
out.contains(&r.because),
"the reasoning is the whole point:\n{out}"
);
assert!(
out.contains(&r.considered),
"when it was considered:\n{out}"
);
assert!(
out.contains("Noncommercial"),
"the licence problem must survive:\n{out}"
);
}
#[test]
fn a_removed_entry_says_how_to_get_it_back() {
let m = manifest();
let c = catalog(&m, vec![]);
let out = render(&c.why("codegraph"), &m.version);
assert!(out.contains("not installed here"), "{out}");
assert!(out.contains("omh init"), "no way back:\n{out}");
}
#[test]
fn an_unknown_name_prints_the_alternatives_it_does_know() {
let m = manifest();
let c = catalog(&m, vec![]);
let out = render(&c.why("lienar"), "2026.08");
assert!(out.contains("codegraph"), "{out}");
assert!(!out.contains("omh's choice"), "guessed at a match:\n{out}");
}
}