use crate::AppState;
use anyhow::Result;
#[derive(Default, Clone)]
pub struct PromptFactsCache {
pub triples: Vec<(String, String, String)>,
pub formatted: String,
}
pub const HOT_PREDICATES: &[&str] = &[
"is_alias_for",
"has_convention",
"is_fact",
"is_shorthand_for",
];
pub fn is_hot_predicate(p: &str) -> bool {
HOT_PREDICATES.contains(&p)
}
fn section_heading(predicate: &str) -> &str {
match predicate {
"is_alias_for" => "Aliases",
"has_convention" => "Conventions",
"is_fact" => "Facts",
"is_shorthand_for" => "Shorthands",
other => other,
}
}
pub fn build_prompt_context(triples: &[(String, String, String)]) -> String {
let mut sections: Vec<(&str, Vec<&(String, String, String)>)> =
HOT_PREDICATES.iter().map(|p| (*p, Vec::new())).collect();
for triple in triples {
if let Some(slot) = sections.iter_mut().find(|(p, _)| *p == triple.1.as_str()) {
slot.1.push(triple);
}
}
if sections.iter().all(|(_, v)| v.is_empty()) {
return String::new();
}
let mut out = String::new();
out.push_str("## Project Context (from memory palace)\n");
for (predicate, items) in sections {
if items.is_empty() {
continue;
}
out.push('\n');
out.push_str("### ");
out.push_str(section_heading(predicate));
out.push('\n');
for (subject, _predicate, object) in items {
match predicate {
"is_alias_for" | "is_shorthand_for" => {
out.push_str("- ");
out.push_str(subject);
out.push_str(" → ");
out.push_str(object);
out.push('\n');
}
_ => {
out.push_str("- ");
out.push_str(object);
out.push('\n');
}
}
}
}
out
}
pub async fn gather_hot_triples(state: &AppState) -> Result<Vec<(String, String, String)>> {
const PER_PALACE_LIMIT: usize = 1024;
let mut out = Vec::new();
for palace_id in state.registry.list() {
let handle = match state.registry.get(&palace_id) {
Some(h) => h,
None => continue, };
match handle.kg.list_active(PER_PALACE_LIMIT, 0).await {
Ok(triples) => {
for t in triples {
if is_hot_predicate(&t.predicate) {
out.push((t.subject, t.predicate, t.object));
}
}
}
Err(e) => {
tracing::warn!(
palace = %palace_id.as_str(),
"skipping palace during prompt-fact gather: {e:#}",
);
}
}
}
Ok(out)
}
pub async fn rebuild_prompt_cache(state: &AppState) -> Result<()> {
let triples = gather_hot_triples(state).await?;
let formatted = build_prompt_context(&triples);
let cache = state.prompt_context_cache.clone();
let mut guard = cache.write().map_err(|e| {
anyhow::anyhow!("prompt cache lock poisoned: {e}")
})?;
*guard = PromptFactsCache { triples, formatted };
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_hot_predicate_matches_listed() {
for p in HOT_PREDICATES {
assert!(is_hot_predicate(p), "expected hot: {p}");
}
assert!(!is_hot_predicate("works_at"));
assert!(!is_hot_predicate(""));
}
#[test]
fn build_prompt_context_empty_when_no_hot_triples() {
let triples: Vec<(String, String, String)> = vec![
("alice".into(), "works_at".into(), "Acme".into()),
("bob".into(), "lives_in".into(), "Paris".into()),
];
assert_eq!(build_prompt_context(&triples), "");
}
#[test]
fn build_prompt_context_groups_and_formats() {
let triples: Vec<(String, String, String)> = vec![
(
"tga".into(),
"is_alias_for".into(),
"trusty-git-analytics".into(),
),
("tm".into(), "is_alias_for".into(), "trusty-memory".into()),
(
"conv-1".into(),
"has_convention".into(),
"No unwrap() in library code".into(),
),
("fact-1".into(), "is_fact".into(), "MSRV is 1.88".into()),
("alice".into(), "works_at".into(), "Acme".into()),
];
let out = build_prompt_context(&triples);
assert!(out.starts_with("## Project Context (from memory palace)"));
assert!(out.contains("### Aliases"));
assert!(out.contains("- tga → trusty-git-analytics"));
assert!(out.contains("- tm → trusty-memory"));
assert!(out.contains("### Conventions"));
assert!(out.contains("- No unwrap() in library code"));
assert!(out.contains("### Facts"));
assert!(out.contains("- MSRV is 1.88"));
assert!(!out.contains("Acme"));
let aliases_idx = out.find("### Aliases").unwrap();
let conventions_idx = out.find("### Conventions").unwrap();
let facts_idx = out.find("### Facts").unwrap();
assert!(aliases_idx < conventions_idx);
assert!(conventions_idx < facts_idx);
}
}