Skip to main content

spec_drift/domain/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Rule identifiers map 1:1 with the rule catalog in the README.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum RuleId {
8    SymbolAbsence,
9    ConstraintViolation,
10    OutdatedLogic,
11    CompileFailure,
12    DeprecatedUsage,
13    LogicGap,
14    LyingTest,
15    MissingCoverage,
16    GhostCommand,
17    EnvMismatch,
18}
19
20impl RuleId {
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            Self::SymbolAbsence => "symbol_absence",
24            Self::ConstraintViolation => "constraint_violation",
25            Self::OutdatedLogic => "outdated_logic",
26            Self::CompileFailure => "compile_failure",
27            Self::DeprecatedUsage => "deprecated_usage",
28            Self::LogicGap => "logic_gap",
29            Self::LyingTest => "lying_test",
30            Self::MissingCoverage => "missing_coverage",
31            Self::GhostCommand => "ghost_command",
32            Self::EnvMismatch => "env_mismatch",
33        }
34    }
35
36    /// Confidence class — matches the README confidence matrix.
37    /// `--strict` promotes heuristic and experimental rules one severity level.
38    pub fn confidence(&self) -> Confidence {
39        match self {
40            Self::SymbolAbsence
41            | Self::CompileFailure
42            | Self::DeprecatedUsage
43            | Self::GhostCommand => Confidence::Deterministic,
44            Self::ConstraintViolation
45            | Self::LyingTest
46            | Self::MissingCoverage
47            | Self::EnvMismatch => Confidence::Heuristic,
48            Self::OutdatedLogic | Self::LogicGap => Confidence::Experimental,
49        }
50    }
51}
52
53/// Confidence class of a [`RuleId`]. Maps 1:1 to the README confidence matrix.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum Confidence {
57    Deterministic,
58    Heuristic,
59    Experimental,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum Severity {
65    Notice,
66    Warning,
67    Critical,
68}
69
70impl Severity {
71    pub fn glyph(&self) -> &'static str {
72        match self {
73            Self::Notice => "🟡 NOTICE",
74            Self::Warning => "⚠️  WARNING",
75            Self::Critical => "❌ CRITICAL",
76        }
77    }
78
79    /// Promote one step. `Critical` saturates.
80    pub fn promoted(self) -> Self {
81        match self {
82            Self::Notice => Self::Warning,
83            Self::Warning => Self::Critical,
84            Self::Critical => Self::Critical,
85        }
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
90pub struct Location {
91    pub file: PathBuf,
92    pub line: u32,
93}
94
95impl Location {
96    pub fn new(file: impl Into<PathBuf>, line: u32) -> Self {
97        Self {
98            file: file.into(),
99            line,
100        }
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct SpecClaim {
106    pub location: Location,
107    pub kind: ClaimKind,
108    pub text: String,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum ClaimKind {
114    Symbol,
115    Constraint,
116    Command,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct CodeFact {
121    pub location: Location,
122    pub kind: FactKind,
123    pub name: String,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum FactKind {
129    Function,
130    Struct,
131    Enum,
132    Trait,
133    TypeAlias,
134    Module,
135    Constant,
136    Macro,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct Divergence {
141    pub rule: RuleId,
142    pub severity: Severity,
143    pub location: Location,
144    pub stated: String,
145    pub reality: String,
146    pub risk: String,
147    /// Optional blame attribution. `None` when attribution was not requested
148    /// or could not be resolved. Serialized only when present so old baseline
149    /// snapshots continue to parse without change.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub attribution: Option<Attribution>,
152}
153
154/// Git attribution for the line that carries a spec claim.
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct Attribution {
157    /// Short commit SHA (first 7 chars).
158    pub commit: String,
159    /// Author name as recorded by git.
160    pub author: String,
161    /// Author date in `YYYY-MM-DD` form.
162    pub date: String,
163    /// Commit summary (subject line).
164    pub summary: String,
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn rule_id_round_trips_through_json() {
173        let r = RuleId::SymbolAbsence;
174        let json = serde_json::to_string(&r).unwrap();
175        assert_eq!(json, "\"symbol_absence\"");
176        let back: RuleId = serde_json::from_str(&json).unwrap();
177        assert_eq!(back, r);
178    }
179
180    #[test]
181    fn severity_orders_notice_lt_warning_lt_critical() {
182        assert!(Severity::Notice < Severity::Warning);
183        assert!(Severity::Warning < Severity::Critical);
184    }
185
186    #[test]
187    fn severity_promoted_saturates_at_critical() {
188        assert_eq!(Severity::Notice.promoted(), Severity::Warning);
189        assert_eq!(Severity::Warning.promoted(), Severity::Critical);
190        assert_eq!(Severity::Critical.promoted(), Severity::Critical);
191    }
192
193    #[test]
194    fn confidence_matches_readme_matrix() {
195        assert_eq!(RuleId::SymbolAbsence.confidence(), Confidence::Deterministic);
196        assert_eq!(RuleId::GhostCommand.confidence(), Confidence::Deterministic);
197        assert_eq!(RuleId::LyingTest.confidence(), Confidence::Heuristic);
198        assert_eq!(RuleId::EnvMismatch.confidence(), Confidence::Heuristic);
199        assert_eq!(RuleId::OutdatedLogic.confidence(), Confidence::Experimental);
200    }
201
202    #[test]
203    fn location_sorts_by_file_then_line() {
204        let a = Location::new("a.rs", 10);
205        let b = Location::new("a.rs", 20);
206        let c = Location::new("b.rs", 1);
207        let mut v = vec![c.clone(), b.clone(), a.clone()];
208        v.sort();
209        assert_eq!(v, vec![a, b, c]);
210    }
211}