1use crate::{AttrMap, EdgeSnapshot, FxIndexMap, GraphError};
9use fnx_runtime::{
10 CgseValue, CompatibilityMode, DecisionAction, DecisionRecord, EvidenceLedger, EvidenceTerm,
11 RuntimePolicy, bool_evidence, count_evidence,
12};
13use indexmap::{IndexMap, IndexSet};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21struct DirectedEdgeKey {
22 source: String,
23 target: String,
24}
25
26impl DirectedEdgeKey {
27 fn new(source: &str, target: &str) -> Self {
28 Self {
29 source: source.to_owned(),
30 target: target.to_owned(),
31 }
32 }
33}
34
35#[derive(Hash, PartialEq, Eq, Clone, Copy)]
36struct DirectedEdgeKeyRef<'a> {
37 source: &'a str,
38 target: &'a str,
39}
40
41impl<'a> DirectedEdgeKeyRef<'a> {
42 fn new(source: &'a str, target: &'a str) -> Self {
43 Self { source, target }
44 }
45}
46
47impl<'a> indexmap::Equivalent<DirectedEdgeKey> for DirectedEdgeKeyRef<'a> {
48 fn equivalent(&self, key: &DirectedEdgeKey) -> bool {
49 self.source == key.source && self.target == key.target
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct DiGraphSnapshot {
59 pub mode: CompatibilityMode,
60 pub nodes: Vec<String>,
61 #[serde(default)]
64 pub node_attrs: std::collections::BTreeMap<String, AttrMap>,
65 pub edges: Vec<EdgeSnapshot>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct MultiDiEdgeSnapshot {
71 pub source: String,
72 pub target: String,
73 pub key: usize,
74 pub attrs: AttrMap,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct MultiDiGraphSnapshot {
79 pub mode: CompatibilityMode,
80 pub nodes: Vec<String>,
81 #[serde(default)]
83 pub node_attrs: std::collections::BTreeMap<String, AttrMap>,
84 pub edges: Vec<MultiDiEdgeSnapshot>,
85}
86
87#[derive(Debug)]
96pub struct DiCsr {
97 pub succ_offsets: Vec<usize>,
98 pub succ_targets: Vec<u32>,
99 pub pred_offsets: Vec<usize>,
100 pub pred_targets: Vec<u32>,
101}
102
103impl DiCsr {
104 #[must_use]
105 pub fn successors(&self, idx: usize) -> &[u32] {
106 &self.succ_targets[self.succ_offsets[idx]..self.succ_offsets[idx + 1]]
107 }
108
109 #[must_use]
110 pub fn predecessors(&self, idx: usize) -> &[u32] {
111 &self.pred_targets[self.pred_offsets[idx]..self.pred_offsets[idx + 1]]
112 }
113}
114
115type DiCsrCache = std::sync::Arc<std::sync::RwLock<Option<(u64, std::sync::Arc<DiCsr>)>>>;
116type DiAllIntCache = std::sync::Arc<std::sync::RwLock<Option<(u64, String, bool)>>>;
117
118#[derive(Debug)]
122pub struct MultiDiCsr {
123 pub succ_offsets: Vec<usize>,
124 pub succ_targets: Vec<u32>,
125 pub pred_offsets: Vec<usize>,
126 pub pred_targets: Vec<u32>,
127}
128
129impl MultiDiCsr {
130 #[must_use]
131 pub fn successors(&self, idx: usize) -> &[u32] {
132 &self.succ_targets[self.succ_offsets[idx]..self.succ_offsets[idx + 1]]
133 }
134
135 #[must_use]
136 pub fn predecessors(&self, idx: usize) -> &[u32] {
137 &self.pred_targets[self.pred_offsets[idx]..self.pred_offsets[idx + 1]]
138 }
139}
140
141type MultiDiCsrCache = std::sync::Arc<std::sync::RwLock<Option<(u64, std::sync::Arc<MultiDiCsr>)>>>;
142
143type EdgePairMap = std::collections::HashMap<(usize, usize), usize, rustc_hash::FxBuildHasher>;
145
146#[derive(Debug, Default)]
155pub(crate) struct EdgePairIndex {
156 map: std::sync::RwLock<Option<(u64, EdgePairMap)>>,
157 probed_revision: std::sync::atomic::AtomicU64,
159 probes_since_build: std::sync::atomic::AtomicUsize,
161}
162
163impl Clone for EdgePairIndex {
164 fn clone(&self) -> Self {
165 Self::default()
166 }
167}
168
169const PAIR_INDEX_WARMUP_FLOOR: usize = 64;
184
185#[inline]
186fn pair_index_warmup(edge_pairs: usize) -> usize {
187 PAIR_INDEX_WARMUP_FLOOR.max(edge_pairs / 2)
188}
189
190#[derive(Debug, Clone)]
191pub struct DiGraph {
192 mode: CompatibilityMode,
193 revision: u64,
194 node_order: crate::NodeOrder,
196 node_attrs: Vec<AttrMap>,
197 succ_indices: crate::SlotRows,
199 pred_indices: crate::SlotRows,
200 edges: crate::FxIndexMap<(usize, usize), AttrMap>,
201 runtime_policy: RuntimePolicy,
202 csr_cache: DiCsrCache,
203 all_int_cache: DiAllIntCache,
204}
205
206impl DiGraph {
207 pub fn clear_edges(&mut self) {
214 if self.edges.is_empty() {
215 return;
216 }
217 self.edges.clear();
218 for row in self.succ_indices.rows_mut() {
219 row.clear();
220 }
221 for row in self.pred_indices.rows_mut() {
222 row.clear();
223 }
224 self.revision = self.revision.saturating_add(1);
225 }
226
227 #[must_use]
230 pub fn clone_with_fresh_policy(&self) -> Self {
231 Self {
232 mode: self.mode,
233 revision: self.revision,
234 node_order: self.node_order.clone(),
235 node_attrs: self.node_attrs.clone(),
236 succ_indices: self.succ_indices.clone(),
237 pred_indices: self.pred_indices.clone(),
238 edges: self.edges.clone(),
239 runtime_policy: RuntimePolicy::new(self.mode),
240 csr_cache: self.csr_cache.clone(),
241 all_int_cache: self.all_int_cache.clone(),
242 }
243 }
244
245 #[must_use]
250 pub fn new(mode: CompatibilityMode) -> Self {
251 Self {
252 mode,
253 revision: 0,
254 node_order: crate::NodeOrder::default(),
255 node_attrs: Vec::new(),
256 succ_indices: crate::SlotRows::default(),
257 pred_indices: crate::SlotRows::default(),
258 edges: crate::FxIndexMap::default(),
259 runtime_policy: RuntimePolicy::new(mode),
260 csr_cache: std::sync::Arc::default(),
261 all_int_cache: std::sync::Arc::default(),
262 }
263 }
264
265 #[must_use]
266 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
267 let mode = runtime_policy.mode();
268 Self {
269 mode,
270 revision: 0,
271 node_order: crate::NodeOrder::default(),
272 node_attrs: Vec::new(),
273 succ_indices: crate::SlotRows::default(),
274 pred_indices: crate::SlotRows::default(),
275 edges: crate::FxIndexMap::default(),
276 runtime_policy,
277 csr_cache: std::sync::Arc::default(),
278 all_int_cache: std::sync::Arc::default(),
279 }
280 }
281
282 #[must_use]
297 pub fn induced_subgraph_ordered(&self, order: &[usize], runtime_policy: RuntimePolicy) -> Self {
298 let mut position = vec![usize::MAX; self.node_order.slot_count()];
299 for (slot, &parent_idx) in order.iter().enumerate() {
300 let parent_slot = self.node_order.get_index(parent_idx).unwrap().1;
301 position[parent_slot] = slot;
302 }
303
304 let mut node_order = crate::NodeOrder::default();
305 node_order.reserve(order.len());
306 let mut node_attrs = Vec::with_capacity(order.len());
307
308 for &parent_idx in order {
309 let (name, parent_slot) = self
310 .node_order
311 .get_index(parent_idx)
312 .expect("induced subgraph order must hold valid parent node indices");
313 node_order.push(name.clone());
314 node_attrs.push(self.node_attrs[parent_slot].clone());
315 }
316
317 let mut succ_indices = vec![Vec::new(); order.len()];
318 let mut pred_indices = vec![Vec::new(); order.len()];
319 let mut edges = crate::FxIndexMap::default();
320 for (new_slot, &parent_idx) in order.iter().enumerate() {
321 let parent_slot = self.node_order.get_index(parent_idx).unwrap().1;
322 for &parent_target in &self.succ_indices[parent_slot] {
323 let other = position[parent_target];
324 if other == usize::MAX {
325 continue;
326 }
327 let attrs = self
328 .edges
329 .get(&(parent_slot, parent_target))
330 .cloned()
331 .unwrap_or_default();
332 edges.insert((new_slot, other), attrs);
333 succ_indices[new_slot].push(other);
334 pred_indices[other].push(new_slot);
335 }
336 }
337
338 Self {
339 mode: runtime_policy.mode(),
340 revision: 0,
341 node_order,
342 node_attrs,
343 succ_indices: crate::SlotRows::new(succ_indices),
344 pred_indices: crate::SlotRows::new(pred_indices),
345 edges,
346 runtime_policy,
347 csr_cache: std::sync::Arc::default(),
348 all_int_cache: std::sync::Arc::default(),
349 }
350 }
351
352 #[must_use]
353 pub fn strict() -> Self {
354 Self::new(CompatibilityMode::Strict)
355 }
356
357 #[must_use]
358 pub fn hardened() -> Self {
359 Self::new(CompatibilityMode::Hardened)
360 }
361
362 #[must_use]
367 pub fn mode(&self) -> CompatibilityMode {
368 self.mode
369 }
370
371 #[must_use]
372 pub fn node_count(&self) -> usize {
373 self.node_order.len()
374 }
375
376 #[must_use]
377 pub fn edge_count(&self) -> usize {
378 self.edges.len()
379 }
380
381 #[must_use]
382 pub fn revision(&self) -> u64 {
383 self.revision
384 }
385
386 #[must_use]
391 pub fn csr(&self) -> std::sync::Arc<DiCsr> {
392 if let Ok(guard) = self.csr_cache.read()
393 && let Some((rev, csr)) = guard.as_ref()
394 && *rev == self.revision
395 {
396 return csr.clone();
397 }
398 let built = std::sync::Arc::new(self.build_csr());
399 if let Ok(mut guard) = self.csr_cache.write() {
400 *guard = Some((self.revision, built.clone()));
401 }
402 built
403 }
404
405 #[must_use]
408 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
409 if let Ok(guard) = self.all_int_cache.read()
410 && let Some((rev, a, v)) = guard.as_ref()
411 && *rev == self.revision
412 && a == attr
413 {
414 return *v;
415 }
416 let v = self
417 .edges
418 .values()
419 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
420 if let Ok(mut guard) = self.all_int_cache.write() {
421 *guard = Some((self.revision, attr.to_owned(), v));
422 }
423 v
424 }
425
426 fn build_csr(&self) -> DiCsr {
427 let n = self.node_order.len();
430 let mut succ_offsets = Vec::with_capacity(n + 1);
431 let mut pred_offsets = Vec::with_capacity(n + 1);
432 let mut succ_targets = Vec::new();
433 let mut pred_targets = Vec::new();
434 succ_offsets.push(0);
435 pred_offsets.push(0);
436
437 let mut slot_to_pos = vec![0; self.node_order.slot_count()];
438 for (pos, (_, slot)) in self.node_order.iter().enumerate() {
439 slot_to_pos[slot] = pos;
440 }
441
442 for slot in self.node_order.values() {
443 succ_targets.extend(
444 self.succ_indices[slot]
445 .iter()
446 .map(|&v| u32::try_from(slot_to_pos[v]).unwrap_or(u32::MAX)),
447 );
448 succ_offsets.push(succ_targets.len());
449 pred_targets.extend(
450 self.pred_indices[slot]
451 .iter()
452 .map(|&u| u32::try_from(slot_to_pos[u]).unwrap_or(u32::MAX)),
453 );
454 pred_offsets.push(pred_targets.len());
455 }
456 DiCsr {
457 succ_offsets,
458 succ_targets,
459 pred_offsets,
460 pred_targets,
461 }
462 }
463
464 #[doc(hidden)]
467 #[must_use]
468 pub fn debug_index_rows_consistent(&self) -> bool {
469 self.succ_indices.len() == self.node_order.slot_count()
473 && self.pred_indices.len() == self.node_order.slot_count()
474 }
475
476 #[must_use]
477 pub fn has_node(&self, node: &str) -> bool {
478 self.node_order.contains_key(node)
479 }
480
481 #[must_use]
483 #[inline]
485 fn edge_pair_key(&self, source: &str, target: &str) -> Option<(usize, usize)> {
486 Some((*self.node_order.get(source)?, *self.node_order.get(target)?))
487 }
488
489 #[must_use]
492 #[inline]
493 pub fn successors_indices(&self, idx: usize) -> Option<&[usize]> {
494 if self.node_order.is_dense() {
495 self.succ_indices.get(idx).map(Vec::as_slice)
496 } else {
497 self.succ_indices
498 .positions(&self.node_order)
499 .get(idx)
500 .map(Vec::as_slice)
501 }
502 }
503
504 #[must_use]
506 #[inline]
507 pub fn predecessors_indices(&self, idx: usize) -> Option<&[usize]> {
508 if self.node_order.is_dense() {
509 self.pred_indices.get(idx).map(Vec::as_slice)
510 } else {
511 self.pred_indices
512 .positions(&self.node_order)
513 .get(idx)
514 .map(Vec::as_slice)
515 }
516 }
517
518 pub fn has_edge(&self, source: &str, target: &str) -> bool {
519 self.edge_pair_key(source, target)
520 .is_some_and(|k| self.edges.contains_key(&k))
521 }
522
523 #[must_use]
537 pub fn has_edge_by_indices(&self, source_idx: usize, target_idx: usize) -> bool {
538 let Some(s_slot) = self.node_order.slot_at(source_idx) else {
539 return false;
540 };
541 let Some(t_slot) = self.node_order.slot_at(target_idx) else {
542 return false;
543 };
544 self.edges.contains_key(&(s_slot, t_slot))
545 }
546
547 #[must_use]
548 pub fn nodes_ordered(&self) -> Vec<&str> {
549 self.node_order.keys().map(String::as_str).collect()
550 }
551
552 #[must_use]
556 pub fn node_index_matches_int(&self, idx: usize) -> bool {
557 self.node_order
558 .get_index(idx)
559 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
560 }
561
562 #[must_use]
564 pub fn node_names_are_positions(&self) -> bool {
565 self.node_order.names_are_positions()
566 }
567
568 #[must_use]
569 pub fn get_node_index(&self, node: &str) -> Option<usize> {
570 self.node_order.get_index_of(node)
571 }
572
573 #[must_use]
574 pub fn get_node_name(&self, index: usize) -> Option<&str> {
575 self.node_order.get_index(index).map(|(k, _)| k.as_str())
576 }
577
578 #[must_use]
582 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
583 let &idx = self.node_order.get(node)?;
586 Some(
587 self.succ_indices[idx]
588 .iter()
589 .map(|&i| {
590 self.node_order.names[i]
591 .as_deref()
592 .expect("index rows hold valid node slots")
593 })
594 .collect(),
595 )
596 }
597
598 #[must_use]
599 pub fn successors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
600 let &idx = self.node_order.get(node)?;
602 Some(self.succ_indices[idx].iter().map(move |&i| {
603 self.node_order.names[i]
604 .as_deref()
605 .expect("index rows hold valid node slots")
606 }))
607 }
608
609 #[must_use]
612 pub fn out_edge_attrs(&self, node: &str) -> Option<impl Iterator<Item = &AttrMap> + '_> {
613 let &slot = self.node_order.get(node)?;
614 Some(self.succ_indices[slot].iter().map(move |&target| {
615 self.edges
616 .get(&(slot, target))
617 .expect("successor rows have matching edge storage")
618 }))
619 }
620
621 #[must_use]
624 pub fn in_edge_attrs(&self, node: &str) -> Option<impl Iterator<Item = &AttrMap> + '_> {
625 let &slot = self.node_order.get(node)?;
626 Some(self.pred_indices[slot].iter().map(move |&source| {
627 self.edges
628 .get(&(source, slot))
629 .expect("predecessor rows have matching edge storage")
630 }))
631 }
632
633 #[must_use]
635 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
636 let &idx = self.node_order.get(node)?;
638 Some(
639 self.pred_indices[idx]
640 .iter()
641 .map(|&i| {
642 self.node_order.names[i]
643 .as_deref()
644 .expect("index rows hold valid node slots")
645 })
646 .collect(),
647 )
648 }
649
650 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
654 for (node, order) in orders {
656 let Some(&idx) = self.node_order.get(node.as_str()) else {
657 continue;
658 };
659 let rows = if pred {
660 &mut self.pred_indices
661 } else {
662 &mut self.succ_indices
663 };
664 let row = &rows[idx];
665 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
666 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
667 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
668 for v in order {
669 if let Some(&v_idx) = self.node_order.get(v.as_str())
670 && row_set.contains(&v_idx)
671 && placed.insert(v_idx)
672 {
673 new_row.push(v_idx);
674 }
675 }
676 for &v_idx in row {
677 if placed.insert(v_idx) {
678 new_row.push(v_idx);
679 }
680 }
681 rows.rows_mut()[idx] = new_row;
682 }
683 }
684
685 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
693 self.ensure_compact();
704 let mut new_rows: Vec<Vec<usize>> = self
705 .pred_indices
706 .iter()
707 .map(|row| Vec::with_capacity(row.len()))
708 .collect();
709 for (u, succ_row) in self.succ_indices.iter().enumerate() {
710 for &v in succ_row {
711 new_rows[v].push(u);
712 }
713 }
714 *self.pred_indices.rows_mut() = new_rows;
715 }
716
717 pub fn reorder_rows_like(&mut self, source: &Self, swap: bool) {
726 let mut to_source = vec![usize::MAX; self.node_order.slot_count()];
727 let mut from_source = vec![usize::MAX; source.node_order.slot_count()];
728 for (name, slot) in self.node_order.iter() {
729 if let Some(&source_slot) = source.node_order.get(name) {
730 to_source[slot] = source_slot;
731 from_source[source_slot] = slot;
732 }
733 }
734 let (succ_source, pred_source) = if swap {
735 (&source.pred_indices, &source.succ_indices)
736 } else {
737 (&source.succ_indices, &source.pred_indices)
738 };
739 let slots: Vec<usize> = self.node_order.values().collect();
740 let succ_changed = crate::reorder_slot_rows_like(
741 &mut self.succ_indices,
742 succ_source,
743 &slots,
744 &to_source,
745 &from_source,
746 );
747 let pred_changed = crate::reorder_slot_rows_like(
748 &mut self.pred_indices,
749 pred_source,
750 &slots,
751 &to_source,
752 &from_source,
753 );
754 if succ_changed || pred_changed {
755 self.revision = self.revision.saturating_add(1);
756 self.csr_cache = std::sync::Arc::default();
758 }
759 }
760
761 pub fn set_row_orders(&mut self, succ_rows: &[Vec<usize>], pred_rows: &[Vec<usize>]) -> bool {
768 let slots: Vec<usize> = self.node_order.values().collect();
769 let mut succ = self.succ_indices.clone();
770 let mut pred = self.pred_indices.clone();
771 let (Some(succ_changed), Some(pred_changed)) = (
772 crate::set_slot_row_orders(&mut succ, &slots, succ_rows),
773 crate::set_slot_row_orders(&mut pred, &slots, pred_rows),
774 ) else {
775 return false;
776 };
777 if succ_changed || pred_changed {
778 self.succ_indices = succ;
779 self.pred_indices = pred;
780 self.revision = self.revision.saturating_add(1);
781 self.csr_cache = std::sync::Arc::default();
782 }
783 true
784 }
785
786 #[must_use]
787 pub fn predecessors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
788 let &idx = self.node_order.get(node)?;
790 Some(self.pred_indices[idx].iter().map(move |&i| {
791 self.node_order.names[i]
792 .as_deref()
793 .expect("index rows hold valid node slots")
794 }))
795 }
796
797 #[must_use]
799 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
800 self.successors(node)
801 }
802
803 #[must_use]
804 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
805 self.successors_iter(node)
806 }
807
808 #[must_use]
809 pub fn neighbor_count(&self, node: &str) -> usize {
810 self.node_order
812 .get(node)
813 .map_or(0, |&i| self.succ_indices[i].len())
814 }
815
816 #[must_use]
818 pub fn out_degree(&self, node: &str) -> usize {
819 self.node_order
820 .get(node)
821 .map_or(0, |&i| self.succ_indices[i].len())
822 }
823
824 #[must_use]
826 pub fn in_degree(&self, node: &str) -> usize {
827 self.node_order
828 .get(node)
829 .map_or(0, |&i| self.pred_indices[i].len())
830 }
831
832 #[must_use]
834 pub fn degree(&self, node: &str) -> usize {
835 self.node_order.get(node).map_or(0, |&idx| {
839 self.pred_indices[idx].len() + self.succ_indices[idx].len()
840 })
841 }
842
843 #[must_use]
848 pub fn out_degree_by_index(&self, idx: usize) -> usize {
849 self.node_order
850 .slot_at(idx)
851 .map_or(0, |slot| self.succ_indices[slot].len())
852 }
853
854 #[must_use]
855 pub fn in_degree_by_index(&self, idx: usize) -> usize {
856 self.node_order
857 .slot_at(idx)
858 .map_or(0, |slot| self.pred_indices[slot].len())
859 }
860
861 #[must_use]
862 pub fn degree_by_index(&self, idx: usize) -> usize {
863 self.out_degree_by_index(idx) + self.in_degree_by_index(idx)
864 }
865
866 #[must_use]
868 pub fn out_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
869 self.node_order.get(node).map_or_else(Vec::new, |&idx| {
871 self.succ_indices[idx]
872 .iter()
873 .map(|&i| {
874 (
875 node,
876 self.node_order.names[i]
877 .as_deref()
878 .expect("index rows hold valid node slots"),
879 )
880 })
881 .collect()
882 })
883 }
884
885 #[must_use]
887 pub fn in_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
888 self.node_order.get(node).map_or_else(Vec::new, |&idx| {
890 self.pred_indices[idx]
891 .iter()
892 .map(|&i| {
893 (
894 self.node_order.names[i]
895 .as_deref()
896 .expect("index rows hold valid node slots"),
897 node,
898 )
899 })
900 .collect()
901 })
902 }
903
904 #[must_use]
907 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
908 let &slot = self.node_order.get(node)?;
909 Some(&self.node_attrs[slot])
910 }
911
912 #[must_use]
913 #[inline]
914 pub fn node_attrs_by_index(&self, idx: usize) -> Option<&AttrMap> {
915 self.node_order
916 .slot_at(idx)
917 .map(|slot| &self.node_attrs[slot])
918 }
919
920 #[must_use]
922 pub fn edge_attrs(&self, source: &str, target: &str) -> Option<&AttrMap> {
923 self.edges.get(&self.edge_pair_key(source, target)?)
924 }
925
926 #[must_use]
929 #[inline]
930 pub fn edge_attrs_by_indices(&self, source_idx: usize, target_idx: usize) -> Option<&AttrMap> {
931 let s_slot = self.node_order.slot_at(source_idx)?;
932 let t_slot = self.node_order.slot_at(target_idx)?;
933 self.edges.get(&(s_slot, t_slot))
934 }
935
936 #[must_use]
940 pub fn any_edge_has_attr(&self, key: &str) -> bool {
941 self.edges.values().any(|attrs| attrs.contains_key(key))
942 }
943
944 #[must_use]
949 pub fn all_edge_attr_values_scalar(&self) -> bool {
950 self.edges.values().all(|attrs| {
951 attrs.values().all(|v| {
952 matches!(
953 v,
954 fnx_runtime::CgseValue::Int(_)
955 | fnx_runtime::CgseValue::Float(_)
956 | fnx_runtime::CgseValue::Bool(_)
957 )
958 })
959 })
960 }
961
962 #[must_use]
965 pub fn any_node_has_attrs(&self) -> bool {
966 self.node_attrs.iter().any(|attrs| !attrs.is_empty())
967 }
968
969 #[must_use]
970 pub fn any_edge_has_attrs(&self) -> bool {
971 self.edges.values().any(|attrs| !attrs.is_empty())
972 }
973
974 pub fn edges_indexed(&self) -> impl Iterator<Item = ((usize, usize), &AttrMap)> + '_ {
980 let slot_positions = self.node_order.slot_positions();
981 self.edges
982 .iter()
983 .map(move |(&(source, target), attrs)| match &slot_positions {
984 Some(positions) => ((positions[source], positions[target]), attrs),
985 None => ((source, target), attrs),
986 })
987 }
988
989 #[must_use]
990 pub fn evidence_ledger(&self) -> &EvidenceLedger {
991 self.runtime_policy.decision_log()
992 }
993
994 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
995 self.runtime_policy.drain_decision_records()
996 }
997
998 pub fn record_external_recovery(
1001 &mut self,
1002 operation: impl Into<std::borrow::Cow<'static, str>>,
1003 rationale: impl Into<std::borrow::Cow<'static, str>>,
1004 ) {
1005 self.runtime_policy.record(
1006 operation,
1007 DecisionAction::FullValidate,
1008 0.7,
1009 rationale,
1010 Vec::new(),
1011 );
1012 }
1013
1014 #[must_use]
1015 pub fn runtime_policy(&self) -> &RuntimePolicy {
1016 &self.runtime_policy
1017 }
1018
1019 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
1020 self.mode = runtime_policy.mode();
1021 self.runtime_policy = runtime_policy;
1022 }
1023
1024 #[must_use]
1026 pub fn is_directed(&self) -> bool {
1027 true
1028 }
1029
1030 #[must_use]
1032 pub fn is_multigraph(&self) -> bool {
1033 false
1034 }
1035
1036 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
1041 self.add_node_with_attrs(node, AttrMap::new())
1042 }
1043
1044 fn add_node_with_attrs_unrecorded(
1058 &mut self,
1059 node: impl AsRef<str> + Into<String>,
1060 attrs: AttrMap,
1061 ) -> (bool, bool, usize, Option<usize>) {
1062 let node_ref = node.as_ref();
1063 if let Some(&slot) = self.node_order.get(node_ref) {
1064 let bucket = &mut self.node_attrs[slot];
1065 let changed = !attrs.is_empty()
1066 && attrs
1067 .iter()
1068 .any(|(key, value)| bucket.get(key) != Some(value));
1069 bucket.extend(attrs);
1070 if changed {
1071 self.revision = self.revision.saturating_add(1);
1072 }
1073 return (changed, true, bucket.len(), None);
1074 }
1075
1076 let slot = self.node_order.push(node.into());
1077 self.node_attrs.push(attrs);
1078 self.succ_indices.rows_mut().push(Vec::new());
1079 self.pred_indices.rows_mut().push(Vec::new());
1080 let attr_count = self.node_attrs[slot].len();
1081 self.revision = self.revision.saturating_add(1);
1082 (true, false, attr_count, Some(slot))
1083 }
1084
1085 fn resolve_empty_node_for_edge_unrecorded(&mut self, node: &str) -> (usize, bool) {
1086 if let Some(&slot) = self.node_order.get(node) {
1087 return (slot, false);
1088 }
1089 let slot = self.node_order.push(node.to_owned());
1090 self.node_attrs.push(AttrMap::new());
1091 self.succ_indices.rows_mut().push(Vec::new());
1092 self.pred_indices.rows_mut().push(Vec::new());
1093 self.revision = self.revision.saturating_add(1);
1094 (slot, true)
1095 }
1096
1097 pub fn add_node_with_attrs(
1098 &mut self,
1099 node: impl AsRef<str> + Into<String>,
1100 attrs: AttrMap,
1101 ) -> bool {
1102 self.compact_before_growth();
1103 let (changed, existed, attrs_count, _) = self.add_node_with_attrs_unrecorded(node, attrs);
1104 self.record_decision(
1105 "add_node",
1106 0.0,
1107 false,
1108 vec![
1109 EvidenceTerm {
1110 signal: "node_preexisting".into(),
1111 observed_value: bool_evidence(existed),
1112 log_likelihood_ratio: -3.0,
1113 },
1114 EvidenceTerm {
1115 signal: "attrs_count".into(),
1116 observed_value: count_evidence(attrs_count),
1117 log_likelihood_ratio: -1.0,
1118 },
1119 ],
1120 );
1121 changed
1122 }
1123
1124 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
1125 if defaults.is_empty() {
1126 return false;
1127 }
1128 let mut inserted = 0usize;
1129 for slot in self.node_order.values() {
1130 let attrs = &mut self.node_attrs[slot];
1131 for (key, value) in defaults {
1132 if !attrs.contains_key(key) {
1133 attrs.insert(key.clone(), value.clone());
1134 inserted += 1;
1135 }
1136 }
1137 }
1138 if inserted > 0 {
1139 self.revision = self.revision.saturating_add(1);
1140 self.record_decision(
1141 "apply_node_defaults",
1142 0.0,
1143 false,
1144 vec![EvidenceTerm {
1145 signal: "defaults_applied".into(),
1146 observed_value: count_evidence(inserted),
1147 log_likelihood_ratio: 0.0,
1148 }],
1149 );
1150 return true;
1151 }
1152 false
1153 }
1154
1155 pub fn add_edge(
1156 &mut self,
1157 source: impl AsRef<str>,
1158 target: impl AsRef<str>,
1159 ) -> Result<(), GraphError> {
1160 self.add_edge_with_attrs(source, target, AttrMap::new())
1161 }
1162
1163 pub fn add_edge_with_attrs(
1171 &mut self,
1172 source: impl AsRef<str>,
1173 target: impl AsRef<str>,
1174 attrs: AttrMap,
1175 ) -> Result<(), GraphError> {
1176 self.compact_before_growth();
1177 let source = source.as_ref();
1178 let target = target.as_ref();
1179
1180 let unknown_feature = attrs
1181 .keys()
1182 .any(|key| key.starts_with("__fnx_incompatible"));
1183 let self_loop = source == target;
1184 let incompatibility_probability = if unknown_feature {
1185 1.0
1186 } else if self_loop {
1187 0.22
1188 } else {
1189 0.08
1190 };
1191
1192 let action = self
1200 .runtime_policy
1201 .action_for(incompatibility_probability, unknown_feature);
1202
1203 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
1204 self.record_decision(
1205 "add_edge",
1206 incompatibility_probability,
1207 unknown_feature,
1208 vec![EvidenceTerm {
1209 signal: "unknown_incompatible_feature".into(),
1210 observed_value: bool_evidence(unknown_feature),
1211 log_likelihood_ratio: 12.0,
1212 }],
1213 );
1214 return Err(GraphError::FailClosed {
1215 operation: "add_edge",
1216 reason: "incompatible edge metadata".to_owned(),
1217 });
1218 }
1219
1220 let (source_slot, source_autocreated) = self.resolve_empty_node_for_edge_unrecorded(source);
1222 let (target_slot, target_autocreated) = if self_loop {
1223 (source_slot, source_autocreated)
1224 } else {
1225 self.resolve_empty_node_for_edge_unrecorded(target)
1226 };
1227
1228 let edge_key = (source_slot, target_slot);
1229 let (new_edge, changed, edge_attr_count) = match self.edges.entry(edge_key) {
1230 indexmap::map::Entry::Vacant(entry) => {
1231 let count = attrs.len();
1232 entry.insert(attrs);
1233 (true, true, count)
1234 }
1235 indexmap::map::Entry::Occupied(mut entry) => {
1236 let edge_attrs = entry.get_mut();
1237 let changed = !attrs.is_empty()
1238 && attrs
1239 .iter()
1240 .any(|(key, value)| edge_attrs.get(key) != Some(value));
1241 edge_attrs.extend(attrs);
1242 (false, changed, edge_attrs.len())
1243 }
1244 };
1245
1246 if new_edge {
1247 self.succ_indices.rows_mut()[source_slot].push(target_slot);
1248 self.pred_indices.rows_mut()[target_slot].push(source_slot);
1249 }
1250
1251 if changed {
1252 self.revision = self.revision.saturating_add(1);
1253 }
1254
1255 self.record_decision(
1256 "add_edge",
1257 incompatibility_probability,
1258 unknown_feature,
1259 vec![
1260 EvidenceTerm {
1261 signal: "self_loop".into(),
1262 observed_value: bool_evidence(self_loop),
1263 log_likelihood_ratio: -0.5,
1264 },
1265 EvidenceTerm {
1266 signal: "edge_attr_count".into(),
1267 observed_value: count_evidence(edge_attr_count),
1268 log_likelihood_ratio: -2.0,
1269 },
1270 EvidenceTerm {
1271 signal: "source_autocreated".into(),
1272 observed_value: bool_evidence(source_autocreated),
1273 log_likelihood_ratio: -1.25,
1274 },
1275 EvidenceTerm {
1276 signal: "target_autocreated".into(),
1277 observed_value: bool_evidence(target_autocreated),
1278 log_likelihood_ratio: -1.25,
1279 },
1280 ],
1281 );
1282
1283 Ok(())
1284 }
1285
1286 #[must_use]
1297 pub fn extend_edges_unrecorded<I, S, T>(&mut self, edges: I) -> usize
1298 where
1299 I: IntoIterator<Item = (S, T)>,
1300 S: Into<String>,
1301 T: Into<String>,
1302 {
1303 self.compact_before_growth();
1304 let iterator = edges.into_iter();
1305 let (lower_bound, _) = iterator.size_hint();
1306 self.node_order.reserve(lower_bound);
1307 self.edges.reserve(lower_bound);
1308
1309 let mut inserted = 0usize;
1310 for (source, target) in iterator {
1311 let source = source.into();
1312 let target = target.into();
1313 let (s_slot, _) = match self.node_order.get(&source) {
1314 Some(&slot) => (slot, false),
1315 None => {
1316 let slot = self.node_order.push(source.clone());
1317 self.node_attrs.push(AttrMap::new());
1318 self.succ_indices.rows_mut().push(Vec::new());
1319 self.pred_indices.rows_mut().push(Vec::new());
1320 (slot, true)
1321 }
1322 };
1323 let (t_slot, _) = if source == target {
1324 (s_slot, false)
1325 } else {
1326 match self.node_order.get(&target) {
1327 Some(&slot) => (slot, false),
1328 None => {
1329 let slot = self.node_order.push(target.clone());
1330 self.node_attrs.push(AttrMap::new());
1331 self.succ_indices.rows_mut().push(Vec::new());
1332 self.pred_indices.rows_mut().push(Vec::new());
1333 (slot, true)
1334 }
1335 }
1336 };
1337 let edge_key = (s_slot, t_slot);
1338 if self.edges.contains_key(&edge_key) {
1339 continue;
1340 }
1341 self.edges.insert(edge_key, AttrMap::new());
1342 self.succ_indices.rows_mut()[s_slot].push(t_slot);
1343 self.pred_indices.rows_mut()[t_slot].push(s_slot);
1344 inserted += 1;
1345 }
1346
1347 if inserted > 0 {
1348 self.revision = self
1349 .revision
1350 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1351 self.record_decision(
1352 "extend_edges_unrecorded",
1353 0.0,
1354 false,
1355 vec![EvidenceTerm {
1356 signal: "batch_edge_count".into(),
1357 observed_value: count_evidence(inserted),
1358 log_likelihood_ratio: -1.0,
1359 }],
1360 );
1361 }
1362
1363 inserted
1364 }
1365
1366 pub fn extend_existing_index_edges_unrecorded<I>(&mut self, edges: I) -> usize
1375 where
1376 I: IntoIterator<Item = (usize, usize)>,
1377 {
1378 self.compact_before_growth();
1379 let iterator = edges.into_iter();
1380 self.edges.reserve(iterator.size_hint().0);
1381
1382 let mut inserted = 0usize;
1383 for (s_idx, t_idx) in iterator {
1384 debug_assert!(s_idx < self.node_order.len());
1385 debug_assert!(t_idx < self.node_order.len());
1386 let s_slot = self
1387 .node_order
1388 .slot_at(s_idx)
1389 .expect("validated source index");
1390 let t_slot = self
1391 .node_order
1392 .slot_at(t_idx)
1393 .expect("validated target index");
1394 let edge_key = (s_slot, t_slot);
1395 if self.edges.contains_key(&edge_key) {
1396 continue;
1397 }
1398 self.edges.insert(edge_key, AttrMap::new());
1399 self.succ_indices.rows_mut()[s_slot].push(t_slot);
1400 self.pred_indices.rows_mut()[t_slot].push(s_slot);
1401 inserted += 1;
1402 }
1403
1404 if inserted > 0 {
1405 self.revision = self
1406 .revision
1407 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1408 self.record_decision(
1409 "extend_edges_unrecorded",
1410 0.0,
1411 false,
1412 vec![EvidenceTerm {
1413 signal: "batch_edge_count".into(),
1414 observed_value: count_evidence(inserted),
1415 log_likelihood_ratio: -1.0,
1416 }],
1417 );
1418 }
1419
1420 inserted
1421 }
1422
1423 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
1428 where
1429 I: IntoIterator<Item = N>,
1430 N: Into<String>,
1431 {
1432 self.compact_before_growth();
1433 let iterator = nodes.into_iter();
1434 let (lower_bound, _) = iterator.size_hint();
1435 self.node_order.reserve(lower_bound);
1436
1437 let mut inserted = 0usize;
1438 for node in iterator {
1439 let node = node.into();
1440 if self.node_order.contains_key(&node) {
1441 continue;
1442 }
1443 self.node_order.push(node);
1444 self.node_attrs.push(AttrMap::new());
1445 self.succ_indices.rows_mut().push(Vec::new());
1446 self.pred_indices.rows_mut().push(Vec::new());
1447 inserted += 1;
1448 }
1449 if inserted > 0 {
1450 self.revision = self
1451 .revision
1452 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1453 self.record_decision(
1454 "extend_nodes_unrecorded",
1455 0.0,
1456 false,
1457 vec![EvidenceTerm {
1458 signal: "batch_node_count".into(),
1459 observed_value: count_evidence(inserted),
1460 log_likelihood_ratio: -1.0,
1461 }],
1462 );
1463 }
1464 inserted
1465 }
1466
1467 #[must_use]
1473 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
1474 where
1475 I: IntoIterator<Item = (String, AttrMap)>,
1476 {
1477 self.compact_before_growth();
1478 let iterator = nodes.into_iter();
1479 let (lower_bound, _) = iterator.size_hint();
1480 self.node_order.reserve(lower_bound);
1481
1482 let mut inserted = 0usize;
1483 for (node, attrs) in iterator {
1484 if let Some(&slot) = self.node_order.get(&node) {
1485 self.node_attrs[slot].extend(attrs);
1486 continue;
1487 }
1488 self.node_order.push(node);
1489 self.node_attrs.push(attrs);
1490 self.succ_indices.rows_mut().push(Vec::new());
1491 self.pred_indices.rows_mut().push(Vec::new());
1492 inserted += 1;
1493 }
1494 if inserted > 0 {
1495 self.revision = self
1496 .revision
1497 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1498 self.record_decision(
1499 "extend_nodes_unrecorded",
1500 0.0,
1501 false,
1502 vec![EvidenceTerm {
1503 signal: "batch_node_count".into(),
1504 observed_value: count_evidence(inserted),
1505 log_likelihood_ratio: -1.0,
1506 }],
1507 );
1508 }
1509 inserted
1510 }
1511
1512 #[must_use]
1519 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1520 where
1521 I: IntoIterator<Item = (String, String, AttrMap)>,
1522 {
1523 self.compact_before_growth();
1524 let iterator = edges.into_iter();
1525 let (lower_bound, _) = iterator.size_hint();
1526 self.node_order.reserve(lower_bound);
1527 self.edges.reserve(lower_bound);
1528
1529 let mut inserted = 0usize;
1530 let mut merged_changed = false;
1531 for (source, target, attrs) in iterator {
1532 let (s_slot, _) = match self.node_order.get(&source) {
1533 Some(&slot) => (slot, false),
1534 None => {
1535 let slot = self.node_order.push(source.clone());
1536 self.node_attrs.push(AttrMap::new());
1537 self.succ_indices.rows_mut().push(Vec::new());
1538 self.pred_indices.rows_mut().push(Vec::new());
1539 (slot, true)
1540 }
1541 };
1542 let (t_slot, _) = if source == target {
1543 (s_slot, false)
1544 } else {
1545 match self.node_order.get(&target) {
1546 Some(&slot) => (slot, false),
1547 None => {
1548 let slot = self.node_order.push(target.clone());
1549 self.node_attrs.push(AttrMap::new());
1550 self.succ_indices.rows_mut().push(Vec::new());
1551 self.pred_indices.rows_mut().push(Vec::new());
1552 (slot, true)
1553 }
1554 }
1555 };
1556 let edge_key = (s_slot, t_slot);
1557 if let Some(existing) = self.edges.get_mut(&edge_key) {
1558 if attrs.is_empty() {
1559 continue;
1560 }
1561 if attrs
1562 .iter()
1563 .any(|(key, value)| existing.get(key) != Some(value))
1564 {
1565 merged_changed = true;
1566 }
1567 existing.extend(attrs);
1568 continue;
1569 }
1570 self.edges.insert(edge_key, attrs);
1571 self.succ_indices.rows_mut()[s_slot].push(t_slot);
1572 self.pred_indices.rows_mut()[t_slot].push(s_slot);
1573 inserted += 1;
1574 }
1575 if inserted > 0 || merged_changed {
1576 self.revision = self
1577 .revision
1578 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1579 self.record_decision(
1580 "extend_edges_unrecorded",
1581 0.0,
1582 false,
1583 vec![EvidenceTerm {
1584 signal: "batch_edge_count".into(),
1585 observed_value: count_evidence(inserted),
1586 log_likelihood_ratio: -1.0,
1587 }],
1588 );
1589 }
1590 inserted
1591 }
1592
1593 #[must_use]
1598 pub fn extend_prepared_edges_with_attrs_row_staged_unrecorded<I, N>(
1599 &mut self,
1600 new_nodes: N,
1601 edges: I,
1602 ) -> usize
1603 where
1604 I: IntoIterator<Item = (String, String, AttrMap)>,
1605 N: IntoIterator<Item = String>,
1606 {
1607 self.compact_before_growth();
1608 for node in new_nodes {
1609 if self.node_order.contains_key(&node) {
1610 continue;
1611 }
1612 self.node_order.push(node);
1613 self.node_attrs.push(AttrMap::new());
1614 self.succ_indices.rows_mut().push(Vec::new());
1615 self.pred_indices.rows_mut().push(Vec::new());
1616 }
1617
1618 let mut inserted = 0usize;
1619 let mut merged_changed = false;
1620
1621 for (source, target, attrs) in edges {
1622 let s_slot = *self.node_order.get(&source).expect("source created above");
1623 let t_slot = *self.node_order.get(&target).expect("target created above");
1624 let edge_key = (s_slot, t_slot);
1625 if let Some(existing) = self.edges.get_mut(&edge_key) {
1626 if attrs.is_empty() {
1627 continue;
1628 }
1629 if attrs
1630 .iter()
1631 .any(|(key, value)| existing.get(key) != Some(value))
1632 {
1633 merged_changed = true;
1634 }
1635 existing.extend(attrs);
1636 continue;
1637 }
1638
1639 self.succ_indices.rows_mut()[s_slot].push(t_slot);
1640 self.pred_indices.rows_mut()[t_slot].push(s_slot);
1641 self.edges.insert(edge_key, attrs);
1642 inserted += 1;
1643 }
1644
1645 if inserted > 0 || merged_changed {
1646 self.revision = self
1647 .revision
1648 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1649 self.record_decision(
1650 "extend_edges_unrecorded",
1651 0.0,
1652 false,
1653 vec![EvidenceTerm {
1654 signal: "batch_edge_count".into(),
1655 observed_value: count_evidence(inserted),
1656 log_likelihood_ratio: -1.0,
1657 }],
1658 );
1659 }
1660
1661 inserted
1662 }
1663
1664 #[must_use]
1673 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1679 where
1680 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1681 {
1682 self.compact_before_growth();
1683 let node_count = self.node_order.len();
1684 let edges = edges.into_iter();
1685 self.edges.reserve(edges.size_hint().0);
1686 let mut inserted = 0usize;
1687 let mut merged_changed = false;
1688 for (source_idx, target_idx, attrs) in edges {
1689 debug_assert!(source_idx < node_count && target_idx < node_count);
1690 let s_slot = self
1691 .node_order
1692 .slot_at(source_idx)
1693 .expect("validated source index");
1694 let t_slot = self
1695 .node_order
1696 .slot_at(target_idx)
1697 .expect("validated target index");
1698 let edge_key = (s_slot, t_slot);
1699 if let Some(existing) = self.edges.get_mut(&edge_key) {
1700 if attrs.is_empty() {
1701 continue;
1702 }
1703 if attrs
1704 .iter()
1705 .any(|(key, value)| existing.get(key) != Some(value))
1706 {
1707 merged_changed = true;
1708 }
1709 existing.extend(attrs);
1710 continue;
1711 }
1712 self.edges.insert(edge_key, attrs);
1713 self.succ_indices.rows_mut()[s_slot].push(t_slot);
1714 self.pred_indices.rows_mut()[t_slot].push(s_slot);
1715 inserted += 1;
1716 }
1717 if inserted > 0 || merged_changed {
1718 self.revision = self
1719 .revision
1720 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1721 }
1722 inserted
1723 }
1724
1725 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
1726 &mut self,
1727 nodes: N,
1728 edges: I,
1729 ) -> usize
1730 where
1731 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1732 N: IntoIterator<Item = String>,
1733 {
1734 if !self.node_order.is_empty()
1735 || !self.succ_indices.is_empty()
1736 || !self.pred_indices.is_empty()
1737 || !self.edges.is_empty()
1738 {
1739 return 0;
1740 }
1741
1742 let node_labels: Vec<String> = nodes.into_iter().collect();
1743 let node_count = node_labels.len();
1744 self.node_order = crate::NodeOrder::from_names(node_labels);
1745 self.node_attrs = vec![AttrMap::new(); node_count];
1746 *self.succ_indices.rows_mut() = vec![Vec::new(); node_count];
1747 *self.pred_indices.rows_mut() = vec![Vec::new(); node_count];
1748
1749 let mut inserted = 0usize;
1750 let mut merged_changed = false;
1751 let edges = edges.into_iter();
1752 self.edges.reserve(edges.size_hint().0);
1753 for (source_idx, target_idx, attrs) in edges {
1754 if source_idx >= node_count || target_idx >= node_count {
1755 continue;
1756 }
1757 let edge_key = (source_idx, target_idx);
1758 if let Some(existing) = self.edges.get_mut(&edge_key) {
1759 if attrs.is_empty() {
1760 continue;
1761 }
1762 if attrs
1763 .iter()
1764 .any(|(key, value)| existing.get(key) != Some(value))
1765 {
1766 merged_changed = true;
1767 }
1768 existing.extend(attrs);
1769 continue;
1770 }
1771 self.edges.insert(edge_key, attrs);
1772 self.succ_indices.rows_mut()[source_idx].push(target_idx);
1773 self.pred_indices.rows_mut()[target_idx].push(source_idx);
1774 inserted += 1;
1775 }
1776
1777 if node_count > 0 || inserted > 0 || merged_changed {
1778 self.revision = self.revision.saturating_add(
1779 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
1780 );
1781 self.record_decision(
1782 "extend_fresh_index_edges_unrecorded",
1783 0.0,
1784 false,
1785 vec![
1786 EvidenceTerm {
1787 signal: "batch_node_count".into(),
1788 observed_value: count_evidence(node_count),
1789 log_likelihood_ratio: -1.0,
1790 },
1791 EvidenceTerm {
1792 signal: "batch_edge_count".into(),
1793 observed_value: count_evidence(inserted),
1794 log_likelihood_ratio: -1.0,
1795 },
1796 ],
1797 );
1798 }
1799
1800 inserted
1801 }
1802
1803 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
1804 if defaults.is_empty() {
1805 return false;
1806 }
1807 let mut inserted = 0usize;
1808 for attrs in self.edges.values_mut() {
1809 for (key, value) in defaults {
1810 if !attrs.contains_key(key) {
1811 attrs.insert(key.clone(), value.clone());
1812 inserted += 1;
1813 }
1814 }
1815 }
1816 if inserted > 0 {
1817 self.revision = self.revision.saturating_add(1);
1818 self.record_decision(
1819 "apply_edge_defaults",
1820 0.0,
1821 false,
1822 vec![EvidenceTerm {
1823 signal: "defaults_applied".into(),
1824 observed_value: count_evidence(inserted),
1825 log_likelihood_ratio: 0.0,
1826 }],
1827 );
1828 return true;
1829 }
1830 false
1831 }
1832
1833 pub fn replace_edge_attrs(&mut self, source: &str, target: &str, attrs: AttrMap) -> bool {
1835 let Some(edge_key) = self.edge_pair_key(source, target) else {
1836 return false;
1837 };
1838 if let Some(slot) = self.edges.get_mut(&edge_key) {
1839 if *slot != attrs {
1840 *slot = attrs;
1841 self.revision = self.revision.saturating_add(1);
1842 }
1843 true
1844 } else {
1845 false
1846 }
1847 }
1848
1849 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
1851 if let Some(&slot) = self.node_order.get(node) {
1852 if self.node_attrs[slot] != attrs {
1853 self.node_attrs[slot] = attrs;
1854 self.revision = self.revision.saturating_add(1);
1855 }
1856 true
1857 } else {
1858 false
1859 }
1860 }
1861
1862 pub fn remove_edge(&mut self, source: &str, target: &str) -> bool {
1864 let (Some(&s_slot), Some(&t_slot)) =
1865 (self.node_order.get(source), self.node_order.get(target))
1866 else {
1867 return false;
1868 };
1869 let pair = (s_slot, t_slot);
1870 let removed = self.edges.swap_remove(&pair).is_some();
1871 if removed {
1872 self.succ_indices.rows_mut()[s_slot].retain(|&i| i != t_slot);
1873 self.pred_indices.rows_mut()[t_slot].retain(|&i| i != s_slot);
1874 self.revision = self.revision.saturating_add(1);
1875 }
1876 removed
1877 }
1878
1879 pub fn remove_node(&mut self, node: &str) -> bool {
1881 let removed = self.remove_node_deferred_compaction(node);
1882 if removed {
1883 self.compact_when_sparse();
1884 }
1885 removed
1886 }
1887
1888 fn remove_node_deferred_compaction(&mut self, node: &str) -> bool {
1889 let Some((slot, popped)) = self.node_order.remove(node) else {
1891 return false;
1892 };
1893
1894 let succ_rows = self.succ_indices.rows_mut();
1896 let pred_rows = self.pred_indices.rows_mut();
1897 let succs = std::mem::take(&mut succ_rows[slot]);
1898 for &t in &succs {
1899 self.edges.swap_remove(&(slot, t));
1900 if t != slot {
1901 pred_rows[t].retain(|&i| i != slot);
1902 }
1903 }
1904 let preds = std::mem::take(&mut pred_rows[slot]);
1905 for &s in &preds {
1906 self.edges.swap_remove(&(s, slot));
1907 if s != slot {
1908 succ_rows[s].retain(|&i| i != slot);
1909 }
1910 }
1911
1912 if popped {
1913 self.node_attrs.pop();
1914 succ_rows.pop();
1915 pred_rows.pop();
1916 } else {
1917 self.node_attrs[slot].clear();
1918 }
1919
1920 self.revision = self.revision.saturating_add(1);
1921 true
1922 }
1923
1924 fn compact_when_sparse(&mut self) {
1927 let removed = self.node_order.slot_count() - self.node_order.len();
1928 if removed > self.node_order.len() {
1929 self.compact_internal();
1930 }
1931 }
1932
1933 fn compact_before_growth(&mut self) {
1936 if !self.node_order.is_dense() {
1937 self.compact_internal();
1938 }
1939 }
1940
1941 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
1942 where
1943 I: IntoIterator<Item = &'a str>,
1944 {
1945 let mut removed_nodes = 0usize;
1946 let old_edge_count = self.edges.len();
1947 for node in nodes {
1948 if self.remove_node_deferred_compaction(node) {
1949 removed_nodes += 1;
1950 }
1951 }
1952 let removed_edges = old_edge_count.saturating_sub(self.edges.len());
1953 if removed_nodes > 0 {
1954 self.compact_when_sparse();
1955 }
1956 (removed_nodes, removed_edges)
1957 }
1958
1959 #[inline]
1962 #[must_use]
1963 pub fn is_compact(&self) -> bool {
1964 self.node_order.is_dense()
1965 }
1966
1967 pub fn ensure_compact(&mut self) {
1968 if self.is_compact() {
1969 return;
1970 }
1971 self.compact_internal();
1972 }
1973
1974 fn compact_internal(&mut self) {
1975 let n = self.node_order.len();
1976 let old_slots: Vec<usize> = self.node_order.values().collect();
1977 let old_to_new = self.node_order.compact();
1978
1979 let mut new_node_attrs = Vec::with_capacity(n);
1980 let mut new_succ_indices = Vec::with_capacity(n);
1981 let mut new_pred_indices = Vec::with_capacity(n);
1982 let succ_rows = self.succ_indices.rows_mut();
1983 let pred_rows = self.pred_indices.rows_mut();
1984 for old_slot in old_slots {
1985 new_node_attrs.push(std::mem::take(&mut self.node_attrs[old_slot]));
1986 new_succ_indices.push(std::mem::take(&mut succ_rows[old_slot]));
1987 new_pred_indices.push(std::mem::take(&mut pred_rows[old_slot]));
1988 }
1989
1990 for row in &mut new_succ_indices {
1992 for target in row {
1993 *target = old_to_new[*target];
1994 }
1995 }
1996 for row in &mut new_pred_indices {
1997 for source in row {
1998 *source = old_to_new[*source];
1999 }
2000 }
2001
2002 let old_edges = std::mem::take(&mut self.edges);
2004 let mut new_edges =
2005 FxIndexMap::with_capacity_and_hasher(old_edges.len(), rustc_hash::FxBuildHasher);
2006
2007 for ((old_s, old_t), attrs) in old_edges {
2008 new_edges.insert((old_to_new[old_s], old_to_new[old_t]), attrs);
2009 }
2010
2011 self.node_attrs = new_node_attrs;
2012 *self.succ_indices.rows_mut() = new_succ_indices;
2013 *self.pred_indices.rows_mut() = new_pred_indices;
2014 self.edges = new_edges;
2015 self.revision = self.revision.saturating_add(1);
2016 }
2017
2018 #[must_use]
2025 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
2026 let mut ordered = Vec::with_capacity(self.edges.len());
2027
2028 for (node, u) in self.node_order.iter() {
2029 for &t in &self.succ_indices[u] {
2030 let target = self.node_order.names[t]
2031 .as_deref()
2032 .expect("valid target slot");
2033 if let Some(attrs) = self.edges.get(&(u, t)) {
2034 ordered.push(EdgeSnapshot {
2035 left: node.clone(),
2036 right: target.to_owned(),
2037 attrs: attrs.clone(),
2038 });
2039 }
2040 }
2041 }
2042
2043 ordered
2044 }
2045
2046 #[must_use]
2047 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, &AttrMap)> {
2048 let mut ordered = Vec::with_capacity(self.edges.len());
2049 let mut slot_to_pos = vec![0; self.node_order.slot_count()];
2050 for (pos, (_, slot)) in self.node_order.iter().enumerate() {
2051 slot_to_pos[slot] = pos;
2052 }
2053
2054 for (u_pos, (_, u)) in self.node_order.iter().enumerate() {
2055 for &t in &self.succ_indices[u] {
2056 if let Some(attrs) = self.edges.get(&(u, t)) {
2057 let t_pos = slot_to_pos[t];
2058 ordered.push((u_pos, t_pos, attrs));
2059 }
2060 }
2061 }
2062
2063 ordered
2064 }
2065
2066 #[must_use]
2067 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
2068 let mut ordered = Vec::with_capacity(self.edges.len());
2069
2070 for (u_name, u) in self.node_order.iter() {
2071 for &t in &self.succ_indices[u] {
2072 if let Some(attrs) = self.edges.get(&(u, t)) {
2073 let t_name = self.node_order.names[t]
2074 .as_deref()
2075 .expect("valid target slot");
2076 ordered.push((u_name.as_str(), t_name, attrs));
2077 }
2078 }
2079 }
2080
2081 ordered
2082 }
2083
2084 #[must_use]
2092 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
2093 let mut total: i128 = 0;
2094 for attrs in self.edges.values() {
2095 let value = match attrs.get(weight) {
2096 Some(CgseValue::Int(v)) => i128::from(*v),
2097 Some(_) => return None,
2098 None => 1,
2099 };
2100 total = total.checked_add(value)?;
2101 }
2102 Some(total)
2103 }
2104
2105 #[must_use]
2120 pub fn reversed(&self) -> Self {
2121 let slot_count = self.node_order.slot_count();
2122 let mut succ_indices: Vec<Vec<usize>> = vec![Vec::new(); slot_count];
2123 let mut pred_indices: Vec<Vec<usize>> = vec![Vec::new(); slot_count];
2124 for (i, (succ_row, pred_row)) in succ_indices
2125 .iter_mut()
2126 .zip(pred_indices.iter_mut())
2127 .enumerate()
2128 {
2129 succ_row.reserve(self.pred_indices[i].len());
2132 pred_row.reserve(self.succ_indices[i].len());
2133 }
2134 let mut edges: crate::FxIndexMap<(usize, usize), AttrMap> =
2135 crate::FxIndexMap::with_capacity_and_hasher(
2136 self.edges.len(),
2137 rustc_hash::FxBuildHasher,
2138 );
2139 for u in self.node_order.values() {
2140 for &t in &self.succ_indices[u] {
2141 succ_indices[t].push(u);
2143 pred_indices[u].push(t);
2144 if let Some(attrs) = self.edges.get(&(u, t)) {
2145 edges.insert((t, u), attrs.clone());
2146 }
2147 }
2148 }
2149 Self {
2150 mode: self.mode,
2151 revision: 0,
2152 node_order: self.node_order.clone(),
2153 node_attrs: self.node_attrs.clone(),
2154 succ_indices: crate::SlotRows::new(succ_indices),
2155 pred_indices: crate::SlotRows::new(pred_indices),
2156 edges,
2157 runtime_policy: self.runtime_policy.clone(),
2158 csr_cache: std::sync::Arc::default(),
2159 all_int_cache: std::sync::Arc::default(),
2160 }
2161 }
2162
2163 #[must_use]
2165 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
2166 let mut ordered = Vec::with_capacity(self.edges.len());
2167 let mut slot_to_pos = vec![0; self.node_order.slot_count()];
2168 for (pos, (_, slot)) in self.node_order.iter().enumerate() {
2169 slot_to_pos[slot] = pos;
2170 }
2171
2172 for (u_pos, (_, u)) in self.node_order.iter().enumerate() {
2173 for &t in &self.succ_indices[u] {
2174 if self.edges.contains_key(&(u, t)) {
2175 let t_pos = slot_to_pos[t];
2176 ordered.push((u_pos, t_pos));
2177 }
2178 }
2179 }
2180
2181 ordered
2182 }
2183
2184 #[must_use]
2185 pub fn snapshot(&self) -> DiGraphSnapshot {
2186 let mut node_attrs: std::collections::BTreeMap<String, AttrMap> =
2187 std::collections::BTreeMap::new();
2188 for (name, slot) in self.node_order.iter() {
2189 if !self.node_attrs[slot].is_empty() {
2190 node_attrs.insert(name.clone(), self.node_attrs[slot].clone());
2191 }
2192 }
2193 DiGraphSnapshot {
2194 mode: self.mode,
2195 nodes: self.node_order.keys().cloned().collect(),
2196 node_attrs,
2197 edges: self.edges_ordered(),
2198 }
2199 }
2200
2201 #[must_use]
2205 pub fn to_undirected(&self) -> crate::Graph {
2206 let mut g = crate::Graph::with_runtime_policy(self.runtime_policy.clone());
2207 for (node, slot) in self.node_order.iter() {
2208 g.add_node_with_attrs(node.clone(), self.node_attrs[slot].clone());
2209 }
2210 let edges: Vec<(String, String, AttrMap)> = self
2224 .edges_ordered()
2225 .into_iter()
2226 .map(|snap| (snap.left, snap.right, snap.attrs))
2227 .collect();
2228 let _ = g.extend_edges_with_attrs_unrecorded(edges);
2229 g
2230 }
2231
2232 fn record_decision(
2237 &mut self,
2238 operation: &'static str,
2239 incompatibility_probability: f64,
2240 unknown_incompatible_feature: bool,
2241 evidence: Vec<EvidenceTerm>,
2242 ) -> DecisionAction {
2243 let action = self
2244 .runtime_policy
2245 .action_for(incompatibility_probability, unknown_incompatible_feature);
2246 self.runtime_policy.record(
2247 operation,
2248 action,
2249 incompatibility_probability,
2250 "argmin expected loss over {allow,full_validate,fail_closed}",
2251 evidence,
2252 );
2253 action
2254 }
2255}
2256
2257#[derive(Debug, Clone, Default)]
2264struct MultiDiNodes {
2265 order: crate::NodeOrder,
2266 attrs: Vec<AttrMap>,
2267}
2268
2269impl MultiDiNodes {
2270 fn len(&self) -> usize {
2271 self.order.len()
2272 }
2273
2274 fn is_empty(&self) -> bool {
2275 self.order.is_empty()
2276 }
2277
2278 fn contains_key(&self, name: &str) -> bool {
2279 self.order.contains_key(name)
2280 }
2281
2282 fn get(&self, name: &str) -> Option<&AttrMap> {
2283 Some(&self.attrs[*self.order.get(name)?])
2284 }
2285
2286 fn get_mut(&mut self, name: &str) -> Option<&mut AttrMap> {
2287 let slot = *self.order.get(name)?;
2288 Some(&mut self.attrs[slot])
2289 }
2290
2291 fn get_index_of(&self, name: &str) -> Option<usize> {
2293 self.order.get_index_of(name)
2294 }
2295
2296 fn get_index(&self, position: usize) -> Option<(&String, &AttrMap)> {
2297 let (name, slot) = self.order.get_index(position)?;
2298 Some((name, &self.attrs[slot]))
2299 }
2300
2301 fn keys(&self) -> impl Iterator<Item = &String> + '_ {
2302 self.order.keys()
2303 }
2304
2305 fn iter(&self) -> impl Iterator<Item = (&String, &AttrMap)> + '_ {
2306 self.order
2307 .iter()
2308 .map(|(name, slot)| (name, &self.attrs[slot]))
2309 }
2310
2311 fn reserve(&mut self, additional: usize) {
2312 self.order.reserve(additional);
2313 self.attrs.reserve(additional);
2314 }
2315
2316 fn push(&mut self, name: String, attrs: AttrMap) -> &mut AttrMap {
2318 let slot = self.order.push(name);
2319 debug_assert_eq!(slot, self.attrs.len());
2320 self.attrs.push(attrs);
2321 &mut self.attrs[slot]
2322 }
2323
2324 fn insert(&mut self, name: String, attrs: AttrMap) -> Option<AttrMap> {
2326 if let Some(existing) = self.get_mut(&name) {
2327 return Some(std::mem::replace(existing, attrs));
2328 }
2329 self.push(name, attrs);
2330 None
2331 }
2332
2333 fn remove(&mut self, name: &str) -> Option<AttrMap> {
2336 let (slot, popped) = self.order.remove(name)?;
2337 let attrs = if popped {
2338 self.attrs.pop().expect("attributes are slot-parallel")
2339 } else {
2340 std::mem::take(&mut self.attrs[slot])
2341 };
2342 if self.order.slot_count() - self.order.len() > self.order.len() {
2343 let old_to_new = self.order.compact();
2344 self.attrs = std::mem::take(&mut self.attrs)
2345 .into_iter()
2346 .zip(old_to_new)
2347 .filter_map(|(attrs, new)| (new != usize::MAX).then_some(attrs))
2348 .collect();
2349 }
2350 Some(attrs)
2351 }
2352
2353 fn positions(&self) -> NodePositions<'_> {
2356 NodePositions {
2357 order: &self.order,
2358 table: self.order.slot_positions(),
2359 }
2360 }
2361}
2362
2363struct NodePositions<'a> {
2365 order: &'a crate::NodeOrder,
2366 table: Option<Vec<usize>>,
2367}
2368
2369impl NodePositions<'_> {
2370 fn of(&self, name: &str) -> Option<usize> {
2371 let slot = *self.order.get(name)?;
2372 Some(self.table.as_ref().map_or(slot, |table| table[slot]))
2373 }
2374}
2375
2376type MultiDiRows =
2380 rustc_hash::FxHashMap<String, crate::FxIndexMap<String, crate::FxIndexSet<usize>>>;
2381
2382#[derive(Debug, Clone)]
2383pub struct MultiDiGraph {
2384 mode: CompatibilityMode,
2385 revision: u64,
2386 nodes: MultiDiNodes,
2387 successors: MultiDiRows,
2388 predecessors: MultiDiRows,
2389 edges: crate::FxIndexMap<DirectedEdgeKey, crate::FxIndexMap<usize, AttrMap>>,
2392 runtime_policy: RuntimePolicy,
2393 edge_count: usize,
2394 csr_cache: MultiDiCsrCache,
2395 edge_pair_index: EdgePairIndex,
2419}
2420
2421impl MultiDiGraph {
2422 #[must_use]
2428 pub fn get_node_index(&self, node: &str) -> Option<usize> {
2429 self.nodes.get_index_of(node)
2430 }
2431
2432 #[must_use]
2435 pub fn clone_with_fresh_policy(&self) -> Self {
2436 Self {
2437 mode: self.mode,
2438 revision: self.revision,
2439 nodes: self.nodes.clone(),
2440 successors: self.successors.clone(),
2441 predecessors: self.predecessors.clone(),
2442 edges: self.edges.clone(),
2443 runtime_policy: RuntimePolicy::new(self.mode),
2444 edge_count: self.edge_count,
2445 csr_cache: self.csr_cache.clone(),
2446 edge_pair_index: EdgePairIndex::default(),
2447 }
2448 }
2449
2450 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
2456 let mut order_map: IndexMap<String, Vec<String>> =
2464 IndexMap::with_capacity(self.predecessors.len());
2465 for u in self.nodes.keys() {
2466 let Some(succ_row) = self.successors.get(u.as_str()) else {
2467 continue;
2468 };
2469 for v in succ_row.keys() {
2470 order_map.entry(v.clone()).or_default().push(u.clone());
2471 }
2472 }
2473 let orders: Vec<(String, Vec<String>)> = order_map.into_iter().collect();
2474 self.apply_row_orders(&orders, true);
2475 }
2476
2477 pub fn set_row_orders(
2482 &mut self,
2483 succ: &[(String, Vec<String>)],
2484 pred: &[(String, Vec<String>)],
2485 ) {
2486 self.apply_row_orders(succ, false);
2487 self.apply_row_orders(pred, true);
2488 self.revision = self.revision.saturating_add(1);
2489 self.csr_cache = std::sync::Arc::default();
2491 }
2492
2493 pub fn reorder_rows_like(&mut self, source: &Self, swap: bool) {
2501 fn reorder(rows: &mut MultiDiRows, source_rows: &MultiDiRows) -> bool {
2502 let mut changed = false;
2503 for (node, row) in rows.iter_mut() {
2504 let Some(source_row) = source_rows.get(node.as_str()) else {
2505 continue;
2506 };
2507 if row.len() < 2 {
2508 continue;
2509 }
2510 let rank = |w: &String| source_row.get_index_of(w.as_str()).unwrap_or(usize::MAX);
2511 if !row.keys().map(rank).is_sorted() {
2512 row.sort_by_cached_key(|w, _| rank(w));
2513 changed = true;
2514 }
2515 }
2516 changed
2517 }
2518 let (succ_source, pred_source) = if swap {
2519 (&source.predecessors, &source.successors)
2520 } else {
2521 (&source.successors, &source.predecessors)
2522 };
2523 let succ_changed = reorder(&mut self.successors, succ_source);
2524 let pred_changed = reorder(&mut self.predecessors, pred_source);
2525 if succ_changed || pred_changed {
2526 self.revision = self.revision.saturating_add(1);
2527 self.csr_cache = std::sync::Arc::default();
2528 }
2529 }
2530
2531 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
2535 let map = if pred {
2536 &mut self.predecessors
2537 } else {
2538 &mut self.successors
2539 };
2540 for (node, order) in orders {
2541 let Some(row) = map.get_mut(node.as_str()) else {
2542 continue;
2543 };
2544 let mut old = std::mem::take(row);
2545 let mut new_row =
2546 crate::FxIndexMap::with_capacity_and_hasher(old.len(), rustc_hash::FxBuildHasher);
2547 for v in order {
2548 if let Some((k, val)) = old.shift_remove_entry(v.as_str()) {
2549 new_row.insert(k, val);
2550 }
2551 }
2552 for (k, val) in old {
2553 new_row.insert(k, val);
2554 }
2555 *row = new_row;
2556 }
2557 }
2558
2559 #[must_use]
2560 pub fn new(mode: CompatibilityMode) -> Self {
2561 Self {
2562 mode,
2563 revision: 0,
2564 nodes: MultiDiNodes::default(),
2565 successors: MultiDiRows::default(),
2566 predecessors: MultiDiRows::default(),
2567 edges: crate::FxIndexMap::default(),
2568 runtime_policy: RuntimePolicy::new(mode),
2569 edge_count: 0,
2570 csr_cache: std::sync::Arc::default(),
2571 edge_pair_index: EdgePairIndex::default(),
2572 }
2573 }
2574
2575 #[must_use]
2576 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
2577 let mode = runtime_policy.mode();
2578 Self {
2579 mode,
2580 revision: 0,
2581 nodes: MultiDiNodes::default(),
2582 successors: MultiDiRows::default(),
2583 predecessors: MultiDiRows::default(),
2584 edges: crate::FxIndexMap::default(),
2585 runtime_policy,
2586 edge_count: 0,
2587 csr_cache: std::sync::Arc::default(),
2588 edge_pair_index: EdgePairIndex::default(),
2589 }
2590 }
2591
2592 #[must_use]
2593 pub fn strict() -> Self {
2594 Self::new(CompatibilityMode::Strict)
2595 }
2596
2597 #[must_use]
2598 pub fn hardened() -> Self {
2599 Self::new(CompatibilityMode::Hardened)
2600 }
2601
2602 #[must_use]
2603 pub fn mode(&self) -> CompatibilityMode {
2604 self.mode
2605 }
2606
2607 #[must_use]
2608 pub fn node_count(&self) -> usize {
2609 self.nodes.len()
2610 }
2611
2612 #[must_use]
2613 pub fn edge_count(&self) -> usize {
2614 self.edge_count
2615 }
2616
2617 #[must_use]
2618 pub fn number_of_selfloops(&self) -> usize {
2619 self.edges
2620 .iter()
2621 .filter(|(edge_key, _)| edge_key.source == edge_key.target)
2622 .map(|(_, edge_bucket)| edge_bucket.len())
2623 .sum()
2624 }
2625
2626 #[must_use]
2629 pub fn edge_keys(&self, source: &str, target: &str) -> Option<Vec<usize>> {
2630 self.successors
2631 .get(source)?
2632 .get(target)
2633 .map(|keys| keys.iter().copied().collect())
2634 }
2635
2636 pub fn edge_keys_iter(
2638 &self,
2639 source: &str,
2640 target: &str,
2641 ) -> Option<impl Iterator<Item = &usize>> {
2642 self.successors
2643 .get(source)?
2644 .get(target)
2645 .map(|keys| keys.iter())
2646 }
2647
2648 pub fn parallel_edge_counts(&self) -> impl Iterator<Item = (&str, &str, usize)> {
2651 self.edges
2652 .iter()
2653 .filter(|(_, keys)| keys.len() > 1)
2654 .map(|(edge, keys)| (edge.source.as_str(), edge.target.as_str(), keys.len()))
2655 }
2656
2657 #[must_use]
2658 pub fn revision(&self) -> u64 {
2659 self.revision
2660 }
2661
2662 #[must_use]
2666 pub fn csr(&self) -> std::sync::Arc<MultiDiCsr> {
2667 if let Ok(guard) = self.csr_cache.read()
2668 && let Some((rev, csr)) = guard.as_ref()
2669 && *rev == self.revision
2670 {
2671 return csr.clone();
2672 }
2673 let built = std::sync::Arc::new(self.build_csr());
2674 if let Ok(mut guard) = self.csr_cache.write() {
2675 *guard = Some((self.revision, built.clone()));
2676 }
2677 built
2678 }
2679
2680 fn build_csr(&self) -> MultiDiCsr {
2681 let n = self.nodes.len();
2682 let distinct_succ_edges: usize = self.successors.values().map(IndexMap::len).sum();
2683 let distinct_pred_edges: usize = self.predecessors.values().map(IndexMap::len).sum();
2684 let mut succ_offsets = Vec::with_capacity(n + 1);
2685 let mut pred_offsets = Vec::with_capacity(n + 1);
2686 let mut succ_targets = Vec::with_capacity(distinct_succ_edges);
2687 let mut pred_targets = Vec::with_capacity(distinct_pred_edges);
2688 succ_offsets.push(0);
2689 pred_offsets.push(0);
2690 let positions = self.nodes.positions();
2691 for node in self.nodes.keys() {
2692 if let Some(row) = self.successors.get(node.as_str()) {
2693 succ_targets.extend(row.keys().filter_map(|target| {
2694 positions
2695 .of(target.as_str())
2696 .map(|idx| u32::try_from(idx).unwrap_or(u32::MAX))
2697 }));
2698 }
2699 succ_offsets.push(succ_targets.len());
2700
2701 if let Some(row) = self.predecessors.get(node.as_str()) {
2702 pred_targets.extend(row.keys().filter_map(|source| {
2703 positions
2704 .of(source.as_str())
2705 .map(|idx| u32::try_from(idx).unwrap_or(u32::MAX))
2706 }));
2707 }
2708 pred_offsets.push(pred_targets.len());
2709 }
2710 MultiDiCsr {
2711 succ_offsets,
2712 succ_targets,
2713 pred_offsets,
2714 pred_targets,
2715 }
2716 }
2717
2718 #[must_use]
2719 pub fn has_node(&self, node: &str) -> bool {
2720 self.nodes.contains_key(node)
2721 }
2722
2723 #[must_use]
2724 pub fn has_edge(&self, source: &str, target: &str) -> bool {
2725 self.edges
2726 .get(&DirectedEdgeKeyRef::new(source, target))
2727 .is_some_and(|edge_bucket| !edge_bucket.is_empty())
2728 }
2729
2730 fn build_edge_pair_index(&self) -> EdgePairMap {
2738 let mut built =
2739 EdgePairMap::with_capacity_and_hasher(self.edges.len(), rustc_hash::FxBuildHasher);
2740 let positions = self.nodes.positions();
2741 for (position, edge_key) in self.edges.keys().enumerate() {
2742 if let Some(source_idx) = positions.of(edge_key.source.as_str())
2743 && let Some(target_idx) = positions.of(edge_key.target.as_str())
2744 {
2745 built.insert((source_idx, target_idx), position);
2746 }
2747 }
2748 built
2749 }
2750
2751 fn edge_pair_position(&self, source_idx: usize, target_idx: usize) -> Option<Option<usize>> {
2758 use std::sync::atomic::Ordering;
2759 if let Ok(guard) = self.edge_pair_index.map.read()
2760 && let Some((stamp, map)) = guard.as_ref()
2761 && *stamp == self.revision
2762 {
2763 return Some(map.get(&(source_idx, target_idx)).copied());
2764 }
2765 let revision = self.revision;
2766 if self.edge_pair_index.probed_revision.load(Ordering::Relaxed) != revision {
2767 self.edge_pair_index
2768 .probed_revision
2769 .store(revision, Ordering::Relaxed);
2770 self.edge_pair_index
2771 .probes_since_build
2772 .store(1, Ordering::Relaxed);
2773 return None;
2774 }
2775 let seen = self
2776 .edge_pair_index
2777 .probes_since_build
2778 .fetch_add(1, Ordering::Relaxed)
2779 + 1;
2780 if seen < pair_index_warmup(self.edges.len()) {
2781 return None;
2782 }
2783 let built = self.build_edge_pair_index();
2784 let found = built.get(&(source_idx, target_idx)).copied();
2785 if let Ok(mut guard) = self.edge_pair_index.map.write() {
2786 *guard = Some((revision, built));
2787 }
2788 Some(found)
2789 }
2790
2791 fn pair_index_note_edge_added(
2803 &mut self,
2804 previous_revision: u64,
2805 source: &str,
2806 target: &str,
2807 new_pair_position: Option<usize>,
2808 ) {
2809 match self.edge_pair_index.map.get_mut() {
2810 Ok(Some((stamp, _))) if *stamp == previous_revision => {}
2811 _ => return,
2812 }
2813 let resolved = match new_pair_position {
2814 Some(position) => match (
2815 self.nodes.get_index_of(source),
2816 self.nodes.get_index_of(target),
2817 ) {
2818 (Some(source_idx), Some(target_idx)) => Some(((source_idx, target_idx), position)),
2819 _ => {
2821 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2822 *slot = None;
2823 }
2824 return;
2825 }
2826 },
2827 None => None,
2828 };
2829 let current = self.revision;
2830 if let Ok(Some((stamp, map))) = self.edge_pair_index.map.get_mut() {
2831 if let Some((pair, position)) = resolved {
2832 map.insert(pair, position);
2833 }
2834 *stamp = current;
2835 }
2836 }
2837
2838 fn edge_pair_index_is_live(&self, at_revision: u64) -> bool {
2841 matches!(
2842 self.edge_pair_index.map.read().as_deref(),
2843 Ok(Some((stamp, _))) if *stamp == at_revision
2844 )
2845 }
2846
2847 fn pair_index_note_edge_removed(
2859 &mut self,
2860 previous_revision: u64,
2861 source: &str,
2862 target: &str,
2863 dropped_position: Option<usize>,
2864 ) {
2865 match self.edge_pair_index.map.get_mut() {
2866 Ok(Some((stamp, _))) if *stamp == previous_revision => {}
2867 _ => return,
2868 }
2869 let removed_pair = match dropped_position {
2870 Some(_) => match (
2871 self.nodes.get_index_of(source),
2872 self.nodes.get_index_of(target),
2873 ) {
2874 (Some(source_idx), Some(target_idx)) => Some((source_idx, target_idx)),
2875 _ => {
2876 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2877 *slot = None;
2878 }
2879 return;
2880 }
2881 },
2882 None => None,
2883 };
2884 let relocated = match dropped_position {
2886 Some(position) if position < self.edges.len() => match self.edges.get_index(position) {
2887 Some((moved_key, _)) => {
2888 let moved_source = moved_key.source.clone();
2889 let moved_target = moved_key.target.clone();
2890 match (
2891 self.nodes.get_index_of(moved_source.as_str()),
2892 self.nodes.get_index_of(moved_target.as_str()),
2893 ) {
2894 (Some(source_idx), Some(target_idx)) => {
2895 Some(((source_idx, target_idx), position))
2896 }
2897 _ => {
2898 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2899 *slot = None;
2900 }
2901 return;
2902 }
2903 }
2904 }
2905 None => None,
2906 },
2907 _ => None,
2908 };
2909 let current = self.revision;
2910 if let Ok(Some((stamp, map))) = self.edge_pair_index.map.get_mut() {
2911 if let Some(pair) = removed_pair {
2912 map.remove(&pair);
2913 }
2914 if let Some((pair, position)) = relocated {
2915 map.insert(pair, position);
2916 }
2917 *stamp = current;
2918 }
2919 }
2920
2921 #[must_use]
2931 pub fn has_edge_by_indices(&self, source_idx: usize, target_idx: usize) -> bool {
2932 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2933 return position.is_some();
2934 }
2935 match (
2936 self.get_node_name(source_idx),
2937 self.get_node_name(target_idx),
2938 ) {
2939 (Some(source), Some(target)) => self.has_edge(source, target),
2940 _ => false,
2941 }
2942 }
2943
2944 #[must_use]
2953 pub fn edge_key_count_by_indices(&self, source_idx: usize, target_idx: usize) -> usize {
2954 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2955 return position
2956 .and_then(|found| self.edges.get_index(found))
2957 .map_or(0, |(_, keyed)| keyed.len());
2958 }
2959 match (
2960 self.get_node_name(source_idx),
2961 self.get_node_name(target_idx),
2962 ) {
2963 (Some(source), Some(target)) => self
2964 .edges
2965 .get(&DirectedEdgeKeyRef::new(source, target))
2966 .map_or(0, IndexMap::len),
2967 _ => 0,
2968 }
2969 }
2970
2971 #[must_use]
2976 pub fn edge_attrs_by_indices(
2977 &self,
2978 source_idx: usize,
2979 target_idx: usize,
2980 key: usize,
2981 ) -> Option<&AttrMap> {
2982 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2983 let (_, keyed) = self.edges.get_index(position?)?;
2984 return keyed.get(&key);
2985 }
2986 let source = self.get_node_name(source_idx)?;
2987 let target = self.get_node_name(target_idx)?;
2988 self.edges
2989 .get(&DirectedEdgeKeyRef::new(source, target))
2990 .and_then(|bucket| bucket.get(&key))
2991 }
2992
2993 #[must_use]
2994 pub fn nodes_ordered(&self) -> Vec<&str> {
2995 self.nodes.keys().map(String::as_str).collect()
2996 }
2997
2998 #[must_use]
3000 #[inline]
3001 pub fn get_node_name(&self, index: usize) -> Option<&str> {
3002 self.nodes.get_index(index).map(|(name, _)| name.as_str())
3003 }
3004
3005 #[must_use]
3008 pub fn node_index_matches_int(&self, idx: usize) -> bool {
3009 self.nodes
3010 .get_index(idx)
3011 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
3012 }
3013
3014 #[must_use]
3015 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
3016 self.successors
3017 .get(node)
3018 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
3019 }
3020
3021 #[must_use]
3022 pub fn successors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
3023 self.successors
3024 .get(node)
3025 .map(|neighbors| neighbors.keys().map(String::as_str))
3026 }
3027
3028 #[must_use]
3029 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
3030 self.predecessors
3031 .get(node)
3032 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
3033 }
3034
3035 #[must_use]
3036 pub fn predecessors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
3037 self.predecessors
3038 .get(node)
3039 .map(|neighbors| neighbors.keys().map(String::as_str))
3040 }
3041
3042 #[must_use]
3043 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
3044 self.successors(node)
3045 }
3046
3047 #[must_use]
3048 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
3049 self.successors_iter(node)
3050 }
3051
3052 #[must_use]
3053 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
3054 self.nodes.get(node)
3055 }
3056
3057 #[must_use]
3058 pub fn edge_attrs(&self, source: &str, target: &str, key: usize) -> Option<&AttrMap> {
3059 self.edges
3060 .get(&DirectedEdgeKeyRef::new(source, target))
3061 .and_then(|edge_bucket| edge_bucket.get(&key))
3062 }
3063
3064 #[must_use]
3065 pub fn edge_attr_values(
3066 &self,
3067 source: &str,
3068 target: &str,
3069 ) -> Option<impl Iterator<Item = &AttrMap> + '_> {
3070 self.edges
3071 .get(&DirectedEdgeKeyRef::new(source, target))
3072 .map(|edge_bucket| edge_bucket.values())
3073 }
3074
3075 #[must_use]
3076 pub fn evidence_ledger(&self) -> &EvidenceLedger {
3077 self.runtime_policy.decision_log()
3078 }
3079
3080 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
3081 self.runtime_policy.drain_decision_records()
3082 }
3083
3084 pub fn record_external_recovery(
3087 &mut self,
3088 operation: impl Into<std::borrow::Cow<'static, str>>,
3089 rationale: impl Into<std::borrow::Cow<'static, str>>,
3090 ) {
3091 self.runtime_policy.record(
3092 operation,
3093 DecisionAction::FullValidate,
3094 0.7,
3095 rationale,
3096 Vec::new(),
3097 );
3098 }
3099
3100 #[must_use]
3101 pub fn runtime_policy(&self) -> &RuntimePolicy {
3102 &self.runtime_policy
3103 }
3104
3105 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
3106 self.mode = runtime_policy.mode();
3107 self.runtime_policy = runtime_policy;
3108 }
3109
3110 #[must_use]
3111 pub fn is_directed(&self) -> bool {
3112 true
3113 }
3114
3115 #[must_use]
3116 pub fn is_multigraph(&self) -> bool {
3117 true
3118 }
3119
3120 #[must_use]
3122 pub fn out_degree(&self, node: &str) -> usize {
3123 self.successors
3124 .get(node)
3125 .map_or(0, |succs| succs.values().map(IndexSet::len).sum())
3126 }
3127
3128 #[must_use]
3130 pub fn in_degree(&self, node: &str) -> usize {
3131 self.predecessors
3132 .get(node)
3133 .map_or(0, |preds| preds.values().map(IndexSet::len).sum())
3134 }
3135
3136 #[must_use]
3138 pub fn degree(&self, node: &str) -> usize {
3139 self.in_degree(node) + self.out_degree(node)
3140 }
3141
3142 #[must_use]
3150 pub fn isolates(&self) -> Vec<String> {
3151 self.nodes
3152 .keys()
3153 .filter(|node| self.is_isolate(node.as_str()))
3154 .cloned()
3155 .collect()
3156 }
3157
3158 #[must_use]
3160 pub fn number_of_isolates(&self) -> usize {
3161 self.nodes
3162 .keys()
3163 .filter(|node| self.is_isolate(node.as_str()))
3164 .count()
3165 }
3166
3167 #[must_use]
3170 pub fn is_isolate(&self, node: &str) -> bool {
3171 self.nodes.contains_key(node)
3172 && self.successors.get(node).is_none_or(IndexMap::is_empty)
3173 && self.predecessors.get(node).is_none_or(IndexMap::is_empty)
3174 }
3175
3176 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
3177 self.add_node_with_attrs(node, AttrMap::new())
3178 }
3179
3180 fn add_node_with_attrs_unrecorded(
3198 &mut self,
3199 node: impl AsRef<str> + Into<String>,
3200 attrs: AttrMap,
3201 ) -> (bool, bool, usize) {
3202 let existed = self.nodes.contains_key(node.as_ref());
3203 let mut changed = !existed;
3204 let attrs_count = {
3205 let bucket = if existed {
3206 self.nodes
3207 .get_mut(node.as_ref())
3208 .expect("contains_key reported this node present")
3209 } else {
3210 self.nodes.push(node.as_ref().to_owned(), AttrMap::new())
3211 };
3212 if !attrs.is_empty()
3213 && attrs
3214 .iter()
3215 .any(|(key, value)| bucket.get(key) != Some(value))
3216 {
3217 changed = true;
3218 }
3219 bucket.extend(attrs);
3220 bucket.len()
3221 };
3222 if !self.successors.contains_key(node.as_ref()) {
3223 self.successors
3224 .insert(node.as_ref().to_owned(), IndexMap::default());
3225 }
3226 if !self.predecessors.contains_key(node.as_ref()) {
3227 self.predecessors.insert(node.into(), IndexMap::default());
3228 }
3229 if changed {
3230 self.revision = self.revision.saturating_add(1);
3231 }
3232 (changed, existed, attrs_count)
3233 }
3234
3235 pub fn add_node_with_attrs(
3236 &mut self,
3237 node: impl AsRef<str> + Into<String>,
3238 attrs: AttrMap,
3239 ) -> bool {
3240 let (changed, existed, attrs_count) = self.add_node_with_attrs_unrecorded(node, attrs);
3241 self.record_decision(
3242 "add_node",
3243 0.0,
3244 false,
3245 vec![
3246 EvidenceTerm {
3247 signal: "node_preexisting".into(),
3248 observed_value: bool_evidence(existed),
3249 log_likelihood_ratio: -3.0,
3250 },
3251 EvidenceTerm {
3252 signal: "attrs_count".into(),
3253 observed_value: count_evidence(attrs_count),
3254 log_likelihood_ratio: -1.0,
3255 },
3256 ],
3257 );
3258 changed
3259 }
3260
3261 pub fn add_edge(
3262 &mut self,
3263 source: impl Into<String>,
3264 target: impl Into<String>,
3265 ) -> Result<usize, GraphError> {
3266 self.add_edge_impl(source, target, None, AttrMap::new())
3267 }
3268
3269 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
3273 where
3274 I: IntoIterator<Item = (String, AttrMap)>,
3275 {
3276 let mut inserted = 0usize;
3277 for (node, attrs) in nodes {
3278 if let Some(existing) = self.nodes.get_mut(&node) {
3279 existing.extend(attrs);
3280 continue;
3281 }
3282 self.nodes.insert(node.clone(), attrs);
3283 self.successors.entry(node.clone()).or_default();
3284 self.predecessors.entry(node).or_default();
3285 inserted += 1;
3286 }
3287 if inserted > 0 {
3288 self.revision = self
3289 .revision
3290 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
3291 self.record_decision(
3292 "extend_nodes_with_attrs_unrecorded",
3293 0.0,
3294 false,
3295 vec![EvidenceTerm {
3296 signal: "batch_node_count".into(),
3297 observed_value: count_evidence(inserted),
3298 log_likelihood_ratio: -1.0,
3299 }],
3300 );
3301 }
3302 inserted
3303 }
3304
3305 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
3307 where
3308 I: IntoIterator<Item = N>,
3309 N: Into<String>,
3310 {
3311 let iterator = nodes.into_iter();
3312 let (lower_bound, _) = iterator.size_hint();
3313 self.nodes.reserve(lower_bound);
3314 self.successors.reserve(lower_bound);
3315 self.predecessors.reserve(lower_bound);
3316
3317 let mut inserted = 0usize;
3318 for node in iterator {
3319 let node = node.into();
3320 if self.nodes.contains_key(&node) {
3321 continue;
3322 }
3323 self.nodes.insert(node.clone(), AttrMap::new());
3324 self.successors.entry(node.clone()).or_default();
3325 self.predecessors.entry(node).or_default();
3326 inserted += 1;
3327 }
3328 if inserted > 0 {
3329 self.revision = self
3330 .revision
3331 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
3332 self.record_decision(
3333 "extend_nodes_unrecorded",
3334 0.0,
3335 false,
3336 vec![EvidenceTerm {
3337 signal: "batch_node_count".into(),
3338 observed_value: count_evidence(inserted),
3339 log_likelihood_ratio: -1.0,
3340 }],
3341 );
3342 }
3343 inserted
3344 }
3345
3346 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
3353 where
3354 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
3355 {
3356 let mut inserted = 0usize;
3357 for (source, target, key, attrs) in edges {
3358 if !self.nodes.contains_key(&source) {
3359 self.nodes.insert(source.clone(), AttrMap::new());
3360 self.successors.entry(source.clone()).or_default();
3361 self.predecessors.entry(source.clone()).or_default();
3362 }
3363 if source != target && !self.nodes.contains_key(&target) {
3364 self.nodes.insert(target.clone(), AttrMap::new());
3365 self.successors.entry(target.clone()).or_default();
3366 self.predecessors.entry(target.clone()).or_default();
3367 }
3368 let edge_key = DirectedEdgeKey::new(&source, &target);
3369 let bucket = self.edges.entry(edge_key).or_default();
3370 if !bucket.contains_key(&key) {
3371 self.edge_count += 1;
3372 inserted += 1;
3373 }
3374 bucket.entry(key).or_default().extend(attrs);
3375 self.successors
3376 .entry(source.clone())
3377 .or_default()
3378 .entry(target.clone())
3379 .or_default()
3380 .insert(key);
3381 self.predecessors
3382 .entry(target)
3383 .or_default()
3384 .entry(source)
3385 .or_default()
3386 .insert(key);
3387 }
3388 if inserted > 0 {
3389 self.revision = self
3390 .revision
3391 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
3392 self.record_decision(
3393 "extend_keyed_edges_with_attrs_unrecorded",
3394 0.0,
3395 false,
3396 vec![EvidenceTerm {
3397 signal: "batch_edge_count".into(),
3398 observed_value: count_evidence(inserted),
3399 log_likelihood_ratio: -1.0,
3400 }],
3401 );
3402 }
3403 inserted
3404 }
3405
3406 #[must_use]
3411 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
3412 &mut self,
3413 nodes: N,
3414 edges: I,
3415 ) -> usize
3416 where
3417 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
3418 N: IntoIterator<Item = String>,
3419 {
3420 if !self.nodes.is_empty()
3421 || !self.successors.is_empty()
3422 || !self.predecessors.is_empty()
3423 || !self.edges.is_empty()
3424 {
3425 return 0;
3426 }
3427
3428 let node_labels: Vec<String> = nodes.into_iter().collect();
3429 for node in &node_labels {
3430 self.nodes.insert(node.clone(), AttrMap::new());
3431 self.successors
3432 .insert(node.clone(), crate::FxIndexMap::default());
3433 self.predecessors
3434 .insert(node.clone(), crate::FxIndexMap::default());
3435 }
3436
3437 let node_count = node_labels.len();
3438 let mut inserted = 0usize;
3439 let mut merged_changed = false;
3440 for (source_idx, target_idx, key, attrs) in edges {
3441 let Some(source) = node_labels.get(source_idx) else {
3442 continue;
3443 };
3444 let Some(target) = node_labels.get(target_idx) else {
3445 continue;
3446 };
3447 let edge_key = DirectedEdgeKey::new(source, target);
3448 let bucket = self.edges.entry(edge_key).or_default();
3449 if !bucket.contains_key(&key) {
3450 self.edge_count += 1;
3451 inserted += 1;
3452 }
3453 let edge_attrs = bucket.entry(key).or_default();
3454 if !attrs.is_empty()
3455 && attrs
3456 .iter()
3457 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
3458 {
3459 merged_changed = true;
3460 }
3461 edge_attrs.extend(attrs);
3462 self.successors
3463 .get_mut(source.as_str())
3464 .expect("fresh node row exists")
3465 .entry(target.clone())
3466 .or_default()
3467 .insert(key);
3468 self.predecessors
3469 .get_mut(target.as_str())
3470 .expect("fresh node row exists")
3471 .entry(source.clone())
3472 .or_default()
3473 .insert(key);
3474 }
3475
3476 if node_count > 0 || inserted > 0 || merged_changed {
3477 self.revision = self.revision.saturating_add(
3478 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
3479 );
3480 self.record_decision(
3481 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
3482 0.0,
3483 false,
3484 vec![
3485 EvidenceTerm {
3486 signal: "batch_node_count".into(),
3487 observed_value: count_evidence(node_count),
3488 log_likelihood_ratio: -1.0,
3489 },
3490 EvidenceTerm {
3491 signal: "batch_edge_count".into(),
3492 observed_value: count_evidence(inserted),
3493 log_likelihood_ratio: -1.0,
3494 },
3495 ],
3496 );
3497 }
3498
3499 inserted
3500 }
3501
3502 pub fn add_edge_with_attrs(
3503 &mut self,
3504 source: impl Into<String>,
3505 target: impl Into<String>,
3506 attrs: AttrMap,
3507 ) -> Result<usize, GraphError> {
3508 self.add_edge_impl(source, target, None, attrs)
3509 }
3510
3511 pub fn add_edge_with_key_and_attrs(
3512 &mut self,
3513 source: impl Into<String>,
3514 target: impl Into<String>,
3515 key: usize,
3516 attrs: AttrMap,
3517 ) -> Result<usize, GraphError> {
3518 self.add_edge_impl(source, target, Some(key), attrs)
3519 }
3520
3521 fn add_edge_impl(
3522 &mut self,
3523 source: impl Into<String>,
3524 target: impl Into<String>,
3525 explicit_key: Option<usize>,
3526 attrs: AttrMap,
3527 ) -> Result<usize, GraphError> {
3528 let source = source.into();
3529 let target = target.into();
3530 let previous_revision = self.revision;
3537
3538 let unknown_feature = attrs
3539 .keys()
3540 .any(|key| key.starts_with("__fnx_incompatible"));
3541 let self_loop = source == target;
3542 let incompatibility_probability = if unknown_feature {
3543 1.0
3544 } else if self_loop {
3545 0.22
3546 } else {
3547 0.08
3548 };
3549
3550 let action = self
3558 .runtime_policy
3559 .action_for(incompatibility_probability, unknown_feature);
3560
3561 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
3562 self.record_decision(
3563 "add_edge",
3564 incompatibility_probability,
3565 unknown_feature,
3566 vec![EvidenceTerm {
3567 signal: "unknown_incompatible_feature".into(),
3568 observed_value: bool_evidence(unknown_feature),
3569 log_likelihood_ratio: 12.0,
3570 }],
3571 );
3572 return Err(GraphError::FailClosed {
3573 operation: "add_edge",
3574 reason: "incompatible edge metadata".to_owned(),
3575 });
3576 }
3577
3578 let mut source_autocreated = false;
3581 if !self.nodes.contains_key(&source) {
3582 let _ = self.add_node_with_attrs_unrecorded(source.clone(), AttrMap::new());
3583 source_autocreated = true;
3584 }
3585 let mut target_autocreated = false;
3586 if self_loop {
3587 target_autocreated = source_autocreated;
3588 } else if !self.nodes.contains_key(&target) {
3589 let _ = self.add_node_with_attrs_unrecorded(target.clone(), AttrMap::new());
3590 target_autocreated = true;
3591 }
3592
3593 let edge_key = DirectedEdgeKey::new(&source, &target);
3594 let key = explicit_key.unwrap_or_else(|| {
3595 let edge_bucket = self.edges.get(&edge_key);
3596 let mut k = edge_bucket.map_or(0, |b| b.len());
3597 if let Some(b) = edge_bucket {
3598 while b.contains_key(&k) {
3599 k += 1;
3600 }
3601 }
3602 k
3603 });
3604 let edges_len_before = self.edges.len();
3608 let mut changed;
3609 let edge_attr_count = {
3610 let edge_bucket = self.edges.entry(edge_key.clone()).or_default();
3611 let is_new = !edge_bucket.contains_key(&key);
3612 if is_new {
3613 self.edge_count += 1;
3614 }
3615 changed = is_new;
3616 let edge_attrs = edge_bucket.entry(key).or_default();
3617 if edge_attrs.is_empty() {
3618 if !attrs.is_empty() {
3623 changed = true;
3624 }
3625 *edge_attrs = attrs;
3626 } else {
3627 if !attrs.is_empty()
3628 && attrs
3629 .iter()
3630 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
3631 {
3632 changed = true;
3633 }
3634 edge_attrs.extend(attrs);
3635 }
3636 edge_bucket.len()
3637 };
3638
3639 self.successors
3640 .entry(source.clone())
3641 .or_default()
3642 .entry(target.clone())
3643 .or_default()
3644 .insert(key);
3645 self.predecessors
3646 .entry(target.clone())
3647 .or_default()
3648 .entry(source.clone())
3649 .or_default()
3650 .insert(key);
3651 if changed {
3652 self.revision = self.revision.saturating_add(1);
3653 }
3654 let new_pair_position = if self.edges.len() > edges_len_before {
3657 Some(self.edges.len() - 1)
3658 } else {
3659 None
3660 };
3661 self.pair_index_note_edge_added(previous_revision, &source, &target, new_pair_position);
3662
3663 self.record_decision(
3664 "add_edge",
3665 incompatibility_probability,
3666 unknown_feature,
3667 vec![
3668 EvidenceTerm {
3669 signal: "edge_key".into(),
3670 observed_value: key.to_string().into(),
3671 log_likelihood_ratio: -2.0,
3672 },
3673 EvidenceTerm {
3674 signal: "edge_attr_count".into(),
3675 observed_value: count_evidence(edge_attr_count),
3676 log_likelihood_ratio: -2.0,
3677 },
3678 EvidenceTerm {
3679 signal: "source_autocreated".into(),
3680 observed_value: bool_evidence(source_autocreated),
3681 log_likelihood_ratio: -1.25,
3682 },
3683 EvidenceTerm {
3684 signal: "target_autocreated".into(),
3685 observed_value: bool_evidence(target_autocreated),
3686 log_likelihood_ratio: -1.25,
3687 },
3688 ],
3689 );
3690
3691 Ok(key)
3692 }
3693
3694 pub fn replace_edge_attrs(
3697 &mut self,
3698 source: &str,
3699 target: &str,
3700 key: usize,
3701 attrs: AttrMap,
3702 ) -> bool {
3703 let edge_key = DirectedEdgeKey::new(source, target);
3704 if let Some(bucket) = self.edges.get_mut(&edge_key)
3705 && let Some(slot) = bucket.get_mut(&key)
3706 {
3707 if *slot != attrs {
3708 *slot = attrs;
3709 self.revision = self.revision.saturating_add(1);
3710 }
3711 return true;
3712 }
3713 false
3714 }
3715
3716 pub fn set_edge_attr_value(
3720 &mut self,
3721 source: &str,
3722 target: &str,
3723 key: usize,
3724 attr_key: &str,
3725 value: CgseValue,
3726 ) -> bool {
3727 let edge_key = DirectedEdgeKey::new(source, target);
3728 if let Some(bucket) = self.edges.get_mut(&edge_key)
3729 && let Some(attrs) = bucket.get_mut(&key)
3730 {
3731 if attrs.get(attr_key) != Some(&value) {
3732 attrs.insert(attr_key.to_owned(), value);
3733 self.revision = self.revision.saturating_add(1);
3734 }
3735 return true;
3736 }
3737 false
3738 }
3739
3740 pub fn set_ordered_edge_attr_values<I>(&mut self, attr_key: &str, values: I) -> bool
3743 where
3744 I: IntoIterator<Item = CgseValue>,
3745 {
3746 let mut values = values.into_iter();
3747 let mut changed = false;
3748 let mut visited = 0usize;
3749
3750 for node in self.nodes.keys() {
3751 if let Some(neighbors) = self.successors.get(node) {
3752 for target in neighbors.keys() {
3753 let pair = DirectedEdgeKeyRef::new(node, target);
3754 if let Some(edge_bucket) = self.edges.get_mut(&pair) {
3755 for attrs in edge_bucket.values_mut() {
3756 let Some(value) = values.next() else {
3757 return false;
3758 };
3759 if attrs.get(attr_key) != Some(&value) {
3760 attrs.insert(attr_key.to_owned(), value);
3761 changed = true;
3762 }
3763 visited = visited.saturating_add(1);
3764 }
3765 }
3766 }
3767 }
3768 }
3769
3770 if values.next().is_some() || visited != self.edge_count {
3771 return false;
3772 }
3773 if changed {
3774 self.revision = self.revision.saturating_add(1);
3775 }
3776 true
3777 }
3778
3779 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
3781 if let Some(slot) = self.nodes.get_mut(node) {
3782 if *slot != attrs {
3783 *slot = attrs;
3784 self.revision = self.revision.saturating_add(1);
3785 }
3786 true
3787 } else {
3788 false
3789 }
3790 }
3791
3792 pub fn remove_edge(&mut self, source: &str, target: &str, key: Option<usize>) -> bool {
3793 let previous_revision = self.revision;
3795 let edge_key = DirectedEdgeKeyRef::new(source, target);
3796 let removal_key = key.or_else(|| {
3797 self.edges
3798 .get(&edge_key)
3799 .and_then(|edge_bucket| edge_bucket.keys().next_back().copied())
3800 });
3801
3802 let Some(removal_key) = removal_key else {
3803 return false;
3804 };
3805
3806 let removed = self
3807 .edges
3808 .get_mut(&edge_key)
3809 .is_some_and(|edge_bucket| edge_bucket.shift_remove(&removal_key).is_some());
3810 if !removed {
3811 return false;
3812 }
3813
3814 self.edge_count -= 1;
3815
3816 let should_drop_bucket = self.edges.get(&edge_key).is_some_and(IndexMap::is_empty);
3817 let dropped_position =
3820 if should_drop_bucket && self.edge_pair_index_is_live(previous_revision) {
3821 self.edges.get_index_of(&edge_key)
3822 } else {
3823 None
3824 };
3825 if should_drop_bucket {
3826 self.edges.swap_remove(&edge_key);
3833 }
3834
3835 self.remove_successor_key(source, target, removal_key);
3836 self.remove_predecessor_key(target, source, removal_key);
3837 self.revision = self.revision.saturating_add(1);
3838 self.pair_index_note_edge_removed(previous_revision, source, target, dropped_position);
3839 true
3840 }
3841
3842 pub fn clear_edges(&mut self) {
3843 if self.edge_count == 0 {
3844 return;
3845 }
3846
3847 self.edges.clear();
3848 for row in self.successors.values_mut() {
3849 row.clear();
3850 }
3851 for row in self.predecessors.values_mut() {
3852 row.clear();
3853 }
3854 self.edge_count = 0;
3855 self.revision = self.revision.saturating_add(1);
3856 }
3857
3858 pub fn remove_node(&mut self, node: &str) -> bool {
3859 if !self.nodes.contains_key(node) {
3860 return false;
3861 }
3862
3863 let mut removed_count = 0usize;
3877 let succs = self.successors.remove(node).unwrap_or_default();
3878 let preds = self.predecessors.remove(node).unwrap_or_default();
3879 for target in succs.keys() {
3880 if target != node
3881 && let Some(row) = self.predecessors.get_mut(target.as_str())
3882 {
3883 row.shift_remove(node);
3884 }
3885 if let Some(bucket) = self
3886 .edges
3887 .swap_remove(&DirectedEdgeKeyRef::new(node, target))
3888 {
3889 removed_count += bucket.len();
3890 }
3891 }
3892 for source in preds.keys() {
3893 if source != node
3894 && let Some(row) = self.successors.get_mut(source.as_str())
3895 {
3896 row.shift_remove(node);
3897 }
3898 if let Some(bucket) = self
3899 .edges
3900 .swap_remove(&DirectedEdgeKeyRef::new(source, node))
3901 {
3902 removed_count += bucket.len();
3903 }
3904 }
3905 self.edge_count -= removed_count;
3906 self.nodes.remove(node);
3907 self.revision = self.revision.saturating_add(1);
3908 true
3909 }
3910
3911 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
3917 where
3918 I: IntoIterator<Item = &'a str>,
3919 {
3920 let remove_set: rustc_hash::FxHashSet<&str> = nodes
3921 .into_iter()
3922 .filter(|node| self.nodes.contains_key(node))
3923 .collect();
3924 if remove_set.is_empty() {
3925 return (0, 0);
3926 }
3927
3928 let old_node_count = self.nodes.len();
3929 let old_edge_count = self.edge_count;
3930
3931 let mut removed_instances = 0usize;
3939 if remove_set.len().saturating_mul(4) <= old_node_count {
3940 for &rn in &remove_set {
3941 let targets: Vec<String> = match self.successors.get(rn) {
3943 Some(row) => row.keys().cloned().collect(),
3944 None => Vec::new(),
3945 };
3946 for t in &targets {
3947 if t.as_str() != rn
3948 && !remove_set.contains(t.as_str())
3949 && let Some(preds) = self.predecessors.get_mut(t)
3950 {
3951 preds.shift_remove(rn);
3952 }
3953 if let Some(bucket) = self.edges.swap_remove(&DirectedEdgeKeyRef::new(rn, t)) {
3954 removed_instances += bucket.len();
3955 }
3956 }
3957 let sources: Vec<String> = match self.predecessors.get(rn) {
3959 Some(row) => row.keys().cloned().collect(),
3960 None => Vec::new(),
3961 };
3962 for s in &sources {
3963 if s.as_str() != rn
3964 && !remove_set.contains(s.as_str())
3965 && let Some(succs) = self.successors.get_mut(s)
3966 {
3967 succs.shift_remove(rn);
3968 }
3969 if let Some(bucket) = self.edges.swap_remove(&DirectedEdgeKeyRef::new(s, rn)) {
3972 removed_instances += bucket.len();
3973 }
3974 }
3975 }
3976 self.edge_count -= removed_instances;
3977 for &rn in &remove_set {
3978 self.successors.remove(rn);
3979 self.predecessors.remove(rn);
3980 self.nodes.remove(rn);
3981 }
3982 let removed_nodes = old_node_count - self.nodes.len();
3983 let removed_edges = old_edge_count - self.edge_count;
3984 self.revision = self
3985 .revision
3986 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
3987 return (removed_nodes, removed_edges);
3988 }
3989
3990 self.edges.retain(|key, bucket| {
3996 let keep = !remove_set.contains(key.source.as_str())
3997 && !remove_set.contains(key.target.as_str());
3998 if !keep {
3999 removed_instances += bucket.len();
4000 }
4001 keep
4002 });
4003 self.edge_count -= removed_instances;
4004
4005 self.successors
4009 .retain(|node, _| !remove_set.contains(node.as_str()));
4010 for row in self.successors.values_mut() {
4011 row.retain(|target, _| !remove_set.contains(target.as_str()));
4012 }
4013 self.predecessors
4014 .retain(|node, _| !remove_set.contains(node.as_str()));
4015 for row in self.predecessors.values_mut() {
4016 row.retain(|source, _| !remove_set.contains(source.as_str()));
4017 }
4018 for &rn in &remove_set {
4019 self.nodes.remove(rn);
4020 }
4021
4022 let removed_nodes = old_node_count - self.nodes.len();
4023 let removed_edges = old_edge_count - self.edge_count;
4024 self.revision = self
4025 .revision
4026 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
4027 (removed_nodes, removed_edges)
4028 }
4029
4030 #[must_use]
4031 pub fn edges_ordered(&self) -> Vec<MultiDiEdgeSnapshot> {
4032 let mut ordered = Vec::with_capacity(self.edge_count());
4033
4034 for node in self.nodes.keys() {
4035 if let Some(neighbors) = self.successors.get(node) {
4036 for target in neighbors.keys() {
4037 let pair = DirectedEdgeKey::new(node, target);
4038 if let Some(edge_bucket) = self.edges.get(&pair) {
4039 for (key, attrs) in edge_bucket {
4040 ordered.push(MultiDiEdgeSnapshot {
4041 source: node.clone(),
4042 target: target.clone(),
4043 key: *key,
4044 attrs: attrs.clone(),
4045 });
4046 }
4047 }
4048 }
4049 }
4050 }
4051
4052 ordered
4053 }
4054
4055 #[must_use]
4056 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
4057 let mut ordered = Vec::with_capacity(self.edge_count());
4058
4059 for node in self.nodes.keys() {
4060 if let Some(neighbors) = self.successors.get(node) {
4061 for target in neighbors.keys() {
4062 let pair = DirectedEdgeKeyRef::new(node, target);
4063 if let Some(edge_bucket) = self.edges.get(&pair) {
4064 for (key, attrs) in edge_bucket {
4065 ordered.push((node.as_str(), target.as_str(), *key, attrs));
4066 }
4067 }
4068 }
4069 }
4070 }
4071
4072 ordered
4073 }
4074
4075 #[must_use]
4081 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
4082 let mut total: i128 = 0;
4083 for bucket in self.edges.values() {
4084 for attrs in bucket.values() {
4085 let value = match attrs.get(weight) {
4086 Some(CgseValue::Int(v)) => i128::from(*v),
4087 Some(_) => return None,
4088 None => 1,
4089 };
4090 total = total.checked_add(value)?;
4091 }
4092 }
4093 Some(total)
4094 }
4095
4096 pub fn try_for_each_edge_ordered_borrowed<E>(
4097 &self,
4098 mut visit: impl FnMut(&str, &str, usize, &AttrMap) -> Result<(), E>,
4099 ) -> Result<(), E> {
4100 for node in self.nodes.keys() {
4101 if let Some(neighbors) = self.successors.get(node) {
4102 for target in neighbors.keys() {
4103 let pair = DirectedEdgeKeyRef::new(node, target);
4104 if let Some(edge_bucket) = self.edges.get(&pair) {
4105 for (key, attrs) in edge_bucket {
4106 visit(node.as_str(), target.as_str(), *key, attrs)?;
4107 }
4108 }
4109 }
4110 }
4111 }
4112
4113 Ok(())
4114 }
4115
4116 pub fn try_for_each_indexed_edge_ordered_borrowed<E>(
4117 &self,
4118 mut visit: impl FnMut(usize, usize, &str, &str, usize, &AttrMap) -> Result<(), E>,
4119 ) -> Result<(), E> {
4120 let positions = self.nodes.positions();
4121 for (source_index, node) in self.nodes.keys().enumerate() {
4122 if let Some(neighbors) = self.successors.get(node) {
4123 for target in neighbors.keys() {
4124 let Some(target_index) = positions.of(target.as_str()) else {
4125 continue;
4126 };
4127 let pair = DirectedEdgeKeyRef::new(node, target);
4128 if let Some(edge_bucket) = self.edges.get(&pair) {
4129 for (key, attrs) in edge_bucket {
4130 visit(
4131 source_index,
4132 target_index,
4133 node.as_str(),
4134 target.as_str(),
4135 *key,
4136 attrs,
4137 )?;
4138 }
4139 }
4140 }
4141 }
4142 }
4143
4144 Ok(())
4145 }
4146
4147 #[must_use]
4158 pub fn reversed(&self) -> Self {
4159 let mut successors =
4160 MultiDiRows::with_capacity_and_hasher(self.nodes.len(), rustc_hash::FxBuildHasher);
4161 let mut predecessors =
4162 MultiDiRows::with_capacity_and_hasher(self.nodes.len(), rustc_hash::FxBuildHasher);
4163 for node in self.nodes.keys() {
4164 successors.insert(node.clone(), crate::FxIndexMap::default());
4165 predecessors.insert(node.clone(), crate::FxIndexMap::default());
4166 }
4167
4168 let mut edges: crate::FxIndexMap<DirectedEdgeKey, crate::FxIndexMap<usize, AttrMap>> =
4169 crate::FxIndexMap::with_capacity_and_hasher(
4170 self.edges.len(),
4171 rustc_hash::FxBuildHasher,
4172 );
4173 let mut edge_count = 0usize;
4174 for source in self.nodes.keys() {
4175 let Some(neighbors) = self.successors.get(source) else {
4176 continue;
4177 };
4178 for target in neighbors.keys() {
4179 let pair = DirectedEdgeKeyRef::new(source, target);
4180 let Some(edge_bucket) = self.edges.get(&pair) else {
4181 continue;
4182 };
4183 let reversed_pair = DirectedEdgeKey::new(target, source);
4184 let reversed_bucket = edges.entry(reversed_pair).or_default();
4185 let reversed_succ_keys = successors
4186 .get_mut(target.as_str())
4187 .expect("target row exists")
4188 .entry(source.clone())
4189 .or_default();
4190 let reversed_pred_keys = predecessors
4191 .get_mut(source.as_str())
4192 .expect("source row exists")
4193 .entry(target.clone())
4194 .or_default();
4195 for (key, attrs) in edge_bucket {
4196 reversed_bucket.insert(*key, attrs.clone());
4197 reversed_succ_keys.insert(*key);
4198 reversed_pred_keys.insert(*key);
4199 edge_count += 1;
4200 }
4201 }
4202 }
4203
4204 Self {
4205 mode: self.mode,
4206 revision: 0,
4207 nodes: self.nodes.clone(),
4208 successors,
4209 predecessors,
4210 edges,
4211 runtime_policy: self.runtime_policy.clone(),
4212 edge_count,
4213 csr_cache: std::sync::Arc::default(),
4214 edge_pair_index: EdgePairIndex::default(),
4215 }
4216 }
4217
4218 #[must_use]
4226 pub fn out_edges_ordered_borrowed<'a>(
4227 &'a self,
4228 node: &str,
4229 ) -> Vec<(&'a str, &'a str, usize, &'a AttrMap)> {
4230 let mut ordered = Vec::new();
4231 if let Some((src, neighbors)) = self.successors.get_key_value(node) {
4232 for target in neighbors.keys() {
4233 let pair = DirectedEdgeKeyRef::new(src, target);
4234 if let Some(edge_bucket) = self.edges.get(&pair) {
4235 for (key, attrs) in edge_bucket {
4236 ordered.push((src.as_str(), target.as_str(), *key, attrs));
4237 }
4238 }
4239 }
4240 }
4241 ordered
4242 }
4243
4244 #[must_use]
4245 pub fn snapshot(&self) -> MultiDiGraphSnapshot {
4246 let node_attrs: std::collections::BTreeMap<String, AttrMap> = self
4247 .nodes
4248 .iter()
4249 .filter(|(_, attrs)| !attrs.is_empty())
4250 .map(|(name, attrs)| (name.clone(), attrs.clone()))
4251 .collect();
4252 MultiDiGraphSnapshot {
4253 mode: self.mode,
4254 nodes: self.nodes.keys().cloned().collect(),
4255 node_attrs,
4256 edges: self.edges_ordered(),
4257 }
4258 }
4259
4260 fn remove_successor_key(&mut self, source: &str, target: &str, key: usize) {
4261 let mut drop_neighbor = false;
4262 if let Some(neighbors) = self.successors.get_mut(source)
4263 && let Some(keys) = neighbors.get_mut(target)
4264 {
4265 keys.shift_remove(&key);
4266 drop_neighbor = keys.is_empty();
4267 }
4268 if drop_neighbor && let Some(neighbors) = self.successors.get_mut(source) {
4269 neighbors.shift_remove(target);
4270 }
4271 }
4272
4273 fn remove_predecessor_key(&mut self, target: &str, source: &str, key: usize) {
4274 let mut drop_neighbor = false;
4275 if let Some(neighbors) = self.predecessors.get_mut(target)
4276 && let Some(keys) = neighbors.get_mut(source)
4277 {
4278 keys.shift_remove(&key);
4279 drop_neighbor = keys.is_empty();
4280 }
4281 if drop_neighbor && let Some(neighbors) = self.predecessors.get_mut(target) {
4282 neighbors.shift_remove(source);
4283 }
4284 }
4285
4286 fn record_decision(
4287 &mut self,
4288 operation: &'static str,
4289 incompatibility_probability: f64,
4290 unknown_incompatible_feature: bool,
4291 evidence: Vec<EvidenceTerm>,
4292 ) -> DecisionAction {
4293 let action = self
4294 .runtime_policy
4295 .action_for(incompatibility_probability, unknown_incompatible_feature);
4296 self.runtime_policy.record(
4297 operation,
4298 action,
4299 incompatibility_probability,
4300 "argmin expected loss over {allow,full_validate,fail_closed}",
4301 evidence,
4302 );
4303 action
4304 }
4305}
4306
4307#[cfg(test)]
4312mod tests {
4313 use super::*;
4314 use fnx_runtime::{
4315 CgseValue, CompatibilityMode, DecisionAction, DecisionRecord, RuntimePolicy,
4316 };
4317 use proptest::prelude::*;
4318
4319 fn node_name(id: u8) -> String {
4320 format!("n{}", id % 8)
4321 }
4322
4323 fn single_attr(key: &str, value: &str) -> AttrMap {
4324 let mut attrs = AttrMap::new();
4325 attrs.insert(key.to_owned(), CgseValue::from(value));
4326 attrs
4327 }
4328
4329 fn assert_runtime_policy_preserved(source: &RuntimePolicy, result: &RuntimePolicy) {
4330 assert_eq!(result.mode(), source.mode());
4331 assert_eq!(result.allowlist(), source.allowlist());
4332 assert_eq!(result.loss_matrix(), source.loss_matrix());
4333 assert!(result.posterior().observation_count >= source.posterior().observation_count);
4334 assert!(
4335 result
4336 .decision_log()
4337 .records()
4338 .starts_with(source.decision_log().records())
4339 );
4340 }
4341
4342 fn assert_digraph_core_invariants(g: &DiGraph) {
4345 for (&(source_idx, target_idx), _attrs) in &g.edges {
4347 let source = g
4348 .node_order
4349 .name(source_idx)
4350 .expect("edge source index should be a node");
4351 let target = g
4352 .node_order
4353 .name(target_idx)
4354 .expect("edge target index should be a node");
4355 assert!(
4356 g.has_node(source),
4357 "edge source {} should be a node",
4358 source
4359 );
4360 assert!(
4361 g.has_node(target),
4362 "edge target {} should be a node",
4363 target
4364 );
4365 let succs = g
4366 .successors(source)
4367 .expect("source should have successors bucket");
4368 assert!(
4369 succs.contains(&target),
4370 "{} should be in successors of {}",
4371 target,
4372 source
4373 );
4374 let preds = g
4375 .predecessors(target)
4376 .expect("target should have predecessors bucket");
4377 assert!(
4378 preds.contains(&source),
4379 "{} should be in predecessors of {}",
4380 source,
4381 target
4382 );
4383 }
4384
4385 let mut edge_count_from_adj = 0usize;
4387 for node in g.nodes_ordered() {
4388 let succs = g
4389 .successors(node)
4390 .expect("node should have successors bucket");
4391 for s in &succs {
4392 assert!(
4393 g.has_edge(node, s),
4394 "successor {} of {} should have directed edge",
4395 s,
4396 node
4397 );
4398 edge_count_from_adj += 1;
4399 }
4400 let preds = g
4402 .predecessors(node)
4403 .expect("node should have predecessors bucket");
4404 for p in &preds {
4405 assert!(
4406 g.has_edge(p, node),
4407 "predecessor {} of {} should have directed edge",
4408 p,
4409 node
4410 );
4411 }
4412 }
4413 assert_eq!(g.edge_count(), edge_count_from_adj);
4414 }
4415
4416 #[test]
4417 fn reorder_rows_like_restores_pred_rows_and_swaps_for_a_reverse() {
4418 let mut source = DiGraph::strict();
4420 for (u, v) in [("a", "x"), ("c", "x"), ("b", "x"), ("x", "b"), ("x", "a")] {
4421 let _ = source.add_edge(u, v);
4422 }
4423 let mut copy = DiGraph::strict();
4426 for node in ["x", "a", "b", "c"] {
4427 let _ = copy.add_node(node);
4428 }
4429 for (u, v) in [("x", "b"), ("x", "a"), ("a", "x"), ("b", "x"), ("c", "x")] {
4430 let _ = copy.add_edge(u, v);
4431 }
4432 assert_eq!(copy.predecessors("x"), Some(vec!["a", "b", "c"]));
4433 let _ = copy.csr();
4434 let before = copy.revision();
4435 copy.reorder_rows_like(&source, false);
4436 assert_eq!(copy.predecessors("x"), Some(vec!["a", "c", "b"]));
4437 assert_eq!(copy.successors("x"), Some(vec!["b", "a"]));
4438 assert!(copy.revision() > before);
4439 let csr = copy.csr();
4441 let x = copy.get_node_index("x").unwrap();
4442 let names: Vec<&str> = csr
4443 .predecessors(x)
4444 .iter()
4445 .map(|&i| copy.get_node_name(i as usize).unwrap())
4446 .collect();
4447 assert_eq!(names, vec!["a", "c", "b"]);
4448
4449 let mut reverse = DiGraph::strict();
4452 for node in ["x", "a", "b", "c"] {
4453 let _ = reverse.add_node(node);
4454 }
4455 for (u, v) in [("x", "a"), ("x", "c"), ("x", "b"), ("a", "x"), ("b", "x")] {
4456 let _ = reverse.add_edge(u, v);
4457 }
4458 assert_eq!(reverse.predecessors("x"), Some(vec!["a", "b"]));
4459 reverse.reorder_rows_like(&source, true);
4460 assert_eq!(reverse.predecessors("x"), Some(vec!["b", "a"]));
4461 assert_eq!(reverse.successors("x"), Some(vec!["a", "c", "b"]));
4462 }
4463
4464 #[test]
4465 fn set_row_orders_sets_both_families_or_neither() {
4466 let mut g = DiGraph::strict();
4467 for (u, v) in [("a", "b"), ("a", "c"), ("b", "a"), ("c", "a")] {
4468 let _ = g.add_edge(u, v);
4469 }
4470 let _ = g.csr();
4472 assert!(g.set_row_orders(
4473 &[vec![2, 1], vec![0], vec![0]],
4474 &[vec![2, 1], vec![0], vec![0]]
4475 ));
4476 assert_eq!(g.successors("a"), Some(vec!["c", "b"]));
4477 assert_eq!(g.predecessors("a"), Some(vec!["c", "b"]));
4478 let a = g.get_node_index("a").unwrap();
4479 assert_eq!(g.csr().successors(a), &[2, 1]);
4480 assert!(!g.set_row_orders(
4482 &[vec![1, 2], vec![0], vec![0]],
4483 &[vec![1, 1], vec![0], vec![0]]
4484 ));
4485 assert_eq!(g.successors("a"), Some(vec!["c", "b"]));
4486 assert_eq!(g.predecessors("a"), Some(vec!["c", "b"]));
4487 }
4488
4489 #[test]
4490 fn multidigraph_reorder_rows_like_restores_pred_rows_and_swaps_for_a_reverse() {
4491 let mut source = MultiDiGraph::strict();
4494 for (u, v) in [
4495 ("a", "x"),
4496 ("c", "x"),
4497 ("b", "x"),
4498 ("x", "b"),
4499 ("x", "a"),
4500 ("c", "x"),
4501 ] {
4502 let _ = source.add_edge(u, v);
4503 }
4504 let mut copy = MultiDiGraph::strict();
4507 for node in ["x", "a", "b", "c"] {
4508 let _ = copy.add_node(node);
4509 }
4510 for (u, v) in [
4511 ("x", "b"),
4512 ("x", "a"),
4513 ("a", "x"),
4514 ("b", "x"),
4515 ("c", "x"),
4516 ("c", "x"),
4517 ] {
4518 let _ = copy.add_edge(u, v);
4519 }
4520 assert_eq!(copy.predecessors("x"), Some(vec!["a", "b", "c"]));
4521 let _ = copy.csr();
4522 let before = copy.revision();
4523 copy.reorder_rows_like(&source, false);
4524 assert_eq!(copy.predecessors("x"), Some(vec!["a", "c", "b"]));
4525 assert_eq!(copy.successors("x"), Some(vec!["b", "a"]));
4526 assert_eq!(copy.edge_keys("c", "x"), Some(vec![0, 1]));
4527 assert!(copy.revision() > before);
4528 let x = copy.get_node_index("x").unwrap();
4530 let csr = copy.csr();
4531 let names: Vec<&str> = csr
4532 .predecessors(x)
4533 .iter()
4534 .map(|&i| copy.get_node_name(i as usize).unwrap())
4535 .collect();
4536 assert_eq!(names, vec!["a", "c", "b"]);
4537 let after = copy.revision();
4538 copy.reorder_rows_like(&source, false);
4539 assert_eq!(copy.revision(), after, "an ordered graph is left alone");
4540
4541 let mut reverse = MultiDiGraph::strict();
4544 for node in ["x", "a", "b", "c"] {
4545 let _ = reverse.add_node(node);
4546 }
4547 for (u, v) in [("x", "a"), ("x", "b"), ("x", "c"), ("a", "x"), ("b", "x")] {
4548 let _ = reverse.add_edge(u, v);
4549 }
4550 reverse.reorder_rows_like(&source, true);
4551 assert_eq!(reverse.successors("x"), Some(vec!["a", "c", "b"]));
4552 assert_eq!(reverse.predecessors("x"), Some(vec!["b", "a"]));
4553 }
4554
4555 #[test]
4556 fn multidigraph_set_row_orders_orders_named_rows_and_moves_the_revision() {
4557 let mut g = MultiDiGraph::strict();
4558 for (u, v) in [("a", "b"), ("a", "c"), ("a", "b"), ("b", "a"), ("c", "a")] {
4559 let _ = g.add_edge(u, v);
4560 }
4561 let _ = g.csr();
4562 let before = g.revision();
4563 let row = |node: &str, order: &[&str]| {
4564 (
4565 node.to_owned(),
4566 order.iter().map(|&n| n.to_owned()).collect::<Vec<_>>(),
4567 )
4568 };
4569 g.set_row_orders(&[row("a", &["c", "b"])], &[row("a", &["c", "b"])]);
4570 assert_eq!(g.successors("a"), Some(vec!["c", "b"]));
4571 assert_eq!(g.predecessors("a"), Some(vec!["c", "b"]));
4572 assert_eq!(g.edge_keys("a", "b"), Some(vec![0, 1]));
4573 assert!(g.revision() > before);
4574 let a = g.get_node_index("a").unwrap();
4575 let csr = g.csr();
4576 let names: Vec<&str> = csr
4577 .successors(a)
4578 .iter()
4579 .map(|&i| g.get_node_name(i as usize).unwrap())
4580 .collect();
4581 assert_eq!(names, vec!["c", "b"]);
4582 }
4583
4584 #[test]
4585 fn add_node_with_attrs_reports_change_on_existing_node() {
4586 let mut graph = DiGraph::strict();
4587 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
4588 let r1 = graph.revision();
4589 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
4590 let r2 = graph.revision();
4591 assert!(r2 > r1);
4592 let expected = CgseValue::from("blue");
4593 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
4594 }
4595
4596 #[test]
4597 fn multidigraph_add_node_with_attrs_reports_change_on_existing_node() {
4598 let mut graph = MultiDiGraph::strict();
4599 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
4600 let r1 = graph.revision();
4601 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
4602 let r2 = graph.revision();
4603 assert!(r2 > r1);
4604 let expected = CgseValue::from("blue");
4605 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
4606 }
4607
4608 fn assert_multidigraph_core_invariants(g: &MultiDiGraph) {
4609 let mut edge_instances = std::collections::BTreeSet::new();
4610 for node in g.nodes_ordered() {
4611 let successors = g
4612 .successors(node)
4613 .expect("multidigraph nodes should always have successor buckets");
4614 for target in successors {
4615 assert!(g.has_node(target));
4616 assert!(g.has_edge(node, target));
4617 let preds = g
4618 .predecessors(target)
4619 .expect("target should always have predecessor buckets");
4620 assert!(preds.contains(&node));
4621 let keys = g
4622 .edge_keys(node, target)
4623 .expect("parallel directed edge bucket should exist");
4624 for key in keys {
4625 edge_instances.insert((node.to_owned(), target.to_owned(), key));
4626 }
4627 }
4628 }
4629 assert_eq!(g.edge_count(), edge_instances.len());
4630 }
4631
4632 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
4633 assert!(record.ts_unix_ms > 0);
4634 assert!(!record.operation.trim().is_empty());
4635 assert_eq!(record.mode, expected_mode);
4636 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
4637 assert!(!record.rationale.trim().is_empty());
4638 assert!(!record.evidence.is_empty());
4639 for term in &record.evidence {
4640 assert!(!term.signal.trim().is_empty());
4641 assert!(!term.observed_value.trim().is_empty());
4642 }
4643 }
4644
4645 #[test]
4648 fn add_directed_edge_autocreates_nodes() {
4649 let mut g = DiGraph::strict();
4650 g.add_edge("a", "b").unwrap();
4651
4652 assert_eq!(g.node_count(), 2);
4653 assert_eq!(g.edge_count(), 1);
4654 assert!(g.has_edge("a", "b"));
4655 assert!(!g.has_edge("b", "a")); assert_digraph_core_invariants(&g);
4657 }
4658
4659 #[test]
4660 fn directed_edge_asymmetry() {
4661 let mut g = DiGraph::strict();
4662 g.add_edge("a", "b").unwrap();
4663 g.add_edge("b", "a").unwrap();
4664
4665 assert_eq!(g.edge_count(), 2);
4666 assert!(g.has_edge("a", "b"));
4667 assert!(g.has_edge("b", "a"));
4668 assert_digraph_core_invariants(&g);
4669 }
4670
4671 #[test]
4672 fn successors_and_predecessors() {
4673 let mut g = DiGraph::strict();
4674 g.add_edge("a", "b").unwrap();
4675 g.add_edge("a", "c").unwrap();
4676 g.add_edge("d", "a").unwrap();
4677
4678 assert_eq!(g.successors("a"), Some(vec!["b", "c"]));
4679 assert_eq!(g.predecessors("a"), Some(vec!["d"]));
4680 assert_eq!(g.out_degree("a"), 2);
4681 assert_eq!(g.in_degree("a"), 1);
4682 assert_eq!(g.degree("a"), 3);
4683 assert_digraph_core_invariants(&g);
4684 }
4685
4686 #[test]
4687 fn degree_single_lookup_preserves_missing_and_self_loop_semantics() {
4688 let mut g = DiGraph::strict();
4689 g.add_edge("a", "a").expect("self-loop add should succeed");
4690 g.add_edge("a", "b").expect("edge add should succeed");
4691 g.add_edge("c", "a").expect("edge add should succeed");
4692
4693 for node in ["a", "b", "c", "missing"] {
4694 assert_eq!(g.degree(node), g.in_degree(node) + g.out_degree(node));
4695 }
4696 assert_eq!(g.degree("a"), 4);
4697 assert_eq!(g.degree("missing"), 0);
4698 }
4699
4700 #[test]
4701 fn neighbors_returns_successors() {
4702 let mut g = DiGraph::strict();
4703 g.add_edge("a", "b").unwrap();
4704 g.add_edge("c", "a").unwrap();
4705
4706 assert_eq!(g.neighbors("a"), Some(vec!["b"]));
4708 assert_digraph_core_invariants(&g);
4709 }
4710
4711 #[test]
4712 fn in_edges_and_out_edges() {
4713 let mut g = DiGraph::strict();
4714 g.add_edge("x", "y").unwrap();
4715 g.add_edge("z", "y").unwrap();
4716 g.add_edge("y", "w").unwrap();
4717
4718 assert_eq!(g.out_edges("y"), vec![("y", "w")]);
4719 assert_eq!(g.in_edges("y"), vec![("x", "y"), ("z", "y")]);
4720 assert_digraph_core_invariants(&g);
4721 }
4722
4723 #[test]
4724 fn remove_directed_edge() {
4725 let mut g = DiGraph::strict();
4726 g.add_edge("a", "b").unwrap();
4727 g.add_edge("b", "a").unwrap();
4728
4729 assert!(g.remove_edge("a", "b"));
4730 assert!(!g.has_edge("a", "b"));
4731 assert!(g.has_edge("b", "a")); assert_eq!(g.edge_count(), 1);
4733 assert_digraph_core_invariants(&g);
4734 }
4735
4736 #[test]
4737 fn remove_node_removes_all_incident_directed_edges() {
4738 let mut g = DiGraph::strict();
4739 g.add_edge("a", "b").unwrap();
4740 g.add_edge("b", "c").unwrap();
4741 g.add_edge("c", "a").unwrap();
4742 g.add_edge("d", "b").unwrap();
4743
4744 assert!(g.remove_node("b"));
4745 assert_eq!(g.node_count(), 3);
4746 assert!(!g.has_edge("a", "b"));
4747 assert!(!g.has_edge("b", "c"));
4748 assert!(!g.has_edge("d", "b"));
4749 assert!(g.has_edge("c", "a")); assert_digraph_core_invariants(&g);
4751 }
4752
4753 fn assert_digraph_positions_agree_with_names(g: &DiGraph) {
4757 let names: Vec<String> = g.nodes_ordered().into_iter().map(str::to_owned).collect();
4758 let name_of = |p: usize| g.get_node_name(p).expect("position resolves");
4759 let csr = g.csr();
4760 for (position, name) in names.iter().enumerate() {
4761 assert_eq!(g.get_node_index(name), Some(position));
4762 let succ: Vec<&str> = g
4763 .successors_indices(position)
4764 .expect("live position")
4765 .iter()
4766 .map(|&p| name_of(p))
4767 .collect();
4768 assert_eq!(
4769 succ,
4770 g.successors(name).expect("live node"),
4771 "succ of {name}"
4772 );
4773 let pred: Vec<&str> = g
4774 .predecessors_indices(position)
4775 .expect("live position")
4776 .iter()
4777 .map(|&p| name_of(p))
4778 .collect();
4779 assert_eq!(
4780 pred,
4781 g.predecessors(name).expect("live node"),
4782 "pred of {name}"
4783 );
4784 let csr_succ: Vec<&str> = csr
4785 .successors(position)
4786 .iter()
4787 .map(|&p| name_of(p as usize))
4788 .collect();
4789 assert_eq!(csr_succ, succ);
4790 let out_attrs: Vec<&AttrMap> = g.out_edge_attrs(name).expect("live node").collect();
4792 let expected_out: Vec<&AttrMap> = succ
4793 .iter()
4794 .map(|&t| g.edge_attrs(name, t).expect("edge"))
4795 .collect();
4796 assert_eq!(out_attrs, expected_out, "out-edges of {name}");
4797 let in_attrs: Vec<&AttrMap> = g.in_edge_attrs(name).expect("live node").collect();
4798 let expected_in: Vec<&AttrMap> = pred
4799 .iter()
4800 .map(|&s| g.edge_attrs(s, name).expect("edge"))
4801 .collect();
4802 assert_eq!(in_attrs, expected_in, "in-edges of {name}");
4803 }
4804 assert!(g.successors_indices(names.len()).is_none());
4805 for ((source, target), attrs) in g.edges_indexed() {
4806 assert_eq!(Some(attrs), g.edge_attrs(name_of(source), name_of(target)));
4807 }
4808 assert_eq!(g.edges_indexed().count(), g.edge_count());
4809 }
4810
4811 #[test]
4815 fn removal_runs_keep_directed_positional_readers_in_step_with_names() {
4816 let mut state = 0x2545_f491_4f6c_dd1d_u64;
4817 let mut next = |bound: usize| {
4818 state ^= state << 13;
4819 state ^= state >> 7;
4820 state ^= state << 17;
4821 usize::try_from(state % bound as u64).unwrap()
4822 };
4823 for round in 0..40 {
4824 let n = 12 + round % 20;
4825 let mut g = DiGraph::strict();
4826 for i in 0..n {
4827 let _ = g.add_node(format!("n{i}"));
4828 }
4829 for _ in 0..3 * n {
4830 let (u, v) = (next(n), next(n));
4831 let mut attrs = AttrMap::new();
4832 attrs.insert(
4833 "w".into(),
4834 CgseValue::Int(i64::try_from(u * 100 + v).unwrap()),
4835 );
4836 g.add_edge_with_attrs(format!("n{u}"), format!("n{v}"), attrs)
4837 .expect("edge");
4838 }
4839 assert!(g.is_compact());
4840 for _ in 0..n / 3 {
4841 let names: Vec<String> = g.nodes_ordered().into_iter().map(str::to_owned).collect();
4842 let victim = names[next(names.len() - 1)].clone();
4843 assert!(g.remove_node(&victim));
4844 assert!(!g.is_compact(), "round {round}: slots should stay apart");
4845 assert_digraph_positions_agree_with_names(&g);
4846 assert_digraph_core_invariants(&g);
4847 }
4848 g.add_edge("fresh", "n0").expect("edge");
4849 assert!(g.is_compact());
4850 assert_digraph_positions_agree_with_names(&g);
4851 }
4852 }
4853
4854 fn assert_multidigraph_positions_agree_with_names(g: &MultiDiGraph) {
4858 let names = g.nodes_ordered();
4859 assert_eq!(g.node_count(), names.len());
4860 for (position, &name) in names.iter().enumerate() {
4861 assert_eq!(g.get_node_index(name), Some(position), "{name}");
4862 assert_eq!(g.get_node_name(position), Some(name));
4863 }
4864 assert_eq!(g.get_node_name(names.len()), None);
4865 let csr = g.csr();
4866 let named = |row: &[u32]| row.iter().map(|&i| names[i as usize]).collect::<Vec<_>>();
4867 for (position, &name) in names.iter().enumerate() {
4868 assert_eq!(Some(named(csr.successors(position))), g.successors(name));
4869 assert_eq!(
4870 Some(named(csr.predecessors(position))),
4871 g.predecessors(name)
4872 );
4873 }
4874 for (s, &source) in names.iter().enumerate() {
4876 for (t, &target) in names.iter().enumerate() {
4877 assert_eq!(g.has_edge_by_indices(s, t), g.has_edge(source, target));
4878 assert_eq!(
4879 g.edge_key_count_by_indices(s, t),
4880 g.edge_keys(source, target).map_or(0, |keys| keys.len())
4881 );
4882 }
4883 }
4884 let mut indexed = Vec::new();
4885 g.try_for_each_indexed_edge_ordered_borrowed(|s, t, source, target, key, _| {
4886 assert_eq!((names[s], names[t]), (source, target));
4887 indexed.push((source.to_owned(), target.to_owned(), key));
4888 Ok::<(), ()>(())
4889 })
4890 .expect("the walk never fails");
4891 let borrowed: Vec<_> = g
4892 .edges_ordered_borrowed()
4893 .into_iter()
4894 .map(|(source, target, key, _)| (source.to_owned(), target.to_owned(), key))
4895 .collect();
4896 assert_eq!(indexed, borrowed);
4897 }
4898
4899 #[test]
4904 fn multidigraph_removal_runs_keep_positional_readers_in_step_with_names() {
4905 use std::collections::BTreeMap;
4906 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
4907 let mut next = |bound: usize| {
4908 state ^= state << 13;
4909 state ^= state >> 7;
4910 state ^= state << 17;
4911 usize::try_from(state % bound as u64).unwrap()
4912 };
4913 let tagged = |tag: usize| {
4914 let mut attrs = AttrMap::new();
4915 attrs.insert("tag".into(), CgseValue::Int(i64::try_from(tag).unwrap()));
4916 attrs
4917 };
4918 for round in 0..30 {
4919 let n = 10 + round % 17;
4920 let mut g = MultiDiGraph::strict();
4921 let mut order: Vec<String> = Vec::new();
4922 let mut tags: BTreeMap<String, usize> = BTreeMap::new();
4923 let mut edges: BTreeMap<(String, String), usize> = BTreeMap::new();
4924 for i in 0..n {
4925 let name = format!("n{i}");
4926 g.add_node_with_attrs(name.clone(), tagged(i));
4927 tags.insert(name.clone(), i);
4928 order.push(name);
4929 }
4930 for _ in 0..3 * n {
4931 let (u, v) = (format!("n{}", next(n)), format!("n{}", next(n)));
4932 *edges.entry((u.clone(), v.clone())).or_default() += 1;
4933 g.add_edge(u, v).expect("edge");
4934 }
4935 let mut fresh = 0;
4936 let forget = |victim: &str,
4937 order: &mut Vec<String>,
4938 tags: &mut BTreeMap<String, usize>,
4939 edges: &mut BTreeMap<(String, String), usize>| {
4940 order.retain(|name| name != victim);
4941 tags.remove(victim);
4942 edges.retain(|(u, v), _| u != victim && v != victim);
4943 };
4944 for _ in 0..4 * n {
4945 match next(6) {
4946 0..=2 if !order.is_empty() => {
4947 let victim = order[next(order.len())].clone();
4948 assert!(g.remove_node(&victim));
4949 assert!(!g.remove_node(&victim));
4950 forget(&victim, &mut order, &mut tags, &mut edges);
4951 }
4952 3 | 4 => {
4953 let u = format!("f{fresh}");
4954 g.add_node_with_attrs(u.clone(), tagged(1000 + fresh));
4955 tags.insert(u.clone(), 1000 + fresh);
4956 order.push(u.clone());
4957 fresh += 1;
4958 let v = if next(4) == 0 {
4959 u.clone()
4960 } else {
4961 order[next(order.len())].clone()
4962 };
4963 let (source, target) = if next(2) == 0 { (u, v) } else { (v, u) };
4964 *edges.entry((source.clone(), target.clone())).or_default() += 1;
4965 g.add_edge(source, target).expect("edge");
4966 }
4967 5 if order.len() > 4 => {
4968 let victim = order[next(order.len())].clone();
4970 let removed = g.remove_nodes_from([victim.as_str(), "absent"]);
4971 assert_eq!(removed.0, 1);
4972 forget(&victim, &mut order, &mut tags, &mut edges);
4973 }
4974 _ => {}
4975 }
4976 assert_eq!(g.nodes_ordered(), order, "round {round}");
4977 for (name, &tag) in &tags {
4978 assert_eq!(g.node_attrs(name), Some(&tagged(tag)), "{name}");
4979 }
4980 assert_eq!(g.edge_count(), edges.values().sum::<usize>());
4981 for ((u, v), &count) in &edges {
4982 assert_eq!(g.edge_keys(u, v).map(|keys| keys.len()), Some(count));
4983 }
4984 assert_multidigraph_core_invariants(&g);
4985 assert_multidigraph_positions_agree_with_names(&g);
4986 }
4987 let doomed: Vec<String> = order.iter().step_by(2).cloned().collect();
4989 let removed = g.remove_nodes_from(doomed.iter().map(String::as_str));
4990 assert_eq!(removed.0, doomed.len());
4991 for victim in &doomed {
4992 forget(victim, &mut order, &mut tags, &mut edges);
4993 }
4994 assert_eq!(g.nodes_ordered(), order);
4995 assert_eq!(g.edge_count(), edges.values().sum::<usize>());
4996 assert_multidigraph_core_invariants(&g);
4997 assert_multidigraph_positions_agree_with_names(&g);
4998 }
4999 }
5000
5001 #[test]
5002 fn multidigraph_counts_parallel_selfloops_only() {
5003 let mut g = MultiDiGraph::strict();
5004 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
5005 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
5006 let _ = g.add_edge("b", "b").expect("self-loop add should succeed");
5007 let _ = g.add_edge("a", "b").expect("edge add should succeed");
5008 let _ = g.add_edge("b", "a").expect("edge add should succeed");
5009
5010 assert_eq!(g.number_of_selfloops(), 3);
5011 assert_multidigraph_core_invariants(&g);
5012 }
5013
5014 #[test]
5015 fn multidigraph_parallel_edge_counts_lists_only_repeated_pairs() {
5016 let mut g = MultiDiGraph::strict();
5017 for (u, v) in [
5018 ("a", "b"),
5019 ("b", "c"),
5020 ("a", "b"),
5021 ("c", "c"),
5022 ("c", "c"),
5023 ("a", "b"),
5024 ] {
5025 let _ = g.add_edge(u, v).expect("edge add should succeed");
5026 }
5027 let _ = g.add_edge("b", "a").expect("edge add should succeed");
5028 let counts: Vec<_> = g.parallel_edge_counts().collect();
5029 assert_eq!(counts, vec![("a", "b", 3), ("c", "c", 2)]);
5030
5031 let mut simple = MultiDiGraph::strict();
5032 let _ = simple.add_edge("a", "b").expect("edge add should succeed");
5033 let _ = simple.add_edge("b", "a").expect("edge add should succeed");
5034 assert_eq!(simple.parallel_edge_counts().count(), 0);
5035 }
5036
5037 #[test]
5043 #[ignore]
5044 fn ab_bench_multidigraph_remove_node() {
5045 use std::time::Instant;
5046 const N: usize = 1000;
5047 const M: usize = 8000;
5048 const ITERS: usize = 500;
5049 let build = || {
5050 let mut g = MultiDiGraph::new(CompatibilityMode::Strict);
5051 for i in 0..N {
5052 let _ = g.add_node(i.to_string());
5053 }
5054 let mut s: u64 = 0x9E3779B97F4A7C15;
5055 let mut next = || {
5056 s = s
5057 .wrapping_mul(6364136223846793005)
5058 .wrapping_add(1442695040888963407);
5059 (s >> 33) as usize % N
5060 };
5061 for _ in 0..M {
5062 let a = next();
5063 let b = next();
5064 let _ = g.add_edge(a.to_string(), b.to_string());
5065 }
5066 g
5067 };
5068
5069 let mut gnew = build();
5070 let victims: Vec<String> = gnew.nodes.keys().take(ITERS).cloned().collect();
5071 let t = Instant::now();
5072 for node in &victims {
5073 gnew.remove_node(node);
5074 }
5075 let new_t = t.elapsed();
5076
5077 let mut gold = build();
5079 let t = Instant::now();
5080 for node in &victims {
5081 if !gold.nodes.contains_key(node) {
5082 continue;
5083 }
5084 if let Some(succs) = gold.successors.get(node) {
5085 let targets: Vec<String> = succs.keys().cloned().collect();
5086 for target in targets {
5087 if target != *node
5088 && let Some(preds) = gold.predecessors.get_mut(&target)
5089 {
5090 preds.shift_remove(node.as_str());
5091 }
5092 }
5093 }
5094 if let Some(preds) = gold.predecessors.get(node) {
5095 let sources: Vec<String> = preds.keys().cloned().collect();
5096 for source in sources {
5097 if source != *node
5098 && let Some(succs) = gold.successors.get_mut(&source)
5099 {
5100 succs.shift_remove(node.as_str());
5101 }
5102 }
5103 }
5104 let mut rc = 0usize;
5105 let nd = node.clone();
5106 gold.edges.retain(|k, bucket| {
5107 let keep = k.source != nd && k.target != nd;
5108 if !keep {
5109 rc += bucket.len();
5110 }
5111 keep
5112 });
5113 gold.edge_count -= rc;
5114 gold.successors.remove(node);
5115 gold.predecessors.remove(node);
5116 gold.nodes.remove(node);
5117 }
5118 let old_t = t.elapsed();
5119
5120 assert_eq!(gnew.node_count(), gold.node_count());
5121 assert_eq!(gnew.edge_count(), gold.edge_count());
5122 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
5123 eprintln!(
5124 "MultiDiGraph remove_node x{ITERS}: retain {old_t:?} -> swap_remove {new_t:?} = {:.2}x",
5125 old_t.as_secs_f64() / new_t.as_secs_f64()
5126 );
5127 }
5128
5129 #[test]
5130 fn self_loop_directed() {
5131 let mut g = DiGraph::strict();
5132 g.add_edge("a", "a").unwrap();
5133
5134 assert_eq!(g.edge_count(), 1);
5135 assert!(g.has_edge("a", "a"));
5136 assert_eq!(g.out_degree("a"), 1);
5137 assert_eq!(g.in_degree("a"), 1);
5138 assert_digraph_core_invariants(&g);
5139 }
5140
5141 #[test]
5142 fn edge_attrs_directed() {
5143 let mut g = DiGraph::strict();
5144 let mut attrs = AttrMap::new();
5145 attrs.insert("weight".to_owned(), "5".into());
5146 g.add_edge_with_attrs("a", "b", attrs).unwrap();
5147
5148 assert_eq!(
5149 g.edge_attrs("a", "b").unwrap().get("weight"),
5150 Some(&CgseValue::String("5".to_owned()))
5151 );
5152 assert!(g.edge_attrs("b", "a").is_none()); }
5154
5155 #[test]
5156 fn extend_edges_unrecorded_preserves_direction_and_records_once() {
5157 let mut g = DiGraph::strict();
5158 g.add_node("a");
5159 g.add_node("b");
5160 g.add_node("c");
5161 let before = g.evidence_ledger().records().len();
5162
5163 let inserted = g.extend_edges_unrecorded([("a", "b"), ("b", "a"), ("a", "b")]);
5164
5165 assert_eq!(inserted, 2);
5166 assert_eq!(g.edge_count(), 2);
5167 assert!(g.has_edge("a", "b"));
5168 assert!(g.has_edge("b", "a"));
5169 assert!(!g.has_edge("a", "c"));
5170 let records = g.evidence_ledger().records();
5171 assert_eq!(records.len(), before + 1);
5172 assert_eq!(
5173 records.last().map(|record| record.operation.as_ref()),
5174 Some("extend_edges_unrecorded")
5175 );
5176 }
5177
5178 #[test]
5179 fn row_staged_attr_edges_preserve_orders_and_duplicate_merges() {
5180 let mut g = DiGraph::strict();
5181 let before = g.evidence_ledger().records().len();
5182
5183 let inserted = g.extend_prepared_edges_with_attrs_row_staged_unrecorded(
5184 ["b", "x", "a", "t"]
5185 .into_iter()
5186 .map(std::borrow::ToOwned::to_owned),
5187 vec![
5188 ("b".to_owned(), "x".to_owned(), single_attr("first", "bx")),
5189 ("a".to_owned(), "t".to_owned(), single_attr("first", "at")),
5190 ("b".to_owned(), "t".to_owned(), single_attr("first", "bt")),
5191 (
5192 "a".to_owned(),
5193 "t".to_owned(),
5194 single_attr("second", "merge"),
5195 ),
5196 ("t".to_owned(), "t".to_owned(), single_attr("loop", "yes")),
5197 ],
5198 );
5199
5200 assert_eq!(inserted, 4);
5201 assert_eq!(g.nodes_ordered(), vec!["b", "x", "a", "t"]);
5202 assert_eq!(
5203 g.edges_ordered_borrowed()
5204 .into_iter()
5205 .map(|(u, v, _)| (u, v))
5206 .collect::<Vec<_>>(),
5207 vec![("b", "x"), ("b", "t"), ("a", "t"), ("t", "t")]
5208 );
5209 assert_eq!(
5210 g.edges_ordered_indices(),
5211 vec![(0, 1), (0, 3), (2, 3), (3, 3)]
5212 );
5213 assert_eq!(g.successors("b"), Some(vec!["x", "t"]));
5214 assert_eq!(g.predecessors("t"), Some(vec!["a", "b", "t"]));
5215 let merged = g.edge_attrs("a", "t").expect("merged edge should exist");
5216 assert_eq!(merged.get("first"), Some(&CgseValue::from("at")));
5217 assert_eq!(merged.get("second"), Some(&CgseValue::from("merge")));
5218 assert_digraph_core_invariants(&g);
5219 let records = g.evidence_ledger().records();
5220 assert_eq!(records.len(), before + 1);
5221 assert_eq!(
5222 records.last().map(|record| record.operation.as_ref()),
5223 Some("extend_edges_unrecorded")
5224 );
5225 }
5226
5227 #[test]
5228 fn repeated_edge_merges_attrs() {
5229 let mut g = DiGraph::strict();
5230 let mut first = AttrMap::new();
5231 first.insert("weight".to_owned(), "1".into());
5232 g.add_edge_with_attrs("a", "b", first).unwrap();
5233
5234 let mut second = AttrMap::new();
5235 second.insert("color".to_owned(), "red".into());
5236 g.add_edge_with_attrs("a", "b", second).unwrap();
5237
5238 assert_eq!(g.edge_count(), 1);
5239 let attrs = g.edge_attrs("a", "b").unwrap();
5240 assert_eq!(
5241 attrs.get("weight"),
5242 Some(&CgseValue::String("1".to_owned()))
5243 );
5244 assert_eq!(
5245 attrs.get("color"),
5246 Some(&CgseValue::String("red".to_owned()))
5247 );
5248 }
5249
5250 #[test]
5251 fn edges_ordered_preserves_direction() {
5252 let mut g = DiGraph::strict();
5253 g.add_edge("b", "a").unwrap();
5254 g.add_edge("a", "c").unwrap();
5255
5256 let pairs: Vec<(String, String)> = g
5257 .edges_ordered()
5258 .into_iter()
5259 .map(|e| (e.left, e.right))
5260 .collect();
5261 assert_eq!(
5263 pairs,
5264 vec![
5265 ("b".to_owned(), "a".to_owned()),
5266 ("a".to_owned(), "c".to_owned()),
5267 ]
5268 );
5269 }
5270
5271 #[test]
5272 fn type_identity() {
5273 let g = DiGraph::strict();
5274 assert!(g.is_directed());
5275 assert!(!g.is_multigraph());
5276 }
5277
5278 #[test]
5279 fn to_undirected_merges_edges() {
5280 let mut g = DiGraph::strict();
5281 g.add_edge("a", "b").unwrap();
5282 g.add_edge("b", "a").unwrap();
5283 g.add_edge("b", "c").unwrap();
5284
5285 let ug = g.to_undirected();
5286 assert_eq!(ug.node_count(), 3);
5287 assert_eq!(ug.edge_count(), 2); assert!(ug.has_edge("a", "b"));
5289 assert!(ug.has_edge("b", "a")); assert!(ug.has_edge("b", "c"));
5291 }
5292
5293 #[test]
5294 fn to_undirected_preserves_runtime_policy() {
5295 let mut g = DiGraph::hardened();
5296 g.add_edge("a", "b").unwrap();
5297 g.add_edge("b", "a").unwrap();
5298 let expected_policy = g.runtime_policy().clone();
5299
5300 let ug = g.to_undirected();
5301
5302 assert_runtime_policy_preserved(&expected_policy, ug.runtime_policy());
5303 }
5304
5305 #[test]
5313 #[ignore = "measurement; run with --release --ignored --nocapture"]
5314 fn to_undirected_batch_ab() {
5315 use std::hint::black_box;
5316 use std::time::Instant;
5317
5318 let mut input = DiGraph::strict();
5319 for i in 0..200 {
5320 input.add_node(format!("{i}"));
5321 }
5322 for a in 0..200 {
5323 for b in 0..200 {
5324 if a != b {
5325 input.add_edge(format!("{a}"), format!("{b}")).unwrap();
5326 }
5327 }
5328 }
5329
5330 let build = |g: &DiGraph, batch: bool| -> crate::Graph {
5331 let mut out = crate::Graph::with_runtime_policy(g.runtime_policy.clone());
5332 for (node, idx) in g.node_order.iter() {
5333 let attrs = &g.node_attrs[idx];
5334 out.add_node_with_attrs(node.clone(), attrs.clone());
5335 }
5336 if batch {
5337 let edges: Vec<(String, String, AttrMap)> = g
5338 .edges_ordered()
5339 .into_iter()
5340 .map(|snap| (snap.left, snap.right, snap.attrs))
5341 .collect();
5342 let _ = out.extend_edges_with_attrs_unrecorded(edges);
5343 } else {
5344 for snap in g.edges_ordered() {
5345 let _ = out.add_edge_with_attrs(snap.left, snap.right, snap.attrs);
5346 }
5347 }
5348 out
5349 };
5350
5351 let old = build(&input, false);
5352 let new = build(&input, true);
5353 assert_eq!(
5354 old.edges_ordered_borrowed(),
5355 new.edges_ordered_borrowed(),
5356 "batch to_undirected must equal the per-edge baseline"
5357 );
5358 assert_eq!(old.nodes_ordered(), new.nodes_ordered());
5359
5360 let time = |batch: bool| -> f64 {
5361 let t0 = Instant::now();
5362 black_box(build(&input, batch));
5363 t0.elapsed().as_secs_f64()
5364 };
5365 for _ in 0..3 {
5366 black_box(time(true));
5367 black_box(time(false));
5368 }
5369 let median = |v: &[f64]| {
5370 let mut s = v.to_vec();
5371 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5372 s[s.len() / 2]
5373 };
5374 let rounds = 61usize;
5375 let paired = |cand: bool, base_arm: bool| -> Vec<f64> {
5376 let mut v = Vec::with_capacity(rounds);
5377 for round in 0..rounds {
5378 let (tb, tc) = if round.is_multiple_of(2) {
5379 let bt = time(base_arm);
5380 let ct = time(cand);
5381 (bt, ct)
5382 } else {
5383 let ct = time(cand);
5384 let bt = time(base_arm);
5385 (bt, ct)
5386 };
5387 v.push(tb / tc);
5388 }
5389 v
5390 };
5391 let report = |name: &str, ratios: &[f64]| {
5392 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5393 let mut sorted = ratios.to_vec();
5394 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5395 println!(
5396 "TOUNDIR_BATCH_AB {name}: median={:.4}x win_rate={wins}/{rounds} \
5397 p5_p95=[{:.4},{:.4}]",
5398 median(ratios),
5399 sorted[rounds * 5 / 100],
5400 sorted[rounds * 95 / 100],
5401 );
5402 };
5403 println!("TOUNDIR_BATCH_AB K200_complete rounds={rounds} (>1 = batch faster)");
5404 report("BATCH_vs_string", &paired(true, false));
5405 report("NULL_batch_vs_batch", &paired(true, true));
5406 }
5407
5408 #[test]
5409 fn snapshot_roundtrip() {
5410 let mut g = DiGraph::strict();
5411 let mut attrs = AttrMap::new();
5412 attrs.insert("weight".to_owned(), "3".into());
5413 g.add_edge_with_attrs("a", "b", attrs).unwrap();
5414 g.add_edge("b", "c").unwrap();
5415 g.add_edge("c", "a").unwrap();
5416
5417 let snap = g.snapshot();
5418 let mut replayed = DiGraph::new(snap.mode);
5419 for node in &snap.nodes {
5420 let _ = replayed.add_node(node.clone());
5421 }
5422 for edge in &snap.edges {
5423 replayed
5424 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
5425 .unwrap();
5426 }
5427
5428 assert_eq!(replayed.snapshot(), snap);
5429 assert_digraph_core_invariants(&replayed);
5430 }
5431
5432 #[test]
5433 fn multidigraph_tracks_parallel_edges_with_distinct_keys() {
5434 let mut graph = MultiDiGraph::strict();
5435 let first = graph.add_edge("a", "b").expect("edge add should succeed");
5436 let second = graph.add_edge("a", "b").expect("edge add should succeed");
5437
5438 assert_ne!(first, second);
5439 assert_eq!(graph.edge_count(), 2);
5440 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
5441 assert_multidigraph_core_invariants(&graph);
5442 }
5443
5444 #[test]
5445 fn multidigraph_remove_node_clears_incoming_and_outgoing_parallel_edges() {
5446 let mut graph = MultiDiGraph::strict();
5447 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
5448 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
5449 let _ = graph.add_edge("b", "a").expect("edge add should succeed");
5450 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
5451
5452 assert!(graph.remove_node("b"));
5453 assert_eq!(graph.edge_count(), 0);
5454 assert!(!graph.has_node("b"));
5455 assert_eq!(graph.successors("a"), Some(vec![]));
5456 assert_eq!(graph.predecessors("a"), Some(vec![]));
5457 assert_multidigraph_core_invariants(&graph);
5458 }
5459
5460 #[test]
5461 fn multidigraph_clear_edges_preserves_nodes_attrs_and_rows() {
5462 let mut graph = MultiDiGraph::strict();
5463 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
5464 assert!(graph.add_node_with_attrs("b", single_attr("color", "blue")));
5465 let _ = graph
5466 .add_edge_with_key_and_attrs("a", "b", 7, single_attr("weight", "3"))
5467 .expect("edge add should succeed");
5468 let _ = graph
5469 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("weight", "5"))
5470 .expect("edge add should succeed");
5471 let before_revision = graph.revision();
5472
5473 graph.clear_edges();
5474
5475 assert_eq!(graph.node_count(), 2);
5476 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
5477 assert_eq!(graph.edge_count(), 0);
5478 assert_eq!(graph.edges_ordered(), Vec::<MultiDiEdgeSnapshot>::new());
5479 assert_eq!(graph.successors("a"), Some(vec![]));
5480 assert_eq!(graph.predecessors("a"), Some(vec![]));
5481 assert_eq!(
5482 graph.node_attrs("a").unwrap().get("color"),
5483 Some(&CgseValue::from("red"))
5484 );
5485 assert!(graph.revision() > before_revision);
5486 assert_multidigraph_core_invariants(&graph);
5487 }
5488
5489 #[test]
5490 fn multidigraph_roundtrips_sparse_snapshot_keys() {
5491 let mut graph = MultiDiGraph::strict();
5492 assert_eq!(
5493 graph.add_edge("a", "b").expect("edge add should succeed"),
5494 0
5495 );
5496 assert_eq!(
5497 graph.add_edge("a", "b").expect("edge add should succeed"),
5498 1
5499 );
5500 assert_eq!(
5501 graph.add_edge("a", "b").expect("edge add should succeed"),
5502 2
5503 );
5504 assert!(graph.remove_edge("a", "b", Some(1)));
5505 assert_eq!(
5506 graph.add_edge("a", "b").expect("edge add should succeed"),
5507 3
5508 );
5509
5510 let snapshot = graph.snapshot();
5511 assert_eq!(
5512 snapshot
5513 .edges
5514 .iter()
5515 .map(|edge| edge.key)
5516 .collect::<Vec<_>>(),
5517 vec![0, 2, 3]
5518 );
5519
5520 let mut replayed = MultiDiGraph::new(snapshot.mode);
5521 for node in &snapshot.nodes {
5522 let _ = replayed.add_node(node.clone());
5523 }
5524 for edge in &snapshot.edges {
5525 replayed
5526 .add_edge_with_key_and_attrs(
5527 edge.source.clone(),
5528 edge.target.clone(),
5529 edge.key,
5530 edge.attrs.clone(),
5531 )
5532 .expect("snapshot replay should preserve explicit keys");
5533 }
5534
5535 assert_eq!(replayed.snapshot(), snapshot);
5536 assert_multidigraph_core_invariants(&replayed);
5537 }
5538
5539 #[test]
5540 fn runtime_policy_tracks_hardened_multidigraph_recovery() {
5541 let mut g = MultiDiGraph::hardened();
5542 g.add_edge("a", "b")
5543 .expect("hardened multidigraph edge add should succeed");
5544
5545 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
5546 assert!(!g.runtime_policy().decision_log().records().is_empty());
5547 assert!(g.runtime_policy().posterior().observation_count >= 1);
5548 }
5549
5550 fn add_edge_evidence_signals(records: &[DecisionRecord]) -> Vec<Vec<&str>> {
5556 records
5557 .iter()
5558 .filter(|record| record.operation == "add_edge")
5559 .map(|record| {
5560 record
5561 .evidence
5562 .iter()
5563 .map(|term| term.signal.as_ref())
5564 .collect()
5565 })
5566 .collect()
5567 }
5568
5569 #[test]
5577 fn multidigraph_adjacency_iterates_in_insertion_order_under_fxhash() {
5578 let mut graph = MultiDiGraph::strict();
5579 for (source, target) in [
5580 ("zeta", "mid"),
5581 ("zeta", "alpha"),
5582 ("zeta", "omega"),
5583 ("zeta", "beta"),
5584 ("alpha", "zeta"),
5585 ] {
5586 graph
5587 .add_edge_with_attrs(source, target, AttrMap::new())
5588 .expect("edge is allowed");
5589 }
5590 graph
5592 .add_edge_with_attrs("zeta", "alpha", AttrMap::new())
5593 .expect("parallel edge is allowed");
5594
5595 let observed: Vec<(String, String, usize)> = graph
5596 .edges_ordered_borrowed()
5597 .into_iter()
5598 .map(|(source, target, key, _)| (source.to_owned(), target.to_owned(), key))
5599 .collect();
5600 assert_eq!(
5601 observed,
5602 vec![
5603 ("zeta".to_owned(), "mid".to_owned(), 0),
5604 ("zeta".to_owned(), "alpha".to_owned(), 0),
5605 ("zeta".to_owned(), "alpha".to_owned(), 1),
5606 ("zeta".to_owned(), "omega".to_owned(), 0),
5607 ("zeta".to_owned(), "beta".to_owned(), 0),
5608 ("alpha".to_owned(), "zeta".to_owned(), 0),
5609 ],
5610 "adjacency rows must stay insertion-ordered regardless of hasher"
5611 );
5612 }
5613
5614 #[test]
5619 fn multidigraph_fresh_bucket_take_matches_merge_semantics() {
5620 fn weight(value: i64) -> AttrMap {
5621 let mut attrs = AttrMap::new();
5622 attrs.insert("w".to_owned(), CgseValue::Int(value));
5623 attrs
5624 }
5625
5626 let mut graph = MultiDiGraph::strict();
5627 let k0 = graph
5628 .add_edge_with_attrs("a", "b", weight(1))
5629 .expect("edge is allowed");
5630 let k1 = graph
5632 .add_edge_with_attrs("a", "b", weight(2))
5633 .expect("parallel edge is allowed");
5634 assert_ne!(k0, k1);
5635 assert_eq!(
5636 graph.edge_attrs("a", "b", k0).and_then(|a| a.get("w")),
5637 Some(&CgseValue::Int(1))
5638 );
5639 assert_eq!(
5640 graph.edge_attrs("a", "b", k1).and_then(|a| a.get("w")),
5641 Some(&CgseValue::Int(2))
5642 );
5643
5644 let k2 = graph
5647 .add_edge_with_attrs("c", "d", AttrMap::new())
5648 .expect("edge is allowed");
5649 assert!(
5650 graph
5651 .edge_attrs("c", "d", k2)
5652 .expect("edge exists")
5653 .is_empty()
5654 );
5655 graph
5656 .add_edge_with_key_and_attrs("c", "d", k2, weight(7))
5657 .expect("edge is allowed");
5658 assert_eq!(
5659 graph.edge_attrs("c", "d", k2).and_then(|a| a.get("w")),
5660 Some(&CgseValue::Int(7))
5661 );
5662
5663 let mut extra = AttrMap::new();
5665 extra.insert("k".to_owned(), CgseValue::Int(3));
5666 graph
5667 .add_edge_with_key_and_attrs("c", "d", k2, extra)
5668 .expect("edge is allowed");
5669 let stored = graph.edge_attrs("c", "d", k2).expect("edge exists");
5670 assert_eq!(stored.get("w"), Some(&CgseValue::Int(7)));
5671 assert_eq!(stored.get("k"), Some(&CgseValue::Int(3)));
5672 }
5673
5674 #[test]
5682 fn multidigraph_add_node_creates_and_preserves_adjacency_rows() {
5683 let mut graph = MultiDiGraph::strict();
5684
5685 graph.add_node("x");
5688 graph
5689 .add_edge_with_attrs("x", "y", AttrMap::new())
5690 .expect("edge is allowed");
5691 graph
5692 .add_edge_with_attrs("w", "x", AttrMap::new())
5693 .expect("edge is allowed");
5694 assert_eq!(graph.out_degree("x"), 1);
5695 assert_eq!(graph.in_degree("x"), 1);
5696
5697 for _ in 0..5 {
5700 graph.add_node("x");
5701 }
5702 assert_eq!(graph.out_degree("x"), 1);
5703 assert_eq!(graph.in_degree("x"), 1);
5704 assert_eq!(graph.node_count(), 3);
5705
5706 graph
5708 .add_edge_with_attrs("x", "y", AttrMap::new())
5709 .expect("parallel edge is allowed");
5710 assert_eq!(graph.out_degree("x"), 2);
5711 }
5712
5713 #[test]
5721 fn digraph_add_edge_endpoint_resolution_preserves_autocreation_and_direction() {
5722 fn autocreated(graph: &DiGraph) -> (String, String) {
5723 let record = graph
5724 .evidence_ledger()
5725 .records()
5726 .last()
5727 .expect("add_edge files an outcome record");
5728 let read = |signal: &str| {
5729 record
5730 .evidence
5731 .iter()
5732 .find(|term| term.signal == signal)
5733 .map(|term| term.observed_value.to_string())
5734 .unwrap_or_else(|| panic!("{signal} must be present"))
5735 };
5736 (read("source_autocreated"), read("target_autocreated"))
5737 }
5738
5739 let mut graph = DiGraph::strict();
5741 graph
5742 .add_edge_with_attrs("s", "s", AttrMap::new())
5743 .expect("self-loop is allowed");
5744 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
5745 assert_eq!(graph.node_count(), 1);
5746
5747 graph
5749 .add_edge_with_attrs("s", "s", AttrMap::new())
5750 .expect("re-adding a self-loop is allowed");
5751 assert_eq!(
5752 autocreated(&graph),
5753 ("false".to_owned(), "false".to_owned())
5754 );
5755 assert_eq!(graph.node_count(), 1);
5756
5757 graph
5759 .add_edge_with_attrs("s", "t", AttrMap::new())
5760 .expect("edge is allowed");
5761 assert_eq!(autocreated(&graph), ("false".to_owned(), "true".to_owned()));
5762
5763 graph
5766 .add_edge_with_attrs("z", "a", AttrMap::new())
5767 .expect("edge is allowed");
5768 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
5769 assert_eq!(graph.get_node_index("s"), Some(0));
5770 assert_eq!(graph.get_node_index("t"), Some(1));
5771 assert_eq!(graph.get_node_index("z"), Some(2));
5772 assert_eq!(graph.get_node_index("a"), Some(3));
5773
5774 assert!(graph.has_edge("z", "a"));
5776 assert!(!graph.has_edge("a", "z"));
5777 assert!(graph.has_edge("s", "t"));
5778 assert!(!graph.has_edge("t", "s"));
5779 }
5780
5781 #[test]
5782 fn allowed_digraph_add_edge_records_only_the_outcome_decision() {
5783 let mut g = DiGraph::strict();
5784 g.add_edge_with_attrs("a", "b", AttrMap::new())
5785 .expect("edge add should succeed");
5786
5787 assert_eq!(
5788 add_edge_evidence_signals(g.evidence_ledger().records()),
5789 vec![vec![
5790 "self_loop",
5791 "edge_attr_count",
5792 "source_autocreated",
5793 "target_autocreated",
5794 ]]
5795 );
5796 assert_eq!(
5797 g.evidence_ledger()
5798 .records()
5799 .iter()
5800 .filter(|record| record.operation == "add_edge")
5801 .map(|record| record.action)
5802 .collect::<Vec<_>>(),
5803 vec![DecisionAction::Allow]
5804 );
5805 }
5806
5807 #[test]
5808 fn allowed_multidigraph_add_edge_records_only_the_outcome_decision() {
5809 let mut g = MultiDiGraph::strict();
5810 let _ = g
5811 .add_edge_with_attrs("a", "b", AttrMap::new())
5812 .expect("edge add should succeed");
5813
5814 assert_eq!(
5815 add_edge_evidence_signals(g.evidence_ledger().records()),
5816 vec![vec![
5817 "edge_key",
5818 "edge_attr_count",
5819 "source_autocreated",
5820 "target_autocreated",
5821 ]]
5822 );
5823 }
5824
5825 #[test]
5826 fn refused_multidigraph_add_edge_still_records_its_gate_decision() {
5827 let mut g = MultiDiGraph::strict();
5828 let mut attrs = AttrMap::new();
5829 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
5830 let err = g
5831 .add_edge_with_attrs("a", "b", attrs)
5832 .expect_err("strict mode should fail closed");
5833 assert_eq!(
5834 err,
5835 GraphError::FailClosed {
5836 operation: "add_edge",
5837 reason: "incompatible edge metadata".to_owned(),
5838 }
5839 );
5840
5841 assert_eq!(
5842 add_edge_evidence_signals(g.evidence_ledger().records()),
5843 vec![vec!["unknown_incompatible_feature"]]
5844 );
5845 assert_eq!(
5846 g.evidence_ledger().records().last().map(|r| r.action),
5847 Some(DecisionAction::FailClosed)
5848 );
5849 }
5850
5851 #[test]
5852 fn strict_mode_fails_closed_for_incompatible_attrs() {
5853 let mut g = DiGraph::strict();
5854 let mut attrs = AttrMap::new();
5855 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
5856 let err = g
5857 .add_edge_with_attrs("a", "b", attrs)
5858 .expect_err("strict mode should fail closed");
5859
5860 assert_eq!(
5861 err,
5862 GraphError::FailClosed {
5863 operation: "add_edge",
5864 reason: "incompatible edge metadata".to_owned(),
5865 }
5866 );
5867 }
5868
5869 #[test]
5870 fn revision_increments_on_mutations() {
5871 let mut g = DiGraph::strict();
5872 let r0 = g.revision();
5873 let _ = g.add_node("a");
5874 let r1 = g.revision();
5875 assert!(r1 > r0);
5876
5877 g.add_edge("a", "b").unwrap();
5878 let r2 = g.revision();
5879 assert!(r2 > r1);
5880
5881 let _ = g.remove_edge("a", "b");
5882 let r3 = g.revision();
5883 assert!(r3 > r2);
5884 }
5885
5886 #[test]
5887 fn hardened_self_loop_records_allow() {
5888 let mut g = DiGraph::hardened();
5889 g.add_edge("loop", "loop").unwrap();
5890
5891 let record = g
5892 .evidence_ledger()
5893 .records()
5894 .iter()
5895 .rev()
5896 .find(|r| r.operation == "add_edge")
5897 .expect("add_edge should emit ledger row");
5898 assert_decision_record_schema(record, CompatibilityMode::Hardened);
5899 assert_eq!(record.action, DecisionAction::Allow);
5900 }
5901
5902 #[test]
5903 fn runtime_policy_tracks_hardened_digraph_recovery() {
5904 let mut g = DiGraph::hardened();
5905 g.add_edge("a", "b")
5906 .expect("hardened digraph edge add should succeed");
5907
5908 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
5909 assert!(!g.runtime_policy().decision_log().records().is_empty());
5910 assert!(g.runtime_policy().posterior().observation_count >= 1);
5911 }
5912
5913 #[test]
5917 fn has_edge_by_indices_is_oriented_and_range_checked() {
5918 let mut g = DiGraph::strict();
5919 g.add_edge("tail", "head").expect("edge add should succeed");
5920 g.add_edge("head", "third")
5921 .expect("edge add should succeed");
5922
5923 let (tail, head, third) = (
5924 g.get_node_index("tail").expect("tail index"),
5925 g.get_node_index("head").expect("head index"),
5926 g.get_node_index("third").expect("third index"),
5927 );
5928 assert!(g.has_edge_by_indices(tail, head));
5929 assert!(!g.has_edge_by_indices(head, tail));
5930 assert!(!g.has_edge_by_indices(tail, third));
5931 assert!(!g.has_edge_by_indices(third, g.node_count() + 7));
5932 assert!(!g.has_edge_by_indices(g.node_count() + 7, third));
5933
5934 assert!(g.remove_node("tail"), "node removal should succeed");
5937 let head_after = g.get_node_index("head").expect("head index after removal");
5938 let third_after = g
5939 .get_node_index("third")
5940 .expect("third index after removal");
5941 assert!(g.has_edge_by_indices(head_after, third_after));
5942 assert!(!g.has_edge_by_indices(third_after, head_after));
5943 assert!(!g.has_edge_by_indices(head_after, g.node_count()));
5944 }
5945
5946 proptest! {
5949 #[test]
5953 fn prop_has_edge_by_indices_matches_the_name_path(
5954 ops in prop::collection::vec((0_u8..6, 0_u8..6, 0_u8..10), 1..60),
5955 ) {
5956 let mut g = DiGraph::strict();
5957 for (src_id, tgt_id, action) in ops {
5958 let (source, target) = (node_name(src_id), node_name(tgt_id));
5959 match action {
5960 0..=6 => {
5961 prop_assert!(g.add_edge(source, target).is_ok());
5962 }
5963 7 | 8 => {
5964 let _ = g.remove_edge(&source, &target);
5965 }
5966 _ => {
5967 let _ = g.remove_node(&source);
5968 }
5969 }
5970 }
5971
5972 let names: Vec<String> = g.nodes_ordered().iter().map(|n| (*n).to_owned()).collect();
5973 for (i, source) in names.iter().enumerate() {
5974 for (j, target) in names.iter().enumerate() {
5975 prop_assert_eq!(
5976 g.has_edge_by_indices(i, j),
5977 g.has_edge(source, target),
5978 "index pair ({}, {}) disagrees with ({}, {})",
5979 i, j, source, target
5980 );
5981 }
5982 prop_assert!(!g.has_edge_by_indices(i, names.len()));
5983 prop_assert!(!g.has_edge_by_indices(names.len(), i));
5984 }
5985 }
5986
5987 #[test]
5988 fn prop_digraph_invariants_under_mixed_mutations(
5989 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
5990 ) {
5991 let mut g = DiGraph::strict();
5992 let mut last_rev = g.revision();
5993
5994 for (src_id, tgt_id, is_add) in ops {
5995 let src = node_name(src_id);
5996 let tgt = node_name(tgt_id);
5997 if is_add {
5998 prop_assert!(g.add_edge(src, tgt).is_ok());
5999 } else {
6000 let _ = g.remove_edge(&src, &tgt);
6001 }
6002 let rev = g.revision();
6003 prop_assert!(rev >= last_rev);
6004 last_rev = rev;
6005 assert_digraph_core_invariants(&g);
6006 }
6007 }
6008
6009 #[test]
6010 fn prop_digraph_snapshot_deterministic(
6011 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
6012 ) {
6013 let mut g1 = DiGraph::hardened();
6014 let mut g2 = DiGraph::hardened();
6015
6016 for (src_id, tgt_id, attrs_variant) in ops {
6017 let src = node_name(src_id);
6018 let tgt = node_name(tgt_id);
6019 let mut attrs = AttrMap::new();
6020 if attrs_variant == 1 {
6021 attrs.insert("weight".to_owned(), (src_id % 5).to_string().into());
6022 } else if attrs_variant == 2 {
6023 attrs.insert("tag".to_owned(), format!("k{}", tgt_id % 4).into());
6024 }
6025 prop_assert!(g1.add_edge_with_attrs(src.clone(), tgt.clone(), attrs.clone()).is_ok());
6026 prop_assert!(g2.add_edge_with_attrs(src, tgt, attrs).is_ok());
6027 }
6028
6029 prop_assert_eq!(g1.snapshot(), g2.snapshot());
6030 }
6031
6032 #[test]
6033 fn prop_remove_node_clears_all_directed_edges(
6034 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
6035 target_id in 0_u8..8,
6036 ) {
6037 let mut g = DiGraph::strict();
6038 for (src_id, tgt_id) in ops {
6039 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
6040 }
6041
6042 let target = node_name(target_id);
6043 let removed = g.remove_node(&target);
6044 if removed {
6045 prop_assert!(!g.has_node(&target));
6046 for node in g.nodes_ordered() {
6047 prop_assert!(!g.has_edge(node, &target));
6048 prop_assert!(!g.has_edge(&target, node));
6049 }
6050 }
6051 assert_digraph_core_invariants(&g);
6052 }
6053
6054 #[test]
6055 fn prop_directed_edge_count_equals_successor_sum(
6056 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
6057 ) {
6058 let mut g = DiGraph::strict();
6059 for (src_id, tgt_id) in ops {
6060 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
6061 }
6062
6063 let total_out: usize = g.nodes_ordered().iter().map(|n| g.out_degree(n)).sum();
6064 let total_in: usize = g.nodes_ordered().iter().map(|n| g.in_degree(n)).sum();
6065 prop_assert_eq!(g.edge_count(), total_out);
6066 prop_assert_eq!(g.edge_count(), total_in);
6067 }
6068 }
6069}