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;
20pub mod files;
21pub mod http;
22pub mod languages;
23pub mod llm;
24pub mod text;
25
26/// Shared fixtures for tests across every module. Compiled only under
27/// `cfg(test)`, so it adds nothing to the shipped binary.
28#[cfg(test)]
29pub(crate) mod test_support;
30
31/// How the process terminated.
32///
33/// Load-bearing, not cosmetic: a gate that cannot tell "clean" apart from
34/// "could not analyze" green-lights a commit whenever the LLM endpoint is
35/// unreachable, which is worse than having no gate at all.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Exit {
38 /// Analysis completed and found nothing at or above the gating threshold.
39 Clean,
40 /// Analysis completed and found issues that block.
41 FoundIssues,
42 /// One or more files could not be analyzed. Never reported as clean.
43 Unanalyzed,
44 /// Cache-only review found work that has not been reviewed yet.
45 ///
46 /// Distinct from [`Self::Unanalyzed`] so the pre-push hook can warm the
47 /// missing entries and deliberately stop before Git resumes an idle remote
48 /// connection. The next push is then a fast cache lookup.
49 CacheMiss,
50}
51
52impl Exit {
53 /// The process exit status.
54 ///
55 /// Hook scripts and CI branch on these numbers, so they are public API.
56 ///
57 /// Note that clap exits 2 on a usage error without passing through this
58 /// type, so a caller seeing 2 cannot tell a bad flag from a failed
59 /// analysis. That collision is deliberate and safe: both mean "do not let
60 /// this commit through". The distinction to protect is 0 from everything
61 /// else, not 1 from 2.
62 pub const fn code(self) -> u8 {
63 match self {
64 Exit::Clean => 0,
65 Exit::FoundIssues => 1,
66 Exit::Unanalyzed => 2,
67 Exit::CacheMiss => 3,
68 }
69 }
70}
71
72impl From<Exit> for std::process::ExitCode {
73 fn from(exit: Exit) -> Self {
74 std::process::ExitCode::from(exit.code())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn exit_codes_are_stable() {
84 assert_eq!(Exit::Clean.code(), 0);
85 assert_eq!(Exit::FoundIssues.code(), 1);
86 assert_eq!(Exit::Unanalyzed.code(), 2);
87 assert_eq!(Exit::CacheMiss.code(), 3);
88 }
89}