use std::path::Path;
use serde_json::{Value, json};
use super::{FileResult, ImportOptions, ImportStatus, ParsedMemory, write_drawer};
#[derive(Debug, thiserror::Error)]
pub enum RefreshError {
#[error(
"refresh aborted with nothing changed — the stale drawer {drawer_id} is still in \
place: {source:#}"
)]
Aborted {
drawer_id: String,
source: anyhow::Error,
},
#[error(
"DATA LOSS — the stale drawer {drawer_id} was removed and its replacement could not \
be written ({source:#}); this file is no longer in the palace, so do not delete its \
source, and re-run the import to restore it"
)]
ReplacementLost {
drawer_id: String,
source: anyhow::Error,
},
}
pub async fn refresh_drawer(
socket: &Path,
opts: &ImportOptions,
parsed: &ParsedMemory,
drawer_id: &str,
) -> Result<String, RefreshError> {
trusty_common::memory_rpc::call_memory_tool_at(
socket,
"memory_forget",
json!({ "palace": opts.palace, "drawer_id": drawer_id }),
)
.await
.map_err(|source| RefreshError::Aborted {
drawer_id: drawer_id.to_string(),
source,
})?;
write_drawer(socket, opts, parsed).await.map_err(|source| {
tracing::error!(
drawer_id = %drawer_id,
palace = %opts.palace,
"refresh removed a drawer and could not write its replacement: {source:#}"
);
RefreshError::ReplacementLost {
drawer_id: drawer_id.to_string(),
source,
}
})
}
pub async fn verify_findable(socket: &Path, palace: &str, result: &mut FileResult) {
let Some(drawer_id) = result.drawer_id.clone() else {
return;
};
if result.status == ImportStatus::Failed {
return;
}
let stored = stored_phrase(result.status);
match verdict(socket, palace, &drawer_id).await {
Ok(None) => {}
Ok(Some(reason)) => fail(result, format!("{stored}, but is not findable: {reason}")),
Err(e) => fail(
result,
format!("{stored}, but the findability gate could not run: {e:#}"),
),
}
}
fn stored_phrase(status: ImportStatus) -> &'static str {
match status {
ImportStatus::Created => "the drawer was written",
ImportStatus::Refreshed => "the drifted drawer was replaced",
ImportStatus::Skipped => "the drawer was already in the palace",
_ => "the drawer is in the palace",
}
}
async fn verdict(socket: &Path, palace: &str, drawer_id: &str) -> anyhow::Result<Option<String>> {
let answer = trusty_common::memory_rpc::call_memory_tool_at(
socket,
"palace_verify_embedded",
json!({ "palace": palace, "drawer_ids": [drawer_id] }),
)
.await?;
if answer.get("verified").and_then(Value::as_bool) == Some(true) {
return Ok(None);
}
Ok(Some(unverified_reason(&answer)))
}
fn unverified_reason(answer: &Value) -> String {
let has = |key: &str| {
answer
.get(key)
.and_then(Value::as_array)
.is_some_and(|ids| !ids.is_empty())
};
if has("unknown") {
return "the palace holds no drawer with this id".to_string();
}
if has("missing") {
return "the drawer has no vector, so no vector search will return it — \
run palace_reembed"
.to_string();
}
match answer.get("alias_audit").and_then(Value::as_str) {
Some("clean") | None => {
"palace_verify_embedded declined to verify it and named no cause".to_string()
}
Some(state) => format!(
"the palace's vector-key audit is {state}{}",
answer
.get("alias_audit_error")
.and_then(Value::as_str)
.map(|e| format!(" ({e})"))
.unwrap_or_default()
),
}
}
fn fail(result: &mut FileResult, reason: String) {
result.status = ImportStatus::Failed;
result.detail = Some(match result.detail.take() {
Some(prior) => format!("{prior}; {reason}"),
None => reason,
});
}