Skip to main content

mana_core/
resolve.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{anyhow, Context, Result};
4
5use crate::handle::normalize_handle;
6use crate::index::Index;
7use crate::unit::Unit;
8
9/// A unit resolved by ID or by project-scoped human handle.
10#[derive(Debug)]
11pub struct ResolvedUnit {
12    pub unit: Unit,
13    pub path: PathBuf,
14}
15
16/// Resolve an active unit reference as an ID first, then as a unique handle.
17///
18/// Handles are intentionally project-scoped aliases. Ambiguous handles return a
19/// clear error listing matching IDs so callers can ask for a more precise ref.
20pub fn resolve_unit(mana_dir: &Path, reference: &str) -> Result<ResolvedUnit> {
21    let path = match crate::discovery::find_unit_file(mana_dir, reference) {
22        Ok(path) => path,
23        Err(id_error) => match resolve_unit_path_by_handle(mana_dir, reference) {
24            Ok(path) => path,
25            Err(handle_error) => {
26                let handle_message = handle_error.to_string();
27                if handle_message.contains("ambiguous") {
28                    return Err(handle_error);
29                }
30                return Err(handle_error).with_context(|| {
31                    format!("Unit not found by ID or handle: {reference} ({id_error})")
32                });
33            }
34        },
35    };
36
37    let unit = Unit::from_file(&path)
38        .with_context(|| format!("Failed to load unit: {}", path.display()))?;
39    Ok(ResolvedUnit { unit, path })
40}
41
42/// Resolve an active unit path by unique handle.
43pub fn resolve_unit_path_by_handle(mana_dir: &Path, handle: &str) -> Result<PathBuf> {
44    let query = normalize_handle(handle);
45    if query.is_empty() {
46        return Err(anyhow!("Handle cannot be empty"));
47    }
48
49    let index = Index::load_or_rebuild(mana_dir)?;
50    let matches: Vec<_> = index
51        .units
52        .iter()
53        .filter(|entry| {
54            let normalized = entry.handle.as_deref().map(normalize_handle);
55            normalized.as_deref() == Some(query.as_str())
56        })
57        .collect();
58
59    match matches.as_slice() {
60        [] => Err(anyhow!("No unit with handle '{handle}'")),
61        [entry] => crate::discovery::find_unit_file(mana_dir, &entry.id),
62        many => {
63            let choices = many
64                .iter()
65                .map(|entry| format!("  {} — {}", entry.id, entry.title))
66                .collect::<Vec<_>>()
67                .join("\n");
68            Err(anyhow!(
69                "Handle '{handle}' is ambiguous; use a unit ID instead:\n{choices}"
70            ))
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::config::Config;
79    use crate::ops::create::{create, CreateParams};
80    use crate::unit::Unit;
81    use std::fs;
82    use tempfile::TempDir;
83
84    fn setup() -> (TempDir, PathBuf) {
85        let dir = TempDir::new().unwrap();
86        let mana_dir = dir.path().join(".mana");
87        fs::create_dir(&mana_dir).unwrap();
88        Config::default().save(&mana_dir).unwrap();
89        (dir, mana_dir)
90    }
91
92    #[test]
93    fn resolve_unit_finds_unique_handle() {
94        let (_dir, mana_dir) = setup();
95        create(
96            &mana_dir,
97            CreateParams {
98                title: "Implement SQLite-derived index for mana agent context assembly".into(),
99                ..Default::default()
100            },
101        )
102        .unwrap();
103
104        let resolved = resolve_unit(&mana_dir, "sqlite derived index").unwrap();
105        assert_eq!(resolved.unit.id, "1");
106        assert_eq!(
107            resolved.unit.handle.as_deref(),
108            Some("sqlite derived index")
109        );
110    }
111
112    #[test]
113    fn resolve_unit_reports_ambiguous_handle() {
114        let (_dir, mana_dir) = setup();
115        let mut first = Unit::new("1", "First title");
116        first.handle = Some("shared handle".to_string());
117        first.to_file(mana_dir.join("1-first-title.md")).unwrap();
118        let mut second = Unit::new("2", "Second title");
119        second.handle = Some("shared handle".to_string());
120        second.to_file(mana_dir.join("2-second-title.md")).unwrap();
121        Index::build(&mana_dir).unwrap().save(&mana_dir).unwrap();
122
123        let error = resolve_unit(&mana_dir, "shared handle")
124            .unwrap_err()
125            .to_string();
126        assert!(error.contains("ambiguous"));
127        assert!(error.contains("1 — First title"));
128        assert!(error.contains("2 — Second title"));
129    }
130}