- 1
//! `vak memory` (docs/design/29-personal-os.md P1): list/add/forget/ - 2
/// amend over the workspace MEMORY.md tier and the global USER.md profile - 3
/// tier, wired straight onto `vak_core::memory`. Ids are printed so they - 4
/// can be copied into forget/amend. - 5
use std::path::{Path, PathBuf}; - 6
- 7
use vak_core::Core; - 8
use vak_core::memory::{self, NoteBlock}; - 9
- 10
pub fn run_memory(cwd: PathBuf, action: Option<crate::cli::MemoryAction>) -> i32 { - 11
let core = match Core::new(cwd.clone()) { - 12
Ok(c) => c, - 13
Err(e) => { - 14
eprintln!("error: {e}"); - 15
return 2; - 16
} - 17
}; - 18
let home = core.sessions_home(); - 19
let workspace = core.cwd().clone(); - 20
match action.unwrap_or(crate::cli::MemoryAction::List { profile: false }) { - 21
crate::cli::MemoryAction::Clean { older_than_secs } => { - 22
let report = - 23
memory::cleanup_artifacts(&home, std::time::Duration::from_secs(older_than_secs)); - 24
println!( - 25
"cleaned {} locks, {} temp files, {} empty workspace directories", - 26
report.removed_locks, report.removed_temps, report.removed_empty_dirs - 27
); - 28
0 - 29
} - 30
crate::cli::MemoryAction::List { profile } => { - 31
list(&home, &workspace, profile); - 32
0 - 33
} - 34
crate::cli::MemoryAction::Consolidate => match core.consolidate_memory() { - 35
Ok(report) => { - 36
println!("Memory consolidation complete:"); - 37
println!(" Examined notes: {}", report.total_notes_examined); - 38
println!( - 39
" Promoted invariants: {}", - 40
report.promoted_invariants.len() - 41
); - 42
for inv in &report.promoted_invariants { - 43
println!(" + Invariant: \"{inv}\""); - 44
} - 45
println!( - 46
" Detected conflicts: {}", - 47
report.detected_conflicts.len() - 48
); - 49
for conf in &report.detected_conflicts { - 50
println!(" ! Conflict: {conf}"); - 51
} - 52
println!( - 53
" Distilled entities: {}", - 54
report.distilled_entities.len() - 55
); - 56
for ent in &report.distilled_entities { - 57
println!(" * Entity: {ent}"); - 58
} - 59
0 - 60
} - 61
Err(e) => { - 62
eprintln!("error: {e}"); - 63
1 - 64
} - 65
}, - 66
crate::cli::MemoryAction::Add { - 67
text, - 68
kind, - 69
tag, - 70
profile, - 71
} => match add(&home, &workspace, profile, &kind, &tag, &text) { - 72
Ok(note) => { - 73
println!( - 74
"added note {} [{}] to {}", - 75
note.id, - 76
note.kind, - 77
tier(profile) - 78
); - 79
0 - 80
} - 81
Err(e) => { - 82
eprintln!("error: {e}"); - 83
1 - 84
} - 85
}, - 86
crate::cli::MemoryAction::Forget { id, profile } => { - 87
match memory::forget_note(&store_path(&home, &workspace, profile), &id) { - 88
Ok(bytes) => { - 89
println!("forgot {id} ({bytes} bytes removed from {})", tier(profile)); - 90
0 - 91
} - 92
Err(e) => { - 93
eprintln!("error: {e}"); - 94
1 - 95
} - 96
} - 97
} - 98
crate::cli::MemoryAction::Amend { id, text, profile } => { - 99
match memory::amend_note(&store_path(&home, &workspace, profile), &id, &text) { - 100
Ok(()) => { - 101
println!("amended {id} in {}", tier(profile)); - 102
0 - 103
} - 104
Err(e) => { - 105
eprintln!("error: {e}"); - 106
1 - 107
} - 108
} - 109
} - 110
} - 111
} - 112
- 113
fn tier(profile: bool) -> &'static str { - 114
if profile { - 115
"profile (USER.md)" - 116
} else { - 117
"workspace (MEMORY.md)" - 118
} - 119
} - 120
- 121
fn store_path(home: &Path, workspace: &Path, profile: bool) -> PathBuf { - 122
if profile { - 123
memory::profile_path(home) - 124
} else { - 125
home.join("memory") - 126
.join(memory::hash_cwd(workspace)) - 127
.join("MEMORY.md") - 128
} - 129
} - 130
- 131
fn notes_for(home: &Path, workspace: &Path, profile: bool) -> Vec<NoteBlock> { - 132
if profile { - 133
memory::list_profile_notes(home) - 134
} else { - 135
memory::list_notes(home, workspace) - 136
} - 137
} - 138
- 139
fn list(home: &Path, workspace: &Path, profile: bool) { - 140
let notes = notes_for(home, workspace, profile); - 141
if notes.is_empty() { - 142
println!("no memory notes in {}", tier(profile)); - 143
return; - 144
} - 145
for n in ¬es { - 146
println!( - 147
"{} {} [{}]{} session={}", - 148
n.id, - 149
n.ts.to_rfc3339(), - 150
n.kind, - 151
if n.tag.is_empty() { - 152
String::new() - 153
} else { - 154
format!(" tag={}", n.tag) - 155
}, - 156
n.session_id - 157
); - 158
println!(" {}", n.text.replace('\n', "\n ")); - 159
} - 160
} - 161
- 162
fn add( - 163
home: &Path, - 164
workspace: &Path, - 165
profile: bool, - 166
kind: &str, - 167
tag: &str, - 168
text: &str, - 169
) -> Result<NoteBlock, String> { - 170
// CLI-written notes carry a synthetic session id; provenance grammar - 171
// stays identical to tool-written notes. - 172
if profile { - 173
memory::append_profile_note(home, kind, tag, text, "cli") - 174
} else { - 175
memory::append_note(home, workspace, kind, tag, "cli", text) - 176
} - 177
} - 178
- 179
#[cfg(test)] - 180
mod tests { - 181
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 182
use super::*; - 183
- 184
#[test] - 185
fn store_paths_split_tiers() { - 186
let dir = tempfile::tempdir().unwrap(); - 187
let home = dir.path(); - 188
let ws = dir.path().join("ws"); - 189
assert_eq!( - 190
store_path(home, &ws, true), - 191
home.join("memory/user/USER.md") - 192
); - 193
assert_eq!( - 194
store_path(home, &ws, false), - 195
home.join("memory") - 196
.join(memory::hash_cwd(&ws)) - 197
.join("MEMORY.md") - 198
); - 199
} - 200
- 201
#[test] - 202
fn add_list_roundtrip_both_tiers() { - 203
let dir = tempfile::tempdir().unwrap(); - 204
let home = dir.path(); - 205
let ws = dir.path().join("ws"); - 206
std::fs::create_dir_all(&ws).unwrap(); - 207
- 208
let note = add( - 209
home, - 210
&ws, - 211
false, - 212
"decision", - 213
"deploy", - 214
"pause before rollbacks", - 215
) - 216
.unwrap(); - 217
let profile_note = add(home, &ws, true, "preference", "", "vim bindings").unwrap(); - 218
- 219
assert!(note.id.len() == 16); - 220
assert_eq!(note.session_id, "cli"); - 221
let ws_notes = notes_for(home, &ws, false); - 222
assert_eq!(ws_notes.len(), 1); - 223
assert_eq!(ws_notes[0].text, "pause before rollbacks"); - 224
let profile_notes = notes_for(home, &ws, true); - 225
assert_eq!(profile_notes.len(), 1); - 226
assert_eq!(profile_notes[0].id, profile_note.id); - 227
- 228
// forget by printed id removes exactly that block. - 229
let path = store_path(home, &ws, false); - 230
memory::forget_note(&path, ¬e.id).unwrap(); - 231
assert!(notes_for(home, &ws, false).is_empty()); - 232
} - 233
- 234
#[test] - 235
fn empty_add_is_a_value_error() { - 236
let dir = tempfile::tempdir().unwrap(); - 237
let err = add(dir.path(), dir.path(), false, "note", "", " ").unwrap_err(); - 238
assert!(err.contains("empty"), "{err}"); - 239
} - 240
- 241
#[test] - 242
fn empty_listing_renders_without_panicking() { - 243
let dir = tempfile::tempdir().unwrap(); - 244
let ws = dir.path().join("ws"); - 245
list(dir.path(), &ws, false); - 246
list(dir.path(), &ws, true); - 247
} - 248
} - 249
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.