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)]
58pub struct Store {
59 nodes: HashMap<EntityId, Entity>,
60 out_edges: HashMap<EntityId, Vec<Edge>>,
61 in_edges: HashMap<EntityId, Vec<InEdge>>,
62 generation: u64,
63}
64
65impl Store {
66 pub fn new() -> Self {
67 Self {
68 nodes: HashMap::new(),
69 out_edges: HashMap::new(),
70 in_edges: HashMap::new(),
71 generation: 0,
72 }
73 }
74
75 pub fn generation(&self) -> u64 {
78 self.generation
79 }
80
81 pub fn upsert(&mut self, id: EntityId, entity: Entity) {
83 self.generation += 1;
84 if !self.out_edges.contains_key(&id) {
85 self.out_edges.insert(id.clone(), Vec::new());
86 }
87 if !self.in_edges.contains_key(&id) {
88 self.in_edges.insert(id.clone(), Vec::new());
89 }
90 self.nodes.insert(id, entity);
91 }
92
93 pub fn remove(&mut self, id: &EntityId) -> Option<Entity> {
95 self.generation += 1;
96 if let Some(out) = self.out_edges.remove(id) {
98 for edge in &out {
99 if let Some(in_list) = self.in_edges.get_mut(&edge.target) {
100 in_list.retain(|e| &e.from != id);
101 }
102 }
103 }
104 if let Some(inc) = self.in_edges.remove(id) {
106 for edge in &inc {
107 if let Some(out_list) = self.out_edges.get_mut(&edge.from) {
108 out_list.retain(|e| &e.target != id);
109 }
110 }
111 }
112 self.nodes.remove(id)
113 }
114
115 pub fn get(&self, id: &EntityId) -> Option<&Entity> {
116 self.nodes.get(id)
117 }
118
119 pub fn get_mut(&mut self, id: &EntityId) -> Option<&mut Entity> {
120 self.generation += 1;
123 self.nodes.get_mut(id)
124 }
125
126 pub fn contains(&self, id: &EntityId) -> bool {
127 self.nodes.contains_key(id)
128 }
129
130 pub fn all_ids(&self) -> impl Iterator<Item = &EntityId> {
131 self.nodes.keys()
132 }
133
134 pub fn all_entities(&self) -> impl Iterator<Item = &Entity> {
135 self.nodes.values()
136 }
137
138 pub fn remove_entities_by_mem(&mut self, mem: &str) -> usize {
152 self.generation += 1;
153 let to_remove: Vec<EntityId> = self
154 .nodes
155 .keys()
156 .filter(|id| id.mem() == mem)
157 .cloned()
158 .collect();
159 let count = to_remove.len();
160 for id in to_remove {
161 self.remove(&id);
162 }
163 count
164 }
165
166 pub fn len(&self) -> usize {
167 self.nodes.len()
168 }
169
170 pub fn is_empty(&self) -> bool {
171 self.nodes.is_empty()
172 }
173
174 pub fn add_edge(&mut self, from: EntityId, edge: Edge) {
177 self.generation += 1;
178 let target = edge.target.clone();
179 let rel_type = edge.rel_type.clone();
180 let source = edge.source.clone();
181
182 self.out_edges.entry(from.clone()).or_default();
184 self.in_edges.entry(target.clone()).or_default();
185
186 let out_list = self.out_edges.get_mut(&from).unwrap();
188 if let Some(existing) = out_list
189 .iter_mut()
190 .find(|e| e.target == target && e.rel_type == rel_type)
191 {
192 existing.source = source.clone();
193 if let Some(in_list) = self.in_edges.get_mut(&target)
195 && let Some(mirror) = in_list
196 .iter_mut()
197 .find(|e| e.from == from && e.rel_type == rel_type)
198 {
199 mirror.source = source;
200 }
201 } else {
202 out_list.push(edge);
203 self.in_edges.get_mut(&target).unwrap().push(InEdge {
204 rel_type,
205 from,
206 source,
207 });
208 }
209 }
210
211 pub fn remove_edge(&mut self, from: &EntityId, to: &EntityId, rel_type: &str) {
213 self.generation += 1;
214 if let Some(out_list) = self.out_edges.get_mut(from) {
215 out_list.retain(|e| !(e.target == *to && e.rel_type == rel_type));
216 }
217 if let Some(in_list) = self.in_edges.get_mut(to) {
218 in_list.retain(|e| !(e.from == *from && e.rel_type == rel_type));
219 }
220 }
221
222 pub fn remove_edges_from(&mut self, id: &EntityId) {
224 self.generation += 1;
225 if let Some(out) = self.out_edges.get_mut(id) {
226 let edges = std::mem::take(out);
227 for edge in edges {
228 if let Some(in_list) = self.in_edges.get_mut(&edge.target) {
229 in_list.retain(|e| &e.from != id);
230 }
231 }
232 }
233 }
234
235 pub fn outgoing(&self, id: &EntityId) -> &[Edge] {
237 self.out_edges.get(id).map_or(&[], |v| v.as_slice())
238 }
239
240 pub fn incoming(&self, id: &EntityId) -> &[InEdge] {
242 self.in_edges.get(id).map_or(&[], |v| v.as_slice())
243 }
244
245 pub fn rename_node(&mut self, old_id: &EntityId, new_id: EntityId) -> bool {
247 self.generation += 1;
248 if old_id == &new_id {
249 return false;
250 }
251 let Some(mut entity) = self.nodes.remove(old_id) else {
252 return false;
253 };
254 entity.id = new_id.clone();
255 self.nodes.insert(new_id.clone(), entity);
256
257 let out = self.out_edges.remove(old_id).unwrap_or_default();
259 let inc = self.in_edges.remove(old_id).unwrap_or_default();
260 self.out_edges.insert(new_id.clone(), out);
261 self.in_edges.insert(new_id.clone(), inc);
262
263 for edges in self.out_edges.values_mut() {
265 for e in edges.iter_mut() {
266 if e.target == *old_id {
267 e.target = new_id.clone();
268 }
269 }
270 }
271 for edges in self.in_edges.values_mut() {
272 for e in edges.iter_mut() {
273 if e.from == *old_id {
274 e.from = new_id.clone();
275 }
276 }
277 }
278
279 for entity in self.nodes.values_mut() {
287 for rel in entity.relationships.iter_mut() {
288 if rel.target == *old_id {
289 rel.target = new_id.clone();
290 }
291 }
292 }
293 true
294 }
295
296 pub fn edge_count(&self) -> usize {
298 self.out_edges.values().map(|v| v.len()).sum()
299 }
300
301 pub fn clear(&mut self) {
303 self.generation += 1;
304 self.nodes.clear();
305 self.out_edges.clear();
306 self.in_edges.clear();
307 }
308}
309
310impl Default for Store {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::Relationship;
320 use indexmap::IndexMap;
321
322 fn stub_entity(id: &str, mem: &str) -> Entity {
323 Entity {
324 id: EntityId(id.to_string()),
325 title: id.to_string(),
326 entity_type: "spec".to_string(),
327 mem: mem.to_string(),
328 file_path: String::new(),
329 metadata: IndexMap::new(),
330 sections: IndexMap::new(),
331 relationships: Vec::new(),
332 content_hash: String::new(),
333 stub: true,
334 stub_kind: None,
335 heading_spans: std::collections::HashMap::new(),
336 raw_section_headings: Vec::new(),
337 }
338 }
339
340 #[test]
341 fn new_store_is_empty() {
342 let store = Store::new();
343 assert!(store.is_empty());
344 assert_eq!(store.len(), 0);
345 assert_eq!(store.edge_count(), 0);
346 }
347
348 #[test]
349 fn upsert_and_get() {
350 let mut store = Store::new();
351 let id = EntityId("specs--test".to_string());
352 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
353 assert_eq!(store.len(), 1);
354 assert!(store.get(&id).is_some());
355 assert_eq!(store.get(&id).unwrap().title, "specs--test");
356 }
357
358 #[test]
359 fn upsert_replaces_existing() {
360 let mut store = Store::new();
361 let id = EntityId("specs--test".to_string());
362 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
363 let mut updated = stub_entity("specs--test", "specs");
364 updated.title = "Updated Title".to_string();
365 store.upsert(id.clone(), updated);
366 assert_eq!(store.len(), 1);
367 assert_eq!(store.get(&id).unwrap().title, "Updated Title");
368 }
369
370 #[test]
371 fn remove_node_cascades_edges() {
372 let mut store = Store::new();
373 let a = EntityId("specs--a".to_string());
374 let b = EntityId("specs--b".to_string());
375 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
376 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
377 store.add_edge(
378 a.clone(),
379 Edge {
380 rel_type: "USES".to_string(),
381 target: b.clone(),
382 source: EdgeSource::Explicit,
383 },
384 );
385 assert_eq!(store.edge_count(), 1);
386 store.remove(&b);
387 assert_eq!(store.len(), 1);
388 assert_eq!(store.edge_count(), 0);
389 assert!(store.outgoing(&a).is_empty());
390 }
391
392 #[test]
393 fn add_edge_idempotent() {
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
400 store.add_edge(
401 a.clone(),
402 Edge {
403 rel_type: "USES".to_string(),
404 target: b.clone(),
405 source: EdgeSource::Explicit,
406 },
407 );
408 store.add_edge(
410 a.clone(),
411 Edge {
412 rel_type: "USES".to_string(),
413 target: b.clone(),
414 source: EdgeSource::Hierarchy,
415 },
416 );
417 assert_eq!(store.edge_count(), 1);
418 assert_eq!(store.outgoing(&a)[0].source, EdgeSource::Hierarchy);
419 assert_eq!(store.incoming(&b)[0].source, EdgeSource::Hierarchy);
420 }
421
422 #[test]
423 fn bidirectional_edges() {
424 let mut store = Store::new();
425 let a = EntityId("specs--a".to_string());
426 let b = EntityId("specs--b".to_string());
427 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
428 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
429 store.add_edge(
430 a.clone(),
431 Edge {
432 rel_type: "USES".to_string(),
433 target: b.clone(),
434 source: EdgeSource::Explicit,
435 },
436 );
437 assert_eq!(store.outgoing(&a).len(), 1);
438 assert_eq!(store.outgoing(&a)[0].target, b);
439 assert_eq!(store.incoming(&b).len(), 1);
440 assert_eq!(store.incoming(&b)[0].from, a);
441 }
442
443 #[test]
444 fn remove_edges_from() {
445 let mut store = Store::new();
446 let a = EntityId("specs--a".to_string());
447 let b = EntityId("specs--b".to_string());
448 let c = EntityId("specs--c".to_string());
449 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
450 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
451 store.upsert(c.clone(), stub_entity("specs--c", "specs"));
452 store.add_edge(
453 a.clone(),
454 Edge {
455 rel_type: "USES".to_string(),
456 target: b.clone(),
457 source: EdgeSource::Explicit,
458 },
459 );
460 store.add_edge(
461 a.clone(),
462 Edge {
463 rel_type: "USES".to_string(),
464 target: c.clone(),
465 source: EdgeSource::Explicit,
466 },
467 );
468 assert_eq!(store.edge_count(), 2);
469 store.remove_edges_from(&a);
470 assert_eq!(store.edge_count(), 0);
471 assert!(store.outgoing(&a).is_empty());
472 assert!(store.incoming(&b).is_empty());
473 assert!(store.incoming(&c).is_empty());
474 }
475
476 #[test]
477 fn remove_specific_edge() {
478 let mut store = Store::new();
479 let a = EntityId("specs--a".to_string());
480 let b = EntityId("specs--b".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 a.clone(),
493 Edge {
494 rel_type: "PART_OF".to_string(),
495 target: b.clone(),
496 source: EdgeSource::Explicit,
497 },
498 );
499 assert_eq!(store.edge_count(), 2);
500 store.remove_edge(&a, &b, "USES");
501 assert_eq!(store.edge_count(), 1);
502 assert_eq!(store.outgoing(&a)[0].rel_type, "PART_OF");
503 }
504
505 #[test]
506 fn rename_node() {
507 let mut store = Store::new();
508 let a = EntityId("specs--a".to_string());
509 let b = EntityId("specs--b".to_string());
510 let new_a = EntityId("specs--a-renamed".to_string());
511 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
512 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
513 store.add_edge(
514 a.clone(),
515 Edge {
516 rel_type: "USES".to_string(),
517 target: b.clone(),
518 source: EdgeSource::Explicit,
519 },
520 );
521 store.add_edge(
522 b.clone(),
523 Edge {
524 rel_type: "PART_OF".to_string(),
525 target: a.clone(),
526 source: EdgeSource::Explicit,
527 },
528 );
529
530 assert!(store.rename_node(&a, new_a.clone()));
531 assert!(store.get(&a).is_none());
532 assert!(store.get(&new_a).is_some());
533 assert_eq!(store.outgoing(&new_a).len(), 1);
534 assert_eq!(store.incoming(&new_a).len(), 1);
535 assert_eq!(store.incoming(&new_a)[0].from, b);
536 assert_eq!(store.outgoing(&b)[0].target, new_a);
538 }
539
540 #[test]
541 fn rename_node_rewrites_self_loop_in_relationships_vec() {
542 let mut store = Store::new();
548 let old_id = EntityId("specs--selfie".to_string());
549 let new_id = EntityId("specs--selfie-renamed".to_string());
550 let mut entity = stub_entity("specs--selfie", "specs");
551 entity.stub = false;
552 entity.relationships.push(Relationship {
553 rel_type: "REFERENCES".to_string(),
554 target: old_id.clone(),
555 description: None,
556 });
557 store.upsert(old_id.clone(), entity);
558 store.add_edge(
559 old_id.clone(),
560 Edge {
561 rel_type: "REFERENCES".to_string(),
562 target: old_id.clone(),
563 source: EdgeSource::Explicit,
564 },
565 );
566
567 assert!(store.rename_node(&old_id, new_id.clone()));
568
569 let renamed = store.get(&new_id).expect("renamed entity exists");
570 assert_eq!(renamed.relationships.len(), 1);
571 assert_eq!(
572 renamed.relationships[0].target, new_id,
573 "self-loop target inside entity.relationships must be rewritten \
574 to new_id — otherwise write_entity leaks old id to disk"
575 );
576 assert_eq!(store.outgoing(&new_id).len(), 1);
579 assert_eq!(store.outgoing(&new_id)[0].target, new_id);
580 assert_eq!(store.incoming(&new_id).len(), 1);
581 assert_eq!(store.incoming(&new_id)[0].from, new_id);
582 }
583
584 #[test]
585 fn clear_empties_store() {
586 let mut store = Store::new();
587 let a = EntityId("specs--a".to_string());
588 store.upsert(a, stub_entity("specs--a", "specs"));
589 store.clear();
590 assert!(store.is_empty());
591 assert_eq!(store.edge_count(), 0);
592 }
593
594 #[test]
595 fn outgoing_empty_for_unknown_id() {
596 let store = Store::new();
597 assert!(store.outgoing(&EntityId("unknown".to_string())).is_empty());
598 }
599}