use std::collections::{HashMap, HashSet, VecDeque};
pub const MAX_TURNS: usize = 32;
#[derive(Debug, Clone)]
pub struct TurnRecord {
pub turn_id: String,
pub query_fingerprint: String,
pub injected: Vec<InjectedChunk>,
pub suppressed: Vec<SuppressedRecall>,
pub skipped: bool,
}
#[derive(Debug, Clone)]
pub struct InjectedChunk {
pub chunk_id: String,
pub path: String,
pub score: f64,
}
#[derive(Debug, Clone)]
pub struct SuppressedRecall {
pub chunk_id: String,
pub path: String,
pub score: f64,
pub reason: SuppressionReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SuppressionReason {
SameChunkRecentlyInjected,
SameNoteRecentlyInjected,
BelowConfidenceGate,
}
#[derive(Debug)]
pub struct TurnLedger {
turns: VecDeque<TurnRecord>,
injected_chunks: HashMap<String, HashSet<String>>,
injected_notes: HashMap<String, HashSet<String>>,
}
impl Default for TurnLedger {
fn default() -> Self {
Self::new()
}
}
impl TurnLedger {
#[must_use]
pub fn new() -> Self {
Self {
turns: VecDeque::with_capacity(MAX_TURNS),
injected_chunks: HashMap::new(),
injected_notes: HashMap::new(),
}
}
pub fn record_turn(&mut self, record: TurnRecord) {
if self.turns.len() >= MAX_TURNS
&& let Some(evicted) = self.turns.pop_front()
{
self.remove_turn_refs(&evicted.turn_id);
}
for chunk in &record.injected {
self.injected_chunks
.entry(chunk.chunk_id.clone())
.or_default()
.insert(record.turn_id.clone());
self.injected_notes
.entry(chunk.path.clone())
.or_default()
.insert(record.turn_id.clone());
}
self.turns.push_back(record);
}
fn remove_turn_refs(&mut self, turn_id: &str) {
for ids in self.injected_chunks.values_mut() {
ids.remove(turn_id);
}
for ids in self.injected_notes.values_mut() {
ids.remove(turn_id);
}
}
#[must_use]
pub fn chunk_injected_in_last_n(&self, chunk_id: &str, n: usize) -> usize {
let recent_ids: HashSet<&str> = self
.turns
.iter()
.rev()
.take(n)
.map(|t| t.turn_id.as_str())
.collect();
self.injected_chunks.get(chunk_id).map_or(0, |ids| {
ids.iter()
.filter(|id| recent_ids.contains(id.as_str()))
.count()
})
}
#[must_use]
pub fn note_injected_in_last_n(&self, path: &str, n: usize) -> usize {
let recent_ids: HashSet<&str> = self
.turns
.iter()
.rev()
.take(n)
.map(|t| t.turn_id.as_str())
.collect();
self.injected_notes.get(path).map_or(0, |ids| {
ids.iter()
.filter(|id| recent_ids.contains(id.as_str()))
.count()
})
}
#[must_use]
pub fn turns_since_chunk_last_injected(&self, chunk_id: &str) -> Option<usize> {
let turn_ids = self.injected_chunks.get(chunk_id)?;
self.turns
.iter()
.rev()
.position(|t| turn_ids.contains(t.turn_id.as_str()))
}
#[must_use]
pub fn turns_since_note_last_injected(&self, path: &str) -> Option<usize> {
let turn_ids = self.injected_notes.get(path)?;
self.turns
.iter()
.rev()
.position(|t| turn_ids.contains(t.turn_id.as_str()))
}
#[must_use]
pub fn last_fingerprint(&self) -> Option<&str> {
self.turns.back().map(|t| t.query_fingerprint.as_str())
}
}