lean_ctx/core/code_health/
report.rs1use super::ProjectHealth;
7use serde_json::{Value, json};
8
9pub 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
43pub 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}