use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
use trusty_common::memory_core::decay::DecayConfig;
use trusty_common::memory_core::palace::Drawer;
use trusty_common::memory_core::store::rooms::list_room_summaries;
use trusty_common::memory_core::PalaceRegistry;
use super::log_index::InjectionIndex;
use super::signals::Signal;
use crate::commands::prompt_context::format::drawer_preview;
use crate::commands::store_snapshot::with_store_copy;
#[derive(Debug, Clone)]
pub struct CandidateRow {
pub palace: String,
pub drawer_id: uuid::Uuid,
pub room: String,
pub excerpt: String,
pub age_days: f32,
pub injections: u64,
pub share_of_turns: f64,
pub importance: f32,
pub effective_importance: f32,
pub has_expiry: bool,
pub signals: Vec<Signal>,
pub collision_peers: Option<usize>,
pub content_digest: String,
}
#[derive(Debug, Clone)]
pub struct PalaceOutcome {
pub palace: String,
pub drawers_read: usize,
pub error: Option<String>,
}
#[derive(Debug, Default)]
pub struct Census {
pub rows: Vec<CandidateRow>,
pub outcomes: Vec<PalaceOutcome>,
pub drawers_total: usize,
}
pub fn build_census(
registry_dir: &Path,
index: &InjectionIndex,
palace_filter: Option<&str>,
min_injections: u64,
) -> Result<Census> {
let palaces = PalaceRegistry::list_palaces(registry_dir)
.with_context(|| format!("list palaces under {}", registry_dir.display()))?;
let mut census = Census::default();
let decay = DecayConfig::default();
let window_start = index.stats.earliest;
for palace in palaces {
let slug = palace.id.0.clone();
if palace_filter.is_some_and(|f| f != slug) {
continue;
}
match read_palace_drawers(&palace.data_dir) {
Ok((drawers, rooms)) => {
census.outcomes.push(PalaceOutcome {
palace: slug.clone(),
drawers_read: drawers.len(),
error: None,
});
census.drawers_total += drawers.len();
let total = index.total_injections(&slug);
for drawer in &drawers {
let excerpt = drawer_preview(drawer.content());
let injections = index.injections_for(&slug, &excerpt);
if injections < min_injections {
continue;
}
let age_days = DecayConfig::age_days(drawer.created_at);
census.rows.push(CandidateRow {
palace: slug.clone(),
drawer_id: drawer.id,
room: room_label(&rooms, drawer),
excerpt,
age_days,
injections,
share_of_turns: if total == 0 {
0.0
} else {
injections as f64 / total as f64
},
importance: drawer.importance,
effective_importance: decay.effective_importance(
drawer.importance,
age_days,
0.0,
),
has_expiry: drawer.expires_at.is_some(),
signals: super::signals::observe(drawer, age_days, window_start),
collision_peers: None,
content_digest: content_digest(drawer.content()),
});
}
}
Err(e) => census.outcomes.push(PalaceOutcome {
palace: slug,
drawers_read: 0,
error: Some(format!("{e:#}")),
}),
}
}
mark_excerpt_collisions(&mut census.rows);
census.rows.sort_by(|a, b| {
b.injections
.cmp(&a.injections)
.then_with(|| b.age_days.total_cmp(&a.age_days))
});
Ok(census)
}
fn mark_excerpt_collisions(rows: &mut [CandidateRow]) {
let mut counts: HashMap<(&str, &str), usize> = HashMap::new();
for row in rows.iter() {
*counts
.entry((row.palace.as_str(), row.excerpt.as_str()))
.or_default() += 1;
}
let shared: HashMap<(String, String), usize> = counts
.into_iter()
.filter(|(_, n)| *n > 1)
.map(|((p, e), n)| ((p.to_string(), e.to_string()), n))
.collect();
if shared.is_empty() {
return;
}
for row in rows.iter_mut() {
if let Some(n) = shared.get(&(row.palace.clone(), row.excerpt.clone())) {
row.collision_peers = Some(n - 1);
}
}
}
fn content_digest(content: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut hasher);
format!("{:08x}", hasher.finish() as u32)
}
fn read_palace_drawers(
data_dir: &Path,
) -> Result<(
Vec<Drawer>,
Vec<trusty_common::memory_core::store::rooms::RoomSummary>,
)> {
let read = with_store_copy(data_dir, &std::env::temp_dir(), |store| {
let drawers = store.load_drawers().context("load drawers")?;
let store = Arc::new(store);
let rooms = list_room_summaries(&store).unwrap_or_default();
Ok((drawers, rooms))
})?;
Ok(read.unwrap_or_default())
}
fn room_label(
rooms: &[trusty_common::memory_core::store::rooms::RoomSummary],
drawer: &Drawer,
) -> String {
rooms
.iter()
.find(|r| r.id == drawer.room_id)
.map(|r| r.label.clone())
.unwrap_or_else(|| short_id(&drawer.room_id))
}
pub fn short_id(id: &uuid::Uuid) -> String {
id.to_string().chars().take(8).collect()
}