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