Skip to main content

bears/
error.rs

1use std::path::PathBuf;
2
3use crate::task::Status;
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, thiserror::Error)]
8pub enum Error {
9    #[error("not initialized: no `.bears` directory found")]
10    NotInitialized,
11
12    #[error("task not found: {0}")]
13    TaskNotFound(String),
14
15    #[error("ambiguous prefix '{prefix}' matches multiple tasks: {matches}")]
16    AmbiguousPrefix { prefix: String, matches: String },
17
18    #[error("task {0} is not an epic — plans can only be generated for epics")]
19    NotAnEpic(String),
20
21    #[error("parent {0} is not an epic — only epics can have child tasks")]
22    ParentNotEpic(String),
23
24    #[error("adding dependency would create a cycle: {from} -> {to}")]
25    CycleDetected { from: String, to: String },
26
27    #[error("unknown dependency ID(s): {}", ids.join(", "))]
28    UnknownDependency { ids: Vec<String> },
29
30    #[error("invalid config: {reason}")]
31    InvalidConfig { reason: String },
32
33    #[error("{0}")]
34    InvalidUsage(String),
35
36    #[error("invalid frontmatter in {path}: {reason}")]
37    InvalidFrontmatter { path: PathBuf, reason: String },
38
39    #[error("editor failed: {reason}")]
40    EditorFailed { reason: String },
41
42    /// The task cannot be archived because active tasks depend on it.
43    #[error("task {id} is not archivable — active dependents: {}", blockers.join(", "))]
44    NotArchivable { id: String, blockers: Vec<String> },
45
46    /// A workflow shortcut was used on a task whose status does not allow it.
47    ///
48    /// The workflow verbs (`release`, `review`, `reject`, `propose`, `accept`)
49    /// each move a task along one specific edge; setting a status directly is
50    /// always available as the escape hatch.
51    #[error("cannot {action} task {id}: expected status {expected}, but it is {actual}")]
52    InvalidStatus {
53        id: String,
54        action: &'static str,
55        expected: Status,
56        actual: Status,
57    },
58
59    /// A claim was refused because the task already belongs to someone else.
60    ///
61    /// Returned by the claiming primitives (`claim_task`, `assign_task`) after
62    /// a fresh read from disk, so two workers racing for the same task cannot
63    /// both walk away believing they hold it.
64    #[error("task {id} is already claimed by '{assignee}'")]
65    AlreadyClaimed { id: String, assignee: String },
66
67    /// A fenced mutation was refused because the task's assignee no longer
68    /// matches the token the caller claimed it with.
69    ///
70    /// This is the stale-writer guard: a worker whose task has been released,
71    /// reassigned, or reaped gets this error instead of silently clobbering
72    /// the new holder's state.
73    #[error("fence violation on task {id}: expected assignee '{expected}', but it is '{actual}'")]
74    FenceViolation {
75        id: String,
76        expected: String,
77        actual: String,
78    },
79
80    /// The task is not found in the archive.
81    #[error("task not found in archive: {0}")]
82    NotArchived(String),
83
84    #[error(transparent)]
85    Io(#[from] std::io::Error),
86
87    #[error(transparent)]
88    Yaml(#[from] serde_yml::Error),
89
90    #[error(transparent)]
91    Json(#[from] serde_json::Error),
92}