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 }
307 }
308
309 #[test]
310 fn new_store_is_empty() {
311 let store = Store::new();
312 assert!(store.is_empty());
313 assert_eq!(store.len(), 0);
314 assert_eq!(store.edge_count(), 0);
315 }
316
317 #[test]
318 fn upsert_and_get() {
319 let mut store = Store::new();
320 let id = EntityId("specs--test".to_string());
321 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
322 assert_eq!(store.len(), 1);
323 assert!(store.get(&id).is_some());
324 assert_eq!(store.get(&id).unwrap().title, "specs--test");
325 }
326
327 #[test]
328 fn upsert_replaces_existing() {
329 let mut store = Store::new();
330 let id = EntityId("specs--test".to_string());
331 store.upsert(id.clone(), stub_entity("specs--test", "specs"));
332 let mut updated = stub_entity("specs--test", "specs");
333 updated.title = "Updated Title".to_string();
334 store.upsert(id.clone(), updated);
335 assert_eq!(store.len(), 1);
336 assert_eq!(store.get(&id).unwrap().title, "Updated Title");
337 }
338
339 #[test]
340 fn remove_node_cascades_edges() {
341 let mut store = Store::new();
342 let a = EntityId("specs--a".to_string());
343 let b = EntityId("specs--b".to_string());
344 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
345 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
346 store.add_edge(
347 a.clone(),
348 Edge {
349 rel_type: "USES".to_string(),
350 target: b.clone(),
351 source: EdgeSource::Explicit,
352 },
353 );
354 assert_eq!(store.edge_count(), 1);
355 store.remove(&b);
356 assert_eq!(store.len(), 1);
357 assert_eq!(store.edge_count(), 0);
358 assert!(store.outgoing(&a).is_empty());
359 }
360
361 #[test]
362 fn add_edge_idempotent() {
363 let mut store = Store::new();
364 let a = EntityId("specs--a".to_string());
365 let b = EntityId("specs--b".to_string());
366 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
367 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
368
369 store.add_edge(
370 a.clone(),
371 Edge {
372 rel_type: "USES".to_string(),
373 target: b.clone(),
374 source: EdgeSource::Explicit,
375 },
376 );
377 store.add_edge(
379 a.clone(),
380 Edge {
381 rel_type: "USES".to_string(),
382 target: b.clone(),
383 source: EdgeSource::Hierarchy,
384 },
385 );
386 assert_eq!(store.edge_count(), 1);
387 assert_eq!(store.outgoing(&a)[0].source, EdgeSource::Hierarchy);
388 assert_eq!(store.incoming(&b)[0].source, EdgeSource::Hierarchy);
389 }
390
391 #[test]
392 fn bidirectional_edges() {
393 let mut store = Store::new();
394 let a = EntityId("specs--a".to_string());
395 let b = EntityId("specs--b".to_string());
396 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
397 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
398 store.add_edge(
399 a.clone(),
400 Edge {
401 rel_type: "USES".to_string(),
402 target: b.clone(),
403 source: EdgeSource::Explicit,
404 },
405 );
406 assert_eq!(store.outgoing(&a).len(), 1);
407 assert_eq!(store.outgoing(&a)[0].target, b);
408 assert_eq!(store.incoming(&b).len(), 1);
409 assert_eq!(store.incoming(&b)[0].from, a);
410 }
411
412 #[test]
413 fn remove_edges_from() {
414 let mut store = Store::new();
415 let a = EntityId("specs--a".to_string());
416 let b = EntityId("specs--b".to_string());
417 let c = EntityId("specs--c".to_string());
418 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
419 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
420 store.upsert(c.clone(), stub_entity("specs--c", "specs"));
421 store.add_edge(
422 a.clone(),
423 Edge {
424 rel_type: "USES".to_string(),
425 target: b.clone(),
426 source: EdgeSource::Explicit,
427 },
428 );
429 store.add_edge(
430 a.clone(),
431 Edge {
432 rel_type: "USES".to_string(),
433 target: c.clone(),
434 source: EdgeSource::Explicit,
435 },
436 );
437 assert_eq!(store.edge_count(), 2);
438 store.remove_edges_from(&a);
439 assert_eq!(store.edge_count(), 0);
440 assert!(store.outgoing(&a).is_empty());
441 assert!(store.incoming(&b).is_empty());
442 assert!(store.incoming(&c).is_empty());
443 }
444
445 #[test]
446 fn remove_specific_edge() {
447 let mut store = Store::new();
448 let a = EntityId("specs--a".to_string());
449 let b = EntityId("specs--b".to_string());
450 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
451 store.upsert(b.clone(), stub_entity("specs--b", "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: "PART_OF".to_string(),
464 target: b.clone(),
465 source: EdgeSource::Explicit,
466 },
467 );
468 assert_eq!(store.edge_count(), 2);
469 store.remove_edge(&a, &b, "USES");
470 assert_eq!(store.edge_count(), 1);
471 assert_eq!(store.outgoing(&a)[0].rel_type, "PART_OF");
472 }
473
474 #[test]
475 fn rename_node() {
476 let mut store = Store::new();
477 let a = EntityId("specs--a".to_string());
478 let b = EntityId("specs--b".to_string());
479 let new_a = EntityId("specs--a-renamed".to_string());
480 store.upsert(a.clone(), stub_entity("specs--a", "specs"));
481 store.upsert(b.clone(), stub_entity("specs--b", "specs"));
482 store.add_edge(
483 a.clone(),
484 Edge {
485 rel_type: "USES".to_string(),
486 target: b.clone(),
487 source: EdgeSource::Explicit,
488 },
489 );
490 store.add_edge(
491 b.clone(),
492 Edge {
493 rel_type: "PART_OF".to_string(),
494 target: a.clone(),
495 source: EdgeSource::Explicit,
496 },
497 );
498
499 assert!(store.rename_node(&a, new_a.clone()));
500 assert!(store.get(&a).is_none());
501 assert!(store.get(&new_a).is_some());
502 assert_eq!(store.outgoing(&new_a).len(), 1);
503 assert_eq!(store.incoming(&new_a).len(), 1);
504 assert_eq!(store.incoming(&new_a)[0].from, b);
505 assert_eq!(store.outgoing(&b)[0].target, new_a);
507 }
508
509 #[test]
510 fn rename_node_rewrites_self_loop_in_relationships_vec() {
511 let mut store = Store::new();
517 let old_id = EntityId("specs--selfie".to_string());
518 let new_id = EntityId("specs--selfie-renamed".to_string());
519 let mut entity = stub_entity("specs--selfie", "specs");
520 entity.stub = false;
521 entity.relationships.push(Relationship {
522 rel_type: "REFERENCES".to_string(),
523 target: old_id.clone(),
524 description: None,
525 });
526 store.upsert(old_id.clone(), entity);
527 store.add_edge(
528 old_id.clone(),
529 Edge {
530 rel_type: "REFERENCES".to_string(),
531 target: old_id.clone(),
532 source: EdgeSource::Explicit,
533 },
534 );
535
536 assert!(store.rename_node(&old_id, new_id.clone()));
537
538 let renamed = store.get(&new_id).expect("renamed entity exists");
539 assert_eq!(renamed.relationships.len(), 1);
540 assert_eq!(
541 renamed.relationships[0].target, new_id,
542 "self-loop target inside entity.relationships must be rewritten \
543 to new_id — otherwise write_entity leaks old id to disk"
544 );
545 assert_eq!(store.outgoing(&new_id).len(), 1);
548 assert_eq!(store.outgoing(&new_id)[0].target, new_id);
549 assert_eq!(store.incoming(&new_id).len(), 1);
550 assert_eq!(store.incoming(&new_id)[0].from, new_id);
551 }
552
553 #[test]
554 fn clear_empties_store() {
555 let mut store = Store::new();
556 let a = EntityId("specs--a".to_string());
557 store.upsert(a, stub_entity("specs--a", "specs"));
558 store.clear();
559 assert!(store.is_empty());
560 assert_eq!(store.edge_count(), 0);
561 }
562
563 #[test]
564 fn outgoing_empty_for_unknown_id() {
565 let store = Store::new();
566 assert!(store.outgoing(&EntityId("unknown".to_string())).is_empty());
567 }
568}