1use std::fmt;
4use std::process::ExitCode;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8#[repr(u8)]
9pub enum FttsExitCode {
10 Success = 0,
11 Generic = 1,
12 Usage = 2,
13 ModelNotFound = 3,
14 Input = 4,
15 BudgetTimeout = 5,
16 Cancelled = 6,
17 ArtifactFormat = 7,
18 EnrollmentQualityRefusal = 8,
19}
20
21impl FttsExitCode {
22 pub const fn description(self) -> &'static str {
24 match self {
25 Self::Success => "success",
26 Self::Generic => "generic error",
27 Self::Usage => "usage or CLI error",
28 Self::ModelNotFound => "model not found or not resolvable",
29 Self::Input => "input error",
30 Self::BudgetTimeout => "budget or timeout exceeded",
31 Self::Cancelled => "cancelled",
32 Self::ArtifactFormat => "artifact format or version mismatch",
33 Self::EnrollmentQualityRefusal => "enrollment-quality refusal",
34 }
35 }
36
37 pub const fn as_u8(self) -> u8 {
38 self as u8
39 }
40
41 pub fn as_exit_code(self) -> ExitCode {
42 ExitCode::from(self.as_u8())
43 }
44}
45
46#[derive(Debug)]
48pub enum FttsError {
49 Generic(String),
50 Usage(String),
51 ModelNotFound(String),
52 Input(String),
53 BudgetTimeout(String),
54 ArtifactFormat(String),
55 EnrollmentQualityRefusal(String),
56}
57
58impl FttsError {
59 pub const fn remediation(&self) -> &'static str {
64 match self {
65 Self::Generic(_) => {
66 "see the message; if it names an unimplemented phase, the capability is not built yet"
67 }
68 Self::Usage(_) => "re-run with --help to see the accepted argument shapes",
69 Self::ModelNotFound(_) => {
70 "run `ftts pull` to fetch the model (~2.0 GB), or pass --model PATH or set FTTS_MODEL_DIR; `ftts robot health` lists every directory searched"
71 }
72 Self::Input(_) => {
73 "check the input text or file encoding; input must be non-empty UTF-8"
74 }
75 Self::BudgetTimeout(_) => {
76 "raise the budget, shorten the text, or choose a faster profile"
77 }
78 Self::ArtifactFormat(_) => {
79 "regenerate the artifact with a matching ftts version; `ftts robot health` reports the expected format"
80 }
81 Self::EnrollmentQualityRefusal(_) => {
82 "supply a cleaner reference, or pass --force to accept the warned-about quality"
83 }
84 }
85 }
86
87 pub const fn exit_code(&self) -> FttsExitCode {
88 match self {
89 Self::Generic(_) => FttsExitCode::Generic,
90 Self::Usage(_) => FttsExitCode::Usage,
91 Self::ModelNotFound(_) => FttsExitCode::ModelNotFound,
92 Self::Input(_) => FttsExitCode::Input,
93 Self::BudgetTimeout(_) => FttsExitCode::BudgetTimeout,
94 Self::ArtifactFormat(_) => FttsExitCode::ArtifactFormat,
95 Self::EnrollmentQualityRefusal(_) => FttsExitCode::EnrollmentQualityRefusal,
96 }
97 }
98}
99
100impl fmt::Display for FttsError {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::Generic(message)
104 | Self::Usage(message)
105 | Self::ModelNotFound(message)
106 | Self::Input(message)
107 | Self::BudgetTimeout(message)
108 | Self::ArtifactFormat(message)
109 | Self::EnrollmentQualityRefusal(message) => formatter.write_str(message),
110 }
111 }
112}
113
114impl std::error::Error for FttsError {}
115
116#[cfg(test)]
117mod tests {
118 use super::FttsExitCode;
119
120 #[test]
121 fn exit_codes_are_stable() {
122 let cases = [
123 (FttsExitCode::Success, 0),
124 (FttsExitCode::Generic, 1),
125 (FttsExitCode::Usage, 2),
126 (FttsExitCode::ModelNotFound, 3),
127 (FttsExitCode::Input, 4),
128 (FttsExitCode::BudgetTimeout, 5),
129 (FttsExitCode::Cancelled, 6),
130 (FttsExitCode::ArtifactFormat, 7),
131 (FttsExitCode::EnrollmentQualityRefusal, 8),
132 ];
133
134 for (code, expected) in cases {
135 assert_eq!(code.as_u8(), expected, "{}", code.description());
136 }
137 }
138}