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>;