use anyhow::{Context, Result};
use trusty_common::memory_core::palace::PalaceId;
use crate::kg_extract::{extract_triples, ExtractInput};
use crate::{resolve_palace_registry_dir, AppState};
#[derive(Debug, Clone)]
pub struct PalaceRebuildSummary {
pub palace_id: String,
pub drawers_scanned: usize,
pub triples_asserted: usize,
pub error: Option<String>,
}
pub async fn handle_kg_rebuild(palace: Option<String>) -> Result<()> {
let data_dir = trusty_common::resolve_data_dir("trusty-memory")
.context("resolve trusty-memory data dir")?;
let data_root = resolve_palace_registry_dir(data_dir);
let state = AppState::new(data_root);
let loaded = state
.load_palaces_from_disk()
.await
.context("load palaces from disk")?;
tracing::info!(palaces_loaded = loaded, "kg-rebuild: palaces opened");
let summaries = rebuild_palaces(&state, palace.as_deref()).await?;
let mut total_drawers = 0usize;
let mut total_triples = 0usize;
let mut total_errors = 0usize;
for s in &summaries {
if let Some(e) = &s.error {
total_errors += 1;
eprintln!(
"[error] palace={} drawers={} triples={} error={}",
s.palace_id, s.drawers_scanned, s.triples_asserted, e
);
} else {
println!(
"[ok] palace={} drawers={} triples={}",
s.palace_id, s.drawers_scanned, s.triples_asserted
);
}
total_drawers += s.drawers_scanned;
total_triples += s.triples_asserted;
}
println!(
"kg-rebuild complete: {} palaces processed, {} drawers scanned, {} triples asserted, {} errors",
summaries.len(),
total_drawers,
total_triples,
total_errors
);
Ok(())
}
pub async fn rebuild_palaces(
state: &AppState,
palace_filter: Option<&str>,
) -> Result<Vec<PalaceRebuildSummary>> {
let mut out: Vec<PalaceRebuildSummary> = Vec::new();
let palaces = trusty_common::memory_core::PalaceRegistry::list_palaces(&state.data_root)
.unwrap_or_default();
for palace in palaces {
let id = palace.id.0.clone();
if let Some(filter) = palace_filter {
if filter != id {
continue;
}
}
let summary = rebuild_one(state, &id)
.await
.unwrap_or_else(|e| PalaceRebuildSummary {
palace_id: id.clone(),
drawers_scanned: 0,
triples_asserted: 0,
error: Some(format!("{e:#}")),
});
out.push(summary);
}
Ok(out)
}
async fn rebuild_one(state: &AppState, palace_id: &str) -> Result<PalaceRebuildSummary> {
let pid = PalaceId::new(palace_id);
let handle = state
.registry
.open_palace(&state.data_root, &pid)
.with_context(|| format!("open palace {palace_id}"))?;
let drawers = handle.drawers.read().clone();
let mut asserted = 0usize;
for d in &drawers {
let room = room_id_to_label(d.room_id);
let triples = extract_triples(&ExtractInput {
drawer_id: d.id,
content: &d.content,
tags: &d.tags,
room: room.as_deref(),
});
for triple in triples {
let s = triple.subject.clone();
let p = triple.predicate.clone();
match handle.kg.assert(triple).await {
Ok(()) => asserted += 1,
Err(e) => tracing::warn!(
palace = %palace_id,
drawer_id = %d.id,
subject = %s,
predicate = %p,
"kg-rebuild: assert failed (non-fatal): {e:#}",
),
}
}
}
Ok(PalaceRebuildSummary {
palace_id: palace_id.to_string(),
drawers_scanned: drawers.len(),
triples_asserted: asserted,
error: None,
})
}
fn room_id_to_label(_room_id: uuid::Uuid) -> Option<String> {
None
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn kg_rebuild_processes_all_drawers() -> Result<()> {
let tmp = tempfile::tempdir()?;
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(tmp.path().to_path_buf());
let _ = crate::tools::dispatch_tool(&state, "palace_create", json!({"name": "a"})).await?;
let _ = crate::tools::dispatch_tool(&state, "palace_create", json!({"name": "b"})).await?;
let _ = crate::tools::dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "a",
"text": "The Rustc compiler is a fast tool for the Rust language",
"tags": ["compiler"],
"room": "Backend",
}),
)
.await?;
let _ = crate::tools::dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "b",
"text": "Cargo build is a tool that compiles every #rust crate",
"tags": ["tooling"],
}),
)
.await?;
let summaries = rebuild_palaces(&state, None).await?;
assert_eq!(summaries.len(), 2, "expected both palaces processed");
for s in &summaries {
assert!(
s.error.is_none(),
"palace {} errored: {:?}",
s.palace_id,
s.error
);
assert_eq!(
s.drawers_scanned, 1,
"palace {} expected one drawer",
s.palace_id
);
assert!(
s.triples_asserted > 0,
"palace {} expected non-zero triples",
s.palace_id
);
}
Ok(())
}
#[tokio::test]
async fn kg_rebuild_processes_named_palace_only() -> Result<()> {
let tmp = tempfile::tempdir()?;
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(tmp.path().to_path_buf());
let _ = crate::tools::dispatch_tool(&state, "palace_create", json!({"name": "a"})).await?;
let _ = crate::tools::dispatch_tool(&state, "palace_create", json!({"name": "b"})).await?;
let _ = crate::tools::dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "a",
"text": "The Rustc compiler is a fast tool for Rust language users",
"tags": ["compiler"],
}),
)
.await?;
let _ = crate::tools::dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "b",
"text": "Cargo build is a tool that compiles every Rust crate locally",
"tags": ["tooling"],
}),
)
.await?;
let summaries = rebuild_palaces(&state, Some("a")).await?;
assert_eq!(summaries.len(), 1, "only palace 'a' should be processed");
assert_eq!(summaries[0].palace_id, "a");
Ok(())
}
}