difflore_core/sources/
simple_files.rs1use 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}