use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use m1nd_core::error::{M1ndError, M1ndResult};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Letter {
#[serde(default)]
pub id: String,
#[serde(default)]
pub ts: String,
#[serde(default)]
pub agent: String,
#[serde(default)]
pub class: String,
#[serde(default)]
pub repo: String,
#[serde(default)]
pub brain: String,
#[serde(default)]
pub tool: String,
#[serde(default)]
pub what: String,
#[serde(default)]
pub expected: String,
#[serde(default)]
pub snippet: String,
#[serde(default)]
pub answers: Vec<String>,
#[serde(skip)]
pub raw: String,
}
pub const CLASS_MEMORY_MISDELIVERY: &str = "memory_misdelivery";
pub const MISDELIVERY_KINDS: &[&str] = &[
"leak",
"false_absence",
"wrong_store_write",
"misattribution",
"vanished",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Fate {
WetInk,
InFlight,
FiredClay,
External,
}
impl Fate {
pub fn as_str(self) -> &'static str {
match self {
Fate::WetInk => "wet_ink",
Fate::InFlight => "in_flight",
Fate::FiredClay => "fired_clay",
Fate::External => "external",
}
}
pub fn is_open(self) -> bool {
matches!(self, Fate::WetInk | Fate::InFlight)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoxTarget {
Project(PathBuf),
Medulla,
Pending(String),
}
fn expand_home(raw: &str) -> String {
expand_home_with(raw, home_dir())
}
fn expand_home_with(raw: &str, home: Option<PathBuf>) -> String {
let trimmed = raw.trim();
if let Some(rest) = trimmed.strip_prefix("~/") {
if let Some(home) = home {
return home.join(rest).to_string_lossy().to_string();
}
} else if trimmed == "~" {
if let Some(home) = home {
return home.to_string_lossy().to_string();
}
}
trimmed.to_string()
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}
fn strip_parenthetical(raw: &str) -> String {
match raw.find('(') {
Some(i) => raw[..i].trim().to_string(),
None => raw.trim().to_string(),
}
}
pub fn normalize_repo(raw: &str, worktree_base: &str) -> String {
let no_paren = strip_parenthetical(raw);
let expanded = expand_home(&no_paren);
let expanded = expanded.trim().trim_end_matches('/');
let base = Path::new(expanded)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(expanded)
.to_string();
collapse_worktree(&base, worktree_base)
}
fn collapse_worktree(name: &str, worktree_base: &str) -> String {
if name == worktree_base {
return name.to_string();
}
if let Some(rest) = name.strip_prefix(worktree_base) {
if rest.starts_with('-') || rest.is_empty() {
return worktree_base.to_string();
}
}
name.to_string()
}
fn is_projectless(raw: &str, owner_runtime_root: &Path) -> bool {
let no_paren = strip_parenthetical(raw);
let expanded = expand_home(&no_paren);
let expanded_trimmed = expanded.trim().trim_end_matches('/');
let owner_key = owner_runtime_root.to_string_lossy();
let owner_key = owner_key.trim_end_matches('/');
if expanded_trimmed == owner_key {
return true;
}
let low = no_paren.to_ascii_lowercase();
matches!(low.as_str(), "all" | "")
|| low.starts_with("research task")
|| no_paren.trim().is_empty()
}
pub fn resolve_box(
letter: &Letter,
owner_runtime_root: &Path,
worktree_base: &str,
known_repos: &BTreeMap<String, PathBuf>,
) -> BoxTarget {
let field = if !letter.brain.trim().is_empty() {
letter.brain.as_str()
} else {
letter.repo.as_str()
};
if is_projectless(field, owner_runtime_root) {
return BoxTarget::Medulla;
}
let normalized = normalize_repo(field, worktree_base);
let no_paren = strip_parenthetical(field);
let expanded = expand_home(&no_paren);
let as_path = Path::new(expanded.trim().trim_end_matches('/'));
if as_path.is_absolute() && as_path.is_dir() {
return BoxTarget::Project(as_path.to_path_buf());
}
if let Some(dir) = known_repos.get(&normalized) {
if dir.is_dir() {
return BoxTarget::Project(dir.clone());
}
}
BoxTarget::Pending(normalized)
}
pub fn letter_id(raw_line: &str) -> String {
let bytes = raw_line.trim_end_matches(['\n', '\r']).as_bytes();
let digest = Sha256::digest(bytes);
let hex = format!("{digest:x}");
hex[..12].to_string()
}
pub fn parse_letter(raw_line: &str) -> Option<Letter> {
let trimmed = raw_line.trim();
if trimmed.is_empty() {
return None;
}
let mut letter: Letter = serde_json::from_str(trimmed).ok()?;
letter.id = letter_id(raw_line);
letter.raw = trimmed.to_string();
Some(letter)
}
pub fn read_letters(path: &Path) -> M1ndResult<Vec<Letter>> {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(M1ndError::Io(e)),
};
Ok(text.lines().filter_map(parse_letter).collect())
}
pub fn derive_fate(
id: &str,
answered: &BTreeSet<String>,
external_ids: &BTreeSet<String>,
in_flight_ids: &BTreeSet<String>,
) -> Fate {
if external_ids.contains(id) {
return Fate::External;
}
if answered.contains(id) {
return Fate::FiredClay;
}
if in_flight_ids.contains(id) {
return Fate::InFlight;
}
Fate::WetInk
}
pub const CLASS_TRIAGE: &str = "triage";
fn is_receipt(letter: &Letter) -> bool {
letter.class.eq_ignore_ascii_case(CLASS_TRIAGE)
}
pub fn answered_set(letters: &[Letter]) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for l in letters {
if !is_receipt(l) {
continue;
}
for a in &l.answers {
out.insert(a.clone());
}
}
out
}
pub fn in_flight_set(letters: &[Letter], closed: &BTreeSet<String>) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for l in letters {
if is_receipt(l) {
continue;
}
for a in &l.answers {
if !closed.contains(a) {
out.insert(a.clone());
}
}
}
out
}
pub fn answered_by(letters: &[Letter]) -> BTreeMap<String, Vec<String>> {
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
for l in letters {
for a in &l.answers {
out.entry(a.clone()).or_default().push(l.id.clone());
}
}
out
}
pub const BOX_REL_PATH: &str = ".m1nd/inbox.jsonl";
pub const MEDULLA_BOX_FILE: &str = "inbox.jsonl";
const GITIGNORE_LINE: &str = "inbox.jsonl\n";
fn box_path_for_repo(repo_dir: &Path) -> PathBuf {
repo_dir.join(BOX_REL_PATH)
}
pub fn medulla_box_path(owner_runtime_root: &Path) -> PathBuf {
owner_runtime_root.join(MEDULLA_BOX_FILE)
}
fn ensure_box_birth(repo_dir: &Path) -> M1ndResult<()> {
let m1nd_dir = repo_dir.join(".m1nd");
std::fs::create_dir_all(&m1nd_dir).map_err(M1ndError::Io)?;
let gitignore = m1nd_dir.join(".gitignore");
if !gitignore.exists() {
std::fs::write(&gitignore, GITIGNORE_LINE).map_err(M1ndError::Io)?;
}
Ok(())
}
fn box_ids(box_path: &Path) -> M1ndResult<BTreeSet<String>> {
Ok(read_letters(box_path)?.into_iter().map(|l| l.id).collect())
}
fn append_letter(box_path: &Path, letter: &Letter) -> M1ndResult<()> {
use std::io::Write;
if let Some(parent) = box_path.parent() {
std::fs::create_dir_all(parent).map_err(M1ndError::Io)?;
}
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(box_path)
.map_err(M1ndError::Io)?;
writeln!(f, "{}", letter.raw).map_err(M1ndError::Io)?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FiledLetter {
pub id: String,
pub target: String,
pub appended: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionReceipt {
pub filed: Vec<FiledLetter>,
pub spool_total: usize,
pub appended: usize,
pub to_project: usize,
pub to_medulla: usize,
pub pending: Vec<String>,
}
pub fn distribute(
spool_path: &Path,
owner_runtime_root: &Path,
worktree_base: &str,
known_repos: &BTreeMap<String, PathBuf>,
) -> M1ndResult<DistributionReceipt> {
let letters = read_letters(spool_path)?;
let spool_total = letters.len();
let medulla_box = medulla_box_path(owner_runtime_root);
let mut box_id_cache: BTreeMap<PathBuf, BTreeSet<String>> = BTreeMap::new();
let mut filed = Vec::with_capacity(spool_total);
let mut appended = 0usize;
let mut to_project = 0usize;
let mut to_medulla = 0usize;
let mut pending: Vec<String> = Vec::new();
for letter in &letters {
let target = resolve_box(letter, owner_runtime_root, worktree_base, known_repos);
match target {
BoxTarget::Project(repo_dir) => {
ensure_box_birth(&repo_dir)?;
let box_path = box_path_for_repo(&repo_dir);
let ids = match box_id_cache.get_mut(&box_path) {
Some(ids) => ids,
None => {
let ids = box_ids(&box_path)?;
box_id_cache.entry(box_path.clone()).or_insert(ids)
}
};
let is_new = !ids.contains(&letter.id);
if is_new {
append_letter(&box_path, letter)?;
ids.insert(letter.id.clone());
appended += 1;
}
to_project += 1;
filed.push(FiledLetter {
id: letter.id.clone(),
target: format!("project:{}", repo_dir.to_string_lossy()),
appended: is_new,
});
}
BoxTarget::Medulla => {
let ids = match box_id_cache.get_mut(&medulla_box) {
Some(ids) => ids,
None => {
let ids = box_ids(&medulla_box)?;
box_id_cache.entry(medulla_box.clone()).or_insert(ids)
}
};
let is_new = !ids.contains(&letter.id);
if is_new {
append_letter(&medulla_box, letter)?;
ids.insert(letter.id.clone());
appended += 1;
}
to_medulla += 1;
filed.push(FiledLetter {
id: letter.id.clone(),
target: "medulla".to_string(),
appended: is_new,
});
}
BoxTarget::Pending(name) => {
pending.push(name.clone());
filed.push(FiledLetter {
id: letter.id.clone(),
target: format!("pending:{name}"),
appended: false,
});
}
}
}
pending.sort();
pending.dedup();
Ok(DistributionReceipt {
filed,
spool_total,
appended,
to_project,
to_medulla,
pending,
})
}
#[derive(Debug, Clone)]
pub struct KnownBox {
pub label: String,
pub path: PathBuf,
pub reachable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SweepResult {
pub letters: Vec<SweptLetter>,
pub total: usize,
pub open: usize,
pub unreachable: Vec<String>,
pub misdelivery: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SweptLetter {
pub id: String,
pub ts: String,
pub agent: String,
pub class: String,
pub repo: String,
pub brain: String,
pub tool: String,
pub what: String,
pub expected: String,
pub snippet: String,
pub answers: Vec<String>,
pub answered_by: Vec<String>,
pub state: String,
}
fn is_external(letter: &Letter, foreign_tools: &BTreeSet<String>) -> bool {
if foreign_tools.is_empty() {
return false;
}
let tool = letter.tool.to_ascii_lowercase();
foreign_tools.iter().any(|t| tool.contains(t.as_str()))
}
fn view_letters(letters: &[Letter], foreign_tools: &BTreeSet<String>) -> Vec<SweptLetter> {
let answered = answered_set(letters);
let by = answered_by(letters);
let external_ids: BTreeSet<String> = letters
.iter()
.filter(|l| is_external(l, foreign_tools))
.map(|l| l.id.clone())
.collect();
let in_flight_ids: BTreeSet<String> = in_flight_set(letters, &answered);
letters
.iter()
.map(|l| {
let fate = derive_fate(&l.id, &answered, &external_ids, &in_flight_ids);
SweptLetter {
id: l.id.clone(),
ts: l.ts.clone(),
agent: l.agent.clone(),
class: l.class.clone(),
repo: l.repo.clone(),
brain: l.brain.clone(),
tool: l.tool.clone(),
what: l.what.clone(),
expected: l.expected.clone(),
snippet: l.snippet.clone(),
answers: l.answers.clone(),
answered_by: by.get(&l.id).cloned().unwrap_or_default(),
state: fate.as_str().to_string(),
}
})
.collect()
}
pub fn inbox_sweep(
spool_path: &Path,
boxes: &[KnownBox],
foreign_tools: &BTreeSet<String>,
) -> M1ndResult<SweepResult> {
let mut by_id: BTreeMap<String, Letter> = BTreeMap::new();
for l in read_letters(spool_path)? {
by_id.entry(l.id.clone()).or_insert(l);
}
let mut unreachable = Vec::new();
for kb in boxes {
if !kb.reachable {
unreachable.push(kb.label.clone());
continue;
}
for l in read_letters(&kb.path)? {
by_id.entry(l.id.clone()).or_insert(l);
}
}
let union: Vec<Letter> = by_id.into_values().collect();
let viewed = view_letters(&union, foreign_tools);
let total = viewed.len();
let open = viewed
.iter()
.filter(|l| l.state == "wet_ink" || l.state == "in_flight")
.count();
let misdelivery = viewed
.iter()
.filter(|l| l.class == CLASS_MEMORY_MISDELIVERY)
.count();
unreachable.sort();
unreachable.dedup();
Ok(SweepResult {
letters: viewed,
total,
open,
unreachable,
misdelivery,
})
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BoxCounts {
pub wet_ink: usize,
pub in_flight: usize,
pub fired_clay: usize,
pub external: usize,
}
impl BoxCounts {
pub fn open(&self) -> usize {
self.wet_ink + self.in_flight
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MailboxView {
pub letters: Vec<SweptLetter>,
pub counts: BoxCounts,
}
pub fn read_box(box_path: &Path, foreign_tools: &BTreeSet<String>) -> M1ndResult<MailboxView> {
let letters = read_letters(box_path)?;
let viewed = view_letters(&letters, foreign_tools);
let mut counts = BoxCounts::default();
for l in &viewed {
match l.state.as_str() {
"wet_ink" => counts.wet_ink += 1,
"in_flight" => counts.in_flight += 1,
"fired_clay" => counts.fired_clay += 1,
"external" => counts.external += 1,
_ => {}
}
}
Ok(MailboxView {
letters: viewed,
counts,
})
}
pub fn mailbox_open_count(box_path: &Path, foreign_tools: &BTreeSet<String>) -> M1ndResult<usize> {
Ok(read_box(box_path, foreign_tools)?.counts.open())
}
pub fn project_basename(root: &str) -> String {
let no_paren = strip_parenthetical(root);
let expanded = expand_home(&no_paren);
let expanded = expanded.trim().trim_end_matches('/');
Path::new(expanded)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(expanded)
.to_string()
}
pub fn boxes_from_roots(
project_roots: &[String],
worktree_base: &str,
) -> (BTreeMap<String, PathBuf>, Vec<KnownBox>) {
let mut dirs_by_name: BTreeMap<String, BTreeSet<PathBuf>> = BTreeMap::new();
let mut boxes: Vec<KnownBox> = Vec::new();
let mut seen_box: BTreeSet<(String, PathBuf)> = BTreeSet::new();
for root in project_roots {
let dir = PathBuf::from(root.trim());
let name = normalize_repo(root, worktree_base);
if name.is_empty() {
continue;
}
let reachable = dir.is_dir();
if reachable {
dirs_by_name
.entry(name.clone())
.or_default()
.insert(dir.clone());
}
if seen_box.insert((name.clone(), dir.clone())) {
boxes.push(KnownBox {
label: name,
path: box_path_for_repo(&dir),
reachable,
});
}
}
let known: BTreeMap<String, PathBuf> = dirs_by_name
.into_iter()
.filter_map(
|(name, dirs)| match dirs.into_iter().collect::<Vec<_>>().as_slice() {
[only] => Some((name, only.clone())),
_ => None,
},
)
.collect();
(known, boxes)
}
pub fn spool_path_for_runtime(owner_runtime_root: &Path) -> PathBuf {
let mut cur = owner_runtime_root;
while let Some(name) = cur.file_name().and_then(|n| n.to_str()) {
if name == ".m1nd" {
return cur.join("field-reports.jsonl");
}
match cur.parent() {
Some(p) => cur = p,
None => break,
}
}
if let Some(home) = home_dir() {
return home.join(".m1nd").join("field-reports.jsonl");
}
owner_runtime_root.join("field-reports.jsonl")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorMailbox {
pub memory_misdelivery: usize,
pub confusion_rate_7d: usize,
pub spool_letters: usize,
pub pending_distribution: Vec<String>,
pub spool_path: String,
}
fn within_days(ts: &str, now_ms: u64, days: u64) -> bool {
let head: String = ts.chars().take(10).collect();
let parts: Vec<&str> = head.split('-').collect();
if parts.len() != 3 {
return false;
}
let (Ok(y), Ok(m), Ok(d)) = (
parts[0].parse::<i64>(),
parts[1].parse::<i64>(),
parts[2].parse::<i64>(),
) else {
return false;
};
let letter_days = days_from_civil(y, m, d);
let now_days = (now_ms / 86_400_000) as i64;
now_days.saturating_sub(letter_days) <= days as i64 && letter_days <= now_days + 1
}
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
pub fn doctor_mailbox(
owner_runtime_root: &Path,
worktree_base: &str,
known_repos: &BTreeMap<String, PathBuf>,
now_ms: u64,
) -> M1ndResult<DoctorMailbox> {
let spool = spool_path_for_runtime(owner_runtime_root);
let letters = read_letters(&spool)?;
let spool_letters = letters.len();
let mut memory_misdelivery = 0usize;
let mut confusion_rate_7d = 0usize;
let mut pending: Vec<String> = Vec::new();
for l in &letters {
if l.class == CLASS_MEMORY_MISDELIVERY {
memory_misdelivery += 1;
if within_days(&l.ts, now_ms, 7) {
confusion_rate_7d += 1;
}
}
if let BoxTarget::Pending(name) =
resolve_box(l, owner_runtime_root, worktree_base, known_repos)
{
pending.push(name);
}
}
pending.sort();
pending.dedup();
Ok(DoctorMailbox {
memory_misdelivery,
confusion_rate_7d,
spool_letters,
pending_distribution: pending,
spool_path: spool.to_string_lossy().to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
struct Scratch {
dir: PathBuf,
}
impl Scratch {
fn new(tag: &str) -> Self {
let dir = std::env::temp_dir().join(format!(
"m1nd-mailbox-test-{tag}-{}-{}",
std::process::id(),
now_nanos()
));
std::fs::create_dir_all(&dir).expect("mk scratch");
Self { dir }
}
fn path(&self, rel: &str) -> PathBuf {
self.dir.join(rel)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn now_nanos() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
}
fn line(fields: serde_json::Value) -> String {
fields.to_string()
}
fn write_spool(path: &Path, lines: &[String]) {
std::fs::write(path, lines.join("\n") + "\n").expect("write spool");
}
fn mk_repo(scratch: &Scratch, name: &str) -> PathBuf {
let dir = scratch.path(name);
std::fs::create_dir_all(&dir).expect("mk repo dir");
dir
}
#[test]
fn normalize_collapses_worktree_and_annotation_variants() {
assert_eq!(normalize_repo("repo-a", "repo-a"), "repo-a");
assert_eq!(normalize_repo("repo-a-soul", "repo-a"), "repo-a");
assert_eq!(
normalize_repo("repo-a (worktree repo-a-x)", "repo-a"),
"repo-a"
);
assert_eq!(
normalize_repo("repo-a (org-critique, read-only seat)", "repo-a"),
"repo-a"
);
assert_eq!(normalize_repo("repo-b", "repo-a"), "repo-b");
assert_eq!(normalize_repo("/path/to/repo-b", "repo-a"), "repo-b");
}
#[test]
fn expand_home_and_strip_parenthetical() {
let home_path = PathBuf::from("/path/to/home");
let home = Some(home_path.clone());
let expected = home_path.join("repo-b").to_string_lossy().to_string();
assert_eq!(expand_home_with("~/repo-b", home.clone()), expected);
assert_eq!(
expand_home_with("~", home.clone()),
home_path.to_string_lossy()
);
assert_eq!(expand_home_with("repo-b", home), "repo-b");
assert_eq!(strip_parenthetical("repo-a (worktree x)"), "repo-a");
assert_eq!(strip_parenthetical("repo-a"), "repo-a");
}
#[test]
fn distribution_files_each_letter_into_exactly_one_box() {
let s = Scratch::new("distribute");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_a = mk_repo(&s, "repo-a");
let repo_b = mk_repo(&s, "repo-b");
let mut known = BTreeMap::new();
known.insert("repo-a".to_string(), repo_a.clone());
known.insert("repo-b".to_string(), repo_b.clone());
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[
line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"w1"}),
),
line(
serde_json::json!({"ts":"t2","agent":"a","repo":"repo-a-soul","tool":"x","class":"friction","what":"w2"}),
),
line(
serde_json::json!({"ts":"t3","agent":"a","repo":"repo-b","tool":"x","class":"win","what":"w3"}),
),
line(
serde_json::json!({"ts":"t4","agent":"a","repo":"all","tool":"x","class":"friction","what":"w4"}),
),
line(
serde_json::json!({"ts":"t5","agent":"a","repo": runtime.to_string_lossy(),"tool":"x","class":"friction","what":"w5"}),
),
],
);
let receipt = distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(receipt.spool_total, 5);
assert_eq!(receipt.appended, 5, "first run appends all");
assert_eq!(receipt.to_project, 3, "repo-a, repo-a-soul→repo-a, repo-b");
assert_eq!(receipt.to_medulla, 2, "all + owner-runtime");
assert!(receipt.pending.is_empty());
let box_a = read_letters(&box_path_for_repo(&repo_a)).unwrap();
assert_eq!(box_a.len(), 2);
let box_b = read_letters(&box_path_for_repo(&repo_b)).unwrap();
assert_eq!(box_b.len(), 1);
let box_med = read_letters(&medulla_box_path(&runtime)).unwrap();
assert_eq!(box_med.len(), 2);
}
#[test]
fn med_inv_10_no_repo_letter_ever_in_medulla_box() {
let s = Scratch::new("medinv10");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_a = mk_repo(&s, "repo-a");
let mut known = BTreeMap::new();
known.insert("repo-a".to_string(), repo_a.clone());
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[
line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"repo letter"}),
),
line(
serde_json::json!({"ts":"t2","agent":"a","repo":"all","tool":"x","class":"friction","what":"projectless"}),
),
line(
serde_json::json!({"ts":"t3","agent":"a","repo": repo_a.to_string_lossy(),"tool":"x","class":"win","what":"abs path"}),
),
],
);
distribute(&spool, &runtime, "repo-a", &known).unwrap();
let box_med = read_letters(&medulla_box_path(&runtime)).unwrap();
assert_eq!(box_med.len(), 1);
assert_eq!(box_med[0].what, "projectless");
for l in &box_med {
let named_project =
!l.repo.trim().is_empty() && l.repo != "all" && !is_projectless(&l.repo, &runtime);
assert!(
!named_project,
"MED-INV-10 breached: {} in medulla box",
l.repo
);
}
}
#[test]
fn distribution_is_idempotent() {
let s = Scratch::new("idempotent");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_a = mk_repo(&s, "repo-a");
let mut known = BTreeMap::new();
known.insert("repo-a".to_string(), repo_a.clone());
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[
line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"w1"}),
),
line(
serde_json::json!({"ts":"t2","agent":"a","repo":"all","tool":"x","class":"friction","what":"w2"}),
),
],
);
let first = distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(first.appended, 2);
let box_len_1 = read_letters(&box_path_for_repo(&repo_a)).unwrap().len();
let second = distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(second.appended, 0, "re-run appends NOTHING (idempotent)");
let box_len_2 = read_letters(&box_path_for_repo(&repo_a)).unwrap().len();
assert_eq!(box_len_1, box_len_2, "box unchanged on re-run");
}
#[test]
fn git_traveled_letter_survives_distribution_un_clobbered() {
let s = Scratch::new("gitinlet");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_a = mk_repo(&s, "repo-a");
let mut known = BTreeMap::new();
known.insert("repo-a".to_string(), repo_a.clone());
std::fs::create_dir_all(repo_a.join(".m1nd")).unwrap();
let travelled = line(
serde_json::json!({"ts":"tG","agent":"other-machine","repo":"repo-a","tool":"x","class":"win","what":"from another machine"}),
);
std::fs::write(box_path_for_repo(&repo_a), travelled.clone() + "\n").unwrap();
let travelled_id = letter_id(&travelled);
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"local letter"}),
)],
);
distribute(&spool, &runtime, "repo-a", &known).unwrap();
let box_a = read_letters(&box_path_for_repo(&repo_a)).unwrap();
assert_eq!(box_a.len(), 2);
assert!(
box_a.iter().any(|l| l.id == travelled_id),
"git-traveled letter preserved"
);
let again = distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(again.appended, 0);
assert_eq!(read_letters(&box_path_for_repo(&repo_a)).unwrap().len(), 2);
}
#[test]
fn absent_repo_stays_pending_then_files_when_dir_exists() {
let s = Scratch::new("pending");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let known: BTreeMap<String, PathBuf> = BTreeMap::new();
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-c","tool":"x","class":"bug","what":"waiting"}),
)],
);
let first = distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(first.to_project, 0);
assert_eq!(first.to_medulla, 0, "NEVER re-routed to medulla");
assert_eq!(first.pending, vec!["repo-c".to_string()]);
assert!(read_letters(&medulla_box_path(&runtime))
.unwrap()
.is_empty());
let repo_c = mk_repo(&s, "repo-c");
let mut known2 = BTreeMap::new();
known2.insert("repo-c".to_string(), repo_c.clone());
let second = distribute(&spool, &runtime, "repo-a", &known2).unwrap();
assert_eq!(second.to_project, 1);
assert!(second.pending.is_empty());
assert_eq!(read_letters(&box_path_for_repo(&repo_c)).unwrap().len(), 1);
}
#[test]
fn box_birth_writes_consent_deferred_gitignore() {
let s = Scratch::new("birth");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_a = mk_repo(&s, "repo-a");
let mut known = BTreeMap::new();
known.insert("repo-a".to_string(), repo_a.clone());
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"w"}),
)],
);
distribute(&spool, &runtime, "repo-a", &known).unwrap();
let gitignore = repo_a.join(".m1nd/.gitignore");
assert!(gitignore.exists(), "birth writes .gitignore");
let body = std::fs::read_to_string(&gitignore).unwrap();
assert!(
body.contains("inbox.jsonl"),
"ignore covers inbox.jsonl until m1nd init"
);
std::fs::write(&gitignore, "# committed by m1nd init\n").unwrap();
distribute(&spool, &runtime, "repo-a", &known).unwrap();
assert_eq!(
std::fs::read_to_string(&gitignore).unwrap(),
"# committed by m1nd init\n",
"consented .gitignore preserved"
);
}
#[test]
fn fate_derivation_wet_ink_fired_clay_external() {
let l1 = line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"seek","class":"bug","what":"the bug"}),
);
let l1_id = letter_id(&l1);
let l2 = line(
serde_json::json!({"ts":"t2","agent":"a","repo":"repo-a","tool":"context7","class":"friction","what":"external tool friction"}),
);
let receipt = line(
serde_json::json!({"ts":"t3","agent":"triage","repo":"repo-a","tool":"triage","class":"triage","what":"fixed","answers":[l1_id]}),
);
let letters: Vec<Letter> = [l1, l2, receipt]
.iter()
.filter_map(|s| parse_letter(s))
.collect();
let mut foreign = BTreeSet::new();
foreign.insert("context7".to_string());
let viewed = view_letters(&letters, &foreign);
let f = |id: &str| viewed.iter().find(|l| l.id == id).map(|l| l.state.clone());
assert_eq!(
f(&l1_id),
Some("fired_clay".to_string()),
"L1 answered → fired_clay"
);
let l2_view = viewed.iter().find(|l| l.tool == "context7").unwrap();
assert_eq!(l2_view.state, "external", "context7 letter → external");
let receipt_view = viewed.iter().find(|l| l.class == "triage").unwrap();
assert_eq!(
receipt_view.state, "wet_ink",
"unanswered receipt itself → wet_ink"
);
let l1_view = viewed.iter().find(|l| l.id == l1_id).unwrap();
assert_eq!(l1_view.answered_by, vec![receipt_view.id.clone()]);
}
#[test]
fn non_receipt_reference_derives_in_flight_not_fired_clay() {
let l1 = line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"seek","class":"bug","what":"the original bug"}),
);
let l1_id = letter_id(&l1);
let pickup = line(
serde_json::json!({"ts":"t2","agent":"b","repo":"repo-a","tool":"seek","class":"bug","what":"looking into it","answers":[l1_id]}),
);
let letters: Vec<Letter> = [l1.clone(), pickup]
.iter()
.filter_map(|s| parse_letter(s))
.collect();
let viewed = view_letters(&letters, &BTreeSet::new());
let l1_view = viewed.iter().find(|l| l.id == l1_id).unwrap();
assert_eq!(
l1_view.state, "in_flight",
"a non-receipt pickup must make the referenced letter in_flight, got {}",
l1_view.state
);
let in_flight_count = viewed.iter().filter(|l| l.state == "in_flight").count();
assert_eq!(
in_flight_count, 1,
"exactly one picked-up letter is in_flight"
);
let open = viewed
.iter()
.filter(|l| l.state == "wet_ink" || l.state == "in_flight")
.count();
assert!(open >= 1, "in_flight must count toward open, got {open}");
}
#[test]
fn receipt_close_wins_over_a_prior_pickup() {
let l1 = line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"seek","class":"bug","what":"bug"}),
);
let l1_id = letter_id(&l1);
let pickup = line(
serde_json::json!({"ts":"t2","agent":"b","repo":"repo-a","tool":"seek","class":"bug","what":"on it","answers":[l1_id]}),
);
let receipt = line(
serde_json::json!({"ts":"t3","agent":"triage","repo":"repo-a","tool":"triage","class":"triage","what":"fixed","answers":[l1_id]}),
);
let letters: Vec<Letter> = [l1, pickup, receipt]
.iter()
.filter_map(|s| parse_letter(s))
.collect();
let viewed = view_letters(&letters, &BTreeSet::new());
let l1_view = viewed.iter().find(|l| l.id == l1_id).unwrap();
assert_eq!(
l1_view.state, "fired_clay",
"a receipt close wins over a prior pickup, got {}",
l1_view.state
);
}
#[test]
fn external_letters_visible_but_never_counted() {
let s = Scratch::new("external");
let box_path = s.path("box.jsonl");
std::fs::write(
&box_path,
[
line(serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"seek","class":"bug","what":"open bug"})),
line(serde_json::json!({"ts":"t2","agent":"a","repo":"repo-a","tool":"context7","class":"friction","what":"external"})),
]
.join("\n")
+ "\n",
)
.unwrap();
let mut foreign = BTreeSet::new();
foreign.insert("context7".to_string());
let view = read_box(&box_path, &foreign).unwrap();
assert_eq!(view.counts.external, 1, "external is visible");
assert_eq!(view.counts.wet_ink, 1);
assert_eq!(view.counts.open(), 1, "external NEVER inflates abertas");
}
#[test]
fn sweep_unions_spool_and_boxes_each_letter_once() {
let s = Scratch::new("sweep");
let repo_a = mk_repo(&s, "repo-a");
std::fs::create_dir_all(repo_a.join(".m1nd")).unwrap();
let shared = line(
serde_json::json!({"ts":"t1","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"shared"}),
);
let box_only = line(
serde_json::json!({"ts":"t2","agent":"o","repo":"repo-a","tool":"x","class":"win","what":"box only"}),
);
let spool_only = line(
serde_json::json!({"ts":"t3","agent":"a","repo":"all","tool":"x","class":"friction","what":"spool only"}),
);
let spool = s.path("spool.jsonl");
write_spool(&spool, &[shared.clone(), spool_only]);
std::fs::write(
box_path_for_repo(&repo_a),
[shared, box_only].join("\n") + "\n",
)
.unwrap();
let boxes = vec![
KnownBox {
label: "repo-a".into(),
path: box_path_for_repo(&repo_a),
reachable: true,
},
KnownBox {
label: "repo-absent".into(),
path: s.path("nope/.m1nd/inbox.jsonl"),
reachable: false,
},
];
let sweep = inbox_sweep(&spool, &boxes, &BTreeSet::new()).unwrap();
assert_eq!(sweep.total, 3, "3 distinct letters, shared counted ONCE");
assert_eq!(
sweep.unreachable,
vec!["repo-absent".to_string()],
"unreachable box named"
);
}
#[test]
fn misdelivery_class_and_kinds_are_the_closed_vocabulary() {
assert_eq!(CLASS_MEMORY_MISDELIVERY, "memory_misdelivery");
assert_eq!(MISDELIVERY_KINDS.len(), 5);
for k in [
"leak",
"false_absence",
"wrong_store_write",
"misattribution",
"vanished",
] {
assert!(
MISDELIVERY_KINDS.contains(&k),
"kind {k} in the closed vocabulary"
);
}
}
#[test]
fn sweep_counts_memory_misdelivery_letters() {
let s = Scratch::new("misdelivery-count");
let spool = s.path("spool.jsonl");
write_spool(
&spool,
&[
line(
serde_json::json!({"ts":"t1","agent":"probe","repo":"repo-a","tool":"seek","class":"memory_misdelivery","kind":"leak","what":"brain Y claim surfaced in brain X (synthetic)"}),
),
line(
serde_json::json!({"ts":"t2","agent":"a","repo":"repo-a","tool":"north","class":"honesty","what":"unrelated"}),
),
],
);
let sweep = inbox_sweep(&spool, &[], &BTreeSet::new()).unwrap();
assert_eq!(
sweep.misdelivery, 1,
"the memory_misdelivery letter is counted"
);
}
#[test]
fn letter_id_is_stable_sha256_prefix() {
let raw = r#"{"ts":"t","agent":"a","repo":"repo-a","tool":"x","class":"bug","what":"w"}"#;
let id1 = letter_id(raw);
let id2 = letter_id(&format!("{raw}\n")); assert_eq!(id1, id2, "trailing newline does not change the id");
assert_eq!(id1.len(), 12, "id is the 12-hex-char sha256 prefix");
assert!(id1.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn bare_name_resolves_against_unique_roster_basename() {
let s = Scratch::new("bare-name");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let repo_alpha = mk_repo(&s, "place").join("repo-alpha");
std::fs::create_dir_all(&repo_alpha).unwrap();
let roots = vec![repo_alpha.to_string_lossy().to_string()];
let (known, _boxes) = boxes_from_roots(&roots, "some-bound-project");
let bare: Letter =
serde_json::from_str(r#"{"repo":"repo-alpha","tool":"x","class":"bug"}"#).unwrap();
assert_eq!(
resolve_box(&bare, &runtime, "some-bound-project", &known),
BoxTarget::Project(repo_alpha.clone()),
"a bare name resolves to the roster brain whose basename matches uniquely"
);
let variant: Letter = serde_json::from_str(
r#"{"repo":"repo-alpha (worktree repo-alpha-x)","tool":"x","class":"bug"}"#,
)
.unwrap();
assert_eq!(
resolve_box(&variant, &runtime, "some-bound-project", &known),
BoxTarget::Project(repo_alpha),
"a parenthetical worktree variant collapses to the same bare name and resolves"
);
}
#[test]
fn ambiguous_basename_abstains_to_pending_never_guesses() {
let s = Scratch::new("ambiguous-basename");
let runtime = s.path("runtime");
std::fs::create_dir_all(&runtime).unwrap();
let a1 = mk_repo(&s, "place1").join("repo-alpha");
let a2 = mk_repo(&s, "place2").join("repo-alpha");
std::fs::create_dir_all(&a1).unwrap();
std::fs::create_dir_all(&a2).unwrap();
let roots = vec![
a1.to_string_lossy().to_string(),
a2.to_string_lossy().to_string(),
];
let (known, boxes) = boxes_from_roots(&roots, "bound");
assert!(
!known.contains_key("repo-alpha"),
"an ambiguous basename must not resolve to a single root: {known:?}"
);
let bare: Letter =
serde_json::from_str(r#"{"repo":"repo-alpha","tool":"x","class":"bug"}"#).unwrap();
assert_eq!(
resolve_box(&bare, &runtime, "bound", &known),
BoxTarget::Pending("repo-alpha".to_string()),
"an ambiguous bare name abstains to Pending (0-or->1 → never chute)"
);
assert!(
boxes.iter().filter(|b| b.label == "repo-alpha").count() >= 1,
"the ambiguous roots remain visible as named boxes"
);
}
}