1use std::collections::HashSet;
4
5use crate::entity::EntityId;
6use crate::store::Store;
7
8pub fn ancestors(store: &Store, id: &EntityId, hierarchy_rel: &str) -> Vec<EntityId> {
11 let node = match store.get(id) {
12 Some(n) => n,
13 None => return Vec::new(),
14 };
15 let mem = &node.mem;
16
17 let mut result = Vec::new();
18 let mut visited = HashSet::new();
19 visited.insert(id.clone());
20
21 let mut current = id.clone();
22
23 loop {
24 let parent = store
26 .outgoing(¤t)
27 .iter()
28 .find(|e| e.rel_type == hierarchy_rel)
29 .map(|e| e.target.clone());
30
31 let parent_id = match parent {
32 Some(pid) => pid,
33 None => break,
34 };
35
36 if visited.contains(&parent_id) {
38 break;
39 }
40
41 let parent_node = match store.get(&parent_id) {
43 Some(n) => n,
44 None => break,
45 };
46 if parent_node.mem != *mem {
47 break;
48 }
49
50 result.push(parent_id.clone());
51 visited.insert(parent_id.clone());
52 current = parent_id;
53 }
54
55 result
56}
57
58pub fn descendants(store: &Store, id: &EntityId, hierarchy_rel: &str) -> Vec<EntityId> {
61 let node = match store.get(id) {
62 Some(n) => n,
63 None => return Vec::new(),
64 };
65 let mem = &node.mem;
66
67 let mut result = Vec::new();
68 let mut visited = HashSet::new();
69 visited.insert(id.clone());
70
71 let mut queue = vec![id.clone()];
73 let mut head = 0;
74
75 while head < queue.len() {
76 let current = queue[head].clone();
77 head += 1;
78
79 for edge in store.incoming(¤t) {
82 if edge.rel_type != hierarchy_rel {
83 continue;
84 }
85 if visited.contains(&edge.from) {
86 continue;
87 }
88 let child_node = match store.get(&edge.from) {
89 Some(n) => n,
90 None => continue,
91 };
92 if child_node.mem != *mem {
93 continue;
94 }
95 visited.insert(edge.from.clone());
96 result.push(edge.from.clone());
97 queue.push(edge.from.clone());
98 }
99 }
100
101 result
102}
103
104pub fn compute_file_path(store: &Store, id: &EntityId, hierarchy_rel: &str) -> String {
107 let ancs = ancestors(store, id, hierarchy_rel);
108 let name = id.name();
109
110 if ancs.is_empty() {
111 return format!("{name}.md");
112 }
113
114 let segments: Vec<&str> = ancs.iter().rev().map(|a| a.name()).collect();
115 format!("{}/{name}.md", segments.join("/"))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::entity::Entity;
122 use crate::store::{Edge, EdgeSource};
123 use indexmap::IndexMap;
124
125 fn entity(id: &str, mem: &str) -> Entity {
126 Entity {
127 id: EntityId(id.to_string()),
128 title: id.to_string(),
129 entity_type: "spec".to_string(),
130 mem: mem.to_string(),
131 file_path: String::new(),
132 metadata: IndexMap::new(),
133 sections: IndexMap::new(),
134 relationships: Vec::new(),
135 content_hash: String::new(),
136 stub: false,
137 stub_kind: None,
138 heading_spans: std::collections::HashMap::new(),
139 raw_section_headings: Vec::new(),
140 }
141 }
142
143 fn add_part_of(store: &mut Store, child: &str, parent: &str) {
144 store.add_edge(
145 EntityId(child.to_string()),
146 Edge {
147 rel_type: "PART_OF".to_string(),
148 target: EntityId(parent.to_string()),
149 source: EdgeSource::Explicit,
150 },
151 );
152 }
153
154 #[test]
155 fn ancestors_empty_for_root() {
156 let mut store = Store::new();
157 store.upsert(EntityId("s--root".into()), entity("s--root", "s"));
158 let ancs = ancestors(&store, &EntityId("s--root".into()), "PART_OF");
159 assert!(ancs.is_empty());
160 }
161
162 #[test]
163 fn ancestors_single_parent() {
164 let mut store = Store::new();
165 store.upsert(EntityId("s--parent".into()), entity("s--parent", "s"));
166 store.upsert(EntityId("s--child".into()), entity("s--child", "s"));
167 add_part_of(&mut store, "s--child", "s--parent");
168
169 let ancs = ancestors(&store, &EntityId("s--child".into()), "PART_OF");
170 assert_eq!(ancs, vec![EntityId("s--parent".into())]);
171 }
172
173 #[test]
174 fn ancestors_chain() {
175 let mut store = Store::new();
176 store.upsert(EntityId("s--root".into()), entity("s--root", "s"));
177 store.upsert(EntityId("s--mid".into()), entity("s--mid", "s"));
178 store.upsert(EntityId("s--leaf".into()), entity("s--leaf", "s"));
179 add_part_of(&mut store, "s--leaf", "s--mid");
180 add_part_of(&mut store, "s--mid", "s--root");
181
182 let ancs = ancestors(&store, &EntityId("s--leaf".into()), "PART_OF");
183 assert_eq!(
184 ancs,
185 vec![EntityId("s--mid".into()), EntityId("s--root".into())]
186 );
187 }
188
189 #[test]
190 fn ancestors_cycle_detection() {
191 let mut store = Store::new();
192 store.upsert(EntityId("s--a".into()), entity("s--a", "s"));
193 store.upsert(EntityId("s--b".into()), entity("s--b", "s"));
194 add_part_of(&mut store, "s--a", "s--b");
195 add_part_of(&mut store, "s--b", "s--a");
196
197 let ancs = ancestors(&store, &EntityId("s--a".into()), "PART_OF");
198 assert_eq!(ancs, vec![EntityId("s--b".into())]); }
200
201 #[test]
202 fn ancestors_stops_at_mem_boundary() {
203 let mut store = Store::new();
204 store.upsert(EntityId("s--child".into()), entity("s--child", "s"));
205 store.upsert(
206 EntityId("other--parent".into()),
207 entity("other--parent", "other"),
208 );
209 add_part_of(&mut store, "s--child", "other--parent");
210
211 let ancs = ancestors(&store, &EntityId("s--child".into()), "PART_OF");
212 assert!(ancs.is_empty()); }
214
215 #[test]
216 fn descendants_of_root() {
217 let mut store = Store::new();
218 store.upsert(EntityId("s--root".into()), entity("s--root", "s"));
219 store.upsert(EntityId("s--a".into()), entity("s--a", "s"));
220 store.upsert(EntityId("s--b".into()), entity("s--b", "s"));
221 store.upsert(EntityId("s--a/c".into()), entity("s--a/c", "s"));
222 add_part_of(&mut store, "s--a", "s--root");
223 add_part_of(&mut store, "s--b", "s--root");
224 add_part_of(&mut store, "s--a/c", "s--a");
225
226 let desc = descendants(&store, &EntityId("s--root".into()), "PART_OF");
227 assert_eq!(desc.len(), 3);
228 }
229
230 #[test]
231 fn descendants_stops_at_mem_boundary() {
232 let mut store = Store::new();
233 store.upsert(EntityId("s--root".into()), entity("s--root", "s"));
234 store.upsert(
235 EntityId("other--child".into()),
236 entity("other--child", "other"),
237 );
238 add_part_of(&mut store, "other--child", "s--root");
239
240 let desc = descendants(&store, &EntityId("s--root".into()), "PART_OF");
241 assert!(desc.is_empty());
242 }
243
244 #[test]
245 fn descendants_empty_for_leaf() {
246 let mut store = Store::new();
247 store.upsert(EntityId("s--leaf".into()), entity("s--leaf", "s"));
248
249 let desc = descendants(&store, &EntityId("s--leaf".into()), "PART_OF");
250 assert!(desc.is_empty());
251 }
252
253 #[test]
254 fn compute_file_path_root_entity() {
255 let mut store = Store::new();
256 store.upsert(EntityId("s--my-entity".into()), entity("s--my-entity", "s"));
257
258 let path = compute_file_path(&store, &EntityId("s--my-entity".into()), "PART_OF");
259 assert_eq!(path, "my-entity.md");
260 }
261
262 #[test]
263 fn compute_file_path_nested() {
264 let mut store = Store::new();
265 store.upsert(EntityId("s--root".into()), entity("s--root", "s"));
266 store.upsert(EntityId("s--child".into()), entity("s--child", "s"));
267 add_part_of(&mut store, "s--child", "s--root");
268
269 let path = compute_file_path(&store, &EntityId("s--child".into()), "PART_OF");
270 assert_eq!(path, "root/child.md");
271 }
272
273 #[test]
274 fn compute_file_path_deeply_nested() {
275 let mut store = Store::new();
276 store.upsert(EntityId("s--gp".into()), entity("s--gp", "s"));
277 store.upsert(EntityId("s--parent".into()), entity("s--parent", "s"));
278 store.upsert(EntityId("s--child".into()), entity("s--child", "s"));
279 add_part_of(&mut store, "s--child", "s--parent");
280 add_part_of(&mut store, "s--parent", "s--gp");
281
282 let path = compute_file_path(&store, &EntityId("s--child".into()), "PART_OF");
283 assert_eq!(path, "gp/parent/child.md");
284 }
285}