- 1
//! Autonomous Memory Consolidation & Invariant Distillation. - 2
//! - 3
//! Self-supervised pass over workspace episodic notes that: - 4
//! 1. Identifies recurring procedural patterns and promotes them to immutable invariants. - 5
//! 2. Detects contradictory preferences or decisions across sessions. - 6
//! 3. Distills structured entity records from repeated domain facts. - 7
- 8
use std::collections::{BTreeMap, HashMap, HashSet}; - 9
use std::path::Path; - 10
- 11
use chrono::Utc; - 12
use serde::{Deserialize, Serialize}; - 13
- 14
use crate::entities::{self, EntityRecord}; - 15
use crate::memory; - 16
- 17
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 18
pub struct ConsolidationReport { - 19
pub total_notes_examined: usize, - 20
pub promoted_invariants: Vec<String>, - 21
pub detected_conflicts: Vec<String>, - 22
pub distilled_entities: Vec<String>, - 23
} - 24
- 25
/// Analyze episodic notes for this workspace and consolidate them. - 26
#[allow(clippy::collapsible_if)] - 27
pub fn consolidate_memory(home: &Path, cwd: &Path) -> Result<ConsolidationReport, String> { - 28
let notes = memory::list_notes(home, cwd); - 29
let total_notes_examined = notes.len(); - 30
- 31
let mut promoted_invariants = Vec::new(); - 32
let mut detected_conflicts = Vec::new(); - 33
let mut distilled_entities = Vec::new(); - 34
- 35
let existing_invariants: HashSet<String> = notes - 36
.iter() - 37
.filter(|n| n.kind == "invariant") - 38
.map(|n| n.text.trim().to_ascii_lowercase()) - 39
.collect(); - 40
- 41
// 1. Procedural Invariant Promotion - 42
// Group procedural notes by normalized text -> list of session_ids - 43
let mut procedural_map: HashMap<String, (Vec<String>, String, String)> = HashMap::new(); - 44
for note in ¬es { - 45
if note.kind == "procedural" { - 46
let norm = note.text.trim().to_ascii_lowercase(); - 47
let entry = procedural_map - 48
.entry(norm) - 49
.or_insert_with(|| (Vec::new(), note.tag.clone(), note.text.clone())); - 50
if !entry.0.contains(¬e.session_id) { - 51
entry.0.push(note.session_id.clone()); - 52
} - 53
} - 54
} - 55
- 56
for (norm, (sessions, tag, orig_text)) in procedural_map { - 57
let is_modal = norm.contains("always") - 58
|| norm.contains("never") - 59
|| norm.contains("must") - 60
|| norm.contains("invariant"); - 61
let is_multi_session = sessions.len() >= 2; - 62
- 63
if (is_modal || is_multi_session) && !existing_invariants.contains(&norm) { - 64
match memory::append_note(home, cwd, "invariant", &tag, "consolidation", &orig_text) { - 65
Ok(_) => promoted_invariants.push(orig_text), - 66
Err(e) => return Err(format!("failed to promote invariant: {e}")), - 67
} - 68
} - 69
} - 70
- 71
// 2. Contradiction / Conflict Detection - 72
// Group notes by tag - 73
let mut by_tag: HashMap<String, Vec<&memory::NoteBlock>> = HashMap::new(); - 74
for note in ¬es { - 75
if !note.tag.is_empty() { - 76
by_tag.entry(note.tag.clone()).or_default().push(note); - 77
} - 78
} - 79
- 80
for (tag, tag_notes) in by_tag { - 81
for i in 0..tag_notes.len() { - 82
for j in (i + 1)..tag_notes.len() { - 83
let text_a = tag_notes[i].text.to_ascii_lowercase(); - 84
let text_b = tag_notes[j].text.to_ascii_lowercase(); - 85
if is_contradiction(&text_a, &text_b) { - 86
detected_conflicts.push(format!( - 87
"Tag '{tag}': \"{}\" vs \"{}\"", - 88
tag_notes[i].text.trim(), - 89
tag_notes[j].text.trim() - 90
)); - 91
} - 92
} - 93
} - 94
} - 95
- 96
// 3. Entity Distillation from structured facts - 97
let existing_entities = entities::list_entities(home, Some(cwd)); - 98
let existing_entity_names: HashSet<String> = existing_entities - 99
.iter() - 100
.map(|e| e.name.to_ascii_lowercase()) - 101
.collect(); - 102
- 103
for note in ¬es { - 104
if note.kind == "fact" { - 105
if let Some((name, entity_type, summary)) = extract_entity_pattern(¬e.text) { - 106
if !existing_entity_names.contains(&name.to_ascii_lowercase()) { - 107
let mut attrs = BTreeMap::new(); - 108
if !note.tag.is_empty() { - 109
attrs.insert("tag".into(), note.tag.clone()); - 110
} - 111
attrs.insert("source_session".into(), note.session_id.clone()); - 112
- 113
let slug: String = name - 114
.to_ascii_lowercase() - 115
.chars() - 116
.map(|c| if c.is_alphanumeric() { c } else { '-' }) - 117
.collect(); - 118
let record = EntityRecord { - 119
id: slug, - 120
name: name.clone(), - 121
entity_type, - 122
summary, - 123
attributes: attrs, - 124
relations: Vec::new(), - 125
updated_at: Utc::now(), - 126
}; - 127
- 128
if entities::upsert_entity(home, Some(cwd), record).is_ok() { - 129
distilled_entities.push(name); - 130
} - 131
} - 132
} - 133
} - 134
} - 135
- 136
Ok(ConsolidationReport { - 137
total_notes_examined, - 138
promoted_invariants, - 139
detected_conflicts, - 140
distilled_entities, - 141
}) - 142
} - 143
- 144
fn is_contradiction(a: &str, b: &str) -> bool { - 145
let opposing_pairs = [ - 146
("tabs", "spaces"), - 147
("enable", "disable"), - 148
("enabled", "disabled"), - 149
("use", "do not use"), - 150
("allow", "deny"), - 151
("strict", "lenient"), - 152
("always", "never"), - 153
("async", "sync"), - 154
]; - 155
- 156
for (pos, neg) in opposing_pairs { - 157
if (a.contains(pos) && b.contains(neg)) || (a.contains(neg) && b.contains(pos)) { - 158
return true; - 159
} - 160
} - 161
false - 162
} - 163
- 164
fn extract_entity_pattern(text: &str) -> Option<(String, String, String)> { - 165
let lower = text.to_ascii_lowercase(); - 166
let type_markers = [ - 167
(" service is ", "service"), - 168
(" database is ", "database"), - 169
(" cluster is ", "cluster"), - 170
(" api is ", "api"), - 171
(" dataset is ", "dataset"), - 172
(" module is ", "module"), - 173
(" pipeline is ", "pipeline"), - 174
]; - 175
- 176
for (marker, entity_type) in type_markers { - 177
if let Some(pos) = lower.find(marker) { - 178
let is_idx = pos + marker.find(" is ").unwrap_or(0); - 179
let name_part = text[..is_idx].trim(); - 180
let rest = text[is_idx + 4..].trim(); - 181
if !name_part.is_empty() - 182
&& !rest.is_empty() - 183
&& name_part.split_whitespace().count() <= 6 - 184
{ - 185
let name = name_part - 186
.trim_start_matches("The ") - 187
.trim_start_matches("the ") - 188
.trim(); - 189
return Some((name.to_string(), entity_type.to_string(), rest.to_string())); - 190
} - 191
} - 192
} - 193
None - 194
} - 195
- 196
#[cfg(test)] - 197
mod tests { - 198
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 199
use super::*; - 200
- 201
#[test] - 202
fn memory_consolidation_promotes_invariants_and_detects_conflicts() { - 203
let temp = tempfile::tempdir().unwrap(); - 204
let home = temp.path().join("home"); - 205
let cwd = temp.path().join("cwd"); - 206
std::fs::create_dir_all(&home).unwrap(); - 207
std::fs::create_dir_all(&cwd).unwrap(); - 208
- 209
// 1. Invariant candidate: modal word "always" - 210
memory::append_note( - 211
&home, - 212
&cwd, - 213
"procedural", - 214
"db", - 215
"sess-1", - 216
"always verify database migration rollback plan", - 217
) - 218
.unwrap(); - 219
- 220
// 2. Conflicting preference notes under same tag "formatting" - 221
memory::append_note( - 222
&home, - 223
&cwd, - 224
"preference", - 225
"formatting", - 226
"sess-1", - 227
"use tabs for indentation", - 228
) - 229
.unwrap(); - 230
memory::append_note( - 231
&home, - 232
&cwd, - 233
"preference", - 234
"formatting", - 235
"sess-2", - 236
"use spaces for indentation", - 237
) - 238
.unwrap(); - 239
- 240
// 3. Structured fact describing an entity - 241
memory::append_note( - 242
&home, - 243
&cwd, - 244
"fact", - 245
"infra", - 246
"sess-3", - 247
"The Payments API is the external service processing card payments", - 248
) - 249
.unwrap(); - 250
- 251
let report = consolidate_memory(&home, &cwd).unwrap(); - 252
assert_eq!(report.total_notes_examined, 4); - 253
- 254
// Verify invariant promotion - 255
assert_eq!(report.promoted_invariants.len(), 1); - 256
assert!( - 257
report.promoted_invariants[0] - 258
.contains("always verify database migration rollback plan") - 259
); - 260
- 261
// Verify conflict detection - 262
assert_eq!(report.detected_conflicts.len(), 1); - 263
assert!(report.detected_conflicts[0].contains("formatting")); - 264
- 265
// Verify entity distillation - 266
assert_eq!(report.distilled_entities.len(), 1); - 267
assert_eq!(report.distilled_entities[0], "Payments API"); - 268
- 269
// Verify distilled entity exists in entity store - 270
let fetched = entities::get_entity(&home, Some(&cwd), "payments-api").unwrap(); - 271
assert_eq!(fetched.name, "Payments API"); - 272
assert_eq!(fetched.entity_type, "api"); - 273
} - 274
} - 275
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.