返回 CodeWhale
memory.rs
根目录 / crates / tui / src / commands / groups / memory / memory.rs
1 //! One `/memory` surface for the authoritative native store. Never edits the
2 //! old Markdown anchor or reports an index-cache deletion as memory erasure.
3 use crate::commands::CommandResult;
4 use codewhale_command_contract::facets::{
5 CommandMemoryContext, MemoryDeleteScope, MemoryGetOutcome, MemoryImportOutcome,
6 MemoryRememberTarget,
7 };
8 use codewhale_command_contract::handler::{CommandCapabilities, CommandContexts, CommandHandler};
9 use codewhale_command_contract::metadata::{CommandInfo, RegisterCommand};
10 use std::path::Path;
11 const HELP: &str = "Memory is reviewed context, not another instruction layer.\n\n/memory [status|path|search <query>|get <numeric-id>|remember [global|workspace] <note>|import|export|reindex|clear <scope> --confirm|help]\n\nThe `native` prefix is a compatibility alias. `show` shows store status.\nCaptures, including legacy # entry points, become candidates; review in Context Lens.\nCorrect, approve, pin and exclude through Context Lens or the operator CLI.\n`clear` scopes: global, workspace, all. Confirmation removes tracked revision families and dependent checkpoints. Backups, exports and previously sent model context are not erased.\nThe database is authoritative. Never open it in a text editor or delete it as a cache.";
12 fn split(input: &str) -> (&str, &str) {
13 input
14 .trim()
15 .split_once(char::is_whitespace)
16 .map(|(a, b)| (a, b.trim()))
17 .unwrap_or((input.trim(), ""))
18 }
19 fn normalize(input: Option<&str>) -> (&str, &str) {
20 let (command, arg) = split(input.unwrap_or("status"));
21 let (command, arg) = if command == "native" {
22 split(arg)
23 } else {
24 (command, arg)
25 };
26 (
27 if matches!(command, "" | "show") {
28 "status"
29 } else {
30 command
31 },
32 arg,
33 )
34 }
35 fn memory(workspace: &Path, memory: &dyn CommandMemoryContext, arg: Option<&str>) -> CommandResult {
36 if !memory.memory_enabled() {
37 return CommandResult::error(
38 "Memory is disabled. Enable [memory] enabled = true in user configuration, then restart. No store was opened.",
39 );
40 }
41 let (command, arg) = normalize(arg);
42 match command {
43 "help" => CommandResult::message(HELP),
44 "status" => match memory.status() {
45 Ok(s) => CommandResult::message(format!(
46 "Native memory root: {}\nAuthoritative structured database: {}\nUse search/get to inspect entries and Context Lens to review candidates. The legacy Markdown source is not authoritative.",
47 s.root.display(),
48 s.index.display()
49 )),
50 Err(e) => CommandResult::error(format!("Memory status unavailable: {e}")),
51 },
52 "path" => match memory.path() {
53 Ok(root) => CommandResult::message(root.display().to_string()),
54 Err(e) => CommandResult::error(format!("Memory path unavailable: {e}")),
55 },
56 "edit" => CommandResult::message(
57 "Use Context Lens to correct a memory. The authoritative database must not be edited as Markdown.",
58 ),
59 "search" => {
60 if arg.is_empty() {
61 return CommandResult::error("Usage: /memory search <query>");
62 }
63 match memory.search(workspace, arg, 10) {
64 Ok(rows) if rows.is_empty() => {
65 CommandResult::message("No reviewed, current memory matches in this scope.")
66 }
67 Ok(rows) => CommandResult::message(
68 rows.into_iter()
69 .map(|r| {
70 format!(
71 "[untrusted memory; store={}] {}",
72 r.source.display(),
73 r.text
74 )
75 })
76 .collect::<Vec<_>>()
77 .join("\n"),
78 ),
79 Err(e) => CommandResult::error(format!("Memory search failed: {e}")),
80 }
81 }
82 "get" => {
83 let Ok(id) = arg.parse::<i64>() else {
84 return CommandResult::error("Usage: /memory get <numeric-id>");
85 };
86 match memory.get(workspace, id) {
87 Ok(MemoryGetOutcome::Found(r)) => CommandResult::message(format!(
88 "[untrusted memory #{id}; store={}]\n{}",
89 r.source.display(),
90 r.text
91 )),
92 Ok(MemoryGetOutcome::NotFound) => {
93 CommandResult::error("Memory entry was not found in this scope.")
94 }
95 Err(e) => CommandResult::error(format!("Memory read failed: {e}")),
96 }
97 }
98 "remember" => {
99 let (first, rest) = split(arg);
100 let (target, note) = match first {
101 "workspace" => match memory.workspace_id(workspace) {
102 Ok(id) => (MemoryRememberTarget::Workspace { workspace_id: id }, rest),
103 Err(e) => return CommandResult::error(e),
104 },
105 "global" => (MemoryRememberTarget::Global, rest),
106 _ => (MemoryRememberTarget::Global, arg),
107 };
108 if note.is_empty() {
109 return CommandResult::error(
110 "Usage: /memory remember [global|workspace] <complete note>",
111 );
112 }
113 match memory.remember(target, note) {
114 Ok(_) => CommandResult::message(
115 "Memory candidate saved. Review it in Context Lens before it is used as durable context.",
116 ),
117 Err(e) => CommandResult::error(format!("Memory capture failed: {e}")),
118 }
119 }
120 "import" => match memory.import() {
121 Ok(MemoryImportOutcome::Imported { destination }) => CommandResult::message(format!(
122 "Legacy notes imported as candidates into {}. The original file was not deleted.",
123 destination.display()
124 )),
125 Ok(MemoryImportOutcome::Skipped) => {
126 CommandResult::message("No new legacy candidates were imported.")
127 }
128 Err(e) => CommandResult::error(format!("Memory import failed: {e}")),
129 },
130 "export" => match memory.export() {
131 Ok(e) => CommandResult::message(e.content),
132 Err(e) => CommandResult::error(format!("Memory export failed: {e}")),
133 },
134 "reindex" => match memory.reindex() {
135 Ok(r) => CommandResult::message(format!(
136 "Rebuilt the derived search indexes for {} entries. The authoritative database was retained.",
137 r.entry_count
138 )),
139 Err(e) => CommandResult::error(format!("Reindex failed: {e}")),
140 },
141 "clear" | "delete" => {
142 let (scope, confirm) = split(arg);
143 if confirm != "--confirm" || !matches!(scope, "global" | "workspace" | "all") {
144 return CommandResult::error(
145 "Usage: /memory clear <global|workspace|all> --confirm. This removes tracked revision families and dependent checkpoints, not backups, exports or already-sent provider context.",
146 );
147 }
148 let result = match scope {
149 "global" => memory.delete(MemoryDeleteScope::Global),
150 "workspace" => memory.delete_workspace(workspace),
151 _ => memory.delete(MemoryDeleteScope::All),
152 };
153 match result {
154 Ok(_) => CommandResult::message(format!(
155 "Completed explicit {scope} memory deletion. External copies and already-sent context are outside this operation."
156 )),
157 Err(e) => CommandResult::error(format!(
158 "Memory deletion stopped: {e}. Inspect current state before retrying."
159 )),
160 }
161 }
162 _ => CommandResult::error(HELP),
163 }
164 }
165 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
166 name: "memory",
167 aliases: &[],
168 usage: "/memory [status|path|search|get|remember|import|export|reindex|clear|help]",
169 description_key: "cmd_memory_description",
170 };
171 pub(in crate::commands) struct MemoryCmd;
172 impl RegisterCommand<CommandResult> for MemoryCmd {
173 fn info() -> &'static CommandInfo {
174 &COMMAND_INFO
175 }
176 fn handler() -> CommandHandler<CommandResult> {
177 CommandHandler::Contextual {
178 capabilities: CommandCapabilities::WORKSPACE.union(CommandCapabilities::MEMORY),
179 handler: memory_contextual,
180 }
181 }
182 }
183 fn memory_contextual(contexts: CommandContexts<'_>, arg: Option<&str>) -> CommandResult {
184 let parts = contexts.into_parts();
185 let Some(workspace) = parts.workspace.as_deref() else {
186 return CommandResult::error("Command capability unavailable: workspace");
187 };
188 let Some(memory_ctx) = parts.memory.as_deref() else {
189 return CommandResult::error("Command capability unavailable: memory");
190 };
191 memory(&workspace.workspace(), memory_ctx, arg)
192 }
193 #[cfg(test)]
194 mod tests {
195 use super::*;
196 #[test]
197 fn defaults_to_structured_status() {
198 assert_eq!(normalize(None), ("status", ""));
199 }
200 #[test]
201 fn native_is_an_exact_alias() {
202 assert_eq!(
203 normalize(Some("native search multiple words")),
204 ("search", "multiple words")
205 );
206 assert_eq!(normalize(Some("natively")), ("natively", ""));
207 }
208 #[test]
209 fn complete_note_is_preserved() {
210 assert_eq!(
211 split("workspace all the words remain"),
212 ("workspace", "all the words remain")
213 );
214 }
215 #[test]
216 fn show_does_not_read_a_markdown_file() {
217 assert_eq!(normalize(Some("show")), ("status", ""));
218 }
219 #[test]
220 fn clear_requires_scope_and_confirmation() {
221 assert_eq!(normalize(Some("clear")), ("clear", ""));
222 assert_eq!(
223 normalize(Some("clear workspace --confirm")),
224 ("clear", "workspace --confirm")
225 );
226 }
227 }
228
228 lines RUST