Skip to main content

mana_core/api/
mod.rs

1//! # mana-core Public API
2//!
3//! Programmatic access to all mana unit operations. Use this module when embedding
4//! mana in another application — a GUI, MCP server, orchestration daemon, or custom
5//! tooling.
6//!
7//! The API is organized into layers:
8//!
9//! - **Types** — Core data structures re-exported from internal modules
10//! - **Discovery** — Find `.mana/` directories and unit files
11//! - **Query** — Read-only operations (list, get, tree, status, graph)
12//! - **Mutations** — Write operations (create, update, close, delete)
13//! - **Orchestration** — Agent dispatch, context assembly, and verification
14//! - **Facts** — Verified project knowledge with TTL
15//!
16//! ## Quick Start
17//!
18//! ```rust,no_run
19//! use mana_core::api::*;
20//! use std::path::Path;
21//!
22//! // Find the .mana/ directory
23//! let mana_dir = find_mana_dir(Path::new(".")).unwrap();
24//!
25//! // Load the index (cached, rebuilds if stale)
26//! let index = load_index(&mana_dir).unwrap();
27//!
28//! // Get a specific unit
29//! let unit = get_unit(&mana_dir, "1").unwrap();
30//! println!("{}: {}", unit.id, unit.title);
31//! ```
32//!
33//! ## Design Principles
34//!
35//! - **No I/O side effects** — Library functions never print to stdout/stderr.
36//!   All output is returned as structured data.
37//! - **Structured params and results** — Each mutation takes a `Params` struct
38//!   and returns a typed result. No raw argument passing.
39//! - **`&Path` as entry point** — Every function takes `mana_dir: &Path`.
40//!   No global state, no singletons, no `Arc` required.
41//! - **Serializable** — All types derive `Serialize`/`Deserialize` for easy
42//!   IPC (Tauri, JSON-RPC, MCP).
43//! - **Thread-safe** — No interior mutability, no shared global state.
44
45use std::collections::HashMap;
46use std::path::Path;
47
48use anyhow::Result;
49
50use crate::error::{ManaError, ManaResult};
51
52// ---------------------------------------------------------------------------
53// Re-exported core types
54// ---------------------------------------------------------------------------
55
56/// Core unit type representing a single work item.
57pub use crate::unit::{
58    AttemptOutcome, AttemptRecord, OnCloseAction, OnFailAction, RunRecord, RunResult, Status, Unit,
59};
60
61/// Index types for working with the unit cache.
62pub use crate::index::{Index, IndexEntry};
63
64/// Project configuration.
65pub use crate::config::Config;
66
67/// Typed error and result types.
68pub use crate::error::{self, ManaError as Error};
69
70// ---------------------------------------------------------------------------
71// Discovery re-exports
72// ---------------------------------------------------------------------------
73
74/// Find the `.mana/` directory by walking up from `path`.
75///
76/// Searches the given path and all parent directories until a `.mana/`
77/// directory is found.
78///
79/// # Errors
80/// - Returns an error if no `.mana/` directory is found in the hierarchy
81/// - [`ManaError::IoError`] — filesystem failure
82///
83/// # Example
84/// ```rust,no_run
85/// use mana_core::api::find_mana_dir;
86/// use std::path::Path;
87///
88/// let mana_dir = find_mana_dir(Path::new("/some/project/subdir")).unwrap();
89/// ```
90pub use crate::discovery::find_mana_dir;
91
92/// Find the file path for a unit by ID.
93///
94/// Searches the `.mana/` directory for an active (non-archived) unit with
95/// the given ID.
96///
97/// # Errors
98/// - [`ManaError::UnitNotFound`] — no unit file for the given ID
99/// - [`ManaError::InvalidId`] — ID is empty or contains invalid characters
100/// - [`ManaError::IoError`] — filesystem failure
101pub use crate::discovery::find_unit_file;
102
103/// Find the file path for an archived unit by ID.
104///
105/// Searches the `.mana/archive/` tree for a unit that was previously closed.
106///
107/// # Errors
108/// - [`ManaError::UnitNotFound`] — unit ID not found in archive
109/// - [`ManaError::InvalidId`] — ID is empty or contains invalid characters
110pub use crate::discovery::find_archived_unit;
111
112// ---------------------------------------------------------------------------
113// Graph types (new: not in ops modules)
114// ---------------------------------------------------------------------------
115
116/// A node in the unit hierarchy tree, used by [`get_tree`].
117#[derive(Debug, Clone)]
118pub struct TreeNode {
119    /// Unit ID.
120    pub id: String,
121    /// Unit title.
122    pub title: String,
123    /// Unit status.
124    pub status: Status,
125    /// Priority (0 = P0/highest, 4 = P4/lowest).
126    pub priority: u8,
127    /// Whether the unit has a verify command.
128    pub has_verify: bool,
129    /// Child nodes (units whose `parent` field is this unit's ID).
130    pub children: Vec<TreeNode>,
131}
132
133/// A full dependency graph representation.
134///
135/// The graph is a directed acyclic graph where each edge `a -> b`
136/// means "unit `a` depends on unit `b`".
137#[derive(Debug, Clone)]
138pub struct DependencyGraph {
139    /// All nodes in the graph, keyed by unit ID.
140    pub nodes: HashMap<String, GraphNode>,
141    /// Adjacency list: unit ID → list of dependency IDs.
142    pub edges: HashMap<String, Vec<String>>,
143}
144
145/// A node in the dependency graph.
146#[derive(Debug, Clone)]
147pub struct GraphNode {
148    /// Unit ID.
149    pub id: String,
150    /// Unit title.
151    pub title: String,
152    /// Unit status.
153    pub status: Status,
154}
155
156// Re-export orchestration and ops types
157pub use crate::ops::{
158    claim, close, context, create, delete, dep, fact, list, plan, show, update, verify,
159};
160
161pub use crate::ops::context::AgentContext;
162pub use crate::ops::fact::{FactParams, FactResult, VerifyFactsResult};
163pub use crate::ops::run::{BlockedUnit, ReadyQueue, ReadyUnit, RunPlan, RunWave};
164pub use crate::ops::stats::StatsResult;
165pub use crate::ops::status::StatusSummary;
166pub use crate::ops::verify::VerifyResult;
167
168// ---------------------------------------------------------------------------
169// Query functions
170// ---------------------------------------------------------------------------
171
172/// Load a unit by ID.
173///
174/// Finds the unit file in the `.mana/` directory and deserializes it.
175/// Works for active (non-archived) units only. For archived units, use
176/// [`get_archived_unit`].
177///
178/// # Errors
179/// - [`ManaError::UnitNotFound`] — no unit file for the given ID
180/// - [`ManaError::InvalidId`] — ID is empty or contains invalid characters
181/// - [`ManaError::ParseError`] — file cannot be deserialized
182/// - [`ManaError::IoError`] — filesystem failure
183///
184/// # Example
185/// ```rust,no_run
186/// use mana_core::api::get_unit;
187/// use std::path::Path;
188///
189/// let mana_dir = Path::new("/project/.mana");
190/// let unit = get_unit(mana_dir, "42").unwrap();
191/// println!("{}: {}", unit.id, unit.title);
192/// ```
193pub fn get_unit(mana_dir: &Path, id: &str) -> ManaResult<Unit> {
194    let path = find_unit_file(mana_dir, id).map_err(|e| {
195        let msg = e.to_string();
196        if msg.contains("Invalid unit ID") || msg.contains("cannot be empty") {
197            ManaError::InvalidId {
198                id: id.to_string(),
199                reason: msg,
200            }
201        } else {
202            ManaError::UnitNotFound { id: id.to_string() }
203        }
204    })?;
205    Unit::from_file(&path).map_err(|e| ManaError::ParseError {
206        path,
207        reason: e.to_string(),
208    })
209}
210
211/// Load a unit from the archive by ID.
212///
213/// Searches the `.mana/archive/` tree for a unit that was previously closed
214/// and archived.
215///
216/// # Errors
217/// - [`ManaError::UnitNotFound`] — unit ID not found in archive
218/// - [`ManaError::InvalidId`] — ID is empty or contains invalid characters
219/// - [`ManaError::ParseError`] — file cannot be deserialized
220/// - [`ManaError::IoError`] — filesystem failure
221///
222/// # Example
223/// ```rust,no_run
224/// use mana_core::api::get_archived_unit;
225/// use std::path::Path;
226///
227/// let mana_dir = Path::new("/project/.mana");
228/// let unit = get_archived_unit(mana_dir, "42").unwrap();
229/// println!("Closed at: {:?}", unit.closed_at);
230/// ```
231pub fn get_archived_unit(mana_dir: &Path, id: &str) -> ManaResult<Unit> {
232    let path = find_archived_unit(mana_dir, id).map_err(|e| {
233        let msg = e.to_string();
234        if msg.contains("Invalid unit ID") || msg.contains("cannot be empty") {
235            ManaError::InvalidId {
236                id: id.to_string(),
237                reason: msg,
238            }
239        } else {
240            ManaError::UnitNotFound { id: id.to_string() }
241        }
242    })?;
243    Unit::from_file(&path).map_err(|e| ManaError::ParseError {
244        path,
245        reason: e.to_string(),
246    })
247}
248
249/// Load the index, rebuilding from unit files if stale.
250///
251/// The index is a YAML cache that's faster than reading every unit file.
252/// It is automatically rebuilt when unit files are newer than the cached index.
253///
254/// # Errors
255/// - [`ManaError::IndexError`] — index cannot be built, loaded, or saved
256/// - [`ManaError::IoError`] — filesystem failure
257///
258/// # Example
259/// ```rust,no_run
260/// use mana_core::api::load_index;
261/// use std::path::Path;
262///
263/// let index = load_index(Path::new("/project/.mana")).unwrap();
264/// println!("{} units", index.units.len());
265/// ```
266pub fn load_index(mana_dir: &Path) -> ManaResult<Index> {
267    Index::load_or_rebuild(mana_dir).map_err(|e| ManaError::IndexError(e.to_string()))
268}
269
270/// List units with optional filters.
271///
272/// Returns index entries (lightweight unit summaries) for all units matching
273/// the given filters. By default, closed units are excluded.
274///
275/// # Errors
276/// - [`ManaError::IndexError`] — index cannot be loaded
277/// - [`ManaError::IoError`] — filesystem failure
278///
279/// # Example
280/// ```rust,no_run
281/// use mana_core::api::list_units;
282/// use mana_core::ops::list::ListParams;
283/// use std::path::Path;
284///
285/// let mana_dir = Path::new("/project/.mana");
286///
287/// // List all open units
288/// let units = list_units(mana_dir, &ListParams::default()).unwrap();
289///
290/// // List units assigned to alice
291/// let alice_units = list_units(mana_dir, &ListParams {
292///     assignee: Some("alice".to_string()),
293///     ..Default::default()
294/// }).unwrap();
295/// ```
296pub fn list_units(mana_dir: &Path, params: &list::ListParams) -> Result<Vec<IndexEntry>> {
297    crate::ops::list::list(mana_dir, params)
298}
299
300/// Build a unit hierarchy tree rooted at the given unit ID.
301///
302/// Returns a [`TreeNode`] with all descendants nested recursively. Only units
303/// in the active index are included (archived units are excluded).
304///
305/// # Errors
306/// - [`ManaError::UnitNotFound`] — no unit with the given ID in the active index
307/// - [`ManaError::IndexError`] — index cannot be loaded
308///
309/// # Example
310/// ```rust,no_run
311/// use mana_core::api::get_tree;
312/// use std::path::Path;
313///
314/// let tree = get_tree(Path::new("/project/.mana"), "1").unwrap();
315/// println!("{}: {} children", tree.id, tree.children.len());
316/// ```
317pub fn get_tree(mana_dir: &Path, root_id: &str) -> Result<TreeNode> {
318    let index = Index::load_or_rebuild(mana_dir)?;
319    build_tree_node(root_id, &index)
320}
321
322fn build_tree_node(id: &str, index: &Index) -> Result<TreeNode> {
323    let entry = index
324        .units
325        .iter()
326        .find(|e| e.id == id)
327        .ok_or_else(|| anyhow::anyhow!("Unit {} not found", id))?;
328
329    let children: Vec<TreeNode> = index
330        .units
331        .iter()
332        .filter(|e| e.parent.as_deref() == Some(id))
333        .map(|child| build_tree_node(&child.id, index))
334        .collect::<Result<Vec<_>>>()?;
335
336    Ok(TreeNode {
337        id: entry.id.clone(),
338        title: entry.title.clone(),
339        status: entry.status,
340        priority: entry.priority,
341        has_verify: entry.has_verify,
342        children,
343    })
344}
345
346/// Get a categorized project status summary.
347///
348/// Returns units grouped into: features, in-progress (claimed), ready to run,
349/// goals (no verify command), and blocked (dependencies not met).
350///
351/// # Errors
352/// - [`ManaError::IndexError`] — index cannot be loaded
353/// - [`ManaError::IoError`] — filesystem failure
354///
355/// # Example
356/// ```rust,no_run
357/// use mana_core::api::get_status;
358/// use std::path::Path;
359///
360/// let summary = get_status(Path::new("/project/.mana")).unwrap();
361/// println!("Ready: {}, Blocked: {}", summary.ready.len(), summary.blocked.len());
362/// ```
363pub fn get_status(mana_dir: &Path) -> Result<StatusSummary> {
364    crate::ops::status::status(mana_dir)
365}
366
367/// Get aggregate project statistics.
368///
369/// Returns counts by status, priority distribution, completion percentage,
370/// and cost/token metrics from unit history (if available).
371///
372/// # Errors
373/// - [`ManaError::IndexError`] — index cannot be loaded
374/// - [`ManaError::IoError`] — filesystem failure
375///
376/// # Example
377/// ```rust,no_run
378/// use mana_core::api::get_stats;
379/// use std::path::Path;
380///
381/// let stats = get_stats(Path::new("/project/.mana")).unwrap();
382/// println!("Completion: {:.1}%", stats.completion_pct);
383/// println!("Total: {}, Open: {}, Closed: {}", stats.total, stats.open, stats.closed);
384/// ```
385pub fn get_stats(mana_dir: &Path) -> Result<StatsResult> {
386    crate::ops::stats::stats(mana_dir)
387}
388
389// ---------------------------------------------------------------------------
390// Graph functions
391// ---------------------------------------------------------------------------
392
393/// Return units with all dependencies satisfied (ready to dispatch).
394///
395/// A unit is "ready" if it has a verify command, its status is `Open`,
396/// and all of its explicit dependency IDs are closed in the active index
397/// or archived.
398///
399/// # Example
400/// ```rust,no_run
401/// use mana_core::api::{load_index, ready_units};
402/// use std::path::Path;
403///
404/// let mana_dir = Path::new("/project/.mana");
405/// let index = load_index(mana_dir).unwrap();
406/// let ready = ready_units(&index);
407/// for entry in ready {
408///     println!("Ready: {} {}", entry.id, entry.title);
409/// }
410/// ```
411pub fn ready_units(index: &Index) -> Vec<&IndexEntry> {
412    let closed_ids: std::collections::HashSet<&str> = index
413        .units
414        .iter()
415        .filter(|e| e.status == Status::Closed)
416        .map(|e| e.id.as_str())
417        .collect();
418
419    index
420        .units
421        .iter()
422        .filter(|e| {
423            e.status == Status::Open
424                && e.has_verify
425                && e.dependencies
426                    .iter()
427                    .all(|dep| closed_ids.contains(dep.as_str()))
428        })
429        .collect()
430}
431
432/// Build a dependency graph from the active index.
433///
434/// Returns a [`DependencyGraph`] with all units as nodes and explicit
435/// dependency relationships as directed edges (`a -> b` = `a` depends on `b`).
436///
437/// # Example
438/// ```rust,no_run
439/// use mana_core::api::{load_index, dependency_graph};
440/// use std::path::Path;
441///
442/// let mana_dir = Path::new("/project/.mana");
443/// let index = load_index(mana_dir).unwrap();
444/// let graph = dependency_graph(&index);
445/// println!("{} nodes, {} with deps", graph.nodes.len(),
446///     graph.edges.values().filter(|deps| !deps.is_empty()).count());
447/// ```
448pub fn dependency_graph(index: &Index) -> DependencyGraph {
449    let nodes: HashMap<String, GraphNode> = index
450        .units
451        .iter()
452        .map(|e| {
453            (
454                e.id.clone(),
455                GraphNode {
456                    id: e.id.clone(),
457                    title: e.title.clone(),
458                    status: e.status,
459                },
460            )
461        })
462        .collect();
463
464    let edges: HashMap<String, Vec<String>> = index
465        .units
466        .iter()
467        .map(|e| (e.id.clone(), e.dependencies.clone()))
468        .collect();
469
470    DependencyGraph { nodes, edges }
471}
472
473/// Topologically sort all units by dependency order.
474///
475/// Returns a list of unit IDs where each unit appears after all its
476/// dependencies. Units with no dependencies appear first.
477///
478/// # Errors
479/// - Returns an error if a cycle is detected in the dependency graph.
480///
481/// # Example
482/// ```rust,no_run
483/// use mana_core::api::{load_index, topological_sort};
484/// use std::path::Path;
485///
486/// let mana_dir = Path::new("/project/.mana");
487/// let index = load_index(mana_dir).unwrap();
488/// let order = topological_sort(&index).unwrap();
489/// println!("Execution order: {:?}", order);
490/// ```
491pub fn topological_sort(index: &Index) -> Result<Vec<String>> {
492    use std::collections::{HashSet, VecDeque};
493
494    // Build in-degree map and adjacency list
495    let mut in_degree: HashMap<String, usize> = HashMap::new();
496    let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
497
498    for entry in &index.units {
499        in_degree.entry(entry.id.clone()).or_insert(0);
500        for dep_id in &entry.dependencies {
501            in_degree.entry(entry.id.clone()).and_modify(|d| *d += 1);
502            dependents
503                .entry(dep_id.clone())
504                .or_default()
505                .push(entry.id.clone());
506        }
507    }
508
509    // Kahn's algorithm
510    let mut queue: VecDeque<String> = in_degree
511        .iter()
512        .filter(|(_, &deg)| deg == 0)
513        .map(|(id, _)| id.clone())
514        .collect();
515
516    let mut result: Vec<String> = Vec::new();
517    let mut visited: HashSet<String> = HashSet::new();
518
519    while let Some(id) = queue.pop_front() {
520        if visited.contains(&id) {
521            continue;
522        }
523        visited.insert(id.clone());
524        result.push(id.clone());
525
526        if let Some(deps_on_me) = dependents.get(&id) {
527            for dependent in deps_on_me {
528                if let Some(deg) = in_degree.get_mut(dependent) {
529                    *deg = deg.saturating_sub(1);
530                    if *deg == 0 && !visited.contains(dependent) {
531                        queue.push_back(dependent.clone());
532                    }
533                }
534            }
535        }
536    }
537
538    if result.len() != index.units.len() {
539        return Err(anyhow::anyhow!(
540            "Cycle detected in dependency graph: {} of {} units could be ordered",
541            result.len(),
542            index.units.len()
543        ));
544    }
545
546    Ok(result)
547}
548
549// ---------------------------------------------------------------------------
550// Additional graph utilities (re-exported from graph module)
551// ---------------------------------------------------------------------------
552
553/// Build a text dependency tree rooted at a unit ID.
554///
555/// Returns a box-drawing string showing which units depend on the given unit.
556///
557/// # Errors
558/// - Returns an error if the unit ID is not found in the index.
559pub use crate::graph::build_dependency_tree;
560
561/// Build a project-wide dependency graph as a text tree.
562///
563/// Shows all units with no parents as roots, with their dependents branching below.
564///
565/// # Errors
566/// - Returns an error only on unexpected failures.
567pub use crate::graph::build_full_graph;
568
569/// Count total verify attempts across all descendants of a unit.
570///
571/// Includes the unit itself and archived descendants. Used by the circuit
572/// breaker to detect runaway retry loops across a subtree.
573///
574/// # Errors
575/// - Returns an error on I/O failures reading the index.
576pub use crate::graph::count_subtree_attempts;
577
578/// Find all dependency cycles in the graph.
579///
580/// Returns a list of cycle paths (each path is a list of unit IDs forming a cycle).
581/// An empty list means the graph is acyclic.
582///
583/// # Errors
584/// - Returns an error only on unexpected graph traversal failures.
585pub use crate::graph::find_all_cycles;
586
587// Also re-export validate_priority for callers who need to validate
588pub use crate::unit::validate_priority;
589
590/// Detect whether adding an edge from `from_id` to `to_id` would create a cycle.
591///
592/// Returns `true` if the proposed edge would introduce a cycle. Use this
593/// before calling [`add_dep`] to pre-validate the addition.
594///
595/// # Errors
596/// - Returns an error only on unexpected graph traversal failures.
597///
598/// # Example
599/// ```rust,no_run
600/// use mana_core::api::{load_index, detect_cycle};
601/// use std::path::Path;
602///
603/// let mana_dir = Path::new("/project/.mana");
604/// let index = load_index(mana_dir).unwrap();
605/// if detect_cycle(&index, "3", "1").unwrap() {
606///     eprintln!("Cannot add that dependency — would create a cycle");
607/// }
608/// ```
609pub fn detect_cycle(index: &Index, from_id: &str, to_id: &str) -> Result<bool> {
610    crate::graph::detect_cycle(index, from_id, to_id)
611}
612
613// ---------------------------------------------------------------------------
614// Mutation functions
615// ---------------------------------------------------------------------------
616
617/// Create a new unit.
618///
619/// Assigns the next sequential ID (or child ID if `params.parent` is set),
620/// writes the unit file, and rebuilds the index.
621///
622/// # Errors
623/// - [`anyhow::Error`] — validation failure, I/O error, or hook rejection
624///
625/// # Example
626/// ```rust,no_run
627/// use mana_core::api::create_unit;
628/// use mana_core::ops::create::CreateParams;
629/// use std::path::Path;
630///
631/// let result = create_unit(Path::new("/project/.mana"), CreateParams {
632///     title: "Fix the login bug".to_string(),
633///     verify: Some("cargo test --test login".to_string()),
634///     ..Default::default()
635/// }).unwrap();
636/// println!("Created unit {}", result.unit.id);
637/// ```
638pub fn create_unit(mana_dir: &Path, params: create::CreateParams) -> Result<create::CreateResult> {
639    create::create(mana_dir, params)
640}
641
642/// Update a unit's fields.
643///
644/// Only fields set to `Some(...)` are updated. Notes are appended with
645/// a timestamp separator rather than replaced.
646///
647/// # Errors
648/// - [`anyhow::Error`] — unit not found, validation failure, or hook rejection
649///
650/// # Example
651/// ```rust,no_run
652/// use mana_core::api::update_unit;
653/// use mana_core::ops::update::UpdateParams;
654/// use std::path::Path;
655///
656/// let result = update_unit(Path::new("/project/.mana"), "1", UpdateParams {
657///     notes: Some("Discovered the root cause: off-by-one in pagination".to_string()),
658///     ..Default::default()
659/// }).unwrap();
660/// ```
661pub fn update_unit(
662    mana_dir: &Path,
663    id: &str,
664    params: update::UpdateParams,
665) -> Result<update::UpdateResult> {
666    update::update(mana_dir, id, params)
667}
668
669/// Close a unit — run verify, archive, and cascade to parents.
670///
671/// The full close lifecycle:
672/// 1. Pre-close hook (if configured)
673/// 2. Run verify command (unless `opts.force` is true)
674/// 3. Worktree merge (if in worktree mode)
675/// 4. Feature gate (feature units require manual confirmation)
676/// 5. Mark closed and archive
677/// 6. Post-close hook and on_close actions
678/// 7. Auto-close parents whose children are all done
679///
680/// Returns a [`close::CloseOutcome`] that describes what happened — the unit
681/// may have been closed, verify may have failed, or the close may have been
682/// blocked by a hook or feature gate.
683///
684/// # Errors
685/// - [`anyhow::Error`] — unit not found or unexpected I/O failure
686///
687/// # Example
688/// ```rust,no_run
689/// use mana_core::api::close_unit;
690/// use mana_core::ops::close::{CloseOpts, CloseOutcome};
691/// use std::path::Path;
692///
693/// let outcome = close_unit(Path::new("/project/.mana"), "1", CloseOpts {
694///     reason: Some("Implemented and tested".to_string()),
695///     force: false,
696///     defer_verify: false,
697/// }).unwrap();
698///
699/// match outcome {
700///     CloseOutcome::Closed(r) => println!("Closed! Auto-closed parents: {:?}", r.auto_closed_parents),
701///     CloseOutcome::VerifyFailed(r) => eprintln!("Verify failed: {}", r.output),
702///     _ => {}
703/// }
704/// ```
705pub fn close_unit(
706    mana_dir: &Path,
707    id: &str,
708    opts: close::CloseOpts,
709) -> Result<close::CloseOutcome> {
710    close::close(mana_dir, id, opts)
711}
712
713/// Mark a unit as explicitly failed without closing it.
714///
715/// Releases the claim, finalizes the current attempt as `Failed`, appends a
716/// structured failure summary to notes, and returns the unit to `Open` status
717/// for retry.
718///
719/// # Errors
720/// - [`anyhow::Error`] — unit not found or I/O failure
721///
722/// # Example
723/// ```rust,no_run
724/// use mana_core::api::fail_unit;
725/// use std::path::Path;
726///
727/// let unit = fail_unit(Path::new("/project/.mana"), "1",
728///     Some("Blocked by missing auth token".to_string())).unwrap();
729/// assert_eq!(unit.status, mana_core::api::Status::Open);
730/// ```
731pub fn fail_unit(mana_dir: &Path, id: &str, reason: Option<String>) -> Result<Unit> {
732    close::close_failed(mana_dir, id, reason)
733}
734
735/// Delete a unit and remove all references to it from other units' dependencies.
736///
737/// # Errors
738/// - [`anyhow::Error`] — unit not found or I/O failure
739///
740/// # Example
741/// ```rust,no_run
742/// use mana_core::api::delete_unit;
743/// use std::path::Path;
744///
745/// let r = delete_unit(Path::new("/project/.mana"), "1").unwrap();
746/// println!("Deleted: {}", r.title);
747/// ```
748pub fn delete_unit(mana_dir: &Path, id: &str) -> Result<delete::DeleteResult> {
749    delete::delete(mana_dir, id)
750}
751
752/// Reopen a closed unit.
753///
754/// Sets status back to `Open`, clears `closed_at` and `close_reason`,
755/// and rebuilds the index.
756///
757/// # Errors
758/// - [`anyhow::Error`] — unit not found or I/O failure
759///
760/// # Example
761/// ```rust,no_run
762/// use mana_core::api::reopen_unit;
763/// use std::path::Path;
764///
765/// let r = reopen_unit(Path::new("/project/.mana"), "1").unwrap();
766/// println!("Reopened: {}", r.unit.id);
767/// ```
768pub fn reopen_unit(mana_dir: &Path, id: &str) -> Result<crate::ops::reopen::ReopenResult> {
769    crate::ops::reopen::reopen(mana_dir, id)
770}
771
772/// Claim a unit for work.
773///
774/// Sets status to `InProgress`, records who claimed it and when, and starts
775/// a new attempt in the attempt log.
776///
777/// If `params.force` is false and the unit has a verify command with
778/// `fail_first: true`, the verify command is run first. If it already passes,
779/// the claim is rejected (nothing to do). This enforces fail-first/TDD semantics.
780///
781/// # Errors
782/// - [`anyhow::Error`] — unit not found, not open, or verify pre-check failed
783///
784/// # Example
785/// ```rust,no_run
786/// use mana_core::api::claim_unit;
787/// use mana_core::ops::claim::ClaimParams;
788/// use std::path::Path;
789///
790/// let r = claim_unit(Path::new("/project/.mana"), "1", ClaimParams {
791///     by: Some("agent-42".to_string()),
792///     force: true,
793/// }).unwrap();
794/// println!("Claimed by: {}", r.claimer);
795/// ```
796pub fn claim_unit(
797    mana_dir: &Path,
798    id: &str,
799    params: claim::ClaimParams,
800) -> Result<claim::ClaimResult> {
801    claim::claim(mana_dir, id, params)
802}
803
804/// Release a claim on a unit.
805///
806/// Clears `claimed_by`/`claimed_at`, sets status back to `Open`, and marks
807/// the current attempt as `Abandoned`.
808///
809/// # Errors
810/// - [`anyhow::Error`] — unit not found or I/O failure
811///
812/// # Example
813/// ```rust,no_run
814/// use mana_core::api::release_unit;
815/// use std::path::Path;
816///
817/// let r = release_unit(Path::new("/project/.mana"), "1").unwrap();
818/// assert_eq!(r.unit.status, mana_core::api::Status::Open);
819/// ```
820pub fn release_unit(mana_dir: &Path, id: &str) -> Result<claim::ReleaseResult> {
821    claim::release(mana_dir, id)
822}
823
824/// Add a dependency: `from_id` depends on `dep_id`.
825///
826/// Validates both units exist, checks for self-dependency, detects cycles,
827/// and persists the change.
828///
829/// # Errors
830/// - [`anyhow::Error`] — unit not found, self-dependency, or cycle detected
831///
832/// # Example
833/// ```rust,no_run
834/// use mana_core::api::add_dep;
835/// use std::path::Path;
836///
837/// // Unit 3 now depends on unit 2
838/// add_dep(Path::new("/project/.mana"), "3", "2").unwrap();
839/// ```
840pub fn add_dep(mana_dir: &Path, from_id: &str, dep_id: &str) -> Result<dep::DepAddResult> {
841    dep::dep_add(mana_dir, from_id, dep_id)
842}
843
844/// Remove a dependency: `from_id` no longer depends on `dep_id`.
845///
846/// # Errors
847/// - [`anyhow::Error`] — unit not found or dependency not present
848///
849/// # Example
850/// ```rust,no_run
851/// use mana_core::api::remove_dep;
852/// use std::path::Path;
853///
854/// remove_dep(Path::new("/project/.mana"), "3", "2").unwrap();
855/// ```
856pub fn remove_dep(mana_dir: &Path, from_id: &str, dep_id: &str) -> Result<dep::DepRemoveResult> {
857    dep::dep_remove(mana_dir, from_id, dep_id)
858}
859
860// ---------------------------------------------------------------------------
861// Orchestration functions
862// ---------------------------------------------------------------------------
863
864/// Compute which units are ready to dispatch.
865///
866/// Returns a [`ReadyQueue`] with units sorted by priority then critical-path
867/// weight (units blocking the most downstream work come first).
868///
869/// Optionally filters to a specific unit ID or its ready children if
870/// `filter_id` is a parent unit.
871///
872/// Set `simulate = true` to include all open units with verify commands,
873/// even those whose dependencies are not yet met. This is the dry-run mode.
874///
875/// # Errors
876/// - [`anyhow::Error`] — index or I/O failure
877///
878/// # Example
879/// ```rust,no_run
880/// use mana_core::api::compute_ready_queue;
881/// use std::path::Path;
882///
883/// let queue = compute_ready_queue(Path::new("/project/.mana"), None, false).unwrap();
884/// for unit in &queue.units {
885///     println!("Ready: {} (weight={})", unit.id, unit.critical_path_weight);
886/// }
887/// println!("Blocked: {}", queue.blocked.len());
888/// ```
889pub fn compute_ready_queue(
890    mana_dir: &Path,
891    filter_id: Option<&str>,
892    simulate: bool,
893) -> Result<ReadyQueue> {
894    crate::ops::run::compute_ready_queue(mana_dir, filter_id, simulate)
895}
896
897/// Assemble the full agent context for a unit.
898///
899/// Loads the unit, resolves dependency context (which sibling units produce
900/// artifacts this unit requires), reads referenced files, and extracts
901/// structural summaries. Returns a structured [`AgentContext`] ready for
902/// rendering into any format (text prompt, JSON, IPC message).
903///
904/// # Errors
905/// - [`anyhow::Error`] — unit not found or I/O failure
906///
907/// # Example
908/// ```rust,no_run
909/// use mana_core::api::assemble_context;
910/// use std::path::Path;
911///
912/// let ctx = assemble_context(Path::new("/project/.mana"), "1").unwrap();
913/// println!("Rules: {:?}", ctx.rules.is_some());
914/// println!("Files: {}", ctx.files.len());
915/// println!("Dep providers: {}", ctx.dep_providers.len());
916/// ```
917pub fn assemble_context(mana_dir: &Path, id: &str) -> Result<AgentContext> {
918    crate::ops::context::assemble_agent_context(mana_dir, id)
919}
920
921/// Record a verify attempt result on a unit.
922///
923/// Appends an [`AttemptRecord`] to the unit's `attempt_log` and persists.
924/// Use this when an external orchestrator completes a verify cycle and wants
925/// to record the outcome without going through the full close lifecycle.
926///
927/// # Errors
928/// - [`anyhow::Error`] — unit not found or I/O failure
929///
930/// # Example
931/// ```rust,no_run
932/// use mana_core::api::{record_attempt, AttemptRecord, AttemptOutcome};
933/// use std::path::Path;
934/// use chrono::Utc;
935///
936/// let now = Utc::now();
937/// let attempt = AttemptRecord {
938///     num: 1,
939///     outcome: AttemptOutcome::Success,
940///     notes: Some("Passed on first attempt".to_string()),
941///     agent: Some("imp-agent".to_string()),
942///     started_at: Some(now),
943///     finished_at: Some(now),
944/// };
945/// record_attempt(Path::new("/project/.mana"), "1", attempt).unwrap();
946/// ```
947pub fn record_attempt(mana_dir: &Path, id: &str, attempt: AttemptRecord) -> Result<Unit> {
948    use crate::discovery::find_unit_file;
949    use crate::index::Index;
950
951    let unit_path =
952        find_unit_file(mana_dir, id).map_err(|_| anyhow::anyhow!("Unit not found: {}", id))?;
953    let mut unit = Unit::from_file(&unit_path)
954        .map_err(|e| anyhow::anyhow!("Failed to load unit {}: {}", id, e))?;
955
956    unit.attempt_log.push(attempt);
957    unit.updated_at = chrono::Utc::now();
958
959    unit.to_file(&unit_path)
960        .map_err(|e| anyhow::anyhow!("Failed to save unit {}: {}", id, e))?;
961
962    let index = Index::build(mana_dir)?;
963    index.save(mana_dir)?;
964
965    Ok(unit)
966}
967
968/// Run the verify command for a unit without closing it.
969///
970/// Loads the unit, resolves the effective timeout (unit override → config default),
971/// spawns the verify command in a subprocess, and captures all output.
972///
973/// Returns `Ok(None)` if the unit has no verify command.
974///
975/// # Errors
976/// - [`anyhow::Error`] — unit not found, spawn failure, or I/O error
977///
978/// # Example
979/// ```rust,no_run
980/// use mana_core::api::run_verify;
981/// use std::path::Path;
982///
983/// match run_verify(Path::new("/project/.mana"), "1").unwrap() {
984///     Some(result) => {
985///         if result.passed {
986///             println!("Verify passed (exit {:?})", result.exit_code);
987///         } else {
988///             eprintln!("Verify failed:\n{}", result.stderr);
989///         }
990///     }
991///     None => println!("No verify command"),
992/// }
993/// ```
994pub fn run_verify(mana_dir: &Path, id: &str) -> Result<Option<VerifyResult>> {
995    crate::ops::verify::run_verify(mana_dir, id)
996}
997
998// ---------------------------------------------------------------------------
999// Facts functions
1000// ---------------------------------------------------------------------------
1001
1002/// Create a verified fact — a unit that encodes checked project knowledge.
1003///
1004/// Facts differ from regular units in that they:
1005/// - Have `unit_type = "fact"` and the `"fact"` label
1006/// - Require a verify command (the verification is the point)
1007/// - Have a TTL (default 30 days) after which they are considered stale
1008/// - Can reference source file paths for relevance scoring
1009///
1010/// # Errors
1011/// - [`anyhow::Error`] — empty verify command, validation failure, or I/O error
1012///
1013/// # Example
1014/// ```rust,no_run
1015/// use mana_core::api::create_fact;
1016/// use mana_core::ops::fact::FactParams;
1017/// use std::path::Path;
1018///
1019/// let r = create_fact(Path::new("/project/.mana"), FactParams {
1020///     title: "Auth uses RS256 JWT signing".to_string(),
1021///     verify: "grep -q 'RS256' src/auth.rs".to_string(),
1022///     description: Some("JWT tokens are signed with RS256 (not HS256)".to_string()),
1023///     paths: Some("src/auth.rs".to_string()),
1024///     ttl_days: Some(90),
1025///     pass_ok: true,
1026/// }).unwrap();
1027/// println!("Created fact {} (stale after {:?})", r.unit_id, r.unit.stale_after);
1028/// ```
1029pub fn create_fact(mana_dir: &Path, params: fact::FactParams) -> Result<FactResult> {
1030    fact::create_fact(mana_dir, params)
1031}
1032
1033/// Verify all facts and report staleness and failures.
1034///
1035/// Re-runs the verify command for every unit with `unit_type = "fact"`.
1036/// Stale facts (past their `stale_after` date) are reported without re-running.
1037/// Facts that require artifacts produced by failing/stale facts are flagged as
1038/// "suspect" (up to depth 3 in the dependency chain).
1039///
1040/// Facts whose verify passes have their `stale_after` deadline extended.
1041///
1042/// # Errors
1043/// - [`anyhow::Error`] — index or I/O failure
1044///
1045/// # Example
1046/// ```rust,no_run
1047/// use mana_core::api::verify_facts;
1048/// use std::path::Path;
1049///
1050/// let r = verify_facts(Path::new("/project/.mana")).unwrap();
1051/// println!("{}/{} facts verified", r.verified_count, r.total_facts);
1052/// if r.failing_count > 0 {
1053///     println!("{} facts failing!", r.failing_count);
1054/// }
1055/// ```
1056pub fn verify_facts(mana_dir: &Path) -> Result<VerifyFactsResult> {
1057    fact::verify_facts(mana_dir)
1058}
1059
1060// Legacy aliases removed — beans→mana rename complete.