Skip to main content

majordomus_cli/
error.rs

1//! Typed errors and the exit-code contract.
2//!
3//! The codes mirror `docs/CLI.md`: `0` ok, `2` usage, `10` contract unmet, `12` missing
4//! artifact, `13` internal error. A malformed file inside the layer is not an `Error` but a
5//! [`crate::Diagnostic`]; errors are for states where nothing can proceed.
6
7use std::path::PathBuf;
8
9/// Everything that stops a command.
10#[derive(Debug, thiserror::Error)]
11pub enum Error {
12    /// No ancestor of the start directory carries `.ai/manifest.yaml`.
13    #[error(
14        "no Majordomus repository found from {start}: no ancestor directory carries .ai/manifest.yaml"
15    )]
16    RepositoryNotFound {
17        /// Where the search began.
18        start: PathBuf,
19    },
20
21    /// Project data sits under `.majordomus/` with no manifest: the pre-`.ai` layout.
22    #[error(
23        "project data lives under {root}/.majordomus (the pre-.ai layout), which nothing reads any more; run: majordomus migrate"
24    )]
25    LegacyLayout {
26        /// The directory that holds the legacy layout.
27        root: PathBuf,
28    },
29
30    /// The manifest does not parse, or a constraint of its schema fails.
31    #[error("{path}: {reason}")]
32    InvalidManifest {
33        /// The manifest.
34        path: PathBuf,
35        /// What is wrong.
36        reason: String,
37    },
38
39    /// The manifest declares a layer schema this executable does not read.
40    #[error("{path}: unsupported schema '{found}'; this executable reads {supported}")]
41    UnsupportedSchema {
42        /// The manifest.
43        path: PathBuf,
44        /// The schema declared.
45        found: String,
46        /// The schema this executable reads.
47        supported: String,
48    },
49
50    /// `sources.yaml` does not parse or contradicts its own rules.
51    #[error("{path}: {reason}")]
52    InvalidSources {
53        /// The sources file.
54        path: PathBuf,
55        /// What is wrong.
56        reason: String,
57    },
58
59    /// `scope.yaml` does not parse or contradicts its own rules.
60    #[error("{path}: {reason}")]
61    InvalidScope {
62        /// The scope file.
63        path: PathBuf,
64        /// What is wrong.
65        reason: String,
66    },
67
68    /// A file carries keys nothing reads.
69    #[error("{path}: unknown key(s): {}", keys.join(", "))]
70    UnknownKeys {
71        /// The file.
72        path: PathBuf,
73        /// The key paths, in document order.
74        keys: Vec<String>,
75    },
76
77    /// A kinds file or a schema file cannot be used: malformed, contradictory, or redefining a distributed one.
78    #[error("kind schema: {reason}")]
79    KindSchema {
80        /// What is wrong, with the file named.
81        reason: String,
82    },
83
84    /// No directory tried holds `kinds.yaml`.
85    #[error("no share directory holds kinds.yaml; tried {}; pass --share <dir> or set MAJORDOMUS_SHARE", tried.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", "))]
86    ShareNotFound {
87        /// Every directory looked at, in order.
88        tried: Vec<PathBuf>,
89    },
90
91    /// `--strict` was given and the index carries error diagnostics.
92    #[error("declarative state carries {count} error(s); refusing to serve under --strict (run: majordomus mcp --inspect)")]
93    StrictDiagnostics {
94        /// How many.
95        count: usize,
96    },
97
98    /// `git` could not be run or answered with an error, where it was needed.
99    #[error("git: {reason}")]
100    Git {
101        /// What `git` said.
102        reason: String,
103    },
104
105    /// A file or directory could not be read or written.
106    #[error("{path}: {source}")]
107    Io {
108        /// The path.
109        path: PathBuf,
110        #[source]
111        /// The underlying error.
112        source: std::io::Error,
113    },
114
115    /// The stdio transport failed to read or write.
116    #[error("transport: {0}")]
117    Transport(#[source] std::io::Error),
118
119    /// A subsystem refused an operation and carries its own message and exit code. The
120    /// message is the subsystem's, complete, and is printed as it is: rephrasing an
121    /// actionable refusal into a generic one is how a person loses the corrective command.
122    #[error("{reason}")]
123    Refused {
124        /// The exit code the subsystem's own contract assigns.
125        code: u8,
126        /// What failed, why, what was involved, and what to run instead.
127        reason: String,
128    },
129
130    /// A protocol frame could not be encoded, or an internal answer could not be produced.
131    #[error("protocol: {reason}")]
132    Protocol {
133        /// What is wrong.
134        reason: String,
135    },
136
137    /// The capability registry does not build; every violation is listed.
138    #[error("the capability registry does not build:\n{}", errors.iter().map(|e| format!("  {e}")).collect::<Vec<_>>().join("\n"))]
139    Registry {
140        /// The violations, in id order.
141        errors: Vec<crate::capability::RegistryError>,
142    },
143
144    /// The HTTP projection failed: a port that cannot be bound, or an OpenAPI document that cannot be built.
145    #[error("http: {reason}")]
146    Http {
147        /// What is wrong.
148        reason: String,
149    },
150
151    /// The shared server's lease could not be acquired or joined.
152    #[error("lease: {reason}")]
153    Lease {
154        /// What is wrong.
155        reason: String,
156    },
157
158    /// A deployment was named that the layer does not declare.
159    #[error("no deployment '{id}' in this layer (the deployments are the objects under the layer's deployments section)")]
160    DeploymentNotFound {
161        /// The id asked for.
162        id: String,
163    },
164
165    /// A deployment object exists and cannot be read as one.
166    #[error("{reason}")]
167    InvalidDeployment {
168        /// What is wrong, with the file, the key, the value and the correction.
169        reason: String,
170    },
171
172    /// No capability has this id.
173    #[error("unknown capability: {id} (run: majordomus capabilities list)")]
174    CapabilityNotFound {
175        /// The id asked for.
176        id: String,
177    },
178
179    /// A caller named something this repository does not hold. Not the caller's fault in
180    /// the sense an internal error is: the request was well formed and the thing is
181    /// absent, which is the missing-artifact code and not the internal one.
182    #[error("{reason}")]
183    NotFound {
184        /// What was asked for and where the caller can see what exists.
185        reason: String,
186    },
187
188    /// The policy file does not parse, or does not carry what the projections need.
189    #[error("policy {path} is invalid: {reason}")]
190    InvalidPolicy {
191        /// The policy file.
192        path: PathBuf,
193        /// What is wrong.
194        reason: String,
195    },
196    /// A declared provider projection cannot be produced.
197    #[error("projection {target}: {reason}")]
198    InvalidProjection {
199        /// The declared target.
200        target: String,
201        /// What is wrong.
202        reason: String,
203    },
204    /// A web surface's declaration cannot be read, or the topology it would join is not valid.
205    #[error("web surface {surface}: {reason}")]
206    InvalidSurface {
207        /// The surface's id, or the declaration's path when the id is what is wrong.
208        surface: String,
209        /// What is wrong, and what to do about it.
210        reason: String,
211    },
212    /// The distribution model cannot be read, or it breaks an invariant of its own contract.
213    #[error("distribution model {path}: {reason}")]
214    InvalidDistribution {
215        /// Where the model was read from.
216        path: String,
217        /// What is wrong, and what to do about it.
218        reason: String,
219    },
220    /// A release record cannot be read, or it disagrees with the distribution model.
221    #[error("release record {path}: {reason}")]
222    InvalidRelease {
223        /// Where the record was read from.
224        path: String,
225        /// What is wrong, and what to do about it.
226        reason: String,
227    },
228    /// The design declaration, or a projection rendered from it, is inconsistent: a role
229    /// naming no palette entry, a state word filed under two meanings, a value the CSS
230    /// parser would drop in silence. Nothing is written.
231    #[error("design {path}: {reason}")]
232    InvalidDesign {
233        /// The declaration, or the projection that could not be rendered.
234        path: String,
235        /// What is wrong.
236        reason: String,
237    },
238    /// A Rust source file of the crate under inspection cannot be read or parsed, so the
239    /// quality inventory would be built over less than the whole crate. Reported rather
240    /// than skipped: a partial inventory that looked clean would be the one result this
241    /// subsystem must never produce.
242    #[error("rust source {path}: {reason}")]
243    InvalidSource {
244        /// The file, crate-relative when it is inside the crate.
245        path: String,
246        /// What is wrong, and what to do about it.
247        reason: String,
248    },
249
250    /// The layer names something this repository does not hold, so the graph composed from
251    /// it would carry a reference to nothing. Nothing is written: a published graph whose
252    /// reference resolves to no node is a naming defect a reader cannot see, and every
253    /// regeneration would reproduce it.
254    #[error("the composed graph would name what this repository does not hold:\n{}", findings.join("\n"))]
255    UnresolvedRelation {
256        /// One line per finding: the file, the key, what was named, and the correction.
257        findings: Vec<String>,
258    },
259
260    /// `generate --check` found committed projections that differ from the registry, or are missing.
261    #[error("generated artifact(s) stale: {} (run: majordomus generate)", files.join(", "))]
262    Stale {
263        /// Each stale file with `(differs)` or `(missing)`.
264        files: Vec<String>,
265    },
266}
267
268impl Error {
269    /// The process exit code this error maps to, per the exit-code contract.
270    pub fn exit_code(&self) -> u8 {
271        match self {
272            Error::RepositoryNotFound { .. }
273            | Error::LegacyLayout { .. }
274            | Error::ShareNotFound { .. } => 12,
275            Error::InvalidManifest { .. }
276            | Error::UnsupportedSchema { .. }
277            | Error::InvalidSources { .. }
278            | Error::InvalidScope { .. }
279            | Error::UnknownKeys { .. }
280            | Error::StrictDiagnostics { .. }
281            | Error::KindSchema { .. }
282            | Error::Registry { .. }
283            | Error::InvalidPolicy { .. }
284            | Error::InvalidProjection { .. }
285            | Error::InvalidSurface { .. }
286            | Error::InvalidDeployment { .. }
287            | Error::InvalidDistribution { .. }
288            | Error::InvalidRelease { .. }
289            | Error::InvalidDesign { .. }
290            | Error::InvalidSource { .. }
291            | Error::UnresolvedRelation { .. }
292            | Error::Stale { .. } => 10,
293            Error::CapabilityNotFound { .. }
294            | Error::NotFound { .. }
295            | Error::DeploymentNotFound { .. } => 12,
296            Error::Git { .. }
297            | Error::Io { .. }
298            | Error::Transport(_)
299            | Error::Http { .. }
300            | Error::Lease { .. }
301            | Error::Protocol { .. } => 13,
302            Error::Refused { code, .. } => *code,
303        }
304    }
305
306    pub(crate) fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
307        Error::Io {
308            path: path.into(),
309            source,
310        }
311    }
312}
313
314/// A result whose error is [`Error`].
315pub type Result<T> = std::result::Result<T, Error>;