Skip to main content

tokmd_gate/
lib.rs

1//! # tokmd-gate
2//!
3//! **Tier 3 (Policy Evaluation)**
4//!
5//! Policy evaluation engine for CI gating based on analysis receipts.
6//!
7//! ## What belongs here
8//! * Policy rule types and parsing
9//! * JSON Pointer resolution
10//! * Rule evaluation logic
11//! * Ratchet evaluation for trend tracking
12//!
13//! ## Example
14//! ```
15//! use serde_json::json;
16//! use tokmd_gate::{PolicyConfig, evaluate_policy};
17//!
18//! let receipt = json!({"tokens": 42});
19//! let policy = PolicyConfig::from_toml(r#"
20//! [[rules]]
21//! name = "check"
22//! pointer = "/tokens"
23//! op = "lte"
24//! value = 1000
25//! "#).unwrap();
26//! let result = evaluate_policy(&receipt, &policy);
27//! assert!(result.passed);
28//! ```
29
30mod evaluate;
31mod pointer;
32mod ratchet;
33mod types;
34
35pub use evaluate::evaluate_policy;
36pub use pointer::resolve_pointer;
37pub use ratchet::evaluate_ratchet_policy;
38pub use types::{
39    GateError, GateResult, PolicyConfig, PolicyRule, RatchetConfig, RatchetGateResult,
40    RatchetResult, RatchetRule, RuleLevel, RuleOperator, RuleResult,
41};
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use serde_json::json;
47
48    // ── resolve_pointer (public API) ──────────────────────────────────
49    #[test]
50    fn resolve_pointer_simple_path() {
51        let doc = json!({"a": {"b": 42}});
52        assert_eq!(resolve_pointer(&doc, "/a/b"), Some(&json!(42)));
53    }
54
55    #[test]
56    fn resolve_pointer_missing_path() {
57        let doc = json!({"a": 1});
58        assert_eq!(resolve_pointer(&doc, "/b"), None);
59    }
60
61    #[test]
62    fn resolve_pointer_empty_is_whole_doc() {
63        let doc = json!({"x": 1});
64        assert_eq!(resolve_pointer(&doc, ""), Some(&doc));
65    }
66
67    // ── evaluate_policy (public API) ──────────────────────────────────
68    #[test]
69    fn evaluate_policy_all_pass() {
70        let receipt = json!({"tokens": 100, "files": 5});
71        let policy = PolicyConfig {
72            rules: vec![PolicyRule {
73                name: "max_tokens".into(),
74                pointer: "/tokens".into(),
75                op: RuleOperator::Lte,
76                value: Some(json!(1000)),
77                values: None,
78                negate: false,
79                level: RuleLevel::Error,
80                message: None,
81            }],
82            fail_fast: false,
83            allow_missing: false,
84        };
85
86        let result = evaluate_policy(&receipt, &policy);
87        assert!(result.passed);
88        assert_eq!(result.errors, 0);
89        assert_eq!(result.warnings, 0);
90    }
91
92    #[test]
93    fn evaluate_policy_with_failure() {
94        let receipt = json!({"tokens": 2000});
95        let policy = PolicyConfig {
96            rules: vec![PolicyRule {
97                name: "max_tokens".into(),
98                pointer: "/tokens".into(),
99                op: RuleOperator::Lte,
100                value: Some(json!(1000)),
101                values: None,
102                negate: false,
103                level: RuleLevel::Error,
104                message: Some("Too many tokens".into()),
105            }],
106            fail_fast: false,
107            allow_missing: false,
108        };
109
110        let result = evaluate_policy(&receipt, &policy);
111        assert!(!result.passed);
112        assert_eq!(result.errors, 1);
113    }
114
115    #[test]
116    fn evaluate_policy_warn_does_not_fail() {
117        let receipt = json!({"tokens": 2000});
118        let policy = PolicyConfig {
119            rules: vec![PolicyRule {
120                name: "token_warning".into(),
121                pointer: "/tokens".into(),
122                op: RuleOperator::Lte,
123                value: Some(json!(1000)),
124                values: None,
125                negate: false,
126                level: RuleLevel::Warn,
127                message: None,
128            }],
129            fail_fast: false,
130            allow_missing: false,
131        };
132
133        let result = evaluate_policy(&receipt, &policy);
134        assert!(result.passed); // Warnings don't fail
135        assert_eq!(result.warnings, 1);
136    }
137
138    // ── evaluate_ratchet_policy (public API) ──────────────────────────
139    #[test]
140    fn ratchet_policy_pass() {
141        let baseline = json!({"complexity": 10.0});
142        let current = json!({"complexity": 10.5}); // 5% increase
143        let config = RatchetConfig {
144            rules: vec![RatchetRule {
145                pointer: "/complexity".into(),
146                max_increase_pct: Some(10.0),
147                max_value: None,
148                level: RuleLevel::Error,
149                description: None,
150            }],
151            fail_fast: false,
152            allow_missing_baseline: false,
153            allow_missing_current: false,
154        };
155
156        let result = evaluate_ratchet_policy(&config, &baseline, &current);
157        assert!(result.passed);
158        assert_eq!(result.errors, 0);
159    }
160
161    #[test]
162    fn ratchet_policy_fail_regression() {
163        let baseline = json!({"complexity": 10.0});
164        let current = json!({"complexity": 15.0}); // 50% increase
165        let config = RatchetConfig {
166            rules: vec![RatchetRule {
167                pointer: "/complexity".into(),
168                max_increase_pct: Some(10.0),
169                max_value: None,
170                level: RuleLevel::Error,
171                description: None,
172            }],
173            fail_fast: false,
174            allow_missing_baseline: false,
175            allow_missing_current: false,
176        };
177
178        let result = evaluate_ratchet_policy(&config, &baseline, &current);
179        assert!(!result.passed);
180        assert_eq!(result.errors, 1);
181    }
182
183    // ── PolicyConfig parsing ──────────────────────────────────────────
184    #[test]
185    fn policy_config_from_toml() {
186        let toml = r#"
187fail_fast = false
188allow_missing = true
189
190[[rules]]
191name = "check_tokens"
192pointer = "/tokens"
193op = "lte"
194value = 500000
195"#;
196        let policy = PolicyConfig::from_toml(toml).unwrap();
197        assert!(!policy.fail_fast);
198        assert!(policy.allow_missing);
199        assert_eq!(policy.rules.len(), 1);
200        assert_eq!(policy.rules[0].name, "check_tokens");
201    }
202
203    #[test]
204    fn policy_config_default_is_empty() {
205        let policy = PolicyConfig::default();
206        assert!(policy.rules.is_empty());
207        assert!(!policy.fail_fast);
208        assert!(!policy.allow_missing);
209    }
210
211    #[test]
212    fn ratchet_config_from_toml() {
213        let toml = r#"
214fail_fast = true
215allow_missing_baseline = true
216
217[[rules]]
218pointer = "/complexity/avg"
219max_increase_pct = 5.0
220level = "error"
221"#;
222        let config = RatchetConfig::from_toml(toml).unwrap();
223        assert!(config.fail_fast);
224        assert!(config.allow_missing_baseline);
225        assert_eq!(config.rules.len(), 1);
226    }
227
228    // ── GateResult construction ───────────────────────────────────────
229    #[test]
230    fn gate_result_from_empty_results() {
231        let result = GateResult::from_results(vec![]);
232        assert!(result.passed);
233        assert_eq!(result.errors, 0);
234        assert_eq!(result.warnings, 0);
235    }
236
237    #[test]
238    fn ratchet_gate_result_from_empty_results() {
239        let result = RatchetGateResult::from_results(vec![]);
240        assert!(result.passed);
241        assert_eq!(result.errors, 0);
242        assert_eq!(result.warnings, 0);
243    }
244
245    // ── RuleOperator Display ──────────────────────────────────────────
246    #[test]
247    fn rule_operator_display() {
248        assert_eq!(RuleOperator::Gt.to_string(), ">");
249        assert_eq!(RuleOperator::Lte.to_string(), "<=");
250        assert_eq!(RuleOperator::Eq.to_string(), "==");
251        assert_eq!(RuleOperator::In.to_string(), "in");
252        assert_eq!(RuleOperator::Exists.to_string(), "exists");
253    }
254
255    // ── RuleOperator/RuleLevel defaults ───────────────────────────────
256    #[test]
257    fn rule_operator_default_is_eq() {
258        assert_eq!(RuleOperator::default(), RuleOperator::Eq);
259    }
260
261    #[test]
262    fn rule_level_default_is_error() {
263        assert_eq!(RuleLevel::default(), RuleLevel::Error);
264    }
265}