Skip to main content

drep/
lib.rs

1//! drep - a local commit gate.
2//!
3//! Two layers, split by *source* rather than severity:
4//!
5//! - **Deterministic**: the linters and formatters the repository has already
6//!   configured (ruff, eslint, tsc, gofmt, go vet, clippy). Precise enough to
7//!   block a commit.
8//! - **Semantic**: an LLM, told which language it is reading. It informs
9//!   unless `--fail-on` opts it into gating.
10//!
11//! Splitting by source is what makes the gate calibratable. Severity
12//! thresholds over LLM output never were.
13
14pub mod analysis;
15pub mod auth;
16pub mod cli;
17pub mod config;
18pub mod diff;
19pub mod docs;
20mod file_lock;
21pub mod files;
22pub mod http;
23pub mod languages;
24pub mod llm;
25pub mod text;
26
27/// Shared fixtures for tests across every module. Compiled only under
28/// `cfg(test)`, so it adds nothing to the shipped binary.
29#[cfg(test)]
30pub(crate) mod test_support;
31
32/// How the process terminated.
33///
34/// Load-bearing, not cosmetic: a gate that cannot tell "clean" apart from
35/// "could not analyze" green-lights a commit whenever the LLM endpoint is
36/// unreachable, which is worse than having no gate at all.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Exit {
39    /// Analysis completed and found nothing at or above the gating threshold.
40    Clean,
41    /// Analysis completed and found issues that block.
42    FoundIssues,
43    /// One or more files could not be analyzed. Never reported as clean.
44    Unanalyzed,
45    /// Cache-only review found work that has not been reviewed yet.
46    ///
47    /// Distinct from [`Self::Unanalyzed`] so the pre-push hook can warm the
48    /// missing entries and deliberately stop before Git resumes an idle remote
49    /// connection. The next push is then a fast cache lookup.
50    CacheMiss,
51}
52
53impl Exit {
54    /// The process exit status.
55    ///
56    /// Hook scripts and CI branch on these numbers, so they are public API.
57    ///
58    /// Note that clap exits 2 on a usage error without passing through this
59    /// type, so a caller seeing 2 cannot tell a bad flag from a failed
60    /// analysis. That collision is deliberate and safe: both mean "do not let
61    /// this commit through". The distinction to protect is 0 from everything
62    /// else, not 1 from 2.
63    pub const fn code(self) -> u8 {
64        match self {
65            Exit::Clean => 0,
66            Exit::FoundIssues => 1,
67            Exit::Unanalyzed => 2,
68            Exit::CacheMiss => 3,
69        }
70    }
71}
72
73impl From<Exit> for std::process::ExitCode {
74    fn from(exit: Exit) -> Self {
75        std::process::ExitCode::from(exit.code())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn exit_codes_are_stable() {
85        assert_eq!(Exit::Clean.code(), 0);
86        assert_eq!(Exit::FoundIssues.code(), 1);
87        assert_eq!(Exit::Unanalyzed.code(), 2);
88        assert_eq!(Exit::CacheMiss.code(), 3);
89    }
90}