1use crate::entity::{Entity, EntityId};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct Edge {
10 pub rel_type: String,
11 pub target: EntityId,
12 pub source: EdgeSource,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum EdgeSource {
23 Explicit,
25 Hierarchy,
27 BodyLink,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct InEdge {
36 pub rel_type: String,
37 pub from: EntityId,
38 pub source: EdgeSource,
39}
40
41#[derive(Debug, Clone)]
47pub struct Store {
48 nodes: HashMap<EntityId, Entity>,
49 out_edges: HashMap<EntityId, Vec<Edge>>,
50 in_edges: HashMap<EntityId, Vec<InEdge>>,
51}
52
53impl Store {
54 pub fn new() -> Self {
55 Self {
56 nodes: HashMap::new(),
57 out_edges: HashMap::new(),
58 in_edges: HashMap::new(),
59 }
60 }
61
62 pub fn upsert(&mut self, id: EntityId, entity: Entity) {
64 if !self.out_edges.contains_key(&id) {
65 self.out_edges.insert(id.clone(), Vec::new());
66 }
67 if !self.in_edges.contains_key(&id) {
68 self.in_edges.insert(id.clone(), Vec::new());
69 }
70 self.nodes.insert(id, entity);
71 }
72
73 pub fn remove(&mut self, id: &EntityId) -> Option<Entity> {
75 if let Some(out) = self.out_edges.remove(id) {
77 for edge in &out {
78 if let Some(in_list) = self.in_edges.get_mut(&edge.target) {
79 in_list.retain(|e| &e.from != id);
80 }
81 }
82 }
83 if let Some(inc) = self.in_edges.remove(id) {
85 for edge in &inc {
86 if let Some(out_list) = self.out_edges.get_mut(&edge.from) {
87 out_list.retain(|e| &e.target != id);
88 }
89 }
90 }
91 self.nodes.remove(id)
92 }
93
94 pub fn get(&self, id: &EntityId) -> Option<&Entity> {
95 self.nodes.get(id)
96 }
97
98 pub fn get_mut(&mut self, id: &EntityId) -> Option<&mut Entity> {
99 self.nodes.get_mut(id)
100 }
101
102 pub fn contains(&self, id: &EntityId) -> bool {
103 self.nodes.contains_key(id)
104 }
105
106 pub fn all_ids(&self) -> impl Iterator<Item = &EntityId> {
107 self.nodes.keys()
108 }
109
110 pub fn all_entities(&self) -> impl Iterator<Item = &Entity> {
111 self.nodes.values()
112 }
113
114 pub fn remove_entities_by_mem(&mut self, mem: &str) -> usize {
128 let to_remove: Vec<EntityId> = self
129 .nodes
130 .keys()
131 .filter(|id| id.mem() == mem)
132 .cloned()
133 .collect();
134 let count = to_remove.len();
135 for id in to_remove {
136 self.remove(&id);
137 }
138 count
139 }
140
141 pub fn len(&self) -> usize {
142 self.nodes.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
146 self.nodes.is_empty()
147 }
148
149 pub fn add_edge(&mut self, from: EntityId, edge: Edge) {
152 let target = edge.target.clone();
153 let rel_type = edge.rel_type.clone();
154 let source = edge.source.clone();
155
156 self.out_edges.entry(from.clone()).or_default();
158 self.in_edges.entry(target.clone()).or_default();
159
160 let out_list = self.out_edges.get_mut(&from).unwrap();
162 if let Some(existing) = out_list
163 .iter_mut()
164 .find(|e| e.target == target && e.rel_type == rel_type)
165 {
166 existing.source = source.clone();
167 if let Some(in_list) = self.in_edges.get_mut(&target)
169 && let Some(mirror) = in_list
170 .iter_mut()
171 .find(|e| e.from == from && e.rel_type == rel_type)
172 {
173 mirror.source = source;
174 }
175 } else {
176 out_list.push(edge);
177 self.in_edges.get_mut(&target).unwrap().push(InEdge {
178 rel_type,
179 from,
180 source,
181 });
182 }
183 }
184
185 pub fn remove_edge(&mut self, from: &EntityId, to: &EntityId, rel_type: &str) {
187 if let Some(out_list) = self.out_edges.get_mut(from) {
188 out_list.retain(|e| !(e.target == *to && e.rel_type == rel_type));
189 }
190 if let Some(in_list) = self.in_edges.get_mut(to) {
191 in_list.retain(|e| !(e.from == *from && e.rel_type == rel_type));
192 }
193 }
194
195 pub fn remove_edges_from(&mut self, id: &EntityId) {
197 if let Some(out) = self.out_edges.get_mut(id) {
198 let edges = std::mem::take(out);
199 for edge in edges {
200 if let Some(in_list) = self.in_edges.get_mut(&edge.target) {
201 in_list.retain(|e| &e.from != id);
202 }
203 }
204 }
205 }
206
207 pub fn outgoing(&self, id: &EntityId) -> &[Edge] {
209 self.out_edges.get(id).map_or(&[], |v| v.as_slice())
210 }
211
212 pub fn incoming(&self, id: &EntityId) -> &[InEdge] {
214 self.in_edges.get(id).map_or(&[], |v| v.as_slice())
215 }
216
217 pub fn rename_node(&mut self, old_id: &EntityId, new_id: EntityId) -> bool {
219 if old_id == &new_id {
220 return false;
221 }
222 let Some(mut entity) = self.nodes.remove(old_id) else {
223 return false;
224 };
225 entity.id = new_id.clone();
226 self.nodes.insert(new_id.clone(), entity);
227
228 let out = self.out_edges.remove(old_id).unwrap_or_default();
230 let inc = self.in_edges.remove(old_id).unwrap_or_default();
231 self.out_edges.insert(new_id.clone(), out);
232 self.in_edges.insert(new_id.clone(), inc);
233
234 for edges in self.out_edges.values_mut() {
236 for e in edges.iter_mut() {
237 if e.target == *old_id {
238 e.target = new_id.clone();
239 }
240 }
241 }
242 for edges in self.in_edges.values_mut() {
243 for e in edges.iter_mut() {
244 if e.from == *old_id {
245 e.from = new_id.clone();
246 }
247 }
248 }
249
250 for entity in self.nodes.values_mut() {
258 for rel in entity.relationships.iter_mut() {
259 if rel.target == *old_id {
260 rel.target = new_id.clone();
261 }
262 }
263 }
264 true
265 }
266
267 pub fn edge_count(&self) -> usize {
269 self.out_edges.values().map(|v| v.len()).sum()
270 }
271
272 pub fn clear(&mut self) {
274 self.nodes.clear();
275 self.out_edges.clear();
276 self.in_edges.clear();
277 }
278}
279
280impl Default for Store {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::Relationship;
290 use indexmap::IndexMap;
291
292 fn stub_entity(id: &str, mem: &str) -> Entity {
293 Entity {
294 id: EntityId(id.to_string()),
295 title: id.to_string(),
296 entity_type: "spec".to_string(),
297 mem: mem.to_string(),
298 file_path: String::new(),
299 metadata: IndexMap::new(),
300 sections: IndexMap::new(),
301 relationships: Vec::new(),
302 content_hash: String::new(),
303 stub: true,
304 stub_kind: None,
305 heading_spans: std::collections::HashMap::new(),
306 raw_section_headings: Vec::new(),
307 }
308 }
309
310 #[test]
311 fn new_store_is_empty() {
312 let store = Store::new();
313 assert!(store.is_empty());
314 assert_eq!(store.len(), 0);
315 assert_eq!(store.edge_count(), 0);
316 }
317
318 #[test]
319 fn upsert_and_get() {
320 let mut store = Store::new();
321 let id = EntityId("specs--test".to_string());
322 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
323 assert_eq!(store.len(), 1);
324 assert!(store.get(&id).is_some());
325 assert_eq!(store.get(&id).unwrap().title, "specs--test");
326 }
327
328 #[test]
329 fn upsert_replaces_existing() {
330 let mut store = Store::new();
331 let id = EntityId("specs--test".to_string());
332 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
333 let mut updated = stub_entity("specs--test", "specs");
334 updated.title = "Updated Title".to_string();
335 store.upsert(id.clone(), updated);
336 assert_eq!(store.len(), 1);
337 assert_eq!(store.get(&id).unwrap().title, "Updated Title");
338 }
339
340 #[test]
341 fn remove_node_cascades_edges() {
342 let mut store = Store::new();
343 let a = EntityId("specs--a".to_string());
344 let b = EntityId("specs--b".to_string());
345 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
346 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
347 store.add_edge(
348 a.clone(),
349 Edge {
350 rel_type: "USES".to_string(),
351 target: b.clone(),
352 source: EdgeSource::Explicit,
353 },
354 );
355 assert_eq!(store.edge_count(), 1);
356 store.remove(&b);
357 assert_eq!(store.len(), 1);
358 assert_eq!(store.edge_count(), 0);
359 assert!(store.outgoing(&a).is_empty());
360 }
361
362 #[test]
363 fn add_edge_idempotent() {
364 let mut store = Store::new();
365 let a = EntityId("specs--a".to_string());
366 let b = EntityId("specs--b".to_string());
367 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
368 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
369
370 store.add_edge(
371 a.clone(),
372 Edge {
373 rel_type: "USES".to_string(),
374 target: b.clone(),
375 source: EdgeSource::Explicit,
376 },
377 );
378 store.add_edge(
380 a.clone(),
381 Edge {
382 rel_type: "USES".to_string(),
383 target: b.clone(),
384 source: EdgeSource::Hierarchy,
385 },
386 );
387 assert_eq!(store.edge_count(), 1);
388 assert_eq!(store.outgoing(&a)[0].source, EdgeSource::Hierarchy);
389 assert_eq!(store.incoming(&b)[0].source, EdgeSource::Hierarchy);
390 }
391
392 #[test]
393 fn bidirectional_edges() {
394 let mut store = Store::new();
395 let a = EntityId("specs--a".to_string());
396 let b = EntityId("specs--b".to_string());
397 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
398 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
399 store.add_edge(
400 a.clone(),
401 Edge {
402 rel_type: "USES".to_string(),
403 target: b.clone(),
404 source: EdgeSource::Explicit,
405 },
406 );
407 assert_eq!(store.outgoing(&a).len(), 1);
408 assert_eq!(store.outgoing(&a)[0].target, b);
409 assert_eq!(store.incoming(&b).len(), 1);
410 assert_eq!(store.incoming(&b)[0].from, a);
411 }
412
413 #[test]
414 fn remove_edges_from() {
415 let mut store = Store::new();
416 let a = EntityId("specs--a".to_string());
417 let b = EntityId("specs--b".to_string());
418 let c = EntityId("specs--c".to_string());
419 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
420 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
421 store.upsert(c.clone(), stub_entity("specs--c", "specs"));
422 store.add_edge(
423 a.clone(),
424 Edge {
425 rel_type: "USES".to_string(),
426 target: b.clone(),
427 source: EdgeSource::Explicit,
428 },
429 );
430 store.add_edge(
431 a.clone(),
432 Edge {
433 rel_type: "USES".to_string(),
434 target: c.clone(),
435 source: EdgeSource::Explicit,
436 },
437 );
438 assert_eq!(store.edge_count(), 2);
439 store.remove_edges_from(&a);
440 assert_eq!(store.edge_count(), 0);
441 assert!(store.outgoing(&a).is_empty());
442 assert!(store.incoming(&b).is_empty());
443 assert!(store.incoming(&c).is_empty());
444 }
445
446 #[test]
447 fn remove_specific_edge() {
448 let mut store = Store::new();
449 let a = EntityId("specs--a".to_string());
450 let b = EntityId("specs--b".to_string());
451 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
452 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
453 store.add_edge(
454 a.clone(),
455 Edge {
456 rel_type: "USES".to_string(),
457 target: b.clone(),
458 source: EdgeSource::Explicit,
459 },
460 );
461 store.add_edge(
462 a.clone(),
463 Edge {
464 rel_type: "PART_OF".to_string(),
465 target: b.clone(),
466 source: EdgeSource::Explicit,
467 },
468 );
469 assert_eq!(store.edge_count(), 2);
470 store.remove_edge(&a, &b, "USES");
471 assert_eq!(store.edge_count(), 1);
472 assert_eq!(store.outgoing(&a)[0].rel_type, "PART_OF");
473 }
474
475 #[test]
476 fn rename_node() {
477 let mut store = Store::new();
478 let a = EntityId("specs--a".to_string());
479 let b = EntityId("specs--b".to_string());
480 let new_a = EntityId("specs--a-renamed".to_string());
481 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
482 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
483 store.add_edge(
484 a.clone(),
485 Edge {
486 rel_type: "USES".to_string(),
487 target: b.clone(),
488 source: EdgeSource::Explicit,
489 },
490 );
491 store.add_edge(
492 b.clone(),
493 Edge {
494 rel_type: "PART_OF".to_string(),
495 target: a.clone(),
496 source: EdgeSource::Explicit,
497 },
498 );
499
500 assert!(store.rename_node(&a, new_a.clone()));
501 assert!(store.get(&a).is_none());
502 assert!(store.get(&new_a).is_some());
503 assert_eq!(store.outgoing(&new_a).len(), 1);
504 assert_eq!(store.incoming(&new_a).len(), 1);
505 assert_eq!(store.incoming(&new_a)[0].from, b);
506 assert_eq!(store.outgoing(&b)[0].target, new_a);
508 }
509
510 #[test]
511 fn rename_node_rewrites_self_loop_in_relationships_vec() {
512 let mut store = Store::new();
518 let old_id = EntityId("specs--selfie".to_string());
519 let new_id = EntityId("specs--selfie-renamed".to_string());
520 let mut entity = stub_entity("specs--selfie", "specs");
521 entity.stub = false;
522 entity.relationships.push(Relationship {
523 rel_type: "REFERENCES".to_string(),
524 target: old_id.clone(),
525 description: None,
526 });
527 store.upsert(old_id.clone(), entity);
528 store.add_edge(
529 old_id.clone(),
530 Edge {
531 rel_type: "REFERENCES".to_string(),
532 target: old_id.clone(),
533 source: EdgeSource::Explicit,
534 },
535 );
536
537 assert!(store.rename_node(&old_id, new_id.clone()));
538
539 let renamed = store.get(&new_id).expect("renamed entity exists");
540 assert_eq!(renamed.relationships.len(), 1);
541 assert_eq!(
542 renamed.relationships[0].target, new_id,
543 "self-loop target inside entity.relationships must be rewritten \
544 to new_id — otherwise write_entity leaks old id to disk"
545 );
546 assert_eq!(store.outgoing(&new_id).len(), 1);
549 assert_eq!(store.outgoing(&new_id)[0].target, new_id);
550 assert_eq!(store.incoming(&new_id).len(), 1);
551 assert_eq!(store.incoming(&new_id)[0].from, new_id);
552 }
553
554 #[test]
555 fn clear_empties_store() {
556 let mut store = Store::new();
557 let a = EntityId("specs--a".to_string());
558 store.upsert(a, stub_entity("specs--a", "specs"));
559 store.clear();
560 assert!(store.is_empty());
561 assert_eq!(store.edge_count(), 0);
562 }
563
564 #[test]
565 fn outgoing_empty_for_unknown_id() {
566 let store = Store::new();
567 assert!(store.outgoing(&EntityId("unknown".to_string())).is_empty());
568 }
569}