lean_ctx/core/code_health/
mod.rs1pub mod annotate;
13#[cfg(feature = "tree-sitter")]
14pub(crate) mod astutil;
15pub mod cognitive;
16pub mod coupling;
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 annotate::{ReadAnnotation, annotations_for_file};
27pub use cognitive::{FunctionCognitive, cognitive_per_function};
28pub use coupling::{ModuleCoupling, module_coupling};
29pub use delta::{CognitiveDelta, cognitive_delta, format_gate_notice, worst_regression};
30pub use naming::{NamingFinding, cryptic_reason, naming_findings};
31pub use scan::{FileReport, ProjectHealth, scan_project};
32pub use score::{Hotspot, NavigabilityInputs, NavigabilityScore, grade, navigability};
33
34use serde::Serialize;
35
36pub const DEFAULT_COGNITIVE_THRESHOLD: u32 = 15;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum GateMode {
44 Off,
46 #[default]
48 Warn,
49 Block,
51}
52
53impl GateMode {
54 pub fn parse(value: &str) -> Self {
56 match value.trim().to_ascii_lowercase().as_str() {
57 "off" | "false" | "none" | "disabled" => GateMode::Off,
58 "block" | "hard" | "error" => GateMode::Block,
59 _ => GateMode::Warn,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Serialize)]
68pub struct FileHealth {
69 pub functions: Vec<FunctionCognitive>,
70 pub naming: Vec<NamingFinding>,
71}
72
73impl FileHealth {
74 pub fn over_threshold(&self, threshold: u32) -> impl Iterator<Item = &FunctionCognitive> {
76 self.functions
77 .iter()
78 .filter(move |f| f.cognitive > threshold)
79 }
80
81 pub fn worst_cognitive(&self) -> u32 {
83 self.functions
84 .iter()
85 .map(|f| f.cognitive)
86 .max()
87 .unwrap_or(0)
88 }
89}
90
91pub fn analyze_file(source: &str, extension: &str) -> Option<FileHealth> {
97 let functions = cognitive_per_function(source, extension)?;
98 let naming = naming_findings(source, extension).unwrap_or_default();
99 Some(FileHealth { functions, naming })
100}
101
102#[cfg(all(test, feature = "tree-sitter"))]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn analyze_file_combines_signals() {
108 let src = "fn _xfm_q2(a: bool, b: bool) { if a { if b {} } }\n";
109 let health = analyze_file(src, "rs").unwrap();
110 assert_eq!(health.functions.len(), 1);
111 assert_eq!(health.worst_cognitive(), 3);
112 assert_eq!(health.naming.len(), 1, "cryptic name flagged");
113 }
114
115 #[test]
116 fn analyze_file_unsupported_ext_is_none() {
117 assert!(analyze_file("plain text", "txt").is_none());
118 }
119
120 #[test]
121 fn over_threshold_filters() {
122 let src = "fn deep(a: bool) { if a { if a { if a { if a {} } } } }\n";
123 let health = analyze_file(src, "rs").unwrap();
124 assert_eq!(health.over_threshold(5).count(), 1);
126 assert_eq!(health.over_threshold(15).count(), 0);
127 }
128}