Skip to main content

lean_ctx/core/code_health/
report.rs

1//! Shared rendering of a [`ProjectHealth`] report (text + JSON).
2//!
3//! Used by both `lean-ctx health` (CLI) and `ctx_quality` (MCP) so the two
4//! surfaces never drift. Deterministic given the report.
5
6use super::ProjectHealth;
7use serde_json::{Value, json};
8
9/// Human-readable project report.
10pub fn text(health: &ProjectHealth, root: &str, threshold: u32, model: &str) -> String {
11    let s = &health.score;
12    let mut out = String::new();
13    out.push_str(&format!("Code Health — {root}\n"));
14    out.push_str(&format!(
15        "  score: {}/100 ({})   cognitive threshold={threshold}\n",
16        s.score,
17        health.grade()
18    ));
19    out.push_str(&format!(
20        "  functions: {}   over-threshold: {}   worst cognitive: {}\n",
21        s.total_functions, s.over_threshold, s.worst_cognitive
22    ));
23    out.push_str(&format!("  naming findings: {}\n", health.naming_count));
24    out.push_str(&format!(
25        "  quality tax (est.): ${:.2}   model={model}",
26        s.estimated_waste_usd
27    ));
28
29    if s.hotspots.is_empty() {
30        out.push_str("\n\n  no hotspots above threshold — clean.");
31        return out;
32    }
33    out.push_str("\n\n  top hotspots (cognitive complexity):");
34    for h in &s.hotspots {
35        out.push_str(&format!(
36            "\n    {}:{}  {}  cc={}",
37            h.file, h.line, h.symbol, h.cognitive
38        ));
39    }
40    out
41}
42
43/// Machine-readable project report.
44pub fn json(health: &ProjectHealth, root: &str) -> Value {
45    let s = &health.score;
46    let hotspots: Vec<Value> = s
47        .hotspots
48        .iter()
49        .map(|h| {
50            json!({
51                "file": h.file,
52                "symbol": h.symbol,
53                "line": h.line,
54                "cognitive": h.cognitive,
55            })
56        })
57        .collect();
58    json!({
59        "root": root,
60        "score": s.score,
61        "grade": health.grade().to_string(),
62        "total_functions": s.total_functions,
63        "over_threshold": s.over_threshold,
64        "worst_cognitive": s.worst_cognitive,
65        "naming_findings": health.naming_count,
66        "estimated_waste_usd": s.estimated_waste_usd,
67        "hotspots": hotspots,
68    })
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::core::code_health::scan::ProjectHealth;
75    use crate::core::code_health::{Hotspot, NavigabilityScore};
76
77    fn sample() -> ProjectHealth {
78        ProjectHealth {
79            score: NavigabilityScore {
80                score: 72,
81                total_functions: 40,
82                over_threshold: 3,
83                worst_cognitive: 28,
84                import_cycles: 0,
85                estimated_waste_usd: 1.234_5,
86                hotspots: vec![Hotspot {
87                    file: "src/a.rs".into(),
88                    symbol: "do_it".into(),
89                    line: 10,
90                    cognitive: 28,
91                }],
92            },
93            files: Vec::new(),
94            naming_count: 2,
95        }
96    }
97
98    #[test]
99    fn text_is_deterministic_and_informative() {
100        let h = sample();
101        let a = text(&h, "repo", 15, "gpt-5.4");
102        let b = text(&h, "repo", 15, "gpt-5.4");
103        assert_eq!(a, b, "report text must be byte-stable (#498)");
104        assert!(a.contains("score: 72/100 (C)"));
105        assert!(a.contains("quality tax (est.): $1.23"));
106        assert!(a.contains("src/a.rs:10  do_it  cc=28"));
107    }
108
109    #[test]
110    fn json_is_deterministic() {
111        let h = sample();
112        let a = json(&h, "repo");
113        let b = json(&h, "repo");
114        assert_eq!(a, b, "report json must be byte-stable (#498)");
115        assert_eq!(a["score"], 72);
116        assert_eq!(a["grade"], "C");
117        assert_eq!(a["hotspots"][0]["symbol"], "do_it");
118    }
119
120    #[test]
121    fn clean_report_says_clean() {
122        let mut h = sample();
123        h.score.hotspots.clear();
124        h.score.over_threshold = 0;
125        let out = text(&h, "repo", 15, "gpt-5.4");
126        assert!(out.contains("no hotspots above threshold"));
127    }
128}