1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[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 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#[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 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 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub attribution: Option<Attribution>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct Attribution {
157 pub commit: String,
159 pub author: String,
161 pub date: String,
163 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!(
196 RuleId::SymbolAbsence.confidence(),
197 Confidence::Deterministic
198 );
199 assert_eq!(RuleId::GhostCommand.confidence(), Confidence::Deterministic);
200 assert_eq!(RuleId::LyingTest.confidence(), Confidence::Heuristic);
201 assert_eq!(RuleId::EnvMismatch.confidence(), Confidence::Heuristic);
202 assert_eq!(RuleId::OutdatedLogic.confidence(), Confidence::Experimental);
203 }
204
205 #[test]
206 fn location_sorts_by_file_then_line() {
207 let a = Location::new("a.rs", 10);
208 let b = Location::new("a.rs", 20);
209 let c = Location::new("b.rs", 1);
210 let mut v = vec![c.clone(), b.clone(), a.clone()];
211 v.sort();
212 assert_eq!(v, vec![a, b, c]);
213 }
214}