Deploy Poimen Memory K8s cluster with ArgoCD tracking (M2.2, M3.5-M3.7)
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
use crate::EventRecord;
|
||||
use anyhow::Result;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs;
|
||||
|
||||
/// Obsidian vault projector (deterministic, byte-identical).
|
||||
pub struct ObsidianProjector {
|
||||
vault_dir: String,
|
||||
_emit_evidence: bool,
|
||||
}
|
||||
|
||||
/// Vault note metadata (stable frontmatter order).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VaultNote {
|
||||
pub project: String,
|
||||
pub level: String,
|
||||
pub query_id: Option<String>,
|
||||
pub updated: String,
|
||||
pub chunks_seen: u32,
|
||||
pub chunks_used: u32,
|
||||
pub run_id: String,
|
||||
pub body: String,
|
||||
pub parents: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl ObsidianProjector {
|
||||
/// Create projector.
|
||||
pub fn new(_log_dir: &str, vault_dir: &str, emit_evidence: bool) -> Self {
|
||||
Self {
|
||||
vault_dir: vault_dir.to_string(),
|
||||
_emit_evidence: emit_evidence,
|
||||
}
|
||||
}
|
||||
|
||||
/// Project log to vault (deterministic).
|
||||
pub fn project(&self, events: &[EventRecord]) -> Result<()> {
|
||||
fs::create_dir_all(&self.vault_dir)?;
|
||||
|
||||
// Group by project and query
|
||||
let mut by_project: HashMap<String, HashMap<String, Vec<&EventRecord>>> = HashMap::new();
|
||||
|
||||
for event in events {
|
||||
by_project
|
||||
.entry(event.project.clone())
|
||||
.or_insert_with(HashMap::new)
|
||||
.entry(event.query.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(event);
|
||||
}
|
||||
|
||||
// Generate notes per project (in sorted order for determinism)
|
||||
let mut sorted_projects: Vec<_> = by_project.iter().collect();
|
||||
sorted_projects.sort_by_key(|(p, _)| p.as_str());
|
||||
|
||||
for (project, queries) in sorted_projects {
|
||||
let proj_dir = format!("{}/{}", self.vault_dir, project);
|
||||
fs::create_dir_all(&proj_dir)?;
|
||||
|
||||
// Generate index (L2)
|
||||
let index_note = VaultNote {
|
||||
project: project.clone(),
|
||||
level: "L2".to_string(),
|
||||
query_id: None,
|
||||
updated: "2026-01-01".to_string(),
|
||||
chunks_seen: 0,
|
||||
chunks_used: 0,
|
||||
run_id: "index".to_string(),
|
||||
body: String::new(),
|
||||
parents: vec![],
|
||||
};
|
||||
self.write_note(&proj_dir, "index", &index_note)?;
|
||||
|
||||
// Generate per-query notes (L1) in sorted order
|
||||
let mut sorted_queries: Vec<_> = queries.iter().collect();
|
||||
sorted_queries.sort_by_key(|(qid, _)| qid.as_str());
|
||||
|
||||
for (query_id, query_events) in sorted_queries {
|
||||
let (chunks_seen, chunks_used, body, parents) =
|
||||
Self::summarize_query(query_events);
|
||||
|
||||
let note = VaultNote {
|
||||
project: project.clone(),
|
||||
level: "L1".to_string(),
|
||||
query_id: Some(query_id.to_string()),
|
||||
updated: "2026-01-01".to_string(),
|
||||
chunks_seen,
|
||||
chunks_used,
|
||||
run_id: "run1".to_string(),
|
||||
body,
|
||||
parents,
|
||||
};
|
||||
|
||||
self.write_note(&proj_dir, query_id, ¬e)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write note with deterministic formatting.
|
||||
fn write_note(&self, dir: &str, name: &str, note: &VaultNote) -> Result<()> {
|
||||
// Stable frontmatter order (BTreeMap keeps keys sorted)
|
||||
let mut fm = BTreeMap::new();
|
||||
fm.insert("chunks_seen", note.chunks_seen.to_string());
|
||||
fm.insert("chunks_used", note.chunks_used.to_string());
|
||||
fm.insert("level", note.level.clone());
|
||||
fm.insert("project", note.project.clone());
|
||||
if let Some(qid) = ¬e.query_id {
|
||||
fm.insert("query_id", qid.clone());
|
||||
}
|
||||
fm.insert("run_id", note.run_id.clone());
|
||||
fm.insert("updated", note.updated.clone());
|
||||
|
||||
// Build frontmatter
|
||||
let mut content = String::from("---\n");
|
||||
for (k, v) in fm.iter() {
|
||||
content.push_str(&format!("{}: {}\n", k, v));
|
||||
}
|
||||
content.push_str("---\n");
|
||||
|
||||
// Title
|
||||
let title = note.query_id.as_ref().unwrap_or(¬e.project);
|
||||
content.push_str(&format!("# {}\n\n", title));
|
||||
|
||||
// Body
|
||||
if note.body.is_empty() {
|
||||
content.push_str("No evidence found.\n\n");
|
||||
} else {
|
||||
content.push_str(¬e.body);
|
||||
if !note.body.ends_with('\n') {
|
||||
content.push('\n');
|
||||
}
|
||||
content.push('\n');
|
||||
}
|
||||
|
||||
// Provenance (sorted)
|
||||
if !note.parents.is_empty() {
|
||||
content.push_str("## Provenance\n");
|
||||
let mut sorted_parents = note.parents.clone();
|
||||
sorted_parents.sort();
|
||||
for (source, time) in sorted_parents {
|
||||
content.push_str(&format!("- [[{}-{}]]\n", source, time));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure exactly one trailing newline
|
||||
if !content.ends_with('\n') {
|
||||
content.push('\n');
|
||||
}
|
||||
|
||||
// Write to file
|
||||
let path = format!("{}/{}.md", dir, name);
|
||||
fs::write(&path, &content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Summarize query events.
|
||||
fn summarize_query(
|
||||
events: &[&EventRecord],
|
||||
) -> (u32, u32, String, Vec<(String, String)>) {
|
||||
let mut chunks_seen = 0u32;
|
||||
let mut chunks_used = 0u32;
|
||||
let mut body = String::new();
|
||||
let mut parents = Vec::new();
|
||||
|
||||
for event in events.iter() {
|
||||
if event.event_type.contains("Gate") {
|
||||
chunks_seen += 1;
|
||||
}
|
||||
if event.event_type.contains("Evidence") {
|
||||
chunks_used += 1;
|
||||
}
|
||||
// Simplified parent extraction
|
||||
if let Some(obj) = event.data.as_object() {
|
||||
if let Some(parent) = obj.get("parent") {
|
||||
if let Some(s) = parent.as_str() {
|
||||
parents.push((s.to_string(), format!("t{}", event.turn)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chunks_used > 0 {
|
||||
body = format!(
|
||||
"Extracted from {} chunks, using {}\n",
|
||||
chunks_seen, chunks_used
|
||||
);
|
||||
}
|
||||
|
||||
(chunks_seen, chunks_used, body, parents)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user