use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use crate::handle::normalize_handle;
use crate::index::Index;
use crate::unit::Unit;
#[derive(Debug)]
pub struct ResolvedUnit {
pub unit: Unit,
pub path: PathBuf,
}
pub fn resolve_unit(mana_dir: &Path, reference: &str) -> Result<ResolvedUnit> {
let path = match crate::discovery::find_unit_file(mana_dir, reference) {
Ok(path) => path,
Err(id_error) => match resolve_unit_path_by_handle(mana_dir, reference) {
Ok(path) => path,
Err(handle_error) => {
let handle_message = handle_error.to_string();
if handle_message.contains("ambiguous") {
return Err(handle_error);
}
return Err(handle_error).with_context(|| {
format!("Unit not found by ID or handle: {reference} ({id_error})")
});
}
},
};
let unit = Unit::from_file(&path)
.with_context(|| format!("Failed to load unit: {}", path.display()))?;
Ok(ResolvedUnit { unit, path })
}
pub fn resolve_unit_path_by_handle(mana_dir: &Path, handle: &str) -> Result<PathBuf> {
let query = normalize_handle(handle);
if query.is_empty() {
return Err(anyhow!("Handle cannot be empty"));
}
let index = Index::load_or_rebuild(mana_dir)?;
let matches: Vec<_> = index
.units
.iter()
.filter(|entry| {
let normalized = entry.handle.as_deref().map(normalize_handle);
normalized.as_deref() == Some(query.as_str())
})
.collect();
match matches.as_slice() {
[] => Err(anyhow!("No unit with handle '{handle}'")),
[entry] => crate::discovery::find_unit_file(mana_dir, &entry.id),
many => {
let choices = many
.iter()
.map(|entry| format!(" {} — {}", entry.id, entry.title))
.collect::<Vec<_>>()
.join("\n");
Err(anyhow!(
"Handle '{handle}' is ambiguous; use a unit ID instead:\n{choices}"
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::ops::create::{create, CreateParams};
use crate::unit::Unit;
use std::fs;
use tempfile::TempDir;
fn setup() -> (TempDir, PathBuf) {
let dir = TempDir::new().unwrap();
let mana_dir = dir.path().join(".mana");
fs::create_dir(&mana_dir).unwrap();
Config::default().save(&mana_dir).unwrap();
(dir, mana_dir)
}
#[test]
fn resolve_unit_finds_unique_handle() {
let (_dir, mana_dir) = setup();
create(
&mana_dir,
CreateParams {
title: "Implement SQLite-derived index for mana agent context assembly".into(),
..Default::default()
},
)
.unwrap();
let resolved = resolve_unit(&mana_dir, "sqlite derived index").unwrap();
assert_eq!(resolved.unit.id, "1");
assert_eq!(
resolved.unit.handle.as_deref(),
Some("sqlite derived index")
);
}
#[test]
fn resolve_unit_reports_ambiguous_handle() {
let (_dir, mana_dir) = setup();
let mut first = Unit::new("1", "First title");
first.handle = Some("shared handle".to_string());
first.to_file(mana_dir.join("1-first-title.md")).unwrap();
let mut second = Unit::new("2", "Second title");
second.handle = Some("shared handle".to_string());
second.to_file(mana_dir.join("2-second-title.md")).unwrap();
Index::build(&mana_dir).unwrap().save(&mana_dir).unwrap();
let error = resolve_unit(&mana_dir, "shared handle")
.unwrap_err()
.to_string();
assert!(error.contains("ambiguous"));
assert!(error.contains("1 — First title"));
assert!(error.contains("2 — Second title"));
}
}