1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3
4use crate::parser::Parser;
5use crate::types::{ParseError, Spec};
6
7#[derive(Debug)]
12pub struct SpecGraph {
13 specs: Vec<Spec>,
14}
15
16fn collect_ought_files(dir: &std::path::Path) -> Vec<PathBuf> {
18 let mut results = Vec::new();
19 if let Ok(entries) = std::fs::read_dir(dir) {
20 for entry in entries.flatten() {
21 let path = entry.path();
22 if path.is_dir() {
23 results.extend(collect_ought_files(&path));
24 } else if let Some(name) = path.file_name().and_then(|n| n.to_str())
25 && name.ends_with(".ought.md") {
26 results.push(path);
27 }
28 }
29 }
30 results
31}
32
33impl SpecGraph {
34 pub fn from_roots(roots: &[PathBuf]) -> Result<Self, Vec<ParseError>> {
37 let mut all_files = Vec::new();
38 for root in roots {
39 let files = collect_ought_files(root);
40 all_files.extend(files);
41 }
42
43 all_files.sort();
45 all_files.dedup();
46
47 let mut specs = Vec::new();
48 let mut all_errors = Vec::new();
49
50 for file in &all_files {
51 match Parser::parse_file(file) {
52 Ok(spec) => specs.push(spec),
53 Err(errors) => all_errors.extend(errors),
54 }
55 }
56
57 let known_paths: std::collections::HashSet<PathBuf> = specs
59 .iter()
60 .map(|s| s.source_path.clone())
61 .collect();
62
63 for spec in &specs {
64 for req in &spec.metadata.requires {
65 let base_dir = spec.source_path.parent().unwrap_or(Path::new("."));
67 let resolved = base_dir.join(&req.path);
68 let canonical = resolved.canonicalize().unwrap_or(resolved.clone());
69
70 if !canonical.exists() && !known_paths.iter().any(|p| {
71 p.canonicalize().unwrap_or(p.clone()) == canonical
72 }) {
73 all_errors.push(ParseError {
74 file: spec.source_path.clone(),
75 line: 0,
76 message: format!(
77 "unresolved cross-reference: '{}' (resolved to '{}')",
78 req.path.display(),
79 resolved.display()
80 ),
81 });
82 }
83 }
84 }
85
86 let cycle_errors = detect_cycles(&specs);
88 all_errors.extend(cycle_errors);
89
90 if !all_errors.is_empty() {
91 return Err(all_errors);
92 }
93
94 Ok(Self { specs })
95 }
96
97 pub fn specs(&self) -> &[Spec] {
99 &self.specs
100 }
101
102 pub fn topological_order(&self) -> Vec<&Spec> {
105 if self.specs.is_empty() {
106 return Vec::new();
107 }
108
109 let path_to_idx: HashMap<&PathBuf, usize> = self
111 .specs
112 .iter()
113 .enumerate()
114 .map(|(i, s)| (&s.source_path, i))
115 .collect();
116
117 let n = self.specs.len();
118 let mut in_degree = vec![0usize; n];
119 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
120
121 for (i, spec) in self.specs.iter().enumerate() {
122 for req in &spec.metadata.requires {
123 let base_dir = spec.source_path.parent().unwrap_or(std::path::Path::new(""));
125 let resolved = base_dir.join(&req.path);
126
127 let target_idx = path_to_idx.get(&resolved).or_else(|| path_to_idx.get(&req.path));
129
130 if let Some(&j) = target_idx {
131 adjacency[j].push(i);
133 in_degree[i] += 1;
134 }
135 }
136 }
137
138 let mut queue: VecDeque<usize> = VecDeque::new();
140 for (i, °) in in_degree.iter().enumerate() {
141 if deg == 0 {
142 queue.push_back(i);
143 }
144 }
145
146 let mut order = Vec::with_capacity(n);
147 while let Some(node) = queue.pop_front() {
148 order.push(&self.specs[node]);
149 for &neighbor in &adjacency[node] {
150 in_degree[neighbor] -= 1;
151 if in_degree[neighbor] == 0 {
152 queue.push_back(neighbor);
153 }
154 }
155 }
156
157 order
159 }
160
161 pub fn get_by_path(&self, path: &std::path::Path) -> Option<&Spec> {
163 self.specs.iter().find(|s| s.source_path == path)
164 }
165}
166
167fn detect_cycles(specs: &[Spec]) -> Vec<ParseError> {
169 let path_to_idx: HashMap<&PathBuf, usize> = specs
170 .iter()
171 .enumerate()
172 .map(|(i, s)| (&s.source_path, i))
173 .collect();
174
175 let n = specs.len();
176 let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
177
178 for (i, spec) in specs.iter().enumerate() {
179 for req in &spec.metadata.requires {
180 let base_dir = spec
181 .source_path
182 .parent()
183 .unwrap_or(std::path::Path::new(""));
184 let resolved = base_dir.join(&req.path);
185
186 let target_idx = path_to_idx.get(&resolved).or_else(|| path_to_idx.get(&req.path));
187
188 if let Some(&j) = target_idx {
189 adjacency[i].push(j);
190 }
191 }
192 }
193
194 let mut errors = Vec::new();
196 let mut visited = vec![0u8; n]; fn dfs(
199 node: usize,
200 adjacency: &[Vec<usize>],
201 visited: &mut [u8],
202 path: &mut Vec<usize>,
203 specs: &[Spec],
204 errors: &mut Vec<ParseError>,
205 ) {
206 visited[node] = 1;
207 path.push(node);
208
209 for &neighbor in &adjacency[node] {
210 if visited[neighbor] == 1 {
211 let cycle_start = path.iter().position(|&n| n == neighbor).unwrap();
213 let cycle_names: Vec<String> = path[cycle_start..]
214 .iter()
215 .map(|&i| specs[i].source_path.display().to_string())
216 .collect();
217 errors.push(ParseError {
218 file: specs[node].source_path.clone(),
219 line: 0,
220 message: format!(
221 "circular dependency detected: {}",
222 cycle_names.join(" -> ")
223 ),
224 });
225 } else if visited[neighbor] == 0 {
226 dfs(neighbor, adjacency, visited, path, specs, errors);
227 }
228 }
229
230 path.pop();
231 visited[node] = 2;
232 }
233
234 let mut path = Vec::new();
235 for i in 0..n {
236 if visited[i] == 0 {
237 dfs(i, &adjacency, &mut visited, &mut path, specs, &mut errors);
238 }
239 }
240
241 errors
242}