1use std::fmt::Write;
5use std::path::{Path, PathBuf};
6
7const PROJECT_CONFIG_FILES: &[&str] = &["ZEPH.md", ".zeph/config.md"];
8
9#[must_use]
12pub fn discover_project_configs(start: &Path) -> Vec<PathBuf> {
13 let mut configs = Vec::new();
14 let mut current = start.to_path_buf();
15
16 loop {
17 for filename in PROJECT_CONFIG_FILES {
18 let candidate = current.join(filename);
19 if candidate.is_file() {
20 configs.push(candidate);
21 }
22 }
23 if !current.pop() {
24 break;
25 }
26 }
27
28 configs.reverse();
29 configs
30}
31
32#[must_use]
34pub fn load_project_context(configs: &[PathBuf]) -> String {
35 if configs.is_empty() {
36 return String::new();
37 }
38
39 let mut out = String::from("<project_context>\n");
40 for path in configs {
41 if let Ok(content) = std::fs::read_to_string(path) {
42 let source = path.display();
43 let _ = write!(
44 out,
45 " <config source=\"{source}\">\n{content}\n </config>\n"
46 );
47 }
48 }
49 out.push_str("</project_context>");
50 out
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn discover_project_configs_empty_dir() {
59 let dir = tempfile::tempdir().unwrap();
60 let configs = discover_project_configs(dir.path());
61 assert!(configs.is_empty());
62 }
63
64 #[test]
65 fn discover_project_configs_finds_zeph_md() {
66 let dir = tempfile::tempdir().unwrap();
67 std::fs::write(dir.path().join("ZEPH.md"), "# Project").unwrap();
68 let configs = discover_project_configs(dir.path());
69 assert_eq!(configs.len(), 1);
70 assert!(configs[0].ends_with("ZEPH.md"));
71 }
72
73 #[test]
74 fn discover_project_configs_walks_up() {
75 let dir = tempfile::tempdir().unwrap();
76 let child = dir.path().join("sub");
77 std::fs::create_dir(&child).unwrap();
78 std::fs::write(dir.path().join("ZEPH.md"), "# Parent").unwrap();
79 std::fs::write(child.join("ZEPH.md"), "# Child").unwrap();
80
81 let configs = discover_project_configs(&child);
82 assert!(configs.len() >= 2);
83 let parent_idx = configs
85 .iter()
86 .position(|p| p.parent().unwrap() == dir.path())
87 .unwrap();
88 let child_idx = configs
89 .iter()
90 .position(|p| p.parent().unwrap() == child)
91 .unwrap();
92 assert!(parent_idx < child_idx);
93 }
94
95 #[test]
96 fn load_project_context_empty() {
97 let result = load_project_context(&[]);
98 assert!(result.is_empty());
99 }
100
101 #[test]
102 fn load_project_context_concatenates() {
103 let dir = tempfile::tempdir().unwrap();
104 let f1 = dir.path().join("ZEPH.md");
105 let f2 = dir.path().join("other.md");
106 std::fs::write(&f1, "config 1").unwrap();
107 std::fs::write(&f2, "config 2").unwrap();
108
109 let result = load_project_context(&[f1, f2]);
110 assert!(result.starts_with("<project_context>"));
111 assert!(result.ends_with("</project_context>"));
112 assert!(result.contains("config 1"));
113 assert!(result.contains("config 2"));
114 assert!(result.contains("<config source="));
115 }
116}