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 == 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.