Skip to main content

lean_ctx/core/
cognitive_load.rs

1//! Cognitive Load Theory–style scoring for code context (intrinsic / extraneous / germane).
2
3/// Decomposed load estimates in arbitrary comparable units (then normalized).
4#[derive(Debug, Clone, PartialEq)]
5pub struct CognitiveLoadScore {
6    pub intrinsic: f64,
7    pub extraneous: f64,
8    pub germane: f64,
9    pub total: f64,
10    pub recommendation: String,
11}
12
13fn max_brace_depth(s: &str) -> usize {
14    let mut d = 0usize;
15    let mut maxd = 0usize;
16    let mut line_comment = false;
17    let mut block = 0usize;
18    let chars: Vec<char> = s.chars().collect();
19    let mut i = 0;
20    while i < chars.len() {
21        let c = chars[i];
22        if line_comment {
23            if c == '\n' {
24                line_comment = false;
25            }
26            i += 1;
27            continue;
28        }
29        if block > 0 {
30            if c == '/' && i + 1 < chars.len() && chars[i + 1] == '*' {
31                block += 1;
32                i += 2;
33                continue;
34            }
35            if c == '*' && i + 1 < chars.len() && chars[i + 1] == '/' {
36                block = block.saturating_sub(1);
37                i += 2;
38                continue;
39            }
40            i += 1;
41            continue;
42        }
43        if c == '/' && i + 1 < chars.len() {
44            if chars[i + 1] == '/' {
45                line_comment = true;
46                i += 2;
47                continue;
48            }
49            if chars[i + 1] == '*' {
50                block += 1;
51                i += 2;
52                continue;
53            }
54        }
55        match c {
56            '{' | '(' | '[' => {
57                d += 1;
58                maxd = maxd.max(d);
59            }
60            '}' | ')' | ']' => {
61                d = d.saturating_sub(1);
62            }
63            _ => {}
64        }
65        i += 1;
66    }
67    maxd
68}
69
70fn intrinsic_raw(content: &str) -> f64 {
71    let depth = max_brace_depth(content) as f64;
72    let lets = content.matches("let ").count() as f64;
73    let ctrl = ["for ", "while ", "match ", "if ", "else", "loop {"]
74        .iter()
75        .map(|k| content.matches(*k).count() as f64)
76        .sum::<f64>();
77    depth * 0.22 + lets * 0.06 + ctrl * 0.05
78}
79
80fn extraneous_raw(content: &str) -> f64 {
81    let lines: Vec<&str> = content.lines().collect();
82    let n = lines.len().max(1) as f64;
83    let blank = lines.iter().filter(|l| l.trim().is_empty()).count() as f64 / n;
84    let mut comment_lines = 0usize;
85    let mut block = false;
86    for line in &lines {
87        let t = line.trim_start();
88        if block {
89            comment_lines += 1;
90            if t.contains("*/") {
91                block = false;
92            }
93            continue;
94        }
95        if t.starts_with("//") {
96            comment_lines += 1;
97        } else if t.starts_with("/*") {
98            comment_lines += 1;
99            block = !t.contains("*/");
100        }
101    }
102    let comment_ratio = comment_lines as f64 / n;
103    let boiler = [
104        "todo!",
105        "unwrap()",
106        "expect(",
107        "derive(Default)",
108        "println!",
109        "#[",
110    ]
111    .iter()
112    .map(|p| content.matches(*p).count() as f64)
113    .sum::<f64>();
114    comment_ratio * 1.1 + blank * 0.9 + boiler * 0.08
115}
116
117fn camel_type_tokens(content: &str) -> usize {
118    content
119        .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
120        .filter(|tok| tok.len() >= 2 && tok.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
121        .count()
122}
123
124fn germane_raw(content: &str) -> f64 {
125    let n_types = camel_type_tokens(content);
126    let apiish = content.matches('.').count() as f64 * 0.02;
127    let algo = [
128        "sort",
129        "binary",
130        "hash",
131        "graph",
132        "dfs",
133        "bfs",
134        "heap",
135        "recursive",
136    ]
137    .iter()
138    .map(|k| content.to_ascii_lowercase().matches(k).count() as f64)
139    .sum::<f64>();
140    n_types as f64 * 0.09 + apiish + algo * 0.11
141}
142
143fn norm(x: f64) -> f64 {
144    (x / (x + 1.2)).min(1.0)
145}
146
147fn recommend(intr: f64, extr: f64, germ: f64) -> String {
148    if extr >= intr * 1.35 && extr >= 0.28 {
149        return "entropy or aggressive — dominant extraneous noise".to_string();
150    }
151    if intr >= germ * 1.2 && intr >= 0.32 {
152        return "signatures or map — high intrinsic complexity".to_string();
153    }
154    if germ >= intr * 1.05 && germ >= 0.28 {
155        return "full or reference — strong germane / API signal".to_string();
156    }
157    if extr >= 0.22 && extr >= intr {
158        return "aggressive — moderate clutter".to_string();
159    }
160    "auto — balanced load profile".to_string()
161}
162
163/// Score cognitive-load dimensions and suggest a compression mode family.
164pub fn score_cognitive_load(content: &str) -> CognitiveLoadScore {
165    let i = norm(intrinsic_raw(content));
166    let e = norm(extraneous_raw(content));
167    let g = norm(germane_raw(content));
168    let total = i + e + g;
169    let recommendation = recommend(i, e, g);
170    CognitiveLoadScore {
171        intrinsic: i,
172        extraneous: e,
173        germane: g,
174        total,
175        recommendation,
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn simple_script_low_scores() {
185        let s = score_cognitive_load("fn main() {}\n");
186        assert!(s.total < 2.5);
187        assert!(s.intrinsic > 0.0);
188    }
189
190    #[test]
191    fn nested_logic_raises_intrinsic() {
192        let code = r"
193fn x() {
194    if true {
195        for _ in 0..10 {
196            while false {}
197        }
198    }
199}
200";
201        let s = score_cognitive_load(code);
202        assert!(s.intrinsic > score_cognitive_load("fn y() {}").intrinsic);
203    }
204
205    #[test]
206    fn comments_and_blank_lines_raise_extraneous() {
207        let noisy = "// head\n\n// more\nfn z() {}\n";
208        let plain = "fn z() {}\n";
209        assert!(score_cognitive_load(noisy).extraneous > score_cognitive_load(plain).extraneous);
210    }
211
212    #[test]
213    fn types_and_algo_boost_germane() {
214        let api = "fn k(a: HashMap<String, Vec<MyDto>>) { a.sort(); }\n";
215        let s = score_cognitive_load(api);
216        assert!(s.germane > 0.15);
217    }
218
219    #[test]
220    fn recommendation_prefers_entropy_on_noise() {
221        let wall = "// ...\n".repeat(40);
222        let s = score_cognitive_load(&(wall + "\nfn q() {}\n"));
223        assert!(
224            s.recommendation.contains("entropy") || s.recommendation.contains("aggressive"),
225            "{}",
226            s.recommendation
227        );
228    }
229}