Skip to main content

difflore_core/sources/
simple_files.rs

1//! Table-driven sources for plain file / directory-glob ingest.
2
3use std::path::{Path, PathBuf};
4
5use crate::errors::CoreError;
6
7use super::{MemoryDoc, Source, read_file_doc};
8
9struct SingleFileSpec {
10    id: &'static str,
11    label: &'static str,
12    file_name: &'static str,
13}
14
15impl SingleFileSpec {
16    fn detect(&self, repo_root: &Path) -> bool {
17        repo_root.join(self.file_name).is_file()
18    }
19    fn read(&self, repo_root: &Path) -> Result<Vec<MemoryDoc>, CoreError> {
20        let path = repo_root.join(self.file_name);
21        if !path.is_file() {
22            return Ok(Vec::new());
23        }
24        Ok(vec![read_file_doc(self.id, path)?])
25    }
26}
27
28struct DirGlobSpec {
29    id: &'static str,
30    label: &'static str,
31    dir: &'static [&'static str],
32    ext: &'static str,
33}
34
35impl DirGlobSpec {
36    fn resolve_dir(&self, repo_root: &Path) -> PathBuf {
37        let mut p = repo_root.to_path_buf();
38        for seg in self.dir {
39            p.push(seg);
40        }
41        p
42    }
43    fn detect(&self, repo_root: &Path) -> bool {
44        self.resolve_dir(repo_root).is_dir()
45    }
46    fn read(&self, repo_root: &Path) -> Result<Vec<MemoryDoc>, CoreError> {
47        let dir = self.resolve_dir(repo_root);
48        if !dir.is_dir() {
49            return Ok(Vec::new());
50        }
51        let mut docs = Vec::new();
52        for entry in std::fs::read_dir(&dir)? {
53            let entry = entry?;
54            let path = entry.path();
55            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some(self.ext) {
56                docs.push(read_file_doc(self.id, path)?);
57            }
58        }
59        Ok(docs)
60    }
61}
62
63const CLAUDE_MD: SingleFileSpec = SingleFileSpec {
64    id: "claude-md",
65    label: "CLAUDE.md",
66    file_name: "CLAUDE.md",
67};
68
69const AGENTS_MD: SingleFileSpec = SingleFileSpec {
70    id: "agents-md",
71    label: "AGENTS.md",
72    file_name: "AGENTS.md",
73};
74
75const CURSOR_RULES: DirGlobSpec = DirGlobSpec {
76    id: "cursor-rules",
77    label: "Cursor rules",
78    dir: &[".cursor", "rules"],
79    ext: "mdc",
80};
81
82pub struct ClaudeMdSource;
83pub struct AgentsMdSource;
84pub struct CursorRulesSource;
85
86impl Source for ClaudeMdSource {
87    fn id(&self) -> &'static str {
88        CLAUDE_MD.id
89    }
90    fn label(&self) -> &'static str {
91        CLAUDE_MD.label
92    }
93    fn detect(&self, repo_root: &Path) -> bool {
94        CLAUDE_MD.detect(repo_root)
95    }
96    fn read(&self, repo_root: &Path) -> Result<Vec<MemoryDoc>, CoreError> {
97        CLAUDE_MD.read(repo_root)
98    }
99}
100
101impl Source for AgentsMdSource {
102    fn id(&self) -> &'static str {
103        AGENTS_MD.id
104    }
105    fn label(&self) -> &'static str {
106        AGENTS_MD.label
107    }
108    fn detect(&self, repo_root: &Path) -> bool {
109        AGENTS_MD.detect(repo_root)
110    }
111    fn read(&self, repo_root: &Path) -> Result<Vec<MemoryDoc>, CoreError> {
112        AGENTS_MD.read(repo_root)
113    }
114}
115
116impl Source for CursorRulesSource {
117    fn id(&self) -> &'static str {
118        CURSOR_RULES.id
119    }
120    fn label(&self) -> &'static str {
121        CURSOR_RULES.label
122    }
123    fn detect(&self, repo_root: &Path) -> bool {
124        CURSOR_RULES.detect(repo_root)
125    }
126    fn read(&self, repo_root: &Path) -> Result<Vec<MemoryDoc>, CoreError> {
127        CURSOR_RULES.read(repo_root)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use tempfile::TempDir;
135
136    #[test]
137    fn detects_claude_md_when_present() {
138        let dir = TempDir::new().unwrap();
139        assert!(!ClaudeMdSource.detect(dir.path()));
140        std::fs::write(dir.path().join("CLAUDE.md"), "Project memory.").unwrap();
141        assert!(ClaudeMdSource.detect(dir.path()));
142
143        let docs = ClaudeMdSource.read(dir.path()).unwrap();
144        assert_eq!(docs.len(), 1);
145        assert_eq!(docs[0].content.trim(), "Project memory.");
146    }
147
148    #[test]
149    fn detects_agents_md_when_present() {
150        let dir = TempDir::new().unwrap();
151        assert!(!AgentsMdSource.detect(dir.path()));
152        std::fs::write(dir.path().join("AGENTS.md"), "# Agents\nuse rust").unwrap();
153        assert!(AgentsMdSource.detect(dir.path()));
154
155        let docs = AgentsMdSource.read(dir.path()).unwrap();
156        assert_eq!(docs.len(), 1);
157        assert_eq!(docs[0].source_id, "agents-md");
158        assert!(docs[0].content.contains("use rust"));
159    }
160
161    #[test]
162    fn detects_cursor_rules_dir() {
163        let dir = TempDir::new().unwrap();
164        assert!(!CursorRulesSource.detect(dir.path()));
165        let rules = dir.path().join(".cursor").join("rules");
166        std::fs::create_dir_all(&rules).unwrap();
167        std::fs::write(rules.join("a.mdc"), "---\nname: a\n---\nbody").unwrap();
168        std::fs::write(rules.join("README.txt"), "skip").unwrap();
169        assert!(CursorRulesSource.detect(dir.path()));
170
171        let docs = CursorRulesSource.read(dir.path()).unwrap();
172        assert_eq!(docs.len(), 1);
173        assert_eq!(docs[0].source_id, "cursor-rules");
174        assert!(docs[0].content.contains("name: a"));
175    }
176}