1use crate::{AttrMap, EdgeSnapshot, GraphError};
9use fnx_runtime::{CompatibilityMode, DecisionAction, EvidenceLedger, EvidenceTerm, RuntimePolicy};
10use indexmap::{IndexMap, IndexSet};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18struct DirectedEdgeKey {
19 source: String,
20 target: String,
21}
22
23impl DirectedEdgeKey {
24 fn new(source: &str, target: &str) -> Self {
25 Self {
26 source: source.to_owned(),
27 target: target.to_owned(),
28 }
29 }
30}
31
32#[derive(Hash, PartialEq, Eq, Clone, Copy)]
33struct DirectedEdgeKeyRef<'a> {
34 source: &'a str,
35 target: &'a str,
36}
37
38impl<'a> DirectedEdgeKeyRef<'a> {
39 fn new(source: &'a str, target: &'a str) -> Self {
40 Self { source, target }
41 }
42}
43
44impl<'a> indexmap::Equivalent<DirectedEdgeKey> for DirectedEdgeKeyRef<'a> {
45 fn equivalent(&self, key: &DirectedEdgeKey) -> bool {
46 self.source == key.source && self.target == key.target
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct DiGraphSnapshot {
56 pub mode: CompatibilityMode,
57 pub nodes: Vec<String>,
58 #[serde(default)]
61 pub node_attrs: std::collections::BTreeMap<String, AttrMap>,
62 pub edges: Vec<EdgeSnapshot>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct MultiDiEdgeSnapshot {
68 pub source: String,
69 pub target: String,
70 pub key: usize,
71 pub attrs: AttrMap,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct MultiDiGraphSnapshot {
76 pub mode: CompatibilityMode,
77 pub nodes: Vec<String>,
78 #[serde(default)]
80 pub node_attrs: std::collections::BTreeMap<String, AttrMap>,
81 pub edges: Vec<MultiDiEdgeSnapshot>,
82}
83
84#[derive(Debug)]
93pub struct DiCsr {
94 pub succ_offsets: Vec<usize>,
95 pub succ_targets: Vec<u32>,
96 pub pred_offsets: Vec<usize>,
97 pub pred_targets: Vec<u32>,
98}
99
100impl DiCsr {
101 #[must_use]
102 pub fn successors(&self, idx: usize) -> &[u32] {
103 &self.succ_targets[self.succ_offsets[idx]..self.succ_offsets[idx + 1]]
104 }
105
106 #[must_use]
107 pub fn predecessors(&self, idx: usize) -> &[u32] {
108 &self.pred_targets[self.pred_offsets[idx]..self.pred_offsets[idx + 1]]
109 }
110}
111
112type DiCsrCache = std::sync::Arc<std::sync::RwLock<Option<(u64, std::sync::Arc<DiCsr>)>>>;
113type DiAllIntCache = std::sync::Arc<std::sync::RwLock<Option<(u64, String, bool)>>>;
114
115#[derive(Debug, Clone)]
116pub struct DiGraph {
117 mode: CompatibilityMode,
118 revision: u64,
119 nodes: IndexMap<String, AttrMap>,
120 succ_indices: Vec<Vec<usize>>,
128 pred_indices: Vec<Vec<usize>>,
129 edges: IndexMap<(usize, usize), AttrMap>,
134 runtime_policy: RuntimePolicy,
135 csr_cache: DiCsrCache,
141 all_int_cache: DiAllIntCache,
145}
146
147impl DiGraph {
148 #[must_use]
151 pub fn clone_with_fresh_policy(&self) -> Self {
152 Self {
153 mode: self.mode,
154 revision: self.revision,
155 nodes: self.nodes.clone(),
156 succ_indices: self.succ_indices.clone(),
157 pred_indices: self.pred_indices.clone(),
158 edges: self.edges.clone(),
159 runtime_policy: RuntimePolicy::new(self.mode),
160 csr_cache: self.csr_cache.clone(),
161 all_int_cache: self.all_int_cache.clone(),
162 }
163 }
164
165 #[must_use]
170 pub fn new(mode: CompatibilityMode) -> Self {
171 Self {
172 mode,
173 revision: 0,
174 nodes: IndexMap::new(),
175 succ_indices: Vec::new(),
176 pred_indices: Vec::new(),
177 edges: IndexMap::new(),
178 runtime_policy: RuntimePolicy::new(mode),
179 csr_cache: std::sync::Arc::default(),
180 all_int_cache: std::sync::Arc::default(),
181 }
182 }
183
184 #[must_use]
185 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
186 let mode = runtime_policy.mode();
187 Self {
188 mode,
189 revision: 0,
190 nodes: IndexMap::new(),
191 succ_indices: Vec::new(),
192 pred_indices: Vec::new(),
193 edges: IndexMap::new(),
194 runtime_policy,
195 csr_cache: std::sync::Arc::default(),
196 all_int_cache: std::sync::Arc::default(),
197 }
198 }
199
200 #[must_use]
201 pub fn strict() -> Self {
202 Self::new(CompatibilityMode::Strict)
203 }
204
205 #[must_use]
206 pub fn hardened() -> Self {
207 Self::new(CompatibilityMode::Hardened)
208 }
209
210 #[must_use]
215 pub fn mode(&self) -> CompatibilityMode {
216 self.mode
217 }
218
219 #[must_use]
220 pub fn node_count(&self) -> usize {
221 self.nodes.len()
222 }
223
224 #[must_use]
225 pub fn edge_count(&self) -> usize {
226 self.edges.len()
227 }
228
229 #[must_use]
230 pub fn revision(&self) -> u64 {
231 self.revision
232 }
233
234 #[must_use]
239 pub fn csr(&self) -> std::sync::Arc<DiCsr> {
240 if let Ok(guard) = self.csr_cache.read()
241 && let Some((rev, csr)) = guard.as_ref()
242 && *rev == self.revision
243 {
244 return csr.clone();
245 }
246 let built = std::sync::Arc::new(self.build_csr());
247 if let Ok(mut guard) = self.csr_cache.write() {
248 *guard = Some((self.revision, built.clone()));
249 }
250 built
251 }
252
253 #[must_use]
256 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
257 if let Ok(guard) = self.all_int_cache.read()
258 && let Some((rev, a, v)) = guard.as_ref()
259 && *rev == self.revision
260 && a == attr
261 {
262 return *v;
263 }
264 let v = self
265 .edges
266 .values()
267 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
268 if let Ok(mut guard) = self.all_int_cache.write() {
269 *guard = Some((self.revision, attr.to_owned(), v));
270 }
271 v
272 }
273
274 fn build_csr(&self) -> DiCsr {
275 let n = self.nodes.len();
278 let mut succ_offsets = Vec::with_capacity(n + 1);
279 let mut pred_offsets = Vec::with_capacity(n + 1);
280 let mut succ_targets = Vec::new();
281 let mut pred_targets = Vec::new();
282 succ_offsets.push(0);
283 pred_offsets.push(0);
284 for i in 0..n {
285 succ_targets.extend(
286 self.succ_indices[i]
287 .iter()
288 .map(|&v| u32::try_from(v).unwrap_or(u32::MAX)),
289 );
290 succ_offsets.push(succ_targets.len());
291 pred_targets.extend(
292 self.pred_indices[i]
293 .iter()
294 .map(|&v| u32::try_from(v).unwrap_or(u32::MAX)),
295 );
296 pred_offsets.push(pred_targets.len());
297 }
298 DiCsr {
299 succ_offsets,
300 succ_targets,
301 pred_offsets,
302 pred_targets,
303 }
304 }
305
306 #[doc(hidden)]
309 #[must_use]
310 pub fn debug_index_rows_consistent(&self) -> bool {
311 self.succ_indices.len() == self.nodes.len() && self.pred_indices.len() == self.nodes.len()
315 }
316
317 #[must_use]
318 pub fn has_node(&self, node: &str) -> bool {
319 self.nodes.contains_key(node)
320 }
321
322 #[must_use]
324 #[inline]
326 fn edge_pair_key(&self, source: &str, target: &str) -> Option<(usize, usize)> {
327 Some((
328 self.nodes.get_index_of(source)?,
329 self.nodes.get_index_of(target)?,
330 ))
331 }
332
333 #[must_use]
335 #[inline]
336 pub fn successors_indices(&self, idx: usize) -> Option<&[usize]> {
337 self.succ_indices.get(idx).map(Vec::as_slice)
338 }
339
340 #[must_use]
342 #[inline]
343 pub fn predecessors_indices(&self, idx: usize) -> Option<&[usize]> {
344 self.pred_indices.get(idx).map(Vec::as_slice)
345 }
346
347 pub fn has_edge(&self, source: &str, target: &str) -> bool {
348 self.edge_pair_key(source, target)
349 .is_some_and(|k| self.edges.contains_key(&k))
350 }
351
352 #[must_use]
353 pub fn nodes_ordered(&self) -> Vec<&str> {
354 self.nodes.keys().map(String::as_str).collect()
355 }
356
357 #[must_use]
358 pub fn get_node_index(&self, node: &str) -> Option<usize> {
359 self.nodes.get_index_of(node)
360 }
361
362 #[must_use]
363 pub fn get_node_name(&self, index: usize) -> Option<&str> {
364 self.nodes.get_index(index).map(|(k, _)| k.as_str())
365 }
366
367 #[must_use]
371 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
372 let idx = self.nodes.get_index_of(node)?;
375 Some(
376 self.succ_indices[idx]
377 .iter()
378 .map(|&i| {
379 self.nodes
380 .get_index(i)
381 .expect("index rows hold valid node indices")
382 .0
383 .as_str()
384 })
385 .collect(),
386 )
387 }
388
389 #[must_use]
390 pub fn successors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
391 let idx = self.nodes.get_index_of(node)?;
393 Some(self.succ_indices[idx].iter().map(move |&i| {
394 self.nodes
395 .get_index(i)
396 .expect("index rows hold valid node indices")
397 .0
398 .as_str()
399 }))
400 }
401
402 #[must_use]
404 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
405 let idx = self.nodes.get_index_of(node)?;
407 Some(
408 self.pred_indices[idx]
409 .iter()
410 .map(|&i| {
411 self.nodes
412 .get_index(i)
413 .expect("index rows hold valid node indices")
414 .0
415 .as_str()
416 })
417 .collect(),
418 )
419 }
420
421 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
425 for (node, order) in orders {
427 let Some(idx) = self.nodes.get_index_of(node.as_str()) else {
428 continue;
429 };
430 let rows = if pred {
431 &mut self.pred_indices
432 } else {
433 &mut self.succ_indices
434 };
435 let row = &rows[idx];
436 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
437 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
438 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
439 for v in order {
440 if let Some(v_idx) = self.nodes.get_index_of(v.as_str())
441 && row_set.contains(&v_idx)
442 && placed.insert(v_idx)
443 {
444 new_row.push(v_idx);
445 }
446 }
447 for &v_idx in row {
448 if placed.insert(v_idx) {
449 new_row.push(v_idx);
450 }
451 }
452 rows[idx] = new_row;
453 }
454 }
455
456 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
464 let mut new_rows: Vec<Vec<usize>> = self
472 .pred_indices
473 .iter()
474 .map(|row| Vec::with_capacity(row.len()))
475 .collect();
476 for (u, succ_row) in self.succ_indices.iter().enumerate() {
477 for &v in succ_row {
478 new_rows[v].push(u);
479 }
480 }
481 self.pred_indices = new_rows;
482 }
483
484 #[must_use]
485 pub fn predecessors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
486 let idx = self.nodes.get_index_of(node)?;
488 Some(self.pred_indices[idx].iter().map(move |&i| {
489 self.nodes
490 .get_index(i)
491 .expect("index rows hold valid node indices")
492 .0
493 .as_str()
494 }))
495 }
496
497 #[must_use]
499 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
500 self.successors(node)
501 }
502
503 #[must_use]
504 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
505 self.successors_iter(node)
506 }
507
508 #[must_use]
509 pub fn neighbor_count(&self, node: &str) -> usize {
510 self.nodes
512 .get_index_of(node)
513 .map_or(0, |i| self.succ_indices[i].len())
514 }
515
516 #[must_use]
518 pub fn out_degree(&self, node: &str) -> usize {
519 self.nodes
520 .get_index_of(node)
521 .map_or(0, |i| self.succ_indices[i].len())
522 }
523
524 #[must_use]
526 pub fn in_degree(&self, node: &str) -> usize {
527 self.nodes
528 .get_index_of(node)
529 .map_or(0, |i| self.pred_indices[i].len())
530 }
531
532 #[must_use]
534 pub fn degree(&self, node: &str) -> usize {
535 self.in_degree(node) + self.out_degree(node)
536 }
537
538 #[must_use]
543 pub fn out_degree_by_index(&self, idx: usize) -> usize {
544 self.succ_indices[idx].len()
545 }
546
547 #[must_use]
548 pub fn in_degree_by_index(&self, idx: usize) -> usize {
549 self.pred_indices[idx].len()
550 }
551
552 #[must_use]
553 pub fn degree_by_index(&self, idx: usize) -> usize {
554 self.succ_indices[idx].len() + self.pred_indices[idx].len()
555 }
556
557 #[must_use]
559 pub fn out_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
560 self.nodes.get_index_of(node).map_or_else(Vec::new, |idx| {
562 self.succ_indices[idx]
563 .iter()
564 .map(|&i| {
565 (
566 node,
567 self.nodes
568 .get_index(i)
569 .expect("index rows hold valid node indices")
570 .0
571 .as_str(),
572 )
573 })
574 .collect()
575 })
576 }
577
578 #[must_use]
580 pub fn in_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
581 self.nodes.get_index_of(node).map_or_else(Vec::new, |idx| {
583 self.pred_indices[idx]
584 .iter()
585 .map(|&i| {
586 (
587 self.nodes
588 .get_index(i)
589 .expect("index rows hold valid node indices")
590 .0
591 .as_str(),
592 node,
593 )
594 })
595 .collect()
596 })
597 }
598
599 #[must_use]
602 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
603 self.nodes.get(node)
604 }
605
606 #[must_use]
608 pub fn edge_attrs(&self, source: &str, target: &str) -> Option<&AttrMap> {
609 self.edges.get(&self.edge_pair_key(source, target)?)
610 }
611
612 #[must_use]
615 #[inline]
616 pub fn edge_attrs_by_indices(&self, source_idx: usize, target_idx: usize) -> Option<&AttrMap> {
617 self.edges.get(&(source_idx, target_idx))
618 }
619
620 pub fn edges_indexed(&self) -> impl Iterator<Item = ((usize, usize), &AttrMap)> + '_ {
626 self.edges.iter().map(|(&pair, attrs)| (pair, attrs))
627 }
628
629 #[must_use]
630 pub fn evidence_ledger(&self) -> &EvidenceLedger {
631 self.runtime_policy.decision_log()
632 }
633
634 #[must_use]
635 pub fn runtime_policy(&self) -> &RuntimePolicy {
636 &self.runtime_policy
637 }
638
639 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
640 self.mode = runtime_policy.mode();
641 self.runtime_policy = runtime_policy;
642 }
643
644 #[must_use]
646 pub fn is_directed(&self) -> bool {
647 true
648 }
649
650 #[must_use]
652 pub fn is_multigraph(&self) -> bool {
653 false
654 }
655
656 pub fn add_node(&mut self, node: impl Into<String>) -> bool {
661 self.add_node_with_attrs(node, AttrMap::new())
662 }
663
664 pub fn add_node_with_attrs(&mut self, node: impl Into<String>, attrs: AttrMap) -> bool {
665 let node = node.into();
666 let existed = self.nodes.contains_key(&node);
667 let mut changed = !existed;
668 let attrs_count = {
669 let bucket = self.nodes.entry(node.clone()).or_default();
670 if !attrs.is_empty()
671 && attrs
672 .iter()
673 .any(|(key, value)| bucket.get(key) != Some(value))
674 {
675 changed = true;
676 }
677 bucket.extend(attrs);
678 bucket.len()
679 };
680 if self.succ_indices.len() < self.nodes.len() {
681 self.succ_indices.push(Vec::new());
682 self.pred_indices.push(Vec::new());
683 }
684 if changed {
685 self.revision = self.revision.saturating_add(1);
686 }
687 self.record_decision(
688 "add_node",
689 0.0,
690 false,
691 vec![
692 EvidenceTerm {
693 signal: "node_preexisting".to_owned(),
694 observed_value: existed.to_string(),
695 log_likelihood_ratio: -3.0,
696 },
697 EvidenceTerm {
698 signal: "attrs_count".to_owned(),
699 observed_value: attrs_count.to_string(),
700 log_likelihood_ratio: -1.0,
701 },
702 ],
703 );
704 changed
705 }
706
707 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
708 if defaults.is_empty() {
709 return false;
710 }
711 let mut inserted = 0usize;
712 for attrs in self.nodes.values_mut() {
713 for (key, value) in defaults {
714 if !attrs.contains_key(key) {
715 attrs.insert(key.clone(), value.clone());
716 inserted += 1;
717 }
718 }
719 }
720 if inserted > 0 {
721 self.revision = self.revision.saturating_add(1);
722 self.record_decision(
723 "apply_node_defaults",
724 0.0,
725 false,
726 vec![EvidenceTerm {
727 signal: "defaults_applied".to_owned(),
728 observed_value: inserted.to_string(),
729 log_likelihood_ratio: 0.0,
730 }],
731 );
732 return true;
733 }
734 false
735 }
736
737 pub fn add_edge(
738 &mut self,
739 source: impl Into<String>,
740 target: impl Into<String>,
741 ) -> Result<(), GraphError> {
742 self.add_edge_with_attrs(source, target, AttrMap::new())
743 }
744
745 pub fn add_edge_with_attrs(
746 &mut self,
747 source: impl Into<String>,
748 target: impl Into<String>,
749 attrs: AttrMap,
750 ) -> Result<(), GraphError> {
751 let source = source.into();
752 let target = target.into();
753
754 let unknown_feature = attrs
755 .keys()
756 .any(|key| key.starts_with("__fnx_incompatible"));
757 let self_loop = source == target;
758 let incompatibility_probability = if unknown_feature {
759 1.0
760 } else if self_loop {
761 0.22
762 } else {
763 0.08
764 };
765
766 let action = self.record_decision(
767 "add_edge",
768 incompatibility_probability,
769 unknown_feature,
770 vec![EvidenceTerm {
771 signal: "unknown_incompatible_feature".to_owned(),
772 observed_value: unknown_feature.to_string(),
773 log_likelihood_ratio: 12.0,
774 }],
775 );
776
777 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
778 return Err(GraphError::FailClosed {
779 operation: "add_edge",
780 reason: "incompatible edge metadata".to_owned(),
781 });
782 }
783
784 let mut source_autocreated = false;
786 if !self.nodes.contains_key(&source) {
787 let _ = self.add_node(source.clone());
788 source_autocreated = true;
789 }
790 let mut target_autocreated = false;
791 if self_loop {
792 target_autocreated = source_autocreated;
793 } else if !self.nodes.contains_key(&target) {
794 let _ = self.add_node(target.clone());
795 target_autocreated = true;
796 }
797
798 let edge_key = (
799 self.nodes.get_index_of(&source).expect("autocreated above"),
800 self.nodes.get_index_of(&target).expect("autocreated above"),
801 );
802 let new_edge = !self.edges.contains_key(&edge_key);
803 let mut changed = new_edge;
804 let edge_attr_count = {
805 let edge_attrs = self.edges.entry(edge_key).or_default();
806 if !attrs.is_empty()
807 && attrs
808 .iter()
809 .any(|(key, value)| edge_attrs.get(key) != Some(value))
810 {
811 changed = true;
812 }
813 edge_attrs.extend(attrs);
814 edge_attrs.len()
815 };
816 if new_edge {
819 let s_idx = self
820 .nodes
821 .get_index_of(&source)
822 .expect("source node exists");
823 let t_idx = self
824 .nodes
825 .get_index_of(&target)
826 .expect("target node exists");
827 self.succ_indices[s_idx].push(t_idx);
828 self.pred_indices[t_idx].push(s_idx);
829 }
830
831 if changed {
832 self.revision = self.revision.saturating_add(1);
833 }
834
835 self.record_decision(
836 "add_edge",
837 incompatibility_probability,
838 unknown_feature,
839 vec![
840 EvidenceTerm {
841 signal: "self_loop".to_owned(),
842 observed_value: self_loop.to_string(),
843 log_likelihood_ratio: -0.5,
844 },
845 EvidenceTerm {
846 signal: "edge_attr_count".to_owned(),
847 observed_value: edge_attr_count.to_string(),
848 log_likelihood_ratio: -2.0,
849 },
850 EvidenceTerm {
851 signal: "source_autocreated".to_owned(),
852 observed_value: source_autocreated.to_string(),
853 log_likelihood_ratio: -1.25,
854 },
855 EvidenceTerm {
856 signal: "target_autocreated".to_owned(),
857 observed_value: target_autocreated.to_string(),
858 log_likelihood_ratio: -1.25,
859 },
860 ],
861 );
862
863 Ok(())
864 }
865
866 #[must_use]
877 pub fn extend_edges_unrecorded<I, S, T>(&mut self, edges: I) -> usize
878 where
879 I: IntoIterator<Item = (S, T)>,
880 S: Into<String>,
881 T: Into<String>,
882 {
883 let iterator = edges.into_iter();
889 let (lower_bound, _) = iterator.size_hint();
890 self.nodes.reserve(lower_bound);
891 self.succ_indices.reserve(lower_bound);
892 self.pred_indices.reserve(lower_bound);
893 self.edges.reserve(lower_bound);
894
895 let mut inserted = 0usize;
896 for (source, target) in iterator {
897 let source = source.into();
898 let target = target.into();
899 let s_idx = match self.nodes.get_index_of(&source) {
900 Some(index) => index,
901 None => {
902 let index = self.nodes.len();
903 self.nodes.insert(source.clone(), AttrMap::new());
904 self.succ_indices.push(Vec::new());
905 self.pred_indices.push(Vec::new());
906 index
907 }
908 };
909 let t_idx = if source == target {
910 s_idx
911 } else {
912 match self.nodes.get_index_of(&target) {
913 Some(index) => index,
914 None => {
915 let index = self.nodes.len();
916 self.nodes.insert(target.clone(), AttrMap::new());
917 self.succ_indices.push(Vec::new());
918 self.pred_indices.push(Vec::new());
919 index
920 }
921 }
922 };
923 let edge_key = (s_idx, t_idx);
924 if self.edges.contains_key(&edge_key) {
925 continue;
926 }
927 self.edges.insert(edge_key, AttrMap::new());
928 self.succ_indices[s_idx].push(t_idx);
929 self.pred_indices[t_idx].push(s_idx);
930 inserted += 1;
931 }
932
933 if inserted > 0 {
934 self.revision = self
935 .revision
936 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
937 self.record_decision(
938 "extend_edges_unrecorded",
939 0.0,
940 false,
941 vec![EvidenceTerm {
942 signal: "batch_edge_count".to_owned(),
943 observed_value: inserted.to_string(),
944 log_likelihood_ratio: -1.0,
945 }],
946 );
947 }
948
949 inserted
950 }
951
952 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
957 where
958 I: IntoIterator<Item = N>,
959 N: Into<String>,
960 {
961 let iterator = nodes.into_iter();
962 let (lower_bound, _) = iterator.size_hint();
963 self.nodes.reserve(lower_bound);
964 self.succ_indices.reserve(lower_bound);
965 self.pred_indices.reserve(lower_bound);
966
967 let mut inserted = 0usize;
968 for node in iterator {
969 let node = node.into();
970 if self.nodes.contains_key(&node) {
971 continue;
972 }
973 self.nodes.insert(node.clone(), AttrMap::new());
974 self.succ_indices.push(Vec::new());
975 self.pred_indices.push(Vec::new());
976 inserted += 1;
977 }
978 if inserted > 0 {
979 self.revision = self
980 .revision
981 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
982 self.record_decision(
983 "extend_nodes_unrecorded",
984 0.0,
985 false,
986 vec![EvidenceTerm {
987 signal: "batch_node_count".to_owned(),
988 observed_value: inserted.to_string(),
989 log_likelihood_ratio: -1.0,
990 }],
991 );
992 }
993 inserted
994 }
995
996 #[must_use]
1002 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
1003 where
1004 I: IntoIterator<Item = (String, AttrMap)>,
1005 {
1006 let mut inserted = 0usize;
1007 for (node, attrs) in nodes {
1008 if let Some(existing) = self.nodes.get_mut(&node) {
1009 existing.extend(attrs);
1010 continue;
1011 }
1012 self.nodes.insert(node.clone(), attrs);
1013 self.succ_indices.push(Vec::new());
1014 self.pred_indices.push(Vec::new());
1015 inserted += 1;
1016 }
1017 if inserted > 0 {
1018 self.revision = self
1019 .revision
1020 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1021 self.record_decision(
1022 "extend_nodes_unrecorded",
1023 0.0,
1024 false,
1025 vec![EvidenceTerm {
1026 signal: "batch_node_count".to_owned(),
1027 observed_value: inserted.to_string(),
1028 log_likelihood_ratio: -1.0,
1029 }],
1030 );
1031 }
1032 inserted
1033 }
1034
1035 #[must_use]
1042 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1043 where
1044 I: IntoIterator<Item = (String, String, AttrMap)>,
1045 {
1046 let mut inserted = 0usize;
1047 let mut merged_changed = false;
1048 for (source, target, attrs) in edges {
1049 if !self.nodes.contains_key(&source) {
1050 self.nodes.insert(source.clone(), AttrMap::new());
1051 self.succ_indices.push(Vec::new());
1052 self.pred_indices.push(Vec::new());
1053 }
1054 if source != target && !self.nodes.contains_key(&target) {
1055 self.nodes.insert(target.clone(), AttrMap::new());
1056 self.succ_indices.push(Vec::new());
1057 self.pred_indices.push(Vec::new());
1058 }
1059 let edge_key = (
1060 self.nodes.get_index_of(&source).expect("created above"),
1061 self.nodes.get_index_of(&target).expect("created above"),
1062 );
1063 if let Some(existing) = self.edges.get_mut(&edge_key) {
1064 if !attrs.is_empty()
1065 && attrs
1066 .iter()
1067 .any(|(key, value)| existing.get(key) != Some(value))
1068 {
1069 merged_changed = true;
1070 }
1071 existing.extend(attrs);
1072 continue;
1073 }
1074 self.edges.insert(edge_key, attrs);
1075 let s_idx = self
1076 .nodes
1077 .get_index_of(&source)
1078 .expect("source node exists");
1079 let t_idx = self
1080 .nodes
1081 .get_index_of(&target)
1082 .expect("target node exists");
1083 self.succ_indices[s_idx].push(t_idx);
1084 self.pred_indices[t_idx].push(s_idx);
1085 inserted += 1;
1086 }
1087 if inserted > 0 || merged_changed {
1088 self.revision = self
1089 .revision
1090 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1091 self.record_decision(
1092 "extend_edges_unrecorded",
1093 0.0,
1094 false,
1095 vec![EvidenceTerm {
1096 signal: "batch_edge_count".to_owned(),
1097 observed_value: inserted.to_string(),
1098 log_likelihood_ratio: -1.0,
1099 }],
1100 );
1101 }
1102 inserted
1103 }
1104
1105 #[must_use]
1110 pub fn extend_prepared_edges_with_attrs_row_staged_unrecorded<I, N>(
1111 &mut self,
1112 new_nodes: N,
1113 edges: I,
1114 ) -> usize
1115 where
1116 I: IntoIterator<Item = (String, String, AttrMap)>,
1117 N: IntoIterator<Item = String>,
1118 {
1119 for node in new_nodes {
1120 if self.nodes.contains_key(&node) {
1121 continue;
1122 }
1123 self.nodes.insert(node.clone(), AttrMap::new());
1124 self.succ_indices.push(Vec::new());
1125 self.pred_indices.push(Vec::new());
1126 }
1127
1128 let mut inserted = 0usize;
1129 let mut merged_changed = false;
1130 let mut successor_rows: IndexMap<String, Vec<String>> = IndexMap::new();
1131 let mut predecessor_rows: IndexMap<String, Vec<String>> = IndexMap::new();
1132
1133 for (source, target, attrs) in edges {
1134 let edge_key = (
1135 self.nodes.get_index_of(&source).expect("created above"),
1136 self.nodes.get_index_of(&target).expect("created above"),
1137 );
1138 if let Some(existing) = self.edges.get_mut(&edge_key) {
1139 if !attrs.is_empty()
1140 && attrs
1141 .iter()
1142 .any(|(key, value)| existing.get(key) != Some(value))
1143 {
1144 merged_changed = true;
1145 }
1146 existing.extend(attrs);
1147 continue;
1148 }
1149
1150 successor_rows
1151 .entry(source.clone())
1152 .or_default()
1153 .push(target.clone());
1154 predecessor_rows
1155 .entry(target.clone())
1156 .or_default()
1157 .push(source.clone());
1158 let s_idx = self
1159 .nodes
1160 .get_index_of(&source)
1161 .expect("source node exists");
1162 let t_idx = self
1163 .nodes
1164 .get_index_of(&target)
1165 .expect("target node exists");
1166 self.succ_indices[s_idx].push(t_idx);
1167 self.pred_indices[t_idx].push(s_idx);
1168 self.edges.insert(edge_key, attrs);
1169 inserted += 1;
1170 }
1171
1172 if inserted > 0 || merged_changed {
1173 self.revision = self
1174 .revision
1175 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1176 self.record_decision(
1177 "extend_edges_unrecorded",
1178 0.0,
1179 false,
1180 vec![EvidenceTerm {
1181 signal: "batch_edge_count".to_owned(),
1182 observed_value: inserted.to_string(),
1183 log_likelihood_ratio: -1.0,
1184 }],
1185 );
1186 }
1187
1188 inserted
1189 }
1190
1191 #[must_use]
1200 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1206 where
1207 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1208 {
1209 let node_count = self.nodes.len();
1210 let edges = edges.into_iter();
1211 self.edges.reserve(edges.size_hint().0);
1212 let mut inserted = 0usize;
1213 let mut merged_changed = false;
1214 for (source_idx, target_idx, attrs) in edges {
1215 debug_assert!(source_idx < node_count && target_idx < node_count);
1216 let edge_key = (source_idx, target_idx);
1217 if let Some(existing) = self.edges.get_mut(&edge_key) {
1218 if !attrs.is_empty()
1219 && attrs
1220 .iter()
1221 .any(|(key, value)| existing.get(key) != Some(value))
1222 {
1223 merged_changed = true;
1224 }
1225 existing.extend(attrs);
1226 continue;
1227 }
1228 self.edges.insert(edge_key, attrs);
1229 self.succ_indices[source_idx].push(target_idx);
1230 self.pred_indices[target_idx].push(source_idx);
1231 inserted += 1;
1232 }
1233 if inserted > 0 || merged_changed {
1234 self.revision = self
1235 .revision
1236 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1237 }
1238 inserted
1239 }
1240
1241 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
1242 &mut self,
1243 nodes: N,
1244 edges: I,
1245 ) -> usize
1246 where
1247 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1248 N: IntoIterator<Item = String>,
1249 {
1250 if !self.nodes.is_empty()
1251 || !self.succ_indices.is_empty()
1252 || !self.pred_indices.is_empty()
1253 || !self.edges.is_empty()
1254 {
1255 return 0;
1256 }
1257
1258 let node_labels: Vec<String> = nodes.into_iter().collect();
1259 self.nodes.reserve(node_labels.len());
1260 for node in node_labels {
1261 self.nodes.insert(node, AttrMap::new());
1262 }
1263 let node_count = self.nodes.len();
1264 self.succ_indices = vec![Vec::new(); node_count];
1265 self.pred_indices = vec![Vec::new(); node_count];
1266
1267 let mut inserted = 0usize;
1268 let mut merged_changed = false;
1269 let edges = edges.into_iter();
1270 self.edges.reserve(edges.size_hint().0);
1271 for (source_idx, target_idx, attrs) in edges {
1272 if source_idx >= node_count || target_idx >= node_count {
1273 continue;
1274 }
1275 let edge_key = (source_idx, target_idx);
1276 if let Some(existing) = self.edges.get_mut(&edge_key) {
1277 if !attrs.is_empty()
1278 && attrs
1279 .iter()
1280 .any(|(key, value)| existing.get(key) != Some(value))
1281 {
1282 merged_changed = true;
1283 }
1284 existing.extend(attrs);
1285 continue;
1286 }
1287 self.edges.insert(edge_key, attrs);
1288 self.succ_indices[source_idx].push(target_idx);
1289 self.pred_indices[target_idx].push(source_idx);
1290 inserted += 1;
1291 }
1292
1293 if node_count > 0 || inserted > 0 || merged_changed {
1294 self.revision = self.revision.saturating_add(
1295 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
1296 );
1297 self.record_decision(
1298 "extend_fresh_index_edges_unrecorded",
1299 0.0,
1300 false,
1301 vec![
1302 EvidenceTerm {
1303 signal: "batch_node_count".to_owned(),
1304 observed_value: node_count.to_string(),
1305 log_likelihood_ratio: -1.0,
1306 },
1307 EvidenceTerm {
1308 signal: "batch_edge_count".to_owned(),
1309 observed_value: inserted.to_string(),
1310 log_likelihood_ratio: -1.0,
1311 },
1312 ],
1313 );
1314 }
1315
1316 inserted
1317 }
1318
1319 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
1320 if defaults.is_empty() {
1321 return false;
1322 }
1323 let mut inserted = 0usize;
1324 for attrs in self.edges.values_mut() {
1325 for (key, value) in defaults {
1326 if !attrs.contains_key(key) {
1327 attrs.insert(key.clone(), value.clone());
1328 inserted += 1;
1329 }
1330 }
1331 }
1332 if inserted > 0 {
1333 self.revision = self.revision.saturating_add(1);
1334 self.record_decision(
1335 "apply_edge_defaults",
1336 0.0,
1337 false,
1338 vec![EvidenceTerm {
1339 signal: "defaults_applied".to_owned(),
1340 observed_value: inserted.to_string(),
1341 log_likelihood_ratio: 0.0,
1342 }],
1343 );
1344 return true;
1345 }
1346 false
1347 }
1348
1349 pub fn replace_edge_attrs(&mut self, source: &str, target: &str, attrs: AttrMap) -> bool {
1351 let Some(edge_key) = self.edge_pair_key(source, target) else {
1352 return false;
1353 };
1354 if let Some(slot) = self.edges.get_mut(&edge_key) {
1355 if *slot != attrs {
1356 *slot = attrs;
1357 self.revision = self.revision.saturating_add(1);
1358 }
1359 true
1360 } else {
1361 false
1362 }
1363 }
1364
1365 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
1367 if let Some(slot) = self.nodes.get_mut(node) {
1368 if *slot != attrs {
1369 *slot = attrs;
1370 self.revision = self.revision.saturating_add(1);
1371 }
1372 true
1373 } else {
1374 false
1375 }
1376 }
1377
1378 pub fn remove_edge(&mut self, source: &str, target: &str) -> bool {
1380 let Some(pair) = self.edge_pair_key(source, target) else {
1381 return false;
1382 };
1383 let removed = self.edges.shift_remove(&pair).is_some();
1384 if removed {
1385 if let (Some(s_idx), Some(t_idx)) = (
1386 self.nodes.get_index_of(source),
1387 self.nodes.get_index_of(target),
1388 ) {
1389 self.succ_indices[s_idx].retain(|&i| i != t_idx);
1390 self.pred_indices[t_idx].retain(|&i| i != s_idx);
1391 }
1392 self.revision = self.revision.saturating_add(1);
1393 }
1394 removed
1395 }
1396
1397 pub fn remove_node(&mut self, node: &str) -> bool {
1399 if !self.nodes.contains_key(node) {
1400 return false;
1401 }
1402
1403 let node_idx = self
1415 .nodes
1416 .get_index_of(node)
1417 .expect("node existence checked above");
1418 let targets: Vec<usize> = self.succ_indices[node_idx].clone();
1419 for t in targets {
1420 self.edges.swap_remove(&(node_idx, t));
1421 }
1422 let sources: Vec<usize> = self.pred_indices[node_idx].clone();
1423 for s in sources {
1424 self.edges.swap_remove(&(s, node_idx));
1425 }
1426
1427 let idx = self
1431 .nodes
1432 .get_index_of(node)
1433 .expect("node existence checked above");
1434 self.nodes.shift_remove(node);
1435 self.succ_indices.remove(idx);
1436 self.pred_indices.remove(idx);
1437 for rows in [&mut self.succ_indices, &mut self.pred_indices] {
1438 for row in rows.iter_mut() {
1439 row.retain(|&e| e != idx);
1440 for e in row.iter_mut() {
1441 if *e > idx {
1442 *e -= 1;
1443 }
1444 }
1445 }
1446 }
1447 self.edges = std::mem::take(&mut self.edges)
1450 .into_iter()
1451 .map(|((s, t), attrs)| {
1452 (
1453 (
1454 if s > idx { s - 1 } else { s },
1455 if t > idx { t - 1 } else { t },
1456 ),
1457 attrs,
1458 )
1459 })
1460 .collect();
1461 self.revision = self.revision.saturating_add(1);
1462 true
1463 }
1464
1465 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
1466 where
1467 I: IntoIterator<Item = &'a str>,
1468 {
1469 let remove_set: std::collections::HashSet<&str> = nodes
1470 .into_iter()
1471 .filter(|node| self.nodes.contains_key(*node))
1472 .collect();
1473 if remove_set.is_empty() {
1474 return (0, 0);
1475 }
1476
1477 let old_node_count = self.nodes.len();
1478 let old_edge_count = self.edges.len();
1479
1480 let removed_mask: Vec<bool> = self
1484 .nodes
1485 .keys()
1486 .map(|k| remove_set.contains(k.as_str()))
1487 .collect();
1488 let mut remap: Vec<usize> = Vec::with_capacity(old_node_count);
1489 let mut next = 0usize;
1490 for &gone in &removed_mask {
1491 remap.push(next);
1492 if !gone {
1493 next += 1;
1494 }
1495 }
1496 for rows in [&mut self.succ_indices, &mut self.pred_indices] {
1497 let old_rows = std::mem::take(rows);
1498 *rows = old_rows
1499 .into_iter()
1500 .enumerate()
1501 .filter(|(old_idx, _)| !removed_mask[*old_idx])
1502 .map(|(_, row)| {
1503 row.into_iter()
1504 .filter(|&nbr| !removed_mask[nbr])
1505 .map(|nbr| remap[nbr])
1506 .collect()
1507 })
1508 .collect();
1509 }
1510 self.nodes
1511 .retain(|node, _| !remove_set.contains(node.as_str()));
1512 self.edges
1513 .retain(|&(s, t), _| !removed_mask[s] && !removed_mask[t]);
1514 self.edges = std::mem::take(&mut self.edges)
1516 .into_iter()
1517 .map(|((s, t), attrs)| ((remap[s], remap[t]), attrs))
1518 .collect();
1519
1520 let removed_nodes = old_node_count - self.nodes.len();
1521 let removed_edges = old_edge_count - self.edges.len();
1522 self.revision = self
1523 .revision
1524 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
1525 (removed_nodes, removed_edges)
1526 }
1527
1528 #[must_use]
1535 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
1536 let mut ordered = Vec::with_capacity(self.edges.len());
1537
1538 for (u, row) in self.succ_indices.iter().enumerate() {
1539 let node = self.nodes.get_index(u).expect("valid index").0;
1540 for &t in row {
1541 let target = self.nodes.get_index(t).expect("valid index").0;
1542 if let Some(attrs) = self.edges.get(&(u, t)) {
1543 ordered.push(EdgeSnapshot {
1544 left: node.clone(),
1545 right: target.clone(),
1546 attrs: attrs.clone(),
1547 });
1548 }
1549 }
1550 }
1551
1552 ordered
1553 }
1554
1555 #[must_use]
1556 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
1557 let mut ordered = Vec::with_capacity(self.edges.len());
1558
1559 for (u, row) in self.succ_indices.iter().enumerate() {
1560 let node = self.nodes.get_index(u).expect("valid index").0;
1561 for &t in row {
1562 let target = self.nodes.get_index(t).expect("valid index").0;
1563 if let Some(attrs) = self.edges.get(&(u, t)) {
1564 ordered.push((node.as_str(), target.as_str(), attrs));
1565 }
1566 }
1567 }
1568
1569 ordered
1570 }
1571
1572 #[must_use]
1587 pub fn reversed(&self) -> Self {
1588 let n = self.nodes.len();
1589 let mut succ_indices: Vec<Vec<usize>> = vec![Vec::new(); n];
1590 let mut pred_indices: Vec<Vec<usize>> = vec![Vec::new(); n];
1591 for (i, (succ_row, pred_row)) in succ_indices
1592 .iter_mut()
1593 .zip(pred_indices.iter_mut())
1594 .enumerate()
1595 {
1596 succ_row.reserve(self.pred_indices[i].len());
1599 pred_row.reserve(self.succ_indices[i].len());
1600 }
1601 let mut edges: IndexMap<(usize, usize), AttrMap> =
1602 IndexMap::with_capacity(self.edges.len());
1603 for (u, (source_succs, reversed_preds)) in self
1604 .succ_indices
1605 .iter()
1606 .zip(pred_indices.iter_mut())
1607 .enumerate()
1608 {
1609 for &t in source_succs {
1610 succ_indices[t].push(u);
1612 reversed_preds.push(t);
1613 if let Some(attrs) = self.edges.get(&(u, t)) {
1614 edges.insert((t, u), attrs.clone());
1615 }
1616 }
1617 }
1618 Self {
1619 mode: self.mode,
1620 revision: 0,
1621 nodes: self.nodes.clone(),
1622 succ_indices,
1623 pred_indices,
1624 edges,
1625 runtime_policy: self.runtime_policy.clone(),
1626 csr_cache: std::sync::Arc::default(),
1627 all_int_cache: std::sync::Arc::default(),
1628 }
1629 }
1630
1631 #[must_use]
1633 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
1634 let mut ordered = Vec::with_capacity(self.edges.len());
1635
1636 for (u, row) in self.succ_indices.iter().enumerate() {
1637 for &t in row {
1638 if self.edges.contains_key(&(u, t)) {
1639 ordered.push((u, t));
1640 }
1641 }
1642 }
1643
1644 ordered
1645 }
1646
1647 #[must_use]
1648 pub fn snapshot(&self) -> DiGraphSnapshot {
1649 let node_attrs: std::collections::BTreeMap<String, AttrMap> = self
1650 .nodes
1651 .iter()
1652 .filter(|(_, attrs)| !attrs.is_empty())
1653 .map(|(name, attrs)| (name.clone(), attrs.clone()))
1654 .collect();
1655 DiGraphSnapshot {
1656 mode: self.mode,
1657 nodes: self.nodes.keys().cloned().collect(),
1658 node_attrs,
1659 edges: self.edges_ordered(),
1660 }
1661 }
1662
1663 #[must_use]
1667 pub fn to_undirected(&self) -> crate::Graph {
1668 let mut g = crate::Graph::with_runtime_policy(self.runtime_policy.clone());
1669 for (node, attrs) in &self.nodes {
1670 g.add_node_with_attrs(node.clone(), attrs.clone());
1671 }
1672 for snap in self.edges_ordered() {
1678 let _ = g.add_edge_with_attrs(snap.left, snap.right, snap.attrs);
1679 }
1680 g
1681 }
1682
1683 fn record_decision(
1688 &mut self,
1689 operation: &'static str,
1690 incompatibility_probability: f64,
1691 unknown_incompatible_feature: bool,
1692 evidence: Vec<EvidenceTerm>,
1693 ) -> DecisionAction {
1694 let action = self
1695 .runtime_policy
1696 .action_for(incompatibility_probability, unknown_incompatible_feature);
1697 self.runtime_policy.record(
1698 operation,
1699 action,
1700 incompatibility_probability,
1701 "argmin expected loss over {allow,full_validate,fail_closed}",
1702 evidence,
1703 );
1704 action
1705 }
1706}
1707
1708#[derive(Debug, Clone)]
1709pub struct MultiDiGraph {
1710 mode: CompatibilityMode,
1711 revision: u64,
1712 nodes: IndexMap<String, AttrMap>,
1713 successors: IndexMap<String, IndexMap<String, IndexSet<usize>>>,
1714 predecessors: IndexMap<String, IndexMap<String, IndexSet<usize>>>,
1715 edges: IndexMap<DirectedEdgeKey, IndexMap<usize, AttrMap>>,
1716 runtime_policy: RuntimePolicy,
1717 edge_count: usize,
1718}
1719
1720impl MultiDiGraph {
1721 #[must_use]
1724 pub fn clone_with_fresh_policy(&self) -> Self {
1725 Self {
1726 mode: self.mode,
1727 revision: self.revision,
1728 nodes: self.nodes.clone(),
1729 successors: self.successors.clone(),
1730 predecessors: self.predecessors.clone(),
1731 edges: self.edges.clone(),
1732 runtime_policy: RuntimePolicy::new(self.mode),
1733 edge_count: self.edge_count,
1734 }
1735 }
1736
1737 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
1743 let mut order_map: IndexMap<String, Vec<String>> =
1751 IndexMap::with_capacity(self.predecessors.len());
1752 for (u, succ_row) in &self.successors {
1753 for v in succ_row.keys() {
1754 order_map.entry(v.clone()).or_default().push(u.clone());
1755 }
1756 }
1757 let orders: Vec<(String, Vec<String>)> = order_map.into_iter().collect();
1758 self.apply_row_orders(&orders, true);
1759 }
1760
1761 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
1765 let map = if pred {
1766 &mut self.predecessors
1767 } else {
1768 &mut self.successors
1769 };
1770 for (node, order) in orders {
1771 let Some(row) = map.get_mut(node.as_str()) else {
1772 continue;
1773 };
1774 let mut old = std::mem::take(row);
1775 let mut new_row = IndexMap::with_capacity(old.len());
1776 for v in order {
1777 if let Some((k, val)) = old.shift_remove_entry(v.as_str()) {
1778 new_row.insert(k, val);
1779 }
1780 }
1781 for (k, val) in old {
1782 new_row.insert(k, val);
1783 }
1784 *row = new_row;
1785 }
1786 }
1787
1788 #[must_use]
1789 pub fn new(mode: CompatibilityMode) -> Self {
1790 Self {
1791 mode,
1792 revision: 0,
1793 nodes: IndexMap::new(),
1794 successors: IndexMap::new(),
1795 predecessors: IndexMap::new(),
1796 edges: IndexMap::new(),
1797 runtime_policy: RuntimePolicy::new(mode),
1798 edge_count: 0,
1799 }
1800 }
1801
1802 #[must_use]
1803 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
1804 let mode = runtime_policy.mode();
1805 Self {
1806 mode,
1807 revision: 0,
1808 nodes: IndexMap::new(),
1809 successors: IndexMap::new(),
1810 predecessors: IndexMap::new(),
1811 edges: IndexMap::new(),
1812 runtime_policy,
1813 edge_count: 0,
1814 }
1815 }
1816
1817 #[must_use]
1818 pub fn strict() -> Self {
1819 Self::new(CompatibilityMode::Strict)
1820 }
1821
1822 #[must_use]
1823 pub fn hardened() -> Self {
1824 Self::new(CompatibilityMode::Hardened)
1825 }
1826
1827 #[must_use]
1828 pub fn mode(&self) -> CompatibilityMode {
1829 self.mode
1830 }
1831
1832 #[must_use]
1833 pub fn node_count(&self) -> usize {
1834 self.nodes.len()
1835 }
1836
1837 #[must_use]
1838 pub fn edge_count(&self) -> usize {
1839 self.edge_count
1840 }
1841
1842 #[must_use]
1843 pub fn number_of_selfloops(&self) -> usize {
1844 self.edges
1845 .iter()
1846 .filter(|(edge_key, _)| edge_key.source == edge_key.target)
1847 .map(|(_, edge_bucket)| edge_bucket.len())
1848 .sum()
1849 }
1850
1851 #[must_use]
1854 pub fn edge_keys(&self, source: &str, target: &str) -> Option<Vec<usize>> {
1855 self.successors
1856 .get(source)?
1857 .get(target)
1858 .map(|keys| keys.iter().copied().collect())
1859 }
1860
1861 pub fn edge_keys_iter(
1863 &self,
1864 source: &str,
1865 target: &str,
1866 ) -> Option<impl Iterator<Item = &usize>> {
1867 self.successors
1868 .get(source)?
1869 .get(target)
1870 .map(|keys| keys.iter())
1871 }
1872
1873 #[must_use]
1874 pub fn revision(&self) -> u64 {
1875 self.revision
1876 }
1877
1878 #[must_use]
1879 pub fn has_node(&self, node: &str) -> bool {
1880 self.nodes.contains_key(node)
1881 }
1882
1883 #[must_use]
1884 pub fn has_edge(&self, source: &str, target: &str) -> bool {
1885 self.edges
1886 .get(&DirectedEdgeKeyRef::new(source, target))
1887 .is_some_and(|edge_bucket| !edge_bucket.is_empty())
1888 }
1889
1890 #[must_use]
1891 pub fn nodes_ordered(&self) -> Vec<&str> {
1892 self.nodes.keys().map(String::as_str).collect()
1893 }
1894
1895 #[must_use]
1896 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
1897 self.successors
1898 .get(node)
1899 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
1900 }
1901
1902 #[must_use]
1903 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
1904 self.predecessors
1905 .get(node)
1906 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
1907 }
1908
1909 #[must_use]
1910 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
1911 self.successors(node)
1912 }
1913
1914 #[must_use]
1915 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
1916 self.nodes.get(node)
1917 }
1918
1919 #[must_use]
1920 pub fn edge_attrs(&self, source: &str, target: &str, key: usize) -> Option<&AttrMap> {
1921 self.edges
1922 .get(&DirectedEdgeKeyRef::new(source, target))
1923 .and_then(|edge_bucket| edge_bucket.get(&key))
1924 }
1925
1926 #[must_use]
1927 pub fn evidence_ledger(&self) -> &EvidenceLedger {
1928 self.runtime_policy.decision_log()
1929 }
1930
1931 #[must_use]
1932 pub fn runtime_policy(&self) -> &RuntimePolicy {
1933 &self.runtime_policy
1934 }
1935
1936 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
1937 self.mode = runtime_policy.mode();
1938 self.runtime_policy = runtime_policy;
1939 }
1940
1941 #[must_use]
1942 pub fn is_directed(&self) -> bool {
1943 true
1944 }
1945
1946 #[must_use]
1947 pub fn is_multigraph(&self) -> bool {
1948 true
1949 }
1950
1951 #[must_use]
1953 pub fn out_degree(&self, node: &str) -> usize {
1954 self.successors
1955 .get(node)
1956 .map_or(0, |succs| succs.values().map(IndexSet::len).sum())
1957 }
1958
1959 #[must_use]
1961 pub fn in_degree(&self, node: &str) -> usize {
1962 self.predecessors
1963 .get(node)
1964 .map_or(0, |preds| preds.values().map(IndexSet::len).sum())
1965 }
1966
1967 #[must_use]
1969 pub fn degree(&self, node: &str) -> usize {
1970 self.in_degree(node) + self.out_degree(node)
1971 }
1972
1973 pub fn add_node(&mut self, node: impl Into<String>) -> bool {
1974 self.add_node_with_attrs(node, AttrMap::new())
1975 }
1976
1977 pub fn add_node_with_attrs(&mut self, node: impl Into<String>, attrs: AttrMap) -> bool {
1978 let node = node.into();
1979 let existed = self.nodes.contains_key(&node);
1980 let mut changed = !existed;
1981 let attrs_count = {
1982 let bucket = self.nodes.entry(node.clone()).or_default();
1983 if !attrs.is_empty()
1984 && attrs
1985 .iter()
1986 .any(|(key, value)| bucket.get(key) != Some(value))
1987 {
1988 changed = true;
1989 }
1990 bucket.extend(attrs);
1991 bucket.len()
1992 };
1993 self.successors.entry(node.clone()).or_default();
1994 self.predecessors.entry(node.clone()).or_default();
1995 if changed {
1996 self.revision = self.revision.saturating_add(1);
1997 }
1998 self.record_decision(
1999 "add_node",
2000 0.0,
2001 false,
2002 vec![
2003 EvidenceTerm {
2004 signal: "node_preexisting".to_owned(),
2005 observed_value: existed.to_string(),
2006 log_likelihood_ratio: -3.0,
2007 },
2008 EvidenceTerm {
2009 signal: "attrs_count".to_owned(),
2010 observed_value: attrs_count.to_string(),
2011 log_likelihood_ratio: -1.0,
2012 },
2013 ],
2014 );
2015 changed
2016 }
2017
2018 pub fn add_edge(
2019 &mut self,
2020 source: impl Into<String>,
2021 target: impl Into<String>,
2022 ) -> Result<usize, GraphError> {
2023 self.add_edge_impl(source, target, None, AttrMap::new())
2024 }
2025
2026 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
2030 where
2031 I: IntoIterator<Item = (String, AttrMap)>,
2032 {
2033 let mut inserted = 0usize;
2034 for (node, attrs) in nodes {
2035 if let Some(existing) = self.nodes.get_mut(&node) {
2036 existing.extend(attrs);
2037 continue;
2038 }
2039 self.nodes.insert(node.clone(), attrs);
2040 self.successors.entry(node.clone()).or_default();
2041 self.predecessors.entry(node).or_default();
2042 inserted += 1;
2043 }
2044 if inserted > 0 {
2045 self.revision = self
2046 .revision
2047 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2048 self.record_decision(
2049 "extend_nodes_with_attrs_unrecorded",
2050 0.0,
2051 false,
2052 vec![EvidenceTerm {
2053 signal: "batch_node_count".to_owned(),
2054 observed_value: inserted.to_string(),
2055 log_likelihood_ratio: -1.0,
2056 }],
2057 );
2058 }
2059 inserted
2060 }
2061
2062 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
2069 where
2070 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
2071 {
2072 let mut inserted = 0usize;
2073 for (source, target, key, attrs) in edges {
2074 if !self.nodes.contains_key(&source) {
2075 self.nodes.insert(source.clone(), AttrMap::new());
2076 self.successors.entry(source.clone()).or_default();
2077 self.predecessors.entry(source.clone()).or_default();
2078 }
2079 if source != target && !self.nodes.contains_key(&target) {
2080 self.nodes.insert(target.clone(), AttrMap::new());
2081 self.successors.entry(target.clone()).or_default();
2082 self.predecessors.entry(target.clone()).or_default();
2083 }
2084 let edge_key = DirectedEdgeKey::new(&source, &target);
2085 let bucket = self.edges.entry(edge_key).or_default();
2086 if !bucket.contains_key(&key) {
2087 self.edge_count += 1;
2088 inserted += 1;
2089 }
2090 bucket.entry(key).or_default().extend(attrs);
2091 self.successors
2092 .entry(source.clone())
2093 .or_default()
2094 .entry(target.clone())
2095 .or_default()
2096 .insert(key);
2097 self.predecessors
2098 .entry(target)
2099 .or_default()
2100 .entry(source)
2101 .or_default()
2102 .insert(key);
2103 }
2104 if inserted > 0 {
2105 self.revision = self
2106 .revision
2107 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2108 self.record_decision(
2109 "extend_keyed_edges_with_attrs_unrecorded",
2110 0.0,
2111 false,
2112 vec![EvidenceTerm {
2113 signal: "batch_edge_count".to_owned(),
2114 observed_value: inserted.to_string(),
2115 log_likelihood_ratio: -1.0,
2116 }],
2117 );
2118 }
2119 inserted
2120 }
2121
2122 #[must_use]
2127 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
2128 &mut self,
2129 nodes: N,
2130 edges: I,
2131 ) -> usize
2132 where
2133 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
2134 N: IntoIterator<Item = String>,
2135 {
2136 if !self.nodes.is_empty()
2137 || !self.successors.is_empty()
2138 || !self.predecessors.is_empty()
2139 || !self.edges.is_empty()
2140 {
2141 return 0;
2142 }
2143
2144 let node_labels: Vec<String> = nodes.into_iter().collect();
2145 for node in &node_labels {
2146 self.nodes.insert(node.clone(), AttrMap::new());
2147 self.successors.insert(node.clone(), IndexMap::new());
2148 self.predecessors.insert(node.clone(), IndexMap::new());
2149 }
2150
2151 let node_count = node_labels.len();
2152 let mut inserted = 0usize;
2153 let mut merged_changed = false;
2154 for (source_idx, target_idx, key, attrs) in edges {
2155 let Some(source) = node_labels.get(source_idx) else {
2156 continue;
2157 };
2158 let Some(target) = node_labels.get(target_idx) else {
2159 continue;
2160 };
2161 let edge_key = DirectedEdgeKey::new(source, target);
2162 let bucket = self.edges.entry(edge_key).or_default();
2163 if !bucket.contains_key(&key) {
2164 self.edge_count += 1;
2165 inserted += 1;
2166 }
2167 let edge_attrs = bucket.entry(key).or_default();
2168 if !attrs.is_empty()
2169 && attrs
2170 .iter()
2171 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
2172 {
2173 merged_changed = true;
2174 }
2175 edge_attrs.extend(attrs);
2176 self.successors
2177 .get_mut(source.as_str())
2178 .expect("fresh node row exists")
2179 .entry(target.clone())
2180 .or_default()
2181 .insert(key);
2182 self.predecessors
2183 .get_mut(target.as_str())
2184 .expect("fresh node row exists")
2185 .entry(source.clone())
2186 .or_default()
2187 .insert(key);
2188 }
2189
2190 if node_count > 0 || inserted > 0 || merged_changed {
2191 self.revision = self.revision.saturating_add(
2192 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
2193 );
2194 self.record_decision(
2195 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
2196 0.0,
2197 false,
2198 vec![
2199 EvidenceTerm {
2200 signal: "batch_node_count".to_owned(),
2201 observed_value: node_count.to_string(),
2202 log_likelihood_ratio: -1.0,
2203 },
2204 EvidenceTerm {
2205 signal: "batch_edge_count".to_owned(),
2206 observed_value: inserted.to_string(),
2207 log_likelihood_ratio: -1.0,
2208 },
2209 ],
2210 );
2211 }
2212
2213 inserted
2214 }
2215
2216 pub fn add_edge_with_attrs(
2217 &mut self,
2218 source: impl Into<String>,
2219 target: impl Into<String>,
2220 attrs: AttrMap,
2221 ) -> Result<usize, GraphError> {
2222 self.add_edge_impl(source, target, None, attrs)
2223 }
2224
2225 pub fn add_edge_with_key_and_attrs(
2226 &mut self,
2227 source: impl Into<String>,
2228 target: impl Into<String>,
2229 key: usize,
2230 attrs: AttrMap,
2231 ) -> Result<usize, GraphError> {
2232 self.add_edge_impl(source, target, Some(key), attrs)
2233 }
2234
2235 fn add_edge_impl(
2236 &mut self,
2237 source: impl Into<String>,
2238 target: impl Into<String>,
2239 explicit_key: Option<usize>,
2240 attrs: AttrMap,
2241 ) -> Result<usize, GraphError> {
2242 let source = source.into();
2243 let target = target.into();
2244
2245 let unknown_feature = attrs
2246 .keys()
2247 .any(|key| key.starts_with("__fnx_incompatible"));
2248 let self_loop = source == target;
2249 let incompatibility_probability = if unknown_feature {
2250 1.0
2251 } else if self_loop {
2252 0.22
2253 } else {
2254 0.08
2255 };
2256
2257 let action = self.record_decision(
2258 "add_edge",
2259 incompatibility_probability,
2260 unknown_feature,
2261 vec![EvidenceTerm {
2262 signal: "unknown_incompatible_feature".to_owned(),
2263 observed_value: unknown_feature.to_string(),
2264 log_likelihood_ratio: 12.0,
2265 }],
2266 );
2267
2268 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
2269 return Err(GraphError::FailClosed {
2270 operation: "add_edge",
2271 reason: "incompatible edge metadata".to_owned(),
2272 });
2273 }
2274
2275 let mut source_autocreated = false;
2276 if !self.nodes.contains_key(&source) {
2277 let _ = self.add_node(source.clone());
2278 source_autocreated = true;
2279 }
2280 let mut target_autocreated = false;
2281 if self_loop {
2282 target_autocreated = source_autocreated;
2283 } else if !self.nodes.contains_key(&target) {
2284 let _ = self.add_node(target.clone());
2285 target_autocreated = true;
2286 }
2287
2288 let edge_key = DirectedEdgeKey::new(&source, &target);
2289 let key = explicit_key.unwrap_or_else(|| {
2290 let edge_bucket = self.edges.get(&edge_key);
2291 let mut k = edge_bucket.map_or(0, |b| b.len());
2292 if let Some(b) = edge_bucket {
2293 while b.contains_key(&k) {
2294 k += 1;
2295 }
2296 }
2297 k
2298 });
2299 let mut changed;
2300 let edge_attr_count = {
2301 let edge_bucket = self.edges.entry(edge_key.clone()).or_default();
2302 let is_new = !edge_bucket.contains_key(&key);
2303 if is_new {
2304 self.edge_count += 1;
2305 }
2306 changed = is_new;
2307 let edge_attrs = edge_bucket.entry(key).or_default();
2308 if !attrs.is_empty()
2309 && attrs
2310 .iter()
2311 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
2312 {
2313 changed = true;
2314 }
2315 edge_attrs.extend(attrs);
2316 edge_bucket.len()
2317 };
2318
2319 self.successors
2320 .entry(source.clone())
2321 .or_default()
2322 .entry(target.clone())
2323 .or_default()
2324 .insert(key);
2325 self.predecessors
2326 .entry(target.clone())
2327 .or_default()
2328 .entry(source.clone())
2329 .or_default()
2330 .insert(key);
2331 if changed {
2332 self.revision = self.revision.saturating_add(1);
2333 }
2334
2335 self.record_decision(
2336 "add_edge",
2337 incompatibility_probability,
2338 unknown_feature,
2339 vec![
2340 EvidenceTerm {
2341 signal: "edge_key".to_owned(),
2342 observed_value: key.to_string(),
2343 log_likelihood_ratio: -2.0,
2344 },
2345 EvidenceTerm {
2346 signal: "edge_attr_count".to_owned(),
2347 observed_value: edge_attr_count.to_string(),
2348 log_likelihood_ratio: -2.0,
2349 },
2350 EvidenceTerm {
2351 signal: "source_autocreated".to_owned(),
2352 observed_value: source_autocreated.to_string(),
2353 log_likelihood_ratio: -1.25,
2354 },
2355 EvidenceTerm {
2356 signal: "target_autocreated".to_owned(),
2357 observed_value: target_autocreated.to_string(),
2358 log_likelihood_ratio: -1.25,
2359 },
2360 ],
2361 );
2362
2363 Ok(key)
2364 }
2365
2366 pub fn replace_edge_attrs(
2369 &mut self,
2370 source: &str,
2371 target: &str,
2372 key: usize,
2373 attrs: AttrMap,
2374 ) -> bool {
2375 let edge_key = DirectedEdgeKey::new(source, target);
2376 if let Some(bucket) = self.edges.get_mut(&edge_key)
2377 && let Some(slot) = bucket.get_mut(&key)
2378 {
2379 if *slot != attrs {
2380 *slot = attrs;
2381 self.revision = self.revision.saturating_add(1);
2382 }
2383 return true;
2384 }
2385 false
2386 }
2387
2388 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
2390 if let Some(slot) = self.nodes.get_mut(node) {
2391 if *slot != attrs {
2392 *slot = attrs;
2393 self.revision = self.revision.saturating_add(1);
2394 }
2395 true
2396 } else {
2397 false
2398 }
2399 }
2400
2401 pub fn remove_edge(&mut self, source: &str, target: &str, key: Option<usize>) -> bool {
2402 let edge_key = DirectedEdgeKeyRef::new(source, target);
2403 let removal_key = key.or_else(|| {
2404 self.edges
2405 .get(&edge_key)
2406 .and_then(|edge_bucket| edge_bucket.keys().next_back().copied())
2407 });
2408
2409 let Some(removal_key) = removal_key else {
2410 return false;
2411 };
2412
2413 let removed = self
2414 .edges
2415 .get_mut(&edge_key)
2416 .is_some_and(|edge_bucket| edge_bucket.shift_remove(&removal_key).is_some());
2417 if !removed {
2418 return false;
2419 }
2420
2421 self.edge_count -= 1;
2422
2423 let should_drop_bucket = self.edges.get(&edge_key).is_some_and(IndexMap::is_empty);
2424 if should_drop_bucket {
2425 self.edges.shift_remove(&edge_key);
2426 }
2427
2428 self.remove_successor_key(source, target, removal_key);
2429 self.remove_predecessor_key(target, source, removal_key);
2430 self.revision = self.revision.saturating_add(1);
2431 true
2432 }
2433
2434 pub fn remove_node(&mut self, node: &str) -> bool {
2435 if !self.nodes.contains_key(node) {
2436 return false;
2437 }
2438
2439 let mut removed_count = 0usize;
2448 if let Some(succs) = self.successors.get(node) {
2449 let targets: Vec<String> = succs.keys().cloned().collect();
2450 for target in targets {
2451 if target != node
2452 && let Some(preds) = self.predecessors.get_mut(&target)
2453 {
2454 preds.shift_remove(node);
2455 }
2456 if let Some(bucket) = self
2457 .edges
2458 .swap_remove(&DirectedEdgeKeyRef::new(node, &target))
2459 {
2460 removed_count += bucket.len();
2461 }
2462 }
2463 }
2464 if let Some(preds) = self.predecessors.get(node) {
2465 let sources: Vec<String> = preds.keys().cloned().collect();
2466 for source in sources {
2467 if source != node
2468 && let Some(succs) = self.successors.get_mut(&source)
2469 {
2470 succs.shift_remove(node);
2471 }
2472 if let Some(bucket) = self
2473 .edges
2474 .swap_remove(&DirectedEdgeKeyRef::new(&source, node))
2475 {
2476 removed_count += bucket.len();
2477 }
2478 }
2479 }
2480 self.edge_count -= removed_count;
2481
2482 self.successors.shift_remove(node);
2483 self.predecessors.shift_remove(node);
2484 self.nodes.shift_remove(node);
2485 self.revision = self.revision.saturating_add(1);
2486 true
2487 }
2488
2489 #[must_use]
2490 pub fn edges_ordered(&self) -> Vec<MultiDiEdgeSnapshot> {
2491 let mut ordered = Vec::with_capacity(self.edge_count());
2492
2493 for node in self.nodes.keys() {
2494 if let Some(neighbors) = self.successors.get(node) {
2495 for target in neighbors.keys() {
2496 let pair = DirectedEdgeKey::new(node, target);
2497 if let Some(edge_bucket) = self.edges.get(&pair) {
2498 for (key, attrs) in edge_bucket {
2499 ordered.push(MultiDiEdgeSnapshot {
2500 source: node.clone(),
2501 target: target.clone(),
2502 key: *key,
2503 attrs: attrs.clone(),
2504 });
2505 }
2506 }
2507 }
2508 }
2509 }
2510
2511 ordered
2512 }
2513
2514 #[must_use]
2515 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
2516 let mut ordered = Vec::with_capacity(self.edge_count());
2517
2518 for node in self.nodes.keys() {
2519 if let Some(neighbors) = self.successors.get(node) {
2520 for target in neighbors.keys() {
2521 let pair = DirectedEdgeKeyRef::new(node, target);
2522 if let Some(edge_bucket) = self.edges.get(&pair) {
2523 for (key, attrs) in edge_bucket {
2524 ordered.push((node.as_str(), target.as_str(), *key, attrs));
2525 }
2526 }
2527 }
2528 }
2529 }
2530
2531 ordered
2532 }
2533
2534 pub fn try_for_each_edge_ordered_borrowed<E>(
2535 &self,
2536 mut visit: impl FnMut(&str, &str, usize, &AttrMap) -> Result<(), E>,
2537 ) -> Result<(), E> {
2538 for node in self.nodes.keys() {
2539 if let Some(neighbors) = self.successors.get(node) {
2540 for target in neighbors.keys() {
2541 let pair = DirectedEdgeKeyRef::new(node, target);
2542 if let Some(edge_bucket) = self.edges.get(&pair) {
2543 for (key, attrs) in edge_bucket {
2544 visit(node.as_str(), target.as_str(), *key, attrs)?;
2545 }
2546 }
2547 }
2548 }
2549 }
2550
2551 Ok(())
2552 }
2553
2554 pub fn try_for_each_indexed_edge_ordered_borrowed<E>(
2555 &self,
2556 mut visit: impl FnMut(usize, usize, &str, &str, usize, &AttrMap) -> Result<(), E>,
2557 ) -> Result<(), E> {
2558 for (source_index, node) in self.nodes.keys().enumerate() {
2559 if let Some(neighbors) = self.successors.get(node) {
2560 for target in neighbors.keys() {
2561 let Some(target_index) = self.nodes.get_index_of(target.as_str()) else {
2562 continue;
2563 };
2564 let pair = DirectedEdgeKeyRef::new(node, target);
2565 if let Some(edge_bucket) = self.edges.get(&pair) {
2566 for (key, attrs) in edge_bucket {
2567 visit(
2568 source_index,
2569 target_index,
2570 node.as_str(),
2571 target.as_str(),
2572 *key,
2573 attrs,
2574 )?;
2575 }
2576 }
2577 }
2578 }
2579 }
2580
2581 Ok(())
2582 }
2583
2584 #[must_use]
2595 pub fn reversed(&self) -> Self {
2596 let mut successors: IndexMap<String, IndexMap<String, IndexSet<usize>>> =
2597 IndexMap::with_capacity(self.nodes.len());
2598 let mut predecessors: IndexMap<String, IndexMap<String, IndexSet<usize>>> =
2599 IndexMap::with_capacity(self.nodes.len());
2600 for node in self.nodes.keys() {
2601 successors.insert(node.clone(), IndexMap::new());
2602 predecessors.insert(node.clone(), IndexMap::new());
2603 }
2604
2605 let mut edges: IndexMap<DirectedEdgeKey, IndexMap<usize, AttrMap>> =
2606 IndexMap::with_capacity(self.edges.len());
2607 let mut edge_count = 0usize;
2608 for source in self.nodes.keys() {
2609 let Some(neighbors) = self.successors.get(source) else {
2610 continue;
2611 };
2612 for target in neighbors.keys() {
2613 let pair = DirectedEdgeKeyRef::new(source, target);
2614 let Some(edge_bucket) = self.edges.get(&pair) else {
2615 continue;
2616 };
2617 let reversed_pair = DirectedEdgeKey::new(target, source);
2618 let reversed_bucket = edges.entry(reversed_pair).or_default();
2619 let reversed_succ_keys = successors
2620 .get_mut(target.as_str())
2621 .expect("target row exists")
2622 .entry(source.clone())
2623 .or_default();
2624 let reversed_pred_keys = predecessors
2625 .get_mut(source.as_str())
2626 .expect("source row exists")
2627 .entry(target.clone())
2628 .or_default();
2629 for (key, attrs) in edge_bucket {
2630 reversed_bucket.insert(*key, attrs.clone());
2631 reversed_succ_keys.insert(*key);
2632 reversed_pred_keys.insert(*key);
2633 edge_count += 1;
2634 }
2635 }
2636 }
2637
2638 Self {
2639 mode: self.mode,
2640 revision: 0,
2641 nodes: self.nodes.clone(),
2642 successors,
2643 predecessors,
2644 edges,
2645 runtime_policy: self.runtime_policy.clone(),
2646 edge_count,
2647 }
2648 }
2649
2650 #[must_use]
2658 pub fn out_edges_ordered_borrowed<'a>(
2659 &'a self,
2660 node: &str,
2661 ) -> Vec<(&'a str, &'a str, usize, &'a AttrMap)> {
2662 let mut ordered = Vec::new();
2663 if let Some((src, neighbors)) = self.successors.get_key_value(node) {
2664 for target in neighbors.keys() {
2665 let pair = DirectedEdgeKeyRef::new(src, target);
2666 if let Some(edge_bucket) = self.edges.get(&pair) {
2667 for (key, attrs) in edge_bucket {
2668 ordered.push((src.as_str(), target.as_str(), *key, attrs));
2669 }
2670 }
2671 }
2672 }
2673 ordered
2674 }
2675
2676 #[must_use]
2677 pub fn snapshot(&self) -> MultiDiGraphSnapshot {
2678 let node_attrs: std::collections::BTreeMap<String, AttrMap> = self
2679 .nodes
2680 .iter()
2681 .filter(|(_, attrs)| !attrs.is_empty())
2682 .map(|(name, attrs)| (name.clone(), attrs.clone()))
2683 .collect();
2684 MultiDiGraphSnapshot {
2685 mode: self.mode,
2686 nodes: self.nodes.keys().cloned().collect(),
2687 node_attrs,
2688 edges: self.edges_ordered(),
2689 }
2690 }
2691
2692 fn remove_successor_key(&mut self, source: &str, target: &str, key: usize) {
2693 let mut drop_neighbor = false;
2694 if let Some(neighbors) = self.successors.get_mut(source)
2695 && let Some(keys) = neighbors.get_mut(target)
2696 {
2697 keys.shift_remove(&key);
2698 drop_neighbor = keys.is_empty();
2699 }
2700 if drop_neighbor && let Some(neighbors) = self.successors.get_mut(source) {
2701 neighbors.shift_remove(target);
2702 }
2703 }
2704
2705 fn remove_predecessor_key(&mut self, target: &str, source: &str, key: usize) {
2706 let mut drop_neighbor = false;
2707 if let Some(neighbors) = self.predecessors.get_mut(target)
2708 && let Some(keys) = neighbors.get_mut(source)
2709 {
2710 keys.shift_remove(&key);
2711 drop_neighbor = keys.is_empty();
2712 }
2713 if drop_neighbor && let Some(neighbors) = self.predecessors.get_mut(target) {
2714 neighbors.shift_remove(source);
2715 }
2716 }
2717
2718 fn record_decision(
2719 &mut self,
2720 operation: &'static str,
2721 incompatibility_probability: f64,
2722 unknown_incompatible_feature: bool,
2723 evidence: Vec<EvidenceTerm>,
2724 ) -> DecisionAction {
2725 let action = self
2726 .runtime_policy
2727 .action_for(incompatibility_probability, unknown_incompatible_feature);
2728 self.runtime_policy.record(
2729 operation,
2730 action,
2731 incompatibility_probability,
2732 "argmin expected loss over {allow,full_validate,fail_closed}",
2733 evidence,
2734 );
2735 action
2736 }
2737}
2738
2739#[cfg(test)]
2744mod tests {
2745 use super::*;
2746 use fnx_runtime::{
2747 CgseValue, CompatibilityMode, DecisionAction, DecisionRecord, RuntimePolicy,
2748 };
2749 use proptest::prelude::*;
2750
2751 fn node_name(id: u8) -> String {
2752 format!("n{}", id % 8)
2753 }
2754
2755 fn single_attr(key: &str, value: &str) -> AttrMap {
2756 let mut attrs = AttrMap::new();
2757 attrs.insert(key.to_owned(), CgseValue::from(value));
2758 attrs
2759 }
2760
2761 fn assert_runtime_policy_preserved(source: &RuntimePolicy, result: &RuntimePolicy) {
2762 assert_eq!(result.mode(), source.mode());
2763 assert_eq!(result.allowlist(), source.allowlist());
2764 assert_eq!(result.loss_matrix(), source.loss_matrix());
2765 assert!(result.posterior().observation_count >= source.posterior().observation_count);
2766 assert!(
2767 result
2768 .decision_log()
2769 .records()
2770 .starts_with(source.decision_log().records())
2771 );
2772 }
2773
2774 fn assert_digraph_core_invariants(g: &DiGraph) {
2777 for (&(source_idx, target_idx), _attrs) in &g.edges {
2779 let source = g
2780 .nodes
2781 .get_index(source_idx)
2782 .map(|(name, _)| name.as_str())
2783 .expect("edge source index should be a node");
2784 let target = g
2785 .nodes
2786 .get_index(target_idx)
2787 .map(|(name, _)| name.as_str())
2788 .expect("edge target index should be a node");
2789 assert!(
2790 g.has_node(source),
2791 "edge source {} should be a node",
2792 source
2793 );
2794 assert!(
2795 g.has_node(target),
2796 "edge target {} should be a node",
2797 target
2798 );
2799 let succs = g
2800 .successors(source)
2801 .expect("source should have successors bucket");
2802 assert!(
2803 succs.contains(&target),
2804 "{} should be in successors of {}",
2805 target,
2806 source
2807 );
2808 let preds = g
2809 .predecessors(target)
2810 .expect("target should have predecessors bucket");
2811 assert!(
2812 preds.contains(&source),
2813 "{} should be in predecessors of {}",
2814 source,
2815 target
2816 );
2817 }
2818
2819 let mut edge_count_from_adj = 0usize;
2821 for node in g.nodes_ordered() {
2822 let succs = g
2823 .successors(node)
2824 .expect("node should have successors bucket");
2825 for s in &succs {
2826 assert!(
2827 g.has_edge(node, s),
2828 "successor {} of {} should have directed edge",
2829 s,
2830 node
2831 );
2832 edge_count_from_adj += 1;
2833 }
2834 let preds = g
2836 .predecessors(node)
2837 .expect("node should have predecessors bucket");
2838 for p in &preds {
2839 assert!(
2840 g.has_edge(p, node),
2841 "predecessor {} of {} should have directed edge",
2842 p,
2843 node
2844 );
2845 }
2846 }
2847 assert_eq!(g.edge_count(), edge_count_from_adj);
2848 }
2849
2850 #[test]
2851 fn add_node_with_attrs_reports_change_on_existing_node() {
2852 let mut graph = DiGraph::strict();
2853 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
2854 let r1 = graph.revision();
2855 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
2856 let r2 = graph.revision();
2857 assert!(r2 > r1);
2858 let expected = CgseValue::from("blue");
2859 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
2860 }
2861
2862 #[test]
2863 fn multidigraph_add_node_with_attrs_reports_change_on_existing_node() {
2864 let mut graph = MultiDiGraph::strict();
2865 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
2866 let r1 = graph.revision();
2867 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
2868 let r2 = graph.revision();
2869 assert!(r2 > r1);
2870 let expected = CgseValue::from("blue");
2871 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
2872 }
2873
2874 fn assert_multidigraph_core_invariants(g: &MultiDiGraph) {
2875 let mut edge_instances = std::collections::BTreeSet::new();
2876 for node in g.nodes_ordered() {
2877 let successors = g
2878 .successors(node)
2879 .expect("multidigraph nodes should always have successor buckets");
2880 for target in successors {
2881 assert!(g.has_node(target));
2882 assert!(g.has_edge(node, target));
2883 let preds = g
2884 .predecessors(target)
2885 .expect("target should always have predecessor buckets");
2886 assert!(preds.contains(&node));
2887 let keys = g
2888 .edge_keys(node, target)
2889 .expect("parallel directed edge bucket should exist");
2890 for key in keys {
2891 edge_instances.insert((node.to_owned(), target.to_owned(), key));
2892 }
2893 }
2894 }
2895 assert_eq!(g.edge_count(), edge_instances.len());
2896 }
2897
2898 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
2899 assert!(record.ts_unix_ms > 0);
2900 assert!(!record.operation.trim().is_empty());
2901 assert_eq!(record.mode, expected_mode);
2902 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
2903 assert!(!record.rationale.trim().is_empty());
2904 assert!(!record.evidence.is_empty());
2905 for term in &record.evidence {
2906 assert!(!term.signal.trim().is_empty());
2907 assert!(!term.observed_value.trim().is_empty());
2908 }
2909 }
2910
2911 #[test]
2914 fn add_directed_edge_autocreates_nodes() {
2915 let mut g = DiGraph::strict();
2916 g.add_edge("a", "b").unwrap();
2917
2918 assert_eq!(g.node_count(), 2);
2919 assert_eq!(g.edge_count(), 1);
2920 assert!(g.has_edge("a", "b"));
2921 assert!(!g.has_edge("b", "a")); assert_digraph_core_invariants(&g);
2923 }
2924
2925 #[test]
2926 fn directed_edge_asymmetry() {
2927 let mut g = DiGraph::strict();
2928 g.add_edge("a", "b").unwrap();
2929 g.add_edge("b", "a").unwrap();
2930
2931 assert_eq!(g.edge_count(), 2);
2932 assert!(g.has_edge("a", "b"));
2933 assert!(g.has_edge("b", "a"));
2934 assert_digraph_core_invariants(&g);
2935 }
2936
2937 #[test]
2938 fn successors_and_predecessors() {
2939 let mut g = DiGraph::strict();
2940 g.add_edge("a", "b").unwrap();
2941 g.add_edge("a", "c").unwrap();
2942 g.add_edge("d", "a").unwrap();
2943
2944 assert_eq!(g.successors("a"), Some(vec!["b", "c"]));
2945 assert_eq!(g.predecessors("a"), Some(vec!["d"]));
2946 assert_eq!(g.out_degree("a"), 2);
2947 assert_eq!(g.in_degree("a"), 1);
2948 assert_eq!(g.degree("a"), 3);
2949 assert_digraph_core_invariants(&g);
2950 }
2951
2952 #[test]
2953 fn neighbors_returns_successors() {
2954 let mut g = DiGraph::strict();
2955 g.add_edge("a", "b").unwrap();
2956 g.add_edge("c", "a").unwrap();
2957
2958 assert_eq!(g.neighbors("a"), Some(vec!["b"]));
2960 assert_digraph_core_invariants(&g);
2961 }
2962
2963 #[test]
2964 fn in_edges_and_out_edges() {
2965 let mut g = DiGraph::strict();
2966 g.add_edge("x", "y").unwrap();
2967 g.add_edge("z", "y").unwrap();
2968 g.add_edge("y", "w").unwrap();
2969
2970 assert_eq!(g.out_edges("y"), vec![("y", "w")]);
2971 assert_eq!(g.in_edges("y"), vec![("x", "y"), ("z", "y")]);
2972 assert_digraph_core_invariants(&g);
2973 }
2974
2975 #[test]
2976 fn remove_directed_edge() {
2977 let mut g = DiGraph::strict();
2978 g.add_edge("a", "b").unwrap();
2979 g.add_edge("b", "a").unwrap();
2980
2981 assert!(g.remove_edge("a", "b"));
2982 assert!(!g.has_edge("a", "b"));
2983 assert!(g.has_edge("b", "a")); assert_eq!(g.edge_count(), 1);
2985 assert_digraph_core_invariants(&g);
2986 }
2987
2988 #[test]
2989 fn remove_node_removes_all_incident_directed_edges() {
2990 let mut g = DiGraph::strict();
2991 g.add_edge("a", "b").unwrap();
2992 g.add_edge("b", "c").unwrap();
2993 g.add_edge("c", "a").unwrap();
2994 g.add_edge("d", "b").unwrap();
2995
2996 assert!(g.remove_node("b"));
2997 assert_eq!(g.node_count(), 3);
2998 assert!(!g.has_edge("a", "b"));
2999 assert!(!g.has_edge("b", "c"));
3000 assert!(!g.has_edge("d", "b"));
3001 assert!(g.has_edge("c", "a")); assert_digraph_core_invariants(&g);
3003 }
3004
3005 #[test]
3006 fn multidigraph_counts_parallel_selfloops_only() {
3007 let mut g = MultiDiGraph::strict();
3008 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
3009 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
3010 let _ = g.add_edge("b", "b").expect("self-loop add should succeed");
3011 let _ = g.add_edge("a", "b").expect("edge add should succeed");
3012 let _ = g.add_edge("b", "a").expect("edge add should succeed");
3013
3014 assert_eq!(g.number_of_selfloops(), 3);
3015 assert_multidigraph_core_invariants(&g);
3016 }
3017
3018 #[test]
3024 #[ignore]
3025 fn ab_bench_multidigraph_remove_node() {
3026 use std::time::Instant;
3027 const N: usize = 1000;
3028 const M: usize = 8000;
3029 const ITERS: usize = 500;
3030 let build = || {
3031 let mut g = MultiDiGraph::new(CompatibilityMode::Strict);
3032 for i in 0..N {
3033 let _ = g.add_node(i.to_string());
3034 }
3035 let mut s: u64 = 0x9E3779B97F4A7C15;
3036 let mut next = || {
3037 s = s
3038 .wrapping_mul(6364136223846793005)
3039 .wrapping_add(1442695040888963407);
3040 (s >> 33) as usize % N
3041 };
3042 for _ in 0..M {
3043 let a = next();
3044 let b = next();
3045 let _ = g.add_edge(a.to_string(), b.to_string());
3046 }
3047 g
3048 };
3049
3050 let mut gnew = build();
3051 let victims: Vec<String> = gnew.nodes.keys().take(ITERS).cloned().collect();
3052 let t = Instant::now();
3053 for node in &victims {
3054 gnew.remove_node(node);
3055 }
3056 let new_t = t.elapsed();
3057
3058 let mut gold = build();
3060 let t = Instant::now();
3061 for node in &victims {
3062 if !gold.nodes.contains_key(node) {
3063 continue;
3064 }
3065 if let Some(succs) = gold.successors.get(node) {
3066 let targets: Vec<String> = succs.keys().cloned().collect();
3067 for target in targets {
3068 if target != *node
3069 && let Some(preds) = gold.predecessors.get_mut(&target)
3070 {
3071 preds.shift_remove(node.as_str());
3072 }
3073 }
3074 }
3075 if let Some(preds) = gold.predecessors.get(node) {
3076 let sources: Vec<String> = preds.keys().cloned().collect();
3077 for source in sources {
3078 if source != *node
3079 && let Some(succs) = gold.successors.get_mut(&source)
3080 {
3081 succs.shift_remove(node.as_str());
3082 }
3083 }
3084 }
3085 let mut rc = 0usize;
3086 let nd = node.clone();
3087 gold.edges.retain(|k, bucket| {
3088 let keep = k.source != nd && k.target != nd;
3089 if !keep {
3090 rc += bucket.len();
3091 }
3092 keep
3093 });
3094 gold.edge_count -= rc;
3095 gold.successors.shift_remove(node);
3096 gold.predecessors.shift_remove(node);
3097 gold.nodes.shift_remove(node);
3098 }
3099 let old_t = t.elapsed();
3100
3101 assert_eq!(gnew.node_count(), gold.node_count());
3102 assert_eq!(gnew.edge_count(), gold.edge_count());
3103 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
3104 eprintln!(
3105 "MultiDiGraph remove_node x{ITERS}: retain {old_t:?} -> swap_remove {new_t:?} = {:.2}x",
3106 old_t.as_secs_f64() / new_t.as_secs_f64()
3107 );
3108 }
3109
3110 #[test]
3111 fn self_loop_directed() {
3112 let mut g = DiGraph::strict();
3113 g.add_edge("a", "a").unwrap();
3114
3115 assert_eq!(g.edge_count(), 1);
3116 assert!(g.has_edge("a", "a"));
3117 assert_eq!(g.out_degree("a"), 1);
3118 assert_eq!(g.in_degree("a"), 1);
3119 assert_digraph_core_invariants(&g);
3120 }
3121
3122 #[test]
3123 fn edge_attrs_directed() {
3124 let mut g = DiGraph::strict();
3125 let mut attrs = AttrMap::new();
3126 attrs.insert("weight".to_owned(), "5".into());
3127 g.add_edge_with_attrs("a", "b", attrs).unwrap();
3128
3129 assert_eq!(
3130 g.edge_attrs("a", "b").unwrap().get("weight"),
3131 Some(&CgseValue::String("5".to_owned()))
3132 );
3133 assert!(g.edge_attrs("b", "a").is_none()); }
3135
3136 #[test]
3137 fn extend_edges_unrecorded_preserves_direction_and_records_once() {
3138 let mut g = DiGraph::strict();
3139 g.add_node("a");
3140 g.add_node("b");
3141 g.add_node("c");
3142 let before = g.evidence_ledger().records().len();
3143
3144 let inserted = g.extend_edges_unrecorded([("a", "b"), ("b", "a"), ("a", "b")]);
3145
3146 assert_eq!(inserted, 2);
3147 assert_eq!(g.edge_count(), 2);
3148 assert!(g.has_edge("a", "b"));
3149 assert!(g.has_edge("b", "a"));
3150 assert!(!g.has_edge("a", "c"));
3151 let records = g.evidence_ledger().records();
3152 assert_eq!(records.len(), before + 1);
3153 assert_eq!(
3154 records.last().map(|record| record.operation.as_str()),
3155 Some("extend_edges_unrecorded")
3156 );
3157 }
3158
3159 #[test]
3160 fn row_staged_attr_edges_preserve_orders_and_duplicate_merges() {
3161 let mut g = DiGraph::strict();
3162 let before = g.evidence_ledger().records().len();
3163
3164 let inserted = g.extend_prepared_edges_with_attrs_row_staged_unrecorded(
3165 ["b", "x", "a", "t"]
3166 .into_iter()
3167 .map(std::borrow::ToOwned::to_owned),
3168 vec![
3169 ("b".to_owned(), "x".to_owned(), single_attr("first", "bx")),
3170 ("a".to_owned(), "t".to_owned(), single_attr("first", "at")),
3171 ("b".to_owned(), "t".to_owned(), single_attr("first", "bt")),
3172 (
3173 "a".to_owned(),
3174 "t".to_owned(),
3175 single_attr("second", "merge"),
3176 ),
3177 ("t".to_owned(), "t".to_owned(), single_attr("loop", "yes")),
3178 ],
3179 );
3180
3181 assert_eq!(inserted, 4);
3182 assert_eq!(g.nodes_ordered(), vec!["b", "x", "a", "t"]);
3183 assert_eq!(
3184 g.edges_ordered_borrowed()
3185 .into_iter()
3186 .map(|(u, v, _)| (u, v))
3187 .collect::<Vec<_>>(),
3188 vec![("b", "x"), ("b", "t"), ("a", "t"), ("t", "t")]
3189 );
3190 assert_eq!(
3191 g.edges_ordered_indices(),
3192 vec![(0, 1), (0, 3), (2, 3), (3, 3)]
3193 );
3194 assert_eq!(g.successors("b"), Some(vec!["x", "t"]));
3195 assert_eq!(g.predecessors("t"), Some(vec!["a", "b", "t"]));
3196 let merged = g.edge_attrs("a", "t").expect("merged edge should exist");
3197 assert_eq!(merged.get("first"), Some(&CgseValue::from("at")));
3198 assert_eq!(merged.get("second"), Some(&CgseValue::from("merge")));
3199 assert_digraph_core_invariants(&g);
3200 let records = g.evidence_ledger().records();
3201 assert_eq!(records.len(), before + 1);
3202 assert_eq!(
3203 records.last().map(|record| record.operation.as_str()),
3204 Some("extend_edges_unrecorded")
3205 );
3206 }
3207
3208 #[test]
3209 fn repeated_edge_merges_attrs() {
3210 let mut g = DiGraph::strict();
3211 let mut first = AttrMap::new();
3212 first.insert("weight".to_owned(), "1".into());
3213 g.add_edge_with_attrs("a", "b", first).unwrap();
3214
3215 let mut second = AttrMap::new();
3216 second.insert("color".to_owned(), "red".into());
3217 g.add_edge_with_attrs("a", "b", second).unwrap();
3218
3219 assert_eq!(g.edge_count(), 1);
3220 let attrs = g.edge_attrs("a", "b").unwrap();
3221 assert_eq!(
3222 attrs.get("weight"),
3223 Some(&CgseValue::String("1".to_owned()))
3224 );
3225 assert_eq!(
3226 attrs.get("color"),
3227 Some(&CgseValue::String("red".to_owned()))
3228 );
3229 }
3230
3231 #[test]
3232 fn edges_ordered_preserves_direction() {
3233 let mut g = DiGraph::strict();
3234 g.add_edge("b", "a").unwrap();
3235 g.add_edge("a", "c").unwrap();
3236
3237 let pairs: Vec<(String, String)> = g
3238 .edges_ordered()
3239 .into_iter()
3240 .map(|e| (e.left, e.right))
3241 .collect();
3242 assert_eq!(
3244 pairs,
3245 vec![
3246 ("b".to_owned(), "a".to_owned()),
3247 ("a".to_owned(), "c".to_owned()),
3248 ]
3249 );
3250 }
3251
3252 #[test]
3253 fn type_identity() {
3254 let g = DiGraph::strict();
3255 assert!(g.is_directed());
3256 assert!(!g.is_multigraph());
3257 }
3258
3259 #[test]
3260 fn to_undirected_merges_edges() {
3261 let mut g = DiGraph::strict();
3262 g.add_edge("a", "b").unwrap();
3263 g.add_edge("b", "a").unwrap();
3264 g.add_edge("b", "c").unwrap();
3265
3266 let ug = g.to_undirected();
3267 assert_eq!(ug.node_count(), 3);
3268 assert_eq!(ug.edge_count(), 2); assert!(ug.has_edge("a", "b"));
3270 assert!(ug.has_edge("b", "a")); assert!(ug.has_edge("b", "c"));
3272 }
3273
3274 #[test]
3275 fn to_undirected_preserves_runtime_policy() {
3276 let mut g = DiGraph::hardened();
3277 g.add_edge("a", "b").unwrap();
3278 g.add_edge("b", "a").unwrap();
3279 let expected_policy = g.runtime_policy().clone();
3280
3281 let ug = g.to_undirected();
3282
3283 assert_runtime_policy_preserved(&expected_policy, ug.runtime_policy());
3284 }
3285
3286 #[test]
3287 fn snapshot_roundtrip() {
3288 let mut g = DiGraph::strict();
3289 let mut attrs = AttrMap::new();
3290 attrs.insert("weight".to_owned(), "3".into());
3291 g.add_edge_with_attrs("a", "b", attrs).unwrap();
3292 g.add_edge("b", "c").unwrap();
3293 g.add_edge("c", "a").unwrap();
3294
3295 let snap = g.snapshot();
3296 let mut replayed = DiGraph::new(snap.mode);
3297 for node in &snap.nodes {
3298 let _ = replayed.add_node(node.clone());
3299 }
3300 for edge in &snap.edges {
3301 replayed
3302 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
3303 .unwrap();
3304 }
3305
3306 assert_eq!(replayed.snapshot(), snap);
3307 assert_digraph_core_invariants(&replayed);
3308 }
3309
3310 #[test]
3311 fn multidigraph_tracks_parallel_edges_with_distinct_keys() {
3312 let mut graph = MultiDiGraph::strict();
3313 let first = graph.add_edge("a", "b").expect("edge add should succeed");
3314 let second = graph.add_edge("a", "b").expect("edge add should succeed");
3315
3316 assert_ne!(first, second);
3317 assert_eq!(graph.edge_count(), 2);
3318 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
3319 assert_multidigraph_core_invariants(&graph);
3320 }
3321
3322 #[test]
3323 fn multidigraph_remove_node_clears_incoming_and_outgoing_parallel_edges() {
3324 let mut graph = MultiDiGraph::strict();
3325 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
3326 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
3327 let _ = graph.add_edge("b", "a").expect("edge add should succeed");
3328 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
3329
3330 assert!(graph.remove_node("b"));
3331 assert_eq!(graph.edge_count(), 0);
3332 assert!(!graph.has_node("b"));
3333 assert_eq!(graph.successors("a"), Some(vec![]));
3334 assert_eq!(graph.predecessors("a"), Some(vec![]));
3335 assert_multidigraph_core_invariants(&graph);
3336 }
3337
3338 #[test]
3339 fn multidigraph_roundtrips_sparse_snapshot_keys() {
3340 let mut graph = MultiDiGraph::strict();
3341 assert_eq!(
3342 graph.add_edge("a", "b").expect("edge add should succeed"),
3343 0
3344 );
3345 assert_eq!(
3346 graph.add_edge("a", "b").expect("edge add should succeed"),
3347 1
3348 );
3349 assert_eq!(
3350 graph.add_edge("a", "b").expect("edge add should succeed"),
3351 2
3352 );
3353 assert!(graph.remove_edge("a", "b", Some(1)));
3354 assert_eq!(
3355 graph.add_edge("a", "b").expect("edge add should succeed"),
3356 3
3357 );
3358
3359 let snapshot = graph.snapshot();
3360 assert_eq!(
3361 snapshot
3362 .edges
3363 .iter()
3364 .map(|edge| edge.key)
3365 .collect::<Vec<_>>(),
3366 vec![0, 2, 3]
3367 );
3368
3369 let mut replayed = MultiDiGraph::new(snapshot.mode);
3370 for node in &snapshot.nodes {
3371 let _ = replayed.add_node(node.clone());
3372 }
3373 for edge in &snapshot.edges {
3374 replayed
3375 .add_edge_with_key_and_attrs(
3376 edge.source.clone(),
3377 edge.target.clone(),
3378 edge.key,
3379 edge.attrs.clone(),
3380 )
3381 .expect("snapshot replay should preserve explicit keys");
3382 }
3383
3384 assert_eq!(replayed.snapshot(), snapshot);
3385 assert_multidigraph_core_invariants(&replayed);
3386 }
3387
3388 #[test]
3389 fn runtime_policy_tracks_hardened_multidigraph_recovery() {
3390 let mut g = MultiDiGraph::hardened();
3391 g.add_edge("a", "b")
3392 .expect("hardened multidigraph edge add should succeed");
3393
3394 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
3395 assert!(!g.runtime_policy().decision_log().records().is_empty());
3396 assert!(g.runtime_policy().posterior().observation_count >= 1);
3397 }
3398
3399 #[test]
3400 fn strict_mode_fails_closed_for_incompatible_attrs() {
3401 let mut g = DiGraph::strict();
3402 let mut attrs = AttrMap::new();
3403 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
3404 let err = g
3405 .add_edge_with_attrs("a", "b", attrs)
3406 .expect_err("strict mode should fail closed");
3407
3408 assert_eq!(
3409 err,
3410 GraphError::FailClosed {
3411 operation: "add_edge",
3412 reason: "incompatible edge metadata".to_owned(),
3413 }
3414 );
3415 }
3416
3417 #[test]
3418 fn revision_increments_on_mutations() {
3419 let mut g = DiGraph::strict();
3420 let r0 = g.revision();
3421 let _ = g.add_node("a");
3422 let r1 = g.revision();
3423 assert!(r1 > r0);
3424
3425 g.add_edge("a", "b").unwrap();
3426 let r2 = g.revision();
3427 assert!(r2 > r1);
3428
3429 let _ = g.remove_edge("a", "b");
3430 let r3 = g.revision();
3431 assert!(r3 > r2);
3432 }
3433
3434 #[test]
3435 fn hardened_self_loop_records_allow() {
3436 let mut g = DiGraph::hardened();
3437 g.add_edge("loop", "loop").unwrap();
3438
3439 let record = g
3440 .evidence_ledger()
3441 .records()
3442 .iter()
3443 .rev()
3444 .find(|r| r.operation == "add_edge")
3445 .expect("add_edge should emit ledger row");
3446 assert_decision_record_schema(record, CompatibilityMode::Hardened);
3447 assert_eq!(record.action, DecisionAction::Allow);
3448 }
3449
3450 #[test]
3451 fn runtime_policy_tracks_hardened_digraph_recovery() {
3452 let mut g = DiGraph::hardened();
3453 g.add_edge("a", "b")
3454 .expect("hardened digraph edge add should succeed");
3455
3456 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
3457 assert!(!g.runtime_policy().decision_log().records().is_empty());
3458 assert!(g.runtime_policy().posterior().observation_count >= 1);
3459 }
3460
3461 proptest! {
3464 #[test]
3465 fn prop_digraph_invariants_under_mixed_mutations(
3466 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
3467 ) {
3468 let mut g = DiGraph::strict();
3469 let mut last_rev = g.revision();
3470
3471 for (src_id, tgt_id, is_add) in ops {
3472 let src = node_name(src_id);
3473 let tgt = node_name(tgt_id);
3474 if is_add {
3475 prop_assert!(g.add_edge(src, tgt).is_ok());
3476 } else {
3477 let _ = g.remove_edge(&src, &tgt);
3478 }
3479 let rev = g.revision();
3480 prop_assert!(rev >= last_rev);
3481 last_rev = rev;
3482 assert_digraph_core_invariants(&g);
3483 }
3484 }
3485
3486 #[test]
3487 fn prop_digraph_snapshot_deterministic(
3488 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
3489 ) {
3490 let mut g1 = DiGraph::hardened();
3491 let mut g2 = DiGraph::hardened();
3492
3493 for (src_id, tgt_id, attrs_variant) in ops {
3494 let src = node_name(src_id);
3495 let tgt = node_name(tgt_id);
3496 let mut attrs = AttrMap::new();
3497 if attrs_variant == 1 {
3498 attrs.insert("weight".to_owned(), (src_id % 5).to_string().into());
3499 } else if attrs_variant == 2 {
3500 attrs.insert("tag".to_owned(), format!("k{}", tgt_id % 4).into());
3501 }
3502 prop_assert!(g1.add_edge_with_attrs(src.clone(), tgt.clone(), attrs.clone()).is_ok());
3503 prop_assert!(g2.add_edge_with_attrs(src, tgt, attrs).is_ok());
3504 }
3505
3506 prop_assert_eq!(g1.snapshot(), g2.snapshot());
3507 }
3508
3509 #[test]
3510 fn prop_remove_node_clears_all_directed_edges(
3511 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
3512 target_id in 0_u8..8,
3513 ) {
3514 let mut g = DiGraph::strict();
3515 for (src_id, tgt_id) in ops {
3516 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
3517 }
3518
3519 let target = node_name(target_id);
3520 let removed = g.remove_node(&target);
3521 if removed {
3522 prop_assert!(!g.has_node(&target));
3523 for node in g.nodes_ordered() {
3524 prop_assert!(!g.has_edge(node, &target));
3525 prop_assert!(!g.has_edge(&target, node));
3526 }
3527 }
3528 assert_digraph_core_invariants(&g);
3529 }
3530
3531 #[test]
3532 fn prop_directed_edge_count_equals_successor_sum(
3533 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
3534 ) {
3535 let mut g = DiGraph::strict();
3536 for (src_id, tgt_id) in ops {
3537 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
3538 }
3539
3540 let total_out: usize = g.nodes_ordered().iter().map(|n| g.out_degree(n)).sum();
3541 let total_in: usize = g.nodes_ordered().iter().map(|n| g.in_degree(n)).sum();
3542 prop_assert_eq!(g.edge_count(), total_out);
3543 prop_assert_eq!(g.edge_count(), total_in);
3544 }
3545 }
3546}