lean_ctx/core/
preservation.rs1use 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}