1use std::sync::OnceLock;
14
15use super::tokens::encode_tokens;
16
17static VOCAB_LOG_PROBS: OnceLock<Vec<f64>> = OnceLock::new();
18
19fn get_vocab_log_probs() -> &'static Vec<f64> {
23 VOCAB_LOG_PROBS.get_or_init(|| {
24 let vocab_size = 200_000usize;
25 let h_n: f64 = (1..=vocab_size).map(|r| 1.0 / r as f64).sum();
26 (0..vocab_size)
27 .map(|rank| {
28 let r = rank + 1; let p = 1.0 / (r as f64 * h_n);
30 -p.log2()
31 })
32 .collect()
33 })
34}
35
36pub fn line_surprise(text: &str) -> f64 {
44 let tokens = encode_tokens(text);
45 if tokens.is_empty() {
46 return 0.0;
47 }
48 let log_probs = get_vocab_log_probs();
49 let max_id = log_probs.len();
50
51 let total: f64 = tokens
52 .iter()
53 .map(|&t| {
54 let id = t as usize;
55 if id < max_id {
56 log_probs[id]
57 } else {
58 17.6 }
60 })
61 .sum();
62
63 total / tokens.len() as f64
64}
65
66const DROP_SHARE_ORIGIN: f64 = 0.4;
68const DROP_SHARE_SLOPE: f64 = 0.4;
70const MAX_DROP_SHARE: f64 = 0.6;
72
73#[must_use]
79pub fn drop_share(entropy_threshold: f64) -> f64 {
80 ((entropy_threshold - DROP_SHARE_ORIGIN) * DROP_SHARE_SLOPE).clamp(0.0, MAX_DROP_SHARE)
81}
82
83#[must_use]
94pub fn surprise_floor<'a>(lines: impl IntoIterator<Item = &'a str>, entropy_threshold: f64) -> f64 {
95 let mut scores: Vec<f64> = lines
96 .into_iter()
97 .map(str::trim)
98 .filter(|t| t.len() >= 3)
99 .map(line_surprise)
100 .collect();
101 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
102 let cut = (scores.len() as f64 * drop_share(entropy_threshold)).floor() as usize;
103 if cut == 0 {
104 return f64::NEG_INFINITY;
105 }
106 scores.sort_by(f64::total_cmp);
107 scores[cut.min(scores.len() - 1)]
108}
109
110pub fn should_keep_line(trimmed: &str, surprise_floor: f64) -> bool {
116 trimmed.len() < 3 || line_surprise(trimmed) >= surprise_floor
117}
118
119const KEPT_WINDOW: usize = 64;
141const REDUNDANCY_COSINE: f64 = 0.92;
143
144#[derive(Default)]
146pub struct ScoringCtx {
147 kept: std::collections::VecDeque<Vec<f32>>,
148}
149
150impl ScoringCtx {
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn push_kept(&mut self, embedding: Vec<f32>) {
156 if self.kept.len() >= KEPT_WINDOW {
157 self.kept.pop_front();
158 }
159 self.kept.push_back(embedding);
160 }
161
162 pub fn max_cosine(&self, emb: &[f32]) -> f64 {
164 self.kept
165 .iter()
166 .map(|k| cosine(k, emb))
167 .fold(0.0_f64, f64::max)
168 }
169}
170
171fn cosine(a: &[f32], b: &[f32]) -> f64 {
172 if a.is_empty() || a.len() != b.len() {
173 return 0.0;
174 }
175 let mut dot = 0.0_f64;
176 let mut na = 0.0_f64;
177 let mut nb = 0.0_f64;
178 for (x, y) in a.iter().zip(b.iter()) {
179 dot += f64::from(*x) * f64::from(*y);
180 na += f64::from(*x) * f64::from(*x);
181 nb += f64::from(*y) * f64::from(*y);
182 }
183 if na <= 0.0 || nb <= 0.0 {
184 return 0.0;
185 }
186 dot / (na.sqrt() * nb.sqrt())
187}
188
189pub fn should_keep_line_semantic(
195 trimmed: &str,
196 surprise_floor: f64,
197 embed: &dyn Fn(&str) -> Option<Vec<f32>>,
198 ctx: &mut ScoringCtx,
199) -> bool {
200 if !should_keep_line(trimmed, surprise_floor) {
201 return false;
202 }
203 let Some(emb) = embed(trimmed) else {
204 return true;
205 };
206 if ctx.max_cosine(&emb) >= REDUNDANCY_COSINE {
207 return false;
208 }
209 ctx.push_kept(emb);
210 true
211}
212
213#[cfg(test)]
214pub mod tests {
215 use super::*;
216
217 #[test]
218 fn common_code_has_low_surprise() {
219 let common = "let x = 1;";
220 let s = line_surprise(common);
221 assert!(s > 0.0, "surprise should be positive");
222 }
223
224 #[test]
225 fn rare_identifiers_have_higher_surprise() {
226 let common = "let x = 1;";
227 let rare = "let zygomorphic_validator = XenolithProcessor::new();";
228 assert!(
229 line_surprise(rare) > line_surprise(common),
230 "rare identifiers should have higher surprise"
231 );
232 }
233
234 #[test]
235 fn empty_returns_zero() {
236 assert_eq!(line_surprise(""), 0.0);
237 }
238
239 #[test]
240 fn classify_surprise_is_consistent() {
241 let simple = "let x = 1;";
242 let complex = "ZygomorphicXenolithValidator::process_quantum_state(&mut ctx)";
243 let s_simple = line_surprise(simple);
244 let s_complex = line_surprise(complex);
245 assert!(
246 s_complex > s_simple,
247 "rare identifiers ({s_complex}) should have higher surprise than common code ({s_simple})"
248 );
249 }
250
251 #[test]
252 fn should_keep_preserves_rare_lines() {
253 let rare = "ZygomorphicValidator::process_xenolith(&mut state)";
254 let common = [
255 "let a = 1;",
256 "let b = 2;",
257 "x += 1;",
258 "return None;",
259 "Ok(())",
260 "let mut v = Vec::new();",
261 "if x { y }",
262 "self.len()",
263 "use std::fmt;",
264 ];
265 let mut file = common.to_vec();
266 file.push(rare);
267 let floor = surprise_floor(file.iter().copied(), 1.9);
269 assert!(should_keep_line(rare, floor), "rare line must survive");
270 let shed = common
271 .iter()
272 .filter(|l| !should_keep_line(l, floor))
273 .count();
274 assert!(shed >= 1, "boilerplate is shed first (floor {floor})");
275 assert!(should_keep_line("}", floor), "sub-3-byte lines always stay");
276 }
277
278 #[test]
281 fn drop_share_is_monotonic_and_bounded() {
282 assert_eq!(drop_share(0.0), 0.0);
283 assert_eq!(drop_share(DROP_SHARE_ORIGIN), 0.0);
284 let mut prev = drop_share(0.6);
285 for step in 1..=13 {
286 let t = 0.6 + 0.1 * f64::from(step);
287 let share = drop_share(t);
288 assert!(share > prev, "{t}: {share} must exceed {prev}");
289 prev = share;
290 }
291 assert_eq!(drop_share(10.0), MAX_DROP_SHARE);
292 }
293
294 #[test]
295 fn surprise_floor_cuts_the_requested_share() {
296 let lines: Vec<String> = (0..40)
297 .map(|i| format!("let value_{i} = compute_{}(input);", "x".repeat(i % 7)))
298 .collect();
299 let refs = || lines.iter().map(String::as_str);
300 for t in [0.6, 1.0, 1.4, 1.9] {
301 let floor = surprise_floor(refs(), t);
302 let dropped = refs().filter(|l| !should_keep_line(l, floor)).count();
303 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
304 let cap = (40.0 * drop_share(t)).floor() as usize;
305 assert!(dropped <= cap, "t={t}: dropped {dropped} > cap {cap}");
306 assert_eq!(floor, surprise_floor(refs(), t), "deterministic (#498)");
307 }
308 assert_eq!(surprise_floor(refs(), 0.4), f64::NEG_INFINITY);
309 assert_eq!(surprise_floor(std::iter::empty(), 1.9), f64::NEG_INFINITY);
310 }
311
312 #[allow(clippy::unnecessary_wraps)] fn bow_embed(line: &str) -> Option<Vec<f32>> {
317 let mut v = vec![0.0f32; 64];
318 for tok in line.to_lowercase().split(|c: char| !c.is_alphanumeric()) {
319 if tok.len() < 2 {
320 continue;
321 }
322 let mut h = 0u64;
323 for b in tok.bytes() {
324 h = h.wrapping_mul(31).wrapping_add(u64::from(b));
325 }
326 v[(h % 64) as usize] += 1.0;
327 }
328 Some(v)
329 }
330
331 #[test]
332 fn semantic_dedup_drops_near_duplicate_kept_lines() {
333 let mut ctx = ScoringCtx::new();
334 let embed = |s: &str| bow_embed(s);
335 let a = "fn validate_user_payload(payload: &UserPayload) -> Result<(), ValidationError>";
336 let b = "fn validate_user_payload(payload: &UserPayload) -> Result<(), ValidationError> ";
338 let c = "const MAX_RETRY_BACKOFF_MS: u64 = 30_000;";
339
340 assert!(should_keep_line_semantic(a, 0.5, &embed, &mut ctx));
341 assert!(
342 !should_keep_line_semantic(b, 0.5, &embed, &mut ctx),
343 "near-identical line must be dropped as redundant"
344 );
345 assert!(
346 should_keep_line_semantic(c, 0.5, &embed, &mut ctx),
347 "distinct line stays"
348 );
349 }
350
351 #[test]
352 fn no_embedder_is_identical_to_zipf_path() {
353 let mut ctx = ScoringCtx::new();
354 let none = |_: &str| None;
355 for line in [
356 "fn main() { run(); }",
357 "let x = 1;",
358 "ZygomorphicXenolithValidator::process_quantum_state(&mut ctx)",
359 "// plain comment",
360 ] {
361 assert_eq!(
362 should_keep_line_semantic(line, 1.0, &none, &mut ctx),
363 should_keep_line(line, 1.0),
364 "without embeddings the decision must match the Zipf path: {line}"
365 );
366 }
367 }
368
369 #[test]
370 fn kept_window_is_bounded() {
371 let mut ctx = ScoringCtx::new();
372 for i in 0..200 {
373 ctx.push_kept(vec![i as f32; 8]);
374 }
375 assert!(ctx.kept.len() <= 64);
376 }
377
378 #[test]
379 fn cosine_handles_degenerate_inputs() {
380 assert_eq!(cosine(&[], &[]), 0.0);
381 assert_eq!(cosine(&[1.0], &[1.0, 2.0]), 0.0);
382 assert_eq!(cosine(&[0.0, 0.0], &[0.0, 0.0]), 0.0);
383 assert!((cosine(&[1.0, 0.0], &[1.0, 0.0]) - 1.0).abs() < 1e-9);
384 }
385}