codelens_engine/memory/
frontmatter.rs1use serde::{Deserialize, Serialize};
2
3use super::MemoryTier;
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
7pub struct MemoryFrontmatter {
8 #[serde(default)]
9 pub linked_symbols: Vec<String>,
10 #[serde(default)]
11 pub linked_files: Vec<String>,
12 #[serde(default)]
13 pub linked_analyses: Vec<String>,
14}
15
16pub fn parse_frontmatter(content: &str) -> Option<MemoryFrontmatter> {
18 if !content.starts_with("---") {
19 return None;
20 }
21 let after_first = &content[3..];
22 let end_marker = after_first.find("\n---")?;
23 let yaml_text = &after_first[..end_marker];
24 let mut fm = MemoryFrontmatter::default();
25 let mut current_list: Option<&str> = None;
26 for line in yaml_text.lines() {
27 let line = line.trim();
28 if line.is_empty() || line.starts_with('#') {
29 continue;
30 }
31 if let Some(value) = line.strip_prefix("linked_symbols:") {
32 current_list = Some("symbols");
33 fm.linked_symbols = parse_yaml_list(value);
34 } else if let Some(value) = line.strip_prefix("linked_files:") {
35 current_list = Some("files");
36 fm.linked_files = parse_yaml_list(value);
37 } else if let Some(value) = line.strip_prefix("linked_analyses:") {
38 current_list = Some("analyses");
39 fm.linked_analyses = parse_yaml_list(value);
40 } else if line.starts_with("- ") {
41 let item = line.strip_prefix("- ").unwrap().trim().trim_matches('"');
42 if !item.is_empty() {
43 match current_list {
44 Some("symbols") => fm.linked_symbols.push(item.to_string()),
45 Some("files") => fm.linked_files.push(item.to_string()),
46 Some("analyses") => fm.linked_analyses.push(item.to_string()),
47 _ => {}
48 }
49 }
50 }
51 }
52 if fm.linked_symbols.is_empty() && fm.linked_files.is_empty() && fm.linked_analyses.is_empty() {
53 return None;
54 }
55 Some(fm)
56}
57
58fn parse_yaml_list(value: &str) -> Vec<String> {
59 let v = value.trim();
60 if v.starts_with('[') && v.ends_with(']') {
61 let inner = &v[1..v.len() - 1];
62 inner
63 .split(',')
64 .map(|s| s.trim().trim_matches('"').trim())
65 .filter(|s| !s.is_empty())
66 .map(|s| s.to_string())
67 .collect()
68 } else if v.is_empty() {
69 Vec::new()
70 } else {
71 vec![v.trim_matches('"').to_string()]
72 }
73}
74
75pub fn strip_frontmatter(content: &str) -> &str {
77 if !content.starts_with("---") {
78 return content;
79 }
80 let after_first = &content[3..];
81 if let Some(end_offset) = after_first.find("\n---") {
82 let body_start = end_offset + 4;
83 let body = &content[3 + body_start..];
84 body.trim_start()
85 } else {
86 content
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct MemoryMetadata {
93 pub tier: MemoryTier,
94 pub stale: bool,
95 pub last_modified_secs: Option<u64>,
96 pub linked_symbols: Vec<String>,
97 pub linked_files: Vec<String>,
98 pub linked_analyses: Vec<String>,
99}