lean_ctx/core/code_health/
mod.rs1pub mod annotate;
13#[cfg(feature = "tree-sitter")]
14pub mod astutil;
15#[allow(dead_code)]
16pub mod cognitive;
17pub mod delta;
18pub mod fabric;
19pub mod gate;
20pub mod naming;
21pub mod persist;
22pub mod report;
23pub mod scan;
24pub mod score;
25
26pub use cognitive::{FunctionCognitive, cognitive_per_function};
27pub use delta::{cognitive_delta, format_gate_notice, worst_regression};
28pub use naming::{NamingFinding, naming_findings};
29pub use scan::{ProjectHealth, scan_project};
30pub use score::{Hotspot, NavigabilityInputs, NavigabilityScore, grade, navigability};
31
32use serde::Serialize;
33
34pub const DEFAULT_COGNITIVE_THRESHOLD: u32 = 15;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum GateMode {
42 Off,
44 #[default]
46 Warn,
47 Block,
49}
50
51impl GateMode {
52 pub fn parse(value: &str) -> Self {
54 match value.trim().to_ascii_lowercase().as_str() {
55 "off" | "false" | "none" | "disabled" => GateMode::Off,
56 "block" | "hard" | "error" => GateMode::Block,
57 _ => GateMode::Warn,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Default, PartialEq, Serialize)]
66pub struct FileHealth {
67 pub functions: Vec<FunctionCognitive>,
68 pub naming: Vec<NamingFinding>,
69}
70
71impl FileHealth {
72 pub fn over_threshold(&self, threshold: u32) -> impl Iterator<Item = &FunctionCognitive> {
74 self.functions
75 .iter()
76 .filter(move |f| f.cognitive > threshold)
77 }
78
79 pub fn worst_cognitive(&self) -> u32 {
81 self.functions
82 .iter()
83 .map(|f| f.cognitive)
84 .max()
85 .unwrap_or(0)
86 }
87}
88
89pub fn analyze_file(source: &str, extension: &str) -> Option<FileHealth> {
95 let functions = cognitive_per_function(source, extension)?;
96 let naming = naming_findings(source, extension).unwrap_or_default();
97 Some(FileHealth { functions, naming })
98}
99
100#[cfg(all(test, feature = "tree-sitter"))]
101pub mod tests {
102 use super::*;
103
104 #[test]
105 fn analyze_file_combines_signals() {
106 let src = "fn _xfm_q2(a: bool, b: bool) { if a { if b {} } }\n";
107 let health = analyze_file(src, "rs").unwrap();
108 assert_eq!(health.functions.len(), 1);
109 assert_eq!(health.worst_cognitive(), 3);
110 assert_eq!(health.naming.len(), 1, "cryptic name flagged");
111 }
112
113 #[test]
114 fn analyze_file_unsupported_ext_is_none() {
115 assert!(analyze_file("plain text", "txt").is_none());
116 }
117
118 #[test]
119 fn over_threshold_filters() {
120 let src = "fn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
121 let health = analyze_file(src, "rs").unwrap();
122 assert_eq!(health.over_threshold(5).count(), 1);
124 assert_eq!(health.over_threshold(15).count(), 0);
125 }
126}