Deploy Poimen Memory K8s cluster with ArgoCD tracking (M2.2, M3.5-M3.7)

This commit is contained in:
Story Crater Bot
2026-08-22 23:13:42 -07:00
parent af9c5ba01b
commit 695e115212
67 changed files with 8438 additions and 24 deletions
+193
View File
@@ -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, &note)?;
}
}
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) = &note.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(&note.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(&note.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)
}
}