Skip to main content

lean_ctx/core/
preservation.rs

1use crate::core::deps;
2use crate::core::signatures;
3
4#[derive(Debug, Clone, Default)]
5pub struct PreservationScore {
6    pub functions_total: usize,
7    pub functions_preserved: usize,
8    pub exports_total: usize,
9    pub exports_preserved: usize,
10    pub imports_total: usize,
11    pub imports_preserved: usize,
12}
13
14impl PreservationScore {
15    pub fn function_rate(&self) -> f64 {
16        if self.functions_total == 0 {
17            return 1.0;
18        }
19        self.functions_preserved as f64 / self.functions_total as f64
20    }
21
22    pub fn export_rate(&self) -> f64 {
23        if self.exports_total == 0 {
24            return 1.0;
25        }
26        self.exports_preserved as f64 / self.exports_total as f64
27    }
28
29    pub fn import_rate(&self) -> f64 {
30        if self.imports_total == 0 {
31            return 1.0;
32        }
33        self.imports_preserved as f64 / self.imports_total as f64
34    }
35
36    pub fn overall(&self) -> f64 {
37        let total = self.functions_total + self.exports_total + self.imports_total;
38        if total == 0 {
39            return 1.0;
40        }
41        let preserved = self.functions_preserved + self.exports_preserved + self.imports_preserved;
42        preserved as f64 / total as f64
43    }
44}
45
46pub fn measure(raw_content: &str, compressed_output: &str, ext: &str) -> PreservationScore {
47    let sigs = signatures::extract_signatures(raw_content, ext);
48    let dep_info = deps::extract_deps(raw_content, ext);
49
50    let function_names: Vec<&str> = sigs
51        .iter()
52        .filter(|s| matches!(s.kind, "fn" | "method"))
53        .map(|s| s.name.as_str())
54        .collect();
55
56    let class_names: Vec<&str> = sigs
57        .iter()
58        .filter(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum"))
59        .map(|s| s.name.as_str())
60        .collect();
61
62    let all_symbols: Vec<&str> = function_names
63        .iter()
64        .chain(class_names.iter())
65        .copied()
66        .collect();
67
68    let functions_preserved = all_symbols
69        .iter()
70        .filter(|name| !name.is_empty() && compressed_output.contains(*name))
71        .count();
72
73    let exports_preserved = dep_info
74        .exports
75        .iter()
76        .filter(|e| !e.is_empty() && compressed_output.contains(e.as_str()))
77        .count();
78
79    let imports_preserved = dep_info
80        .imports
81        .iter()
82        .filter(|i| !i.is_empty() && compressed_output.contains(i.as_str()))
83        .count();
84
85    PreservationScore {
86        functions_total: all_symbols.len(),
87        functions_preserved,
88        exports_total: dep_info.exports.len(),
89        exports_preserved,
90        imports_total: dep_info.imports.len(),
91        imports_preserved,
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn measure_reports_full_preservation_when_all_symbols_present() {
101        let raw = r"
102use crate::core::deps;
103
104pub fn build_graph() -> usize { 1 }
105pub struct GraphNode;
106";
107        let compressed = "build_graph GraphNode crate::core::deps";
108
109        let score = measure(raw, compressed, "rs");
110        assert_eq!(score.functions_total, 2);
111        assert_eq!(score.functions_preserved, 2);
112        assert_eq!(score.exports_total, 2);
113        assert_eq!(score.exports_preserved, 2);
114        assert_eq!(score.imports_total, 1);
115        assert_eq!(score.imports_preserved, 1);
116        assert!((score.overall() - 1.0).abs() < f64::EPSILON);
117    }
118
119    #[test]
120    fn measure_detects_missing_symbols() {
121        let raw = r"
122use crate::core::deps;
123pub fn build_graph() -> usize { 1 }
124pub struct GraphNode;
125";
126        let compressed = "build_graph";
127
128        let score = measure(raw, compressed, "rs");
129        assert_eq!(score.functions_total, 2);
130        assert_eq!(score.functions_preserved, 1);
131        assert_eq!(score.exports_total, 2);
132        assert_eq!(score.exports_preserved, 1);
133        assert_eq!(score.imports_total, 1);
134        assert_eq!(score.imports_preserved, 0);
135        assert!(score.overall() < 1.0);
136    }
137
138    #[test]
139    fn measure_defaults_to_full_when_no_trackable_entities() {
140        let raw = "plain text without code signatures";
141        let compressed = "short summary";
142        let score = measure(raw, compressed, "txt");
143
144        assert_eq!(score.functions_total, 0);
145        assert_eq!(score.exports_total, 0);
146        assert_eq!(score.imports_total, 0);
147        assert!((score.function_rate() - 1.0).abs() < f64::EPSILON);
148        assert!((score.export_rate() - 1.0).abs() < f64::EPSILON);
149        assert!((score.import_rate() - 1.0).abs() < f64::EPSILON);
150        assert!((score.overall() - 1.0).abs() < f64::EPSILON);
151    }
152}