use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::semantic::{SemanticUnit, SemanticToken};
pub struct NoveltyTracker {
seen_patterns: HashMap<String, PatternHistory>,
novelty_threshold: f32,
}
#[derive(Debug, Clone)]
struct PatternHistory {
first_seen: u64, last_seen: u64, occurrence_count: u32, peak_novelty: f32, current_value: f32, }
impl NoveltyTracker {
pub fn new() -> Self {
Self {
seen_patterns: HashMap::new(),
novelty_threshold: 0.1, }
}
pub fn calculate_novelty(&mut self, units: &[SemanticUnit]) -> NoveltyScore {
let fingerprint = self.generate_fingerprint(units);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
match self.seen_patterns.get_mut(&fingerprint) {
None => {
let history = PatternHistory {
first_seen: now,
last_seen: now,
occurrence_count: 1,
peak_novelty: 1.0,
current_value: 1.0,
};
self.seen_patterns.insert(fingerprint.clone(), history);
NoveltyScore {
value: 1.0,
is_novel: true,
occurrence: 1,
decay_factor: 0.0,
classification: NoveltyClass::Revolutionary,
}
}
Some(history) => {
history.occurrence_count += 1;
history.last_seen = now;
let time_since_first = (now - history.first_seen) as f32;
let time_decay = 1.0 / (1.0 + time_since_first / 86400.0);
let repetition_decay = 1.0 / (history.occurrence_count as f32).sqrt();
let decay_factor = time_decay * repetition_decay;
history.current_value = history.peak_novelty * decay_factor;
let classification = match history.current_value {
v if v > 0.8 => NoveltyClass::Fresh,
v if v > 0.5 => NoveltyClass::Interesting,
v if v > 0.2 => NoveltyClass::Familiar,
v if v > self.novelty_threshold => NoveltyClass::Stale,
_ => NoveltyClass::BackgroundNoise,
};
NoveltyScore {
value: history.current_value,
is_novel: false,
occurrence: history.occurrence_count,
decay_factor,
classification,
}
}
}
}
fn generate_fingerprint(&self, units: &[SemanticUnit]) -> String {
let mut tokens = Vec::new();
for unit in units {
for token in &unit.tokens {
tokens.push(*token as u8);
}
}
tokens.sort();
tokens.dedup();
tokens.iter()
.map(|t| format!("{:02x}", t))
.collect::<Vec<_>>()
.join("-")
}
pub fn get_top_novel(&self, limit: usize) -> Vec<(String, f32)> {
let mut patterns: Vec<_> = self.seen_patterns
.iter()
.map(|(fp, hist)| (fp.clone(), hist.current_value))
.collect();
patterns.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
patterns.truncate(limit);
patterns
}
pub fn apply_temporal_decay(&mut self, decay_rate: f32) {
for history in self.seen_patterns.values_mut() {
history.current_value *= (1.0 - decay_rate);
}
self.seen_patterns.retain(|_, hist| {
hist.current_value > self.novelty_threshold
});
}
}
#[derive(Debug, Clone)]
pub struct NoveltyScore {
pub value: f32, pub is_novel: bool, pub occurrence: u32, pub decay_factor: f32, pub classification: NoveltyClass,
}
#[derive(Debug, Clone, PartialEq)]
pub enum NoveltyClass {
Revolutionary, Fresh, Interesting, Familiar, Stale, BackgroundNoise, }
impl NoveltyClass {
pub fn emoji(&self) -> &str {
match self {
Self::Revolutionary => "💎", Self::Fresh => "🌟", Self::Interesting => "💡", Self::Familiar => "📝", Self::Stale => "📰", Self::BackgroundNoise => "💤", }
}
}
pub fn generate_novelty_dns(score: &NoveltyScore, base_domain: &str) -> String {
let novelty_prefix = match score.classification {
NoveltyClass::Revolutionary => "revolutionary",
NoveltyClass::Fresh => "fresh",
NoveltyClass::Interesting => "interesting",
NoveltyClass::Familiar => "familiar",
NoveltyClass::Stale => "stale",
NoveltyClass::BackgroundNoise => "noise",
};
format!("{}.{}.q7.is", novelty_prefix, base_domain)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::semantic::{SemanticToken, SemanticUnit};
#[test]
fn test_novelty_decay() {
let mut tracker = NoveltyTracker::new();
let unit = SemanticUnit {
tokens: vec![SemanticToken::ActionLearning, SemanticToken::ContextRust],
metadata: HashMap::new(),
intensity: 0.8,
};
let score1 = tracker.calculate_novelty(&[unit.clone()]);
assert_eq!(score1.classification, NoveltyClass::Revolutionary);
assert_eq!(score1.value, 1.0);
assert!(score1.is_novel);
let score2 = tracker.calculate_novelty(&[unit.clone()]);
assert!(!score2.is_novel);
assert!(score2.value < 1.0);
assert_eq!(score2.occurrence, 2);
for _ in 0..100 {
tracker.calculate_novelty(&[unit.clone()]);
}
let score_final = tracker.calculate_novelty(&[unit.clone()]);
assert!(score_final.value < 0.2);
assert!(score_final.occurrence > 100);
}
#[test]
fn test_novelty_classification() {
let classifications = [
(1.0, NoveltyClass::Revolutionary, "💎"),
(0.9, NoveltyClass::Fresh, "🌟"),
(0.6, NoveltyClass::Interesting, "💡"),
(0.3, NoveltyClass::Familiar, "📝"),
(0.15, NoveltyClass::Stale, "📰"),
(0.05, NoveltyClass::BackgroundNoise, "💤"),
];
for (_, class, emoji) in classifications {
assert_eq!(class.emoji(), emoji);
}
}
}