1use std::path::Path;
2
3use anyhow::{Context, Result, bail};
4
5use super::audit::{AuditRecorder, MemoryAuditEvent};
6use super::frontmatter::{MemoryMetadata, parse_frontmatter};
7use super::paths::{MemoryTier, collect_memory_files, resolve_memory_path, resolve_memory_tier};
8use super::policy::{MemoryPolicy, POLICY_FILE_BASENAME, POLICY_FILENAME};
9
10pub fn list_memory_names(memories_dir: &Path, topic: Option<&str>) -> Vec<String> {
11 let policy = MemoryPolicy::load(memories_dir);
12 list_memory_names_with_policy(memories_dir, topic, &policy)
13}
14
15pub fn list_memory_names_with_policy(
17 memories_dir: &Path,
18 topic: Option<&str>,
19 policy: &MemoryPolicy,
20) -> Vec<String> {
21 let mut names = Vec::new();
22 if !memories_dir.is_dir() {
23 return names;
24 }
25 collect_memory_files(memories_dir, memories_dir, &mut names);
26 names.sort();
27 names.retain(|n| {
28 !n.starts_with(POLICY_FILENAME) && !policy.is_ignored(n)
30 });
31 if let Some(t) = topic {
32 let t = t.trim().trim_matches('/');
33 if !t.is_empty() {
34 names.retain(|n| n == t || n.starts_with(&format!("{t}/")));
35 }
36 }
37 names
38}
39
40pub fn list_all_memory_names(
43 project_dir: &Path,
44 global_dir: Option<&Path>,
45 topic: Option<&str>,
46) -> Vec<(String, MemoryTier)> {
47 let project_memories = project_dir.join(".codelens").join("memories");
48 let project_names = list_memory_names(&project_memories, topic);
49 let mut result: Vec<(String, MemoryTier)> = project_names
50 .into_iter()
51 .map(|n| (n, MemoryTier::Project))
52 .collect();
53
54 if let Some(gdir) = global_dir {
55 let global_names = list_memory_names(gdir, topic);
56 for name in global_names {
57 let prefixed = format!("global/{}", name);
59 if !result.iter().any(|(n, _)| n == &name) {
60 result.push((prefixed, MemoryTier::Global));
61 }
62 }
63 }
64 result
65}
66
67pub fn read_memory(memories_dir: &Path, name: &str) -> Result<String> {
69 let path = resolve_memory_path(memories_dir, name)?;
70 std::fs::read_to_string(&path).with_context(|| format!("memory not found: {name}"))
71}
72
73pub fn read_memory_from_tier(
75 project_dir: &Path,
76 global_dir: Option<&Path>,
77 name: &str,
78) -> Result<(String, MemoryTier)> {
79 let loc = resolve_memory_tier(name, project_dir, global_dir);
80 let content = std::fs::read_to_string(&loc.path)
81 .with_context(|| format!("memory not found: {}", name.trim_start_matches("global/")))?;
82 Ok((content, loc.tier))
83}
84
85pub fn read_memory_with_metadata(
89 project_dir: &Path,
90 global_dir: Option<&Path>,
91 name: &str,
92) -> Result<(String, MemoryMetadata)> {
93 let loc = resolve_memory_tier(name, project_dir, global_dir);
94 let effective_name = name.trim_start_matches("global/");
95 let content = std::fs::read_to_string(&loc.path)
96 .with_context(|| format!("memory not found: {effective_name}"))?;
97 let policy = MemoryPolicy::load(&loc.dir);
98 let modified_secs = std::fs::metadata(&loc.path)
99 .ok()
100 .and_then(|m| m.modified().ok())
101 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
102 .map(|d| d.as_secs());
103 let stale = modified_secs
104 .map(|m| policy.is_stale(effective_name, m))
105 .unwrap_or(false);
106 let fm = parse_frontmatter(&content);
107 Ok((
108 content,
109 MemoryMetadata {
110 tier: loc.tier,
111 stale,
112 last_modified_secs: modified_secs,
113 linked_symbols: fm
114 .as_ref()
115 .map(|f| f.linked_symbols.clone())
116 .unwrap_or_default(),
117 linked_files: fm
118 .as_ref()
119 .map(|f| f.linked_files.clone())
120 .unwrap_or_default(),
121 linked_analyses: fm
122 .as_ref()
123 .map(|f| f.linked_analyses.clone())
124 .unwrap_or_default(),
125 },
126 ))
127}
128
129pub fn write_memory(memories_dir: &Path, name: &str, content: &str) -> Result<()> {
132 if name == POLICY_FILENAME {
134 return write_policy(memories_dir, content);
135 }
136 let policy = MemoryPolicy::load(memories_dir);
137 if policy.is_read_only(name) {
138 bail!("memory '{name}' is read-only (matches policy pattern)");
139 }
140 let path = resolve_memory_path(memories_dir, name)?;
141 if let Some(parent) = path.parent() {
142 std::fs::create_dir_all(parent)?;
143 }
144 std::fs::write(&path, content)?;
145 Ok(())
146}
147
148pub fn write_memory_tiered(
151 project_dir: &Path,
152 global_dir: Option<&Path>,
153 name: &str,
154 content: &str,
155) -> Result<MemoryTier> {
156 write_memory_tiered_rec(project_dir, global_dir, name, content, None)
157}
158
159pub fn write_memory_tiered_rec(
161 project_dir: &Path,
162 global_dir: Option<&Path>,
163 name: &str,
164 content: &str,
165 recorder: Option<&dyn AuditRecorder>,
166) -> Result<MemoryTier> {
167 let (effective_name, force_tier) = if let Some(stripped) = name.strip_prefix("global/") {
169 (stripped.trim_start_matches('/'), Some(MemoryTier::Global))
170 } else {
171 (name, None)
172 };
173
174 if effective_name == POLICY_FILENAME {
175 let dir = match force_tier {
176 Some(MemoryTier::Global) => global_dir
177 .ok_or_else(|| anyhow::anyhow!("global memory directory not available"))?,
178 _ => &project_dir.join(".codelens").join("memories"),
179 };
180 write_policy(dir, content)?;
181 return Ok(force_tier.unwrap_or(MemoryTier::Project));
182 }
183
184 let loc = resolve_memory_tier(name, project_dir, global_dir);
185 let tier_dir = &loc.dir;
186 let policy = MemoryPolicy::load(tier_dir);
187 if policy.is_read_only(effective_name) {
188 bail!("memory '{name}' is read-only (matches policy pattern)");
189 }
190 let path = resolve_memory_path(tier_dir, effective_name)?;
191 if let Some(parent) = path.parent() {
192 std::fs::create_dir_all(parent)?;
193 }
194 let is_new = !path.exists();
195 std::fs::write(&path, content)?;
196 if let Some(rec) = recorder {
197 let event = if is_new {
198 MemoryAuditEvent::Created {
199 tier: loc.tier,
200 path: path.to_string_lossy().to_string(),
201 }
202 } else {
203 MemoryAuditEvent::Updated {
204 tier: loc.tier,
205 path: path.to_string_lossy().to_string(),
206 }
207 };
208 rec.record(&event);
209 }
210 Ok(loc.tier)
211}
212
213pub fn delete_memory(memories_dir: &Path, name: &str) -> Result<()> {
215 if name == POLICY_FILENAME {
216 bail!("cannot delete the policy file; write an empty policy instead");
217 }
218 let policy = MemoryPolicy::load(memories_dir);
219 if policy.is_read_only(name) {
220 bail!("memory '{name}' is read-only and cannot be deleted");
221 }
222 let path = resolve_memory_path(memories_dir, name)?;
223 if !path.is_file() {
224 bail!("memory not found: {name}");
225 }
226 std::fs::remove_file(&path)?;
227 Ok(())
228}
229
230pub fn delete_memory_tiered(
233 project_dir: &Path,
234 global_dir: Option<&Path>,
235 name: &str,
236) -> Result<MemoryTier> {
237 delete_memory_tiered_rec(project_dir, global_dir, name, None)
238}
239
240pub fn delete_memory_tiered_rec(
242 project_dir: &Path,
243 global_dir: Option<&Path>,
244 name: &str,
245 recorder: Option<&dyn AuditRecorder>,
246) -> Result<MemoryTier> {
247 let effective_name = name.trim_start_matches("global/");
248 let loc = resolve_memory_tier(name, project_dir, global_dir);
249 if effective_name == POLICY_FILENAME {
250 bail!("cannot delete the policy file; write an empty policy instead");
251 }
252 let policy = MemoryPolicy::load(&loc.dir);
253 if policy.is_read_only(effective_name) {
254 bail!("memory '{name}' is read-only and cannot be deleted");
255 }
256 if !loc.path.is_file() {
257 bail!("memory not found: {}", effective_name);
258 }
259 let path_str = loc.path.to_string_lossy().to_string();
260 std::fs::remove_file(&loc.path)?;
261 if let Some(rec) = recorder {
262 rec.record(&MemoryAuditEvent::Deleted {
263 tier: loc.tier,
264 path: path_str,
265 });
266 }
267 Ok(loc.tier)
268}
269
270pub fn rename_memory(memories_dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
272 let policy = MemoryPolicy::load(memories_dir);
273 if policy.is_read_only(old_name) {
274 bail!("memory '{old_name}' is read-only and cannot be renamed");
275 }
276 if policy.is_read_only(new_name) {
277 bail!("target name '{new_name}' is read-only and cannot be overwritten");
278 }
279 let old_path = resolve_memory_path(memories_dir, old_name)?;
280 let new_path = resolve_memory_path(memories_dir, new_name)?;
281 if !old_path.is_file() {
282 bail!("memory not found: {old_name}");
283 }
284 if new_path.exists() {
285 bail!("target already exists: {new_name}");
286 }
287 if let Some(parent) = new_path.parent() {
288 std::fs::create_dir_all(parent)?;
289 }
290 std::fs::rename(&old_path, &new_path)?;
291 Ok(())
292}
293
294fn write_policy(memories_dir: &Path, content: &str) -> Result<()> {
295 if let Some(parent) = memories_dir.parent() {
296 std::fs::create_dir_all(parent)?;
297 }
298 std::fs::create_dir_all(memories_dir)?;
299 std::fs::write(memories_dir.join(POLICY_FILE_BASENAME), content)?;
300 Ok(())
301}
302
303pub fn read_policy(memories_dir: &Path) -> Result<String> {
305 let path = memories_dir.join(POLICY_FILE_BASENAME);
306 if path.is_file() {
307 std::fs::read_to_string(&path).with_context(|| "failed to read memory policy")
308 } else {
309 Ok(String::new())
310 }
311}