Skip to main content

tokmd_analysis_fun/
lib.rs

1//! # tokmd-analysis-fun
2//!
3//! Novelty/enrichment computations for analysis receipts.
4//!
5//! This crate intentionally contains *non-core* analysis enrichments so they can be
6//! evolved independently from the main `tokmd-analysis` orchestration crate.
7//! In practice this currently supports the eco-label generator used by `AnalysisPreset::Fun`.
8
9use tokmd_analysis_types::{DerivedReport, EcoLabel, FunReport};
10
11/// Build the fun/eco-label portion of an analysis receipt.
12pub fn build_fun_report(derived: &DerivedReport) -> FunReport {
13    let bytes = derived.totals.bytes as u64;
14    let mb = bytes as f64 / (1024.0 * 1024.0);
15    let (label, score) = fun_band(mb);
16
17    FunReport {
18        eco_label: Some(EcoLabel {
19            score,
20            label: label.to_string(),
21            bytes,
22            notes: format!("Size-based eco label ({} MB)", round_to_two(mb)),
23        }),
24    }
25}
26
27fn fun_band(mb: f64) -> (&'static str, f64) {
28    if mb <= 1.0 {
29        ("A", 95.0)
30    } else if mb <= 10.0 {
31        ("B", 80.0)
32    } else if mb <= 50.0 {
33        ("C", 65.0)
34    } else if mb <= 200.0 {
35        ("D", 45.0)
36    } else {
37        ("E", 30.0)
38    }
39}
40
41fn round_to_two(value: f64) -> f64 {
42    (value * 100.0).round() / 100.0
43}
44
45#[cfg(test)]
46mod tests {
47    use super::{build_fun_report, fun_band};
48    use tokmd_analysis_types::{
49        BoilerplateReport, DerivedReport, DerivedTotals, DistributionReport, FileStatRow,
50        IntegrityReport, LangPurityReport, MaxFileReport, NestingReport, PolyglotReport,
51        RateReport, RateRow, RatioReport, RatioRow, ReadingTimeReport, TestDensityReport,
52        TodoReport, TopOffenders,
53    };
54
55    fn tiny_derived(bytes: usize) -> DerivedReport {
56        let zero_row = FileStatRow {
57            path: "small.rs".to_string(),
58            module: "src".to_string(),
59            lang: "Rust".to_string(),
60            code: 0,
61            comments: 0,
62            blanks: 0,
63            lines: 0,
64            bytes,
65            tokens: 0,
66            doc_pct: Some(0.0),
67            bytes_per_line: Some(0.0),
68            depth: 0,
69        };
70
71        DerivedReport {
72            totals: DerivedTotals {
73                files: 1,
74                code: 1,
75                comments: 0,
76                blanks: 0,
77                lines: 1,
78                bytes,
79                tokens: 1,
80            },
81            doc_density: RatioReport {
82                total: RatioRow {
83                    key: "All".to_string(),
84                    numerator: 0,
85                    denominator: 1,
86                    ratio: 0.0,
87                },
88                by_lang: vec![],
89                by_module: vec![],
90            },
91            whitespace: RatioReport {
92                total: RatioRow {
93                    key: "All".to_string(),
94                    numerator: 0,
95                    denominator: 1,
96                    ratio: 0.0,
97                },
98                by_lang: vec![],
99                by_module: vec![],
100            },
101            verbosity: RateReport {
102                total: RateRow {
103                    key: "All".to_string(),
104                    numerator: 0,
105                    denominator: 1,
106                    rate: 0.0,
107                },
108                by_lang: vec![],
109                by_module: vec![],
110            },
111            max_file: MaxFileReport {
112                overall: zero_row.clone(),
113                by_lang: vec![],
114                by_module: vec![],
115            },
116            lang_purity: LangPurityReport { rows: vec![] },
117            nesting: NestingReport {
118                max: 0,
119                avg: 0.0,
120                by_module: vec![],
121            },
122            test_density: TestDensityReport {
123                test_lines: 0,
124                prod_lines: 0,
125                test_files: 0,
126                prod_files: 0,
127                ratio: 0.0,
128            },
129            boilerplate: BoilerplateReport {
130                infra_lines: 0,
131                logic_lines: 0,
132                ratio: 0.0,
133                infra_langs: vec![],
134            },
135            polyglot: PolyglotReport {
136                lang_count: 0,
137                entropy: 0.0,
138                dominant_lang: "unknown".to_string(),
139                dominant_lines: 0,
140                dominant_pct: 0.0,
141            },
142            distribution: DistributionReport {
143                count: 1,
144                min: 1,
145                max: 1,
146                mean: 0.0,
147                median: 0.0,
148                p90: 0.0,
149                p99: 0.0,
150                gini: 0.0,
151            },
152            histogram: Vec::new(),
153            top: TopOffenders {
154                largest_lines: vec![zero_row.clone()],
155                largest_tokens: vec![zero_row.clone()],
156                largest_bytes: vec![zero_row.clone()],
157                least_documented: vec![zero_row.clone()],
158                most_dense: vec![zero_row],
159            },
160            tree: None,
161            reading_time: ReadingTimeReport {
162                minutes: 0.0,
163                lines_per_minute: 0,
164                basis_lines: 0,
165            },
166            context_window: None,
167            cocomo: None,
168            todo: Some(TodoReport {
169                total: 0,
170                density_per_kloc: 0.0,
171                tags: vec![],
172            }),
173            integrity: IntegrityReport {
174                algo: "sha1".to_string(),
175                hash: "placeholder".to_string(),
176                entries: 0,
177            },
178        }
179    }
180
181    #[test]
182    fn fun_grade_boundaries_are_stable() {
183        assert_eq!(fun_band(0.5), ("A", 95.0));
184        assert_eq!(fun_band(10.0), ("B", 80.0));
185        assert_eq!(fun_band(50.0), ("C", 65.0));
186        assert_eq!(fun_band(200.0), ("D", 45.0));
187        assert_eq!(fun_band(200.1), ("E", 30.0));
188    }
189
190    #[test]
191    fn fun_report_contains_notes_and_bytes() {
192        let bytes = 1024 * 1024;
193        let report = build_fun_report(&tiny_derived(bytes));
194        let eco = report.eco_label.expect("eco_label expected");
195
196        assert_eq!(eco.label, "A");
197        assert_eq!(eco.score, 95.0);
198        assert_eq!(eco.bytes, bytes as u64);
199        assert_eq!(eco.notes, "Size-based eco label (1 MB)");
200    }
201}