use anyhow::{anyhow, Result};
use serde_json::{json, Value};
use uuid::Uuid;
use super::helpers::open_palace_handle;
use super::palace_index::{resolve_palace_or_index, PalaceScope};
use crate::AppState;
fn coverage(handle: &trusty_common::memory_core::PalaceHandle) -> Value {
let health = handle.embed_health();
json!({
"palace": health.palace_id,
"drawer_count": health.drawer_count,
"vector_count": health.vector_count,
"missing": health.missing_vector_ids.len(),
"alias_audit": audit_state(&health.alias_audit),
"alias_audit_error": health.alias_audit.unavailable_reason(),
"vector_key_rows": health.alias_audit.counts().map(|(rows, _)| rows),
"distinct_vector_ids": health.alias_audit.counts().map(|(_, ids)| ids),
"aliased": health.alias_audit.aliased_drawer_ids().map(<[Uuid]>::len),
"embedder_ready": health.embedder_ready,
"healthy": health.is_healthy(),
})
}
fn audit_state(audit: &trusty_common::memory_core::retrieval::AliasAudit) -> &'static str {
if audit.unavailable_reason().is_some() {
"unavailable"
} else if audit.is_clean() {
"clean"
} else {
"aliased"
}
}
pub(crate) async fn handle_palace_verify_embedded(state: &AppState, args: Value) -> Result<Value> {
let palace = match resolve_palace_or_index(state, &args, "palace_verify_embedded").await? {
PalaceScope::Palace(p) => p,
PalaceScope::Index(index) => return Ok(index),
};
let raw = args
.get("drawer_ids")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("palace_verify_embedded: missing 'drawer_ids' (array of UUIDs)"))?;
if raw.is_empty() {
return Err(anyhow!(
"palace_verify_embedded: 'drawer_ids' is empty — nothing to verify"
));
}
let mut requested = Vec::with_capacity(raw.len());
for value in raw {
let text = value.as_str().ok_or_else(|| {
anyhow!("palace_verify_embedded: every entry in 'drawer_ids' must be a UUID string")
})?;
let id = Uuid::parse_str(text)
.map_err(|e| anyhow!("palace_verify_embedded: '{text}' is not a UUID: {e}"))?;
requested.push(id);
}
let handle = open_palace_handle(state, &palace)?;
let health = handle.embed_health();
let live: std::collections::HashSet<Uuid> =
handle.drawers.read().iter().map(|d| d.id).collect();
let no_vector: std::collections::HashSet<Uuid> =
health.missing_vector_ids.iter().copied().collect();
let (mut embedded, mut missing, mut unknown) = (Vec::new(), Vec::new(), Vec::new());
for id in requested {
if !live.contains(&id) {
unknown.push(id.to_string());
} else if no_vector.contains(&id) {
missing.push(id.to_string());
} else {
embedded.push(id.to_string());
}
}
let audit = audit_state(&health.alias_audit);
Ok(json!({
"palace": health.palace_id,
"embedded": embedded,
"missing": missing,
"unknown": unknown,
"alias_audit": audit,
"alias_audit_error": health.alias_audit.unavailable_reason(),
"verified": missing.is_empty() && unknown.is_empty() && audit == "clean",
}))
}
pub(crate) async fn handle_palace_embed_sweep(state: &AppState, _args: Value) -> Result<Value> {
let root = state.data_root.clone();
let palaces = tokio::task::spawn_blocking(move || {
trusty_common::memory_core::PalaceRegistry::list_palaces(&root)
})
.await
.map_err(|e| anyhow!("join list_palaces: {e}"))?
.map_err(|e| anyhow!("list palaces: {e:#}"))?;
let mut rows = Vec::with_capacity(palaces.len());
let (mut unhealthy, mut unreadable) = (0usize, 0usize);
for palace in &palaces {
let id = palace.id.as_str();
let state_for_open = state.clone();
let owned_id = id.to_string();
let opened = tokio::task::spawn_blocking(move || {
open_palace_handle(&state_for_open, &owned_id)
.map(|handle| coverage(&handle))
.map_err(|e| format!("{e:#}"))
})
.await
.unwrap_or_else(|e| Err(format!("join open palace: {e}")));
match opened {
Ok(row) => {
if row.get("healthy").and_then(Value::as_bool) != Some(true) {
unhealthy += 1;
}
rows.push(row);
}
Err(e) => {
unreadable += 1;
tracing::warn!(palace = %id, "embed sweep could not open palace: {e}");
rows.push(json!({ "palace": id, "error": e }));
}
}
}
Ok(json!({
"palaces": rows,
"palace_count": palaces.len(),
"unhealthy": unhealthy,
"unreadable": unreadable,
}))
}