Skip to main content

lean_ctx/core/code_health/
gate.rs

1//! Edit-time code-health gate — shared by `ctx_edit`, `ctx_patch`, and the
2//! native-edit hook.
3//!
4//! Drift prevention is the lever lean-ctx has that a post-hoc scanner does not:
5//! it sits *in the edit path*, so it can catch a complexity regression at the
6//! moment it is introduced. The gate is pure except for reading `[code_health]`
7//! config, and its notice text is deterministic (#498-safe).
8
9use super::{GateMode, cognitive_delta, format_gate_notice, worst_regression};
10use crate::core::config::Config;
11
12/// Decision of the edit-gate for one edit.
13pub enum GateOutcome {
14    /// Allow the write; if `Some`, append this advisory notice to the output.
15    Allow(Option<String>),
16    /// Block the write with this reason (only in `gate="block"` mode).
17    Block(String),
18}
19
20/// Evaluate the gate for an edit from `old`→`new` source in a file with `ext`,
21/// using the active `[code_health]` config (threshold + mode).
22pub fn evaluate(old: &str, new: &str, ext: &str) -> GateOutcome {
23    let cfg = Config::load();
24    evaluate_with(
25        old,
26        new,
27        ext,
28        GateMode::parse(&cfg.code_health.gate),
29        cfg.code_health.cognitive_threshold,
30    )
31}
32
33/// Pure gate evaluation with explicit `mode`/`threshold` — the unit-tested core.
34pub fn evaluate_with(
35    old: &str,
36    new: &str,
37    ext: &str,
38    mode: GateMode,
39    threshold: u32,
40) -> GateOutcome {
41    if matches!(mode, GateMode::Off) {
42        return GateOutcome::Allow(None);
43    }
44    let deltas = cognitive_delta(old, new, ext);
45    let Some(worst) = worst_regression(&deltas, threshold) else {
46        return GateOutcome::Allow(None);
47    };
48    let notice = format_gate_notice(worst, threshold);
49    // Block only a genuine clean→over-threshold regression; otherwise advise.
50    if matches!(mode, GateMode::Block) && worst.crosses_threshold(threshold) {
51        GateOutcome::Block(format!(
52            "{notice}\n(set [code_health] gate=\"warn\" to allow)"
53        ))
54    } else {
55        GateOutcome::Allow(Some(notice))
56    }
57}
58
59#[cfg(all(test, feature = "tree-sitter"))]
60mod tests {
61    use super::*;
62
63    const FLAT: &str = "fn f(a: bool) { if a {} }";
64    // 1+2+3+4+5+6 = 21 cognitive → over the default threshold of 15.
65    const DEEP: &str = "fn f(a: bool) { if a { if a { if a { if a { if a { if a {} } } } } } }";
66
67    #[test]
68    fn off_mode_allows_silently() {
69        match evaluate_with(FLAT, DEEP, "rs", GateMode::Off, 15) {
70            GateOutcome::Allow(None) => {}
71            _ => panic!("off mode must allow with no notice"),
72        }
73    }
74
75    #[test]
76    fn warn_mode_allows_with_notice() {
77        match evaluate_with(FLAT, DEEP, "rs", GateMode::Warn, 15) {
78            GateOutcome::Allow(Some(notice)) => assert!(notice.contains("[CODE HEALTH]")),
79            _ => panic!("warn mode must allow with a notice"),
80        }
81    }
82
83    #[test]
84    fn block_mode_blocks_threshold_crossing() {
85        match evaluate_with(FLAT, DEEP, "rs", GateMode::Block, 15) {
86            GateOutcome::Block(reason) => assert!(reason.contains("[CODE HEALTH]")),
87            GateOutcome::Allow(_) => panic!("block mode must block a clean→over edit"),
88        }
89    }
90
91    #[test]
92    fn no_regression_allows_silently() {
93        match evaluate_with(FLAT, FLAT, "rs", GateMode::Block, 15) {
94            GateOutcome::Allow(None) => {}
95            _ => panic!("unchanged complexity must allow silently"),
96        }
97    }
98}