1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Public error type. Drives CLI exit codes at the lib/cli boundary.
use thiserror::Error;
pub type Result<T> = std::result::Result<T, CodeLoreError>;
#[derive(Debug, Error)]
pub enum CodeLoreError {
#[error("repository error: {0}")]
Repo(String),
#[error("analysis error: {0}")]
Analysis(String),
#[error("output error: {0}")]
Output(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// `std::io::Error` from a **read-side input** path — loading a
/// user-supplied config file (`--team-map`, `--arch-rules-file`),
/// probing a repo path, reading a `.gitignore` chain. Distinct
/// from `Io` (which covers write-side output emitter failures and
/// auto-derives via `#[from]`) because the exit-code mapping
/// differs: read-side input failures map to spec §6.6 exit 3
/// (repo/input error) so CI orchestrators can distinguish "the
/// user pointed me at unreadable input" from "the output pipe
/// broke mid-write". Constructed explicitly at the small set of
/// read-side call sites; never participates in `From` so generic
/// `?` propagation defaults to the write-side `Io` variant.
#[error("I/O error reading input: {0}")]
RepoIo(std::io::Error),
// ------------------------------------------------------------------
// Narrow structured variants. The audit pass found that only ONE
// call site (`codelore-cli::main::exit_code`) pattern-matches on
// CodeLoreError today, so wholesale "every failure mode gets a
// typed variant" would be ceremony without payoff. These three are
// the failure modes where a CONSUMER could meaningfully recover or
// present a better message: malformed user config (point them at
// the line), unknown analysis name (list what's accepted), missing
// blob (signal repo corruption distinctly from generic repo errors).
// ------------------------------------------------------------------
/// User-supplied team-map CSV is malformed. `line` is 1-indexed and
/// `0` means a structural problem before any data row (e.g. missing
/// header). CLI uses this to print a "fix `<path>` line N" hint.
#[error("malformed team-map {} line {line}: {reason}", path.display())]
MalformedTeamMap {
path: std::path::PathBuf,
line: usize,
reason: String,
},
/// `--analysis foo` was passed but `foo` isn't an analysis name we
/// know. `supported` lists what we DO accept so the CLI can echo it.
#[error("unknown analysis {name:?}. Supported: {}", supported.join(", "))]
UnknownAnalysisName {
name: String,
supported: Vec<&'static str>,
},
/// A blob OID resolved from a tree-walk wasn't found in the object
/// database. Almost always means the repo is shallow-cloned (parent
/// ancestry truncated) or corrupted; distinct from generic repo
/// errors so consumers can refuse to retry on bad data.
#[error("blob {oid} not found in object database — repo may be shallow or corrupted")]
BlobNotFound { oid: String },
/// Cross-field validation of CLI options failed (e.g. `--min-coupling`
/// exceeds `--max-coupling`, an out-of-range clone-similarity floor, or
/// `--after` later than `--before`). A pure argument conflict, sharing
/// the exit-2 configuration-error bucket with `MalformedTeamMap`.
#[error("invalid options: {0}")]
InvalidOptions(String),
}
impl CodeLoreError {
/// CLI exit code for this error variant. See spec §6.6.
#[must_use]
pub fn exit_code(&self) -> i32 {
match self {
Self::MalformedTeamMap { .. } | Self::InvalidOptions(_) => 2,
Self::Repo(_) | Self::BlobNotFound { .. } | Self::RepoIo(_) => 3,
Self::Analysis(_) | Self::UnknownAnalysisName { .. } => 4,
Self::Output(_) | Self::Io(_) => 5,
}
}
}