Skip to main content

codelens_engine/memory/
frontmatter.rs

1use serde::{Deserialize, Serialize};
2
3use super::MemoryTier;
4
5/// Parsed YAML frontmatter from a memory markdown file.
6#[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
16/// Parse an optional YAML frontmatter block from memory content.
17pub 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
75/// Strip frontmatter from content, returning just the body text.
76pub 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/// Metadata returned alongside memory content for rich responses.
91#[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}