1use crate::{AttrMap, EdgeSnapshot, 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 nodes: crate::FxIndexMap<String, AttrMap>,
195 succ_indices: Vec<Vec<usize>>,
203 pred_indices: Vec<Vec<usize>>,
204 edges: crate::FxIndexMap<(usize, usize), AttrMap>,
209 runtime_policy: RuntimePolicy,
210 csr_cache: DiCsrCache,
216 all_int_cache: DiAllIntCache,
220}
221
222impl DiGraph {
223 pub fn clear_edges(&mut self) {
230 if self.edges.is_empty() {
231 return;
232 }
233 self.edges.clear();
234 for row in &mut self.succ_indices {
235 row.clear();
236 }
237 for row in &mut self.pred_indices {
238 row.clear();
239 }
240 self.revision = self.revision.saturating_add(1);
241 }
242
243 #[must_use]
246 pub fn clone_with_fresh_policy(&self) -> Self {
247 Self {
248 mode: self.mode,
249 revision: self.revision,
250 nodes: self.nodes.clone(),
251 succ_indices: self.succ_indices.clone(),
252 pred_indices: self.pred_indices.clone(),
253 edges: self.edges.clone(),
254 runtime_policy: RuntimePolicy::new(self.mode),
255 csr_cache: self.csr_cache.clone(),
256 all_int_cache: self.all_int_cache.clone(),
257 }
258 }
259
260 #[must_use]
265 pub fn new(mode: CompatibilityMode) -> Self {
266 Self {
267 mode,
268 revision: 0,
269 nodes: crate::FxIndexMap::default(),
270 succ_indices: Vec::new(),
271 pred_indices: Vec::new(),
272 edges: crate::FxIndexMap::default(),
273 runtime_policy: RuntimePolicy::new(mode),
274 csr_cache: std::sync::Arc::default(),
275 all_int_cache: std::sync::Arc::default(),
276 }
277 }
278
279 #[must_use]
280 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
281 let mode = runtime_policy.mode();
282 Self {
283 mode,
284 revision: 0,
285 nodes: crate::FxIndexMap::default(),
286 succ_indices: Vec::new(),
287 pred_indices: Vec::new(),
288 edges: crate::FxIndexMap::default(),
289 runtime_policy,
290 csr_cache: std::sync::Arc::default(),
291 all_int_cache: std::sync::Arc::default(),
292 }
293 }
294
295 #[must_use]
310 pub fn induced_subgraph_ordered(&self, order: &[usize], runtime_policy: RuntimePolicy) -> Self {
311 let mut position = vec![usize::MAX; self.nodes.len()];
312 for (slot, &parent_idx) in order.iter().enumerate() {
313 position[parent_idx] = slot;
314 }
315
316 let mut nodes = crate::FxIndexMap::default();
317 nodes.reserve(order.len());
318 for &parent_idx in order {
319 let (name, attrs) = self
320 .nodes
321 .get_index(parent_idx)
322 .expect("induced subgraph order must hold valid parent node indices");
323 nodes.insert(name.clone(), attrs.clone());
324 }
325
326 let mut succ_indices = vec![Vec::new(); order.len()];
327 let mut pred_indices = vec![Vec::new(); order.len()];
328 let mut edges = crate::FxIndexMap::default();
329 for (slot, &parent_idx) in order.iter().enumerate() {
330 for &parent_target in &self.succ_indices[parent_idx] {
331 let other = position[parent_target];
332 if other == usize::MAX {
333 continue;
334 }
335 let attrs = self
336 .edges
337 .get(&(parent_idx, parent_target))
338 .cloned()
339 .unwrap_or_default();
340 edges.insert((slot, other), attrs);
341 succ_indices[slot].push(other);
342 pred_indices[other].push(slot);
343 }
344 }
345
346 Self {
347 mode: runtime_policy.mode(),
348 revision: 0,
349 nodes,
350 succ_indices,
351 pred_indices,
352 edges,
353 runtime_policy,
354 csr_cache: std::sync::Arc::default(),
355 all_int_cache: std::sync::Arc::default(),
356 }
357 }
358
359 #[must_use]
360 pub fn strict() -> Self {
361 Self::new(CompatibilityMode::Strict)
362 }
363
364 #[must_use]
365 pub fn hardened() -> Self {
366 Self::new(CompatibilityMode::Hardened)
367 }
368
369 #[must_use]
374 pub fn mode(&self) -> CompatibilityMode {
375 self.mode
376 }
377
378 #[must_use]
379 pub fn node_count(&self) -> usize {
380 self.nodes.len()
381 }
382
383 #[must_use]
384 pub fn edge_count(&self) -> usize {
385 self.edges.len()
386 }
387
388 #[must_use]
389 pub fn revision(&self) -> u64 {
390 self.revision
391 }
392
393 #[must_use]
398 pub fn csr(&self) -> std::sync::Arc<DiCsr> {
399 if let Ok(guard) = self.csr_cache.read()
400 && let Some((rev, csr)) = guard.as_ref()
401 && *rev == self.revision
402 {
403 return csr.clone();
404 }
405 let built = std::sync::Arc::new(self.build_csr());
406 if let Ok(mut guard) = self.csr_cache.write() {
407 *guard = Some((self.revision, built.clone()));
408 }
409 built
410 }
411
412 #[must_use]
415 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
416 if let Ok(guard) = self.all_int_cache.read()
417 && let Some((rev, a, v)) = guard.as_ref()
418 && *rev == self.revision
419 && a == attr
420 {
421 return *v;
422 }
423 let v = self
424 .edges
425 .values()
426 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
427 if let Ok(mut guard) = self.all_int_cache.write() {
428 *guard = Some((self.revision, attr.to_owned(), v));
429 }
430 v
431 }
432
433 fn build_csr(&self) -> DiCsr {
434 let n = self.nodes.len();
437 let mut succ_offsets = Vec::with_capacity(n + 1);
438 let mut pred_offsets = Vec::with_capacity(n + 1);
439 let mut succ_targets = Vec::new();
440 let mut pred_targets = Vec::new();
441 succ_offsets.push(0);
442 pred_offsets.push(0);
443 for i in 0..n {
444 succ_targets.extend(
445 self.succ_indices[i]
446 .iter()
447 .map(|&v| u32::try_from(v).unwrap_or(u32::MAX)),
448 );
449 succ_offsets.push(succ_targets.len());
450 pred_targets.extend(
451 self.pred_indices[i]
452 .iter()
453 .map(|&v| u32::try_from(v).unwrap_or(u32::MAX)),
454 );
455 pred_offsets.push(pred_targets.len());
456 }
457 DiCsr {
458 succ_offsets,
459 succ_targets,
460 pred_offsets,
461 pred_targets,
462 }
463 }
464
465 #[doc(hidden)]
468 #[must_use]
469 pub fn debug_index_rows_consistent(&self) -> bool {
470 self.succ_indices.len() == self.nodes.len() && self.pred_indices.len() == self.nodes.len()
474 }
475
476 #[must_use]
477 pub fn has_node(&self, node: &str) -> bool {
478 self.nodes.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((
487 self.nodes.get_index_of(source)?,
488 self.nodes.get_index_of(target)?,
489 ))
490 }
491
492 #[must_use]
494 #[inline]
495 pub fn successors_indices(&self, idx: usize) -> Option<&[usize]> {
496 self.succ_indices.get(idx).map(Vec::as_slice)
497 }
498
499 #[must_use]
501 #[inline]
502 pub fn predecessors_indices(&self, idx: usize) -> Option<&[usize]> {
503 self.pred_indices.get(idx).map(Vec::as_slice)
504 }
505
506 pub fn has_edge(&self, source: &str, target: &str) -> bool {
507 self.edge_pair_key(source, target)
508 .is_some_and(|k| self.edges.contains_key(&k))
509 }
510
511 #[must_use]
525 pub fn has_edge_by_indices(&self, source_idx: usize, target_idx: usize) -> bool {
526 self.edges.contains_key(&(source_idx, target_idx))
527 }
528
529 #[must_use]
530 pub fn nodes_ordered(&self) -> Vec<&str> {
531 self.nodes.keys().map(String::as_str).collect()
532 }
533
534 #[must_use]
538 pub fn node_index_matches_int(&self, idx: usize) -> bool {
539 self.nodes
540 .get_index(idx)
541 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
542 }
543
544 #[must_use]
545 pub fn get_node_index(&self, node: &str) -> Option<usize> {
546 self.nodes.get_index_of(node)
547 }
548
549 #[must_use]
550 pub fn get_node_name(&self, index: usize) -> Option<&str> {
551 self.nodes.get_index(index).map(|(k, _)| k.as_str())
552 }
553
554 #[must_use]
558 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
559 let idx = self.nodes.get_index_of(node)?;
562 Some(
563 self.succ_indices[idx]
564 .iter()
565 .map(|&i| {
566 self.nodes
567 .get_index(i)
568 .expect("index rows hold valid node indices")
569 .0
570 .as_str()
571 })
572 .collect(),
573 )
574 }
575
576 #[must_use]
577 pub fn successors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
578 let idx = self.nodes.get_index_of(node)?;
580 Some(self.succ_indices[idx].iter().map(move |&i| {
581 self.nodes
582 .get_index(i)
583 .expect("index rows hold valid node indices")
584 .0
585 .as_str()
586 }))
587 }
588
589 #[must_use]
591 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
592 let idx = self.nodes.get_index_of(node)?;
594 Some(
595 self.pred_indices[idx]
596 .iter()
597 .map(|&i| {
598 self.nodes
599 .get_index(i)
600 .expect("index rows hold valid node indices")
601 .0
602 .as_str()
603 })
604 .collect(),
605 )
606 }
607
608 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
612 for (node, order) in orders {
614 let Some(idx) = self.nodes.get_index_of(node.as_str()) else {
615 continue;
616 };
617 let rows = if pred {
618 &mut self.pred_indices
619 } else {
620 &mut self.succ_indices
621 };
622 let row = &rows[idx];
623 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
624 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
625 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
626 for v in order {
627 if let Some(v_idx) = self.nodes.get_index_of(v.as_str())
628 && row_set.contains(&v_idx)
629 && placed.insert(v_idx)
630 {
631 new_row.push(v_idx);
632 }
633 }
634 for &v_idx in row {
635 if placed.insert(v_idx) {
636 new_row.push(v_idx);
637 }
638 }
639 rows[idx] = new_row;
640 }
641 }
642
643 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
651 let mut new_rows: Vec<Vec<usize>> = self
659 .pred_indices
660 .iter()
661 .map(|row| Vec::with_capacity(row.len()))
662 .collect();
663 for (u, succ_row) in self.succ_indices.iter().enumerate() {
664 for &v in succ_row {
665 new_rows[v].push(u);
666 }
667 }
668 self.pred_indices = new_rows;
669 }
670
671 #[must_use]
672 pub fn predecessors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
673 let idx = self.nodes.get_index_of(node)?;
675 Some(self.pred_indices[idx].iter().map(move |&i| {
676 self.nodes
677 .get_index(i)
678 .expect("index rows hold valid node indices")
679 .0
680 .as_str()
681 }))
682 }
683
684 #[must_use]
686 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
687 self.successors(node)
688 }
689
690 #[must_use]
691 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
692 self.successors_iter(node)
693 }
694
695 #[must_use]
696 pub fn neighbor_count(&self, node: &str) -> usize {
697 self.nodes
699 .get_index_of(node)
700 .map_or(0, |i| self.succ_indices[i].len())
701 }
702
703 #[must_use]
705 pub fn out_degree(&self, node: &str) -> usize {
706 self.nodes
707 .get_index_of(node)
708 .map_or(0, |i| self.succ_indices[i].len())
709 }
710
711 #[must_use]
713 pub fn in_degree(&self, node: &str) -> usize {
714 self.nodes
715 .get_index_of(node)
716 .map_or(0, |i| self.pred_indices[i].len())
717 }
718
719 #[must_use]
721 pub fn degree(&self, node: &str) -> usize {
722 self.nodes.get_index_of(node).map_or(0, |idx| {
726 self.pred_indices[idx].len() + self.succ_indices[idx].len()
727 })
728 }
729
730 #[must_use]
735 pub fn out_degree_by_index(&self, idx: usize) -> usize {
736 self.succ_indices[idx].len()
737 }
738
739 #[must_use]
740 pub fn in_degree_by_index(&self, idx: usize) -> usize {
741 self.pred_indices[idx].len()
742 }
743
744 #[must_use]
745 pub fn degree_by_index(&self, idx: usize) -> usize {
746 self.succ_indices[idx].len() + self.pred_indices[idx].len()
747 }
748
749 #[must_use]
751 pub fn out_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
752 self.nodes.get_index_of(node).map_or_else(Vec::new, |idx| {
754 self.succ_indices[idx]
755 .iter()
756 .map(|&i| {
757 (
758 node,
759 self.nodes
760 .get_index(i)
761 .expect("index rows hold valid node indices")
762 .0
763 .as_str(),
764 )
765 })
766 .collect()
767 })
768 }
769
770 #[must_use]
772 pub fn in_edges<'a>(&'a self, node: &'a str) -> Vec<(&'a str, &'a str)> {
773 self.nodes.get_index_of(node).map_or_else(Vec::new, |idx| {
775 self.pred_indices[idx]
776 .iter()
777 .map(|&i| {
778 (
779 self.nodes
780 .get_index(i)
781 .expect("index rows hold valid node indices")
782 .0
783 .as_str(),
784 node,
785 )
786 })
787 .collect()
788 })
789 }
790
791 #[must_use]
794 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
795 self.nodes.get(node)
796 }
797
798 #[must_use]
800 pub fn edge_attrs(&self, source: &str, target: &str) -> Option<&AttrMap> {
801 self.edges.get(&self.edge_pair_key(source, target)?)
802 }
803
804 #[must_use]
807 #[inline]
808 pub fn edge_attrs_by_indices(&self, source_idx: usize, target_idx: usize) -> Option<&AttrMap> {
809 self.edges.get(&(source_idx, target_idx))
810 }
811
812 #[must_use]
816 pub fn any_edge_has_attr(&self, key: &str) -> bool {
817 self.edges.values().any(|attrs| attrs.contains_key(key))
818 }
819
820 #[must_use]
825 pub fn all_edge_attr_values_scalar(&self) -> bool {
826 self.edges.values().all(|attrs| {
827 attrs.values().all(|v| {
828 matches!(
829 v,
830 fnx_runtime::CgseValue::Int(_)
831 | fnx_runtime::CgseValue::Float(_)
832 | fnx_runtime::CgseValue::Bool(_)
833 )
834 })
835 })
836 }
837
838 #[must_use]
841 pub fn any_node_has_attrs(&self) -> bool {
842 self.nodes.values().any(|attrs| !attrs.is_empty())
843 }
844
845 #[must_use]
846 pub fn any_edge_has_attrs(&self) -> bool {
847 self.edges.values().any(|attrs| !attrs.is_empty())
848 }
849
850 pub fn edges_indexed(&self) -> impl Iterator<Item = ((usize, usize), &AttrMap)> + '_ {
856 self.edges.iter().map(|(&pair, attrs)| (pair, attrs))
857 }
858
859 #[must_use]
860 pub fn evidence_ledger(&self) -> &EvidenceLedger {
861 self.runtime_policy.decision_log()
862 }
863
864 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
865 self.runtime_policy.drain_decision_records()
866 }
867
868 #[must_use]
869 pub fn runtime_policy(&self) -> &RuntimePolicy {
870 &self.runtime_policy
871 }
872
873 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
874 self.mode = runtime_policy.mode();
875 self.runtime_policy = runtime_policy;
876 }
877
878 #[must_use]
880 pub fn is_directed(&self) -> bool {
881 true
882 }
883
884 #[must_use]
886 pub fn is_multigraph(&self) -> bool {
887 false
888 }
889
890 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
895 self.add_node_with_attrs(node, AttrMap::new())
896 }
897
898 fn add_node_with_attrs_unrecorded(
912 &mut self,
913 node: impl AsRef<str> + Into<String>,
914 attrs: AttrMap,
915 ) -> (bool, bool, usize) {
916 let existed = self.nodes.contains_key(node.as_ref());
917 let mut changed = !existed;
918 let attrs_count = {
919 let bucket = if existed {
920 self.nodes
921 .get_mut(node.as_ref())
922 .expect("contains_key reported this node present")
923 } else {
924 self.nodes.entry(node.into()).or_default()
925 };
926 if !attrs.is_empty()
927 && attrs
928 .iter()
929 .any(|(key, value)| bucket.get(key) != Some(value))
930 {
931 changed = true;
932 }
933 bucket.extend(attrs);
934 bucket.len()
935 };
936 if self.succ_indices.len() < self.nodes.len() {
937 self.succ_indices.push(Vec::new());
938 self.pred_indices.push(Vec::new());
939 }
940 if changed {
941 self.revision = self.revision.saturating_add(1);
942 }
943 (changed, existed, attrs_count)
944 }
945
946 pub fn add_node_with_attrs(
947 &mut self,
948 node: impl AsRef<str> + Into<String>,
949 attrs: AttrMap,
950 ) -> bool {
951 let (changed, existed, attrs_count) = self.add_node_with_attrs_unrecorded(node, attrs);
952 self.record_decision(
953 "add_node",
954 0.0,
955 false,
956 vec![
957 EvidenceTerm {
958 signal: "node_preexisting".into(),
959 observed_value: bool_evidence(existed),
960 log_likelihood_ratio: -3.0,
961 },
962 EvidenceTerm {
963 signal: "attrs_count".into(),
964 observed_value: count_evidence(attrs_count),
965 log_likelihood_ratio: -1.0,
966 },
967 ],
968 );
969 changed
970 }
971
972 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
973 if defaults.is_empty() {
974 return false;
975 }
976 let mut inserted = 0usize;
977 for attrs in self.nodes.values_mut() {
978 for (key, value) in defaults {
979 if !attrs.contains_key(key) {
980 attrs.insert(key.clone(), value.clone());
981 inserted += 1;
982 }
983 }
984 }
985 if inserted > 0 {
986 self.revision = self.revision.saturating_add(1);
987 self.record_decision(
988 "apply_node_defaults",
989 0.0,
990 false,
991 vec![EvidenceTerm {
992 signal: "defaults_applied".into(),
993 observed_value: count_evidence(inserted),
994 log_likelihood_ratio: 0.0,
995 }],
996 );
997 return true;
998 }
999 false
1000 }
1001
1002 pub fn add_edge(
1003 &mut self,
1004 source: impl AsRef<str>,
1005 target: impl AsRef<str>,
1006 ) -> Result<(), GraphError> {
1007 self.add_edge_with_attrs(source, target, AttrMap::new())
1008 }
1009
1010 pub fn add_edge_with_attrs(
1018 &mut self,
1019 source: impl AsRef<str>,
1020 target: impl AsRef<str>,
1021 attrs: AttrMap,
1022 ) -> Result<(), GraphError> {
1023 let source = source.as_ref();
1024 let target = target.as_ref();
1025
1026 let unknown_feature = attrs
1027 .keys()
1028 .any(|key| key.starts_with("__fnx_incompatible"));
1029 let self_loop = source == target;
1030 let incompatibility_probability = if unknown_feature {
1031 1.0
1032 } else if self_loop {
1033 0.22
1034 } else {
1035 0.08
1036 };
1037
1038 let action = self
1046 .runtime_policy
1047 .action_for(incompatibility_probability, unknown_feature);
1048
1049 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
1050 self.record_decision(
1051 "add_edge",
1052 incompatibility_probability,
1053 unknown_feature,
1054 vec![EvidenceTerm {
1055 signal: "unknown_incompatible_feature".into(),
1056 observed_value: bool_evidence(unknown_feature),
1057 log_likelihood_ratio: 12.0,
1058 }],
1059 );
1060 return Err(GraphError::FailClosed {
1061 operation: "add_edge",
1062 reason: "incompatible edge metadata".to_owned(),
1063 });
1064 }
1065
1066 let mut source_autocreated = false;
1077 let source_idx = match self.nodes.get_index_of(source) {
1078 Some(index) => index,
1079 None => {
1080 let _ = self.add_node_with_attrs_unrecorded(source, AttrMap::new());
1081 source_autocreated = true;
1082 self.nodes.get_index_of(source).expect("autocreated above")
1083 }
1084 };
1085 let mut target_autocreated = false;
1086 let target_idx = if self_loop {
1087 target_autocreated = source_autocreated;
1088 source_idx
1089 } else {
1090 match self.nodes.get_index_of(target) {
1091 Some(index) => index,
1092 None => {
1093 let _ = self.add_node_with_attrs_unrecorded(target, AttrMap::new());
1094 target_autocreated = true;
1095 self.nodes.get_index_of(target).expect("autocreated above")
1096 }
1097 }
1098 };
1099
1100 let edge_key = (source_idx, target_idx);
1101 let new_edge = !self.edges.contains_key(&edge_key);
1102 let mut changed = new_edge;
1103 let edge_attr_count = {
1104 let edge_attrs = self.edges.entry(edge_key).or_default();
1105 if edge_attrs.is_empty() {
1106 if !attrs.is_empty() {
1110 changed = true;
1111 }
1112 *edge_attrs = attrs;
1113 } else {
1114 if !attrs.is_empty()
1115 && attrs
1116 .iter()
1117 .any(|(key, value)| edge_attrs.get(key) != Some(value))
1118 {
1119 changed = true;
1120 }
1121 edge_attrs.extend(attrs);
1122 }
1123 edge_attrs.len()
1124 };
1125 if new_edge {
1128 self.succ_indices[source_idx].push(target_idx);
1134 self.pred_indices[target_idx].push(source_idx);
1135 }
1136
1137 if changed {
1138 self.revision = self.revision.saturating_add(1);
1139 }
1140
1141 self.record_decision(
1142 "add_edge",
1143 incompatibility_probability,
1144 unknown_feature,
1145 vec![
1146 EvidenceTerm {
1147 signal: "self_loop".into(),
1148 observed_value: bool_evidence(self_loop),
1149 log_likelihood_ratio: -0.5,
1150 },
1151 EvidenceTerm {
1152 signal: "edge_attr_count".into(),
1153 observed_value: count_evidence(edge_attr_count),
1154 log_likelihood_ratio: -2.0,
1155 },
1156 EvidenceTerm {
1157 signal: "source_autocreated".into(),
1158 observed_value: bool_evidence(source_autocreated),
1159 log_likelihood_ratio: -1.25,
1160 },
1161 EvidenceTerm {
1162 signal: "target_autocreated".into(),
1163 observed_value: bool_evidence(target_autocreated),
1164 log_likelihood_ratio: -1.25,
1165 },
1166 ],
1167 );
1168
1169 Ok(())
1170 }
1171
1172 #[must_use]
1183 pub fn extend_edges_unrecorded<I, S, T>(&mut self, edges: I) -> usize
1184 where
1185 I: IntoIterator<Item = (S, T)>,
1186 S: Into<String>,
1187 T: Into<String>,
1188 {
1189 let iterator = edges.into_iter();
1195 let (lower_bound, _) = iterator.size_hint();
1196 self.nodes.reserve(lower_bound);
1197 self.succ_indices.reserve(lower_bound);
1198 self.pred_indices.reserve(lower_bound);
1199 self.edges.reserve(lower_bound);
1200
1201 let mut inserted = 0usize;
1202 for (source, target) in iterator {
1203 let source = source.into();
1204 let target = target.into();
1205 let s_idx = match self.nodes.get_index_of(&source) {
1206 Some(index) => index,
1207 None => {
1208 let index = self.nodes.len();
1209 self.nodes.insert(source.clone(), AttrMap::new());
1210 self.succ_indices.push(Vec::new());
1211 self.pred_indices.push(Vec::new());
1212 index
1213 }
1214 };
1215 let t_idx = if source == target {
1216 s_idx
1217 } else {
1218 match self.nodes.get_index_of(&target) {
1219 Some(index) => index,
1220 None => {
1221 let index = self.nodes.len();
1222 self.nodes.insert(target.clone(), AttrMap::new());
1223 self.succ_indices.push(Vec::new());
1224 self.pred_indices.push(Vec::new());
1225 index
1226 }
1227 }
1228 };
1229 let edge_key = (s_idx, t_idx);
1230 if self.edges.contains_key(&edge_key) {
1231 continue;
1232 }
1233 self.edges.insert(edge_key, AttrMap::new());
1234 self.succ_indices[s_idx].push(t_idx);
1235 self.pred_indices[t_idx].push(s_idx);
1236 inserted += 1;
1237 }
1238
1239 if inserted > 0 {
1240 self.revision = self
1241 .revision
1242 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1243 self.record_decision(
1244 "extend_edges_unrecorded",
1245 0.0,
1246 false,
1247 vec![EvidenceTerm {
1248 signal: "batch_edge_count".into(),
1249 observed_value: count_evidence(inserted),
1250 log_likelihood_ratio: -1.0,
1251 }],
1252 );
1253 }
1254
1255 inserted
1256 }
1257
1258 pub fn extend_existing_index_edges_unrecorded<I>(&mut self, edges: I) -> usize
1267 where
1268 I: IntoIterator<Item = (usize, usize)>,
1269 {
1270 let iterator = edges.into_iter();
1271 self.edges.reserve(iterator.size_hint().0);
1272
1273 let mut inserted = 0usize;
1274 for (s_idx, t_idx) in iterator {
1275 debug_assert!(s_idx < self.nodes.len());
1276 debug_assert!(t_idx < self.nodes.len());
1277 let edge_key = (s_idx, t_idx);
1278 if self.edges.contains_key(&edge_key) {
1279 continue;
1280 }
1281 self.edges.insert(edge_key, AttrMap::new());
1282 self.succ_indices[s_idx].push(t_idx);
1283 self.pred_indices[t_idx].push(s_idx);
1284 inserted += 1;
1285 }
1286
1287 if inserted > 0 {
1288 self.revision = self
1289 .revision
1290 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1291 self.record_decision(
1292 "extend_edges_unrecorded",
1293 0.0,
1294 false,
1295 vec![EvidenceTerm {
1296 signal: "batch_edge_count".into(),
1297 observed_value: count_evidence(inserted),
1298 log_likelihood_ratio: -1.0,
1299 }],
1300 );
1301 }
1302
1303 inserted
1304 }
1305
1306 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
1311 where
1312 I: IntoIterator<Item = N>,
1313 N: Into<String>,
1314 {
1315 let iterator = nodes.into_iter();
1316 let (lower_bound, _) = iterator.size_hint();
1317 self.nodes.reserve(lower_bound);
1318 self.succ_indices.reserve(lower_bound);
1319 self.pred_indices.reserve(lower_bound);
1320
1321 let mut inserted = 0usize;
1322 for node in iterator {
1323 let node = node.into();
1324 if self.nodes.contains_key(&node) {
1325 continue;
1326 }
1327 self.nodes.insert(node.clone(), AttrMap::new());
1328 self.succ_indices.push(Vec::new());
1329 self.pred_indices.push(Vec::new());
1330 inserted += 1;
1331 }
1332 if inserted > 0 {
1333 self.revision = self
1334 .revision
1335 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1336 self.record_decision(
1337 "extend_nodes_unrecorded",
1338 0.0,
1339 false,
1340 vec![EvidenceTerm {
1341 signal: "batch_node_count".into(),
1342 observed_value: count_evidence(inserted),
1343 log_likelihood_ratio: -1.0,
1344 }],
1345 );
1346 }
1347 inserted
1348 }
1349
1350 #[must_use]
1356 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
1357 where
1358 I: IntoIterator<Item = (String, AttrMap)>,
1359 {
1360 let mut inserted = 0usize;
1361 for (node, attrs) in nodes {
1362 if let Some(existing) = self.nodes.get_mut(&node) {
1363 existing.extend(attrs);
1364 continue;
1365 }
1366 self.nodes.insert(node.clone(), attrs);
1367 self.succ_indices.push(Vec::new());
1368 self.pred_indices.push(Vec::new());
1369 inserted += 1;
1370 }
1371 if inserted > 0 {
1372 self.revision = self
1373 .revision
1374 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1375 self.record_decision(
1376 "extend_nodes_unrecorded",
1377 0.0,
1378 false,
1379 vec![EvidenceTerm {
1380 signal: "batch_node_count".into(),
1381 observed_value: count_evidence(inserted),
1382 log_likelihood_ratio: -1.0,
1383 }],
1384 );
1385 }
1386 inserted
1387 }
1388
1389 #[must_use]
1396 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1397 where
1398 I: IntoIterator<Item = (String, String, AttrMap)>,
1399 {
1400 let mut inserted = 0usize;
1401 let mut merged_changed = false;
1402 for (source, target, attrs) in edges {
1403 if !self.nodes.contains_key(&source) {
1404 self.nodes.insert(source.clone(), AttrMap::new());
1405 self.succ_indices.push(Vec::new());
1406 self.pred_indices.push(Vec::new());
1407 }
1408 if source != target && !self.nodes.contains_key(&target) {
1409 self.nodes.insert(target.clone(), AttrMap::new());
1410 self.succ_indices.push(Vec::new());
1411 self.pred_indices.push(Vec::new());
1412 }
1413 let edge_key = (
1414 self.nodes.get_index_of(&source).expect("created above"),
1415 self.nodes.get_index_of(&target).expect("created above"),
1416 );
1417 if let Some(existing) = self.edges.get_mut(&edge_key) {
1418 if attrs.is_empty() {
1422 continue;
1423 }
1424 if attrs
1425 .iter()
1426 .any(|(key, value)| existing.get(key) != Some(value))
1427 {
1428 merged_changed = true;
1429 }
1430 existing.extend(attrs);
1431 continue;
1432 }
1433 self.edges.insert(edge_key, attrs);
1434 let s_idx = self
1435 .nodes
1436 .get_index_of(&source)
1437 .expect("source node exists");
1438 let t_idx = self
1439 .nodes
1440 .get_index_of(&target)
1441 .expect("target node exists");
1442 self.succ_indices[s_idx].push(t_idx);
1443 self.pred_indices[t_idx].push(s_idx);
1444 inserted += 1;
1445 }
1446 if inserted > 0 || merged_changed {
1447 self.revision = self
1448 .revision
1449 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1450 self.record_decision(
1451 "extend_edges_unrecorded",
1452 0.0,
1453 false,
1454 vec![EvidenceTerm {
1455 signal: "batch_edge_count".into(),
1456 observed_value: count_evidence(inserted),
1457 log_likelihood_ratio: -1.0,
1458 }],
1459 );
1460 }
1461 inserted
1462 }
1463
1464 #[must_use]
1469 pub fn extend_prepared_edges_with_attrs_row_staged_unrecorded<I, N>(
1470 &mut self,
1471 new_nodes: N,
1472 edges: I,
1473 ) -> usize
1474 where
1475 I: IntoIterator<Item = (String, String, AttrMap)>,
1476 N: IntoIterator<Item = String>,
1477 {
1478 for node in new_nodes {
1479 if self.nodes.contains_key(&node) {
1480 continue;
1481 }
1482 self.nodes.insert(node.clone(), AttrMap::new());
1483 self.succ_indices.push(Vec::new());
1484 self.pred_indices.push(Vec::new());
1485 }
1486
1487 let mut inserted = 0usize;
1488 let mut merged_changed = false;
1489 let mut successor_rows: IndexMap<String, Vec<String>> = IndexMap::new();
1490 let mut predecessor_rows: IndexMap<String, Vec<String>> = IndexMap::new();
1491
1492 for (source, target, attrs) in edges {
1493 let edge_key = (
1494 self.nodes.get_index_of(&source).expect("created above"),
1495 self.nodes.get_index_of(&target).expect("created above"),
1496 );
1497 if let Some(existing) = self.edges.get_mut(&edge_key) {
1498 if attrs.is_empty() {
1502 continue;
1503 }
1504 if attrs
1505 .iter()
1506 .any(|(key, value)| existing.get(key) != Some(value))
1507 {
1508 merged_changed = true;
1509 }
1510 existing.extend(attrs);
1511 continue;
1512 }
1513
1514 successor_rows
1515 .entry(source.clone())
1516 .or_default()
1517 .push(target.clone());
1518 predecessor_rows
1519 .entry(target.clone())
1520 .or_default()
1521 .push(source.clone());
1522 let s_idx = self
1523 .nodes
1524 .get_index_of(&source)
1525 .expect("source node exists");
1526 let t_idx = self
1527 .nodes
1528 .get_index_of(&target)
1529 .expect("target node exists");
1530 self.succ_indices[s_idx].push(t_idx);
1531 self.pred_indices[t_idx].push(s_idx);
1532 self.edges.insert(edge_key, attrs);
1533 inserted += 1;
1534 }
1535
1536 if inserted > 0 || merged_changed {
1537 self.revision = self
1538 .revision
1539 .saturating_add(u64::try_from(inserted.max(1)).unwrap_or(u64::MAX));
1540 self.record_decision(
1541 "extend_edges_unrecorded",
1542 0.0,
1543 false,
1544 vec![EvidenceTerm {
1545 signal: "batch_edge_count".into(),
1546 observed_value: count_evidence(inserted),
1547 log_likelihood_ratio: -1.0,
1548 }],
1549 );
1550 }
1551
1552 inserted
1553 }
1554
1555 #[must_use]
1564 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1570 where
1571 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1572 {
1573 let node_count = self.nodes.len();
1574 let edges = edges.into_iter();
1575 self.edges.reserve(edges.size_hint().0);
1576 let mut inserted = 0usize;
1577 let mut merged_changed = false;
1578 for (source_idx, target_idx, attrs) in edges {
1579 debug_assert!(source_idx < node_count && target_idx < node_count);
1580 let edge_key = (source_idx, target_idx);
1581 if let Some(existing) = self.edges.get_mut(&edge_key) {
1582 if attrs.is_empty() {
1586 continue;
1587 }
1588 if attrs
1589 .iter()
1590 .any(|(key, value)| existing.get(key) != Some(value))
1591 {
1592 merged_changed = true;
1593 }
1594 existing.extend(attrs);
1595 continue;
1596 }
1597 self.edges.insert(edge_key, attrs);
1598 self.succ_indices[source_idx].push(target_idx);
1599 self.pred_indices[target_idx].push(source_idx);
1600 inserted += 1;
1601 }
1602 if inserted > 0 || merged_changed {
1603 self.revision = self
1604 .revision
1605 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1606 }
1607 inserted
1608 }
1609
1610 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
1611 &mut self,
1612 nodes: N,
1613 edges: I,
1614 ) -> usize
1615 where
1616 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1617 N: IntoIterator<Item = String>,
1618 {
1619 if !self.nodes.is_empty()
1620 || !self.succ_indices.is_empty()
1621 || !self.pred_indices.is_empty()
1622 || !self.edges.is_empty()
1623 {
1624 return 0;
1625 }
1626
1627 let node_labels: Vec<String> = nodes.into_iter().collect();
1628 self.nodes.reserve(node_labels.len());
1629 for node in node_labels {
1630 self.nodes.insert(node, AttrMap::new());
1631 }
1632 let node_count = self.nodes.len();
1633 self.succ_indices = vec![Vec::new(); node_count];
1634 self.pred_indices = vec![Vec::new(); node_count];
1635
1636 let mut inserted = 0usize;
1637 let mut merged_changed = false;
1638 let edges = edges.into_iter();
1639 self.edges.reserve(edges.size_hint().0);
1640 for (source_idx, target_idx, attrs) in edges {
1641 if source_idx >= node_count || target_idx >= node_count {
1642 continue;
1643 }
1644 let edge_key = (source_idx, target_idx);
1645 if let Some(existing) = self.edges.get_mut(&edge_key) {
1646 if attrs.is_empty() {
1650 continue;
1651 }
1652 if attrs
1653 .iter()
1654 .any(|(key, value)| existing.get(key) != Some(value))
1655 {
1656 merged_changed = true;
1657 }
1658 existing.extend(attrs);
1659 continue;
1660 }
1661 self.edges.insert(edge_key, attrs);
1662 self.succ_indices[source_idx].push(target_idx);
1663 self.pred_indices[target_idx].push(source_idx);
1664 inserted += 1;
1665 }
1666
1667 if node_count > 0 || inserted > 0 || merged_changed {
1668 self.revision = self.revision.saturating_add(
1669 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
1670 );
1671 self.record_decision(
1672 "extend_fresh_index_edges_unrecorded",
1673 0.0,
1674 false,
1675 vec![
1676 EvidenceTerm {
1677 signal: "batch_node_count".into(),
1678 observed_value: count_evidence(node_count),
1679 log_likelihood_ratio: -1.0,
1680 },
1681 EvidenceTerm {
1682 signal: "batch_edge_count".into(),
1683 observed_value: count_evidence(inserted),
1684 log_likelihood_ratio: -1.0,
1685 },
1686 ],
1687 );
1688 }
1689
1690 inserted
1691 }
1692
1693 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
1694 if defaults.is_empty() {
1695 return false;
1696 }
1697 let mut inserted = 0usize;
1698 for attrs in self.edges.values_mut() {
1699 for (key, value) in defaults {
1700 if !attrs.contains_key(key) {
1701 attrs.insert(key.clone(), value.clone());
1702 inserted += 1;
1703 }
1704 }
1705 }
1706 if inserted > 0 {
1707 self.revision = self.revision.saturating_add(1);
1708 self.record_decision(
1709 "apply_edge_defaults",
1710 0.0,
1711 false,
1712 vec![EvidenceTerm {
1713 signal: "defaults_applied".into(),
1714 observed_value: count_evidence(inserted),
1715 log_likelihood_ratio: 0.0,
1716 }],
1717 );
1718 return true;
1719 }
1720 false
1721 }
1722
1723 pub fn replace_edge_attrs(&mut self, source: &str, target: &str, attrs: AttrMap) -> bool {
1725 let Some(edge_key) = self.edge_pair_key(source, target) else {
1726 return false;
1727 };
1728 if let Some(slot) = self.edges.get_mut(&edge_key) {
1729 if *slot != attrs {
1730 *slot = attrs;
1731 self.revision = self.revision.saturating_add(1);
1732 }
1733 true
1734 } else {
1735 false
1736 }
1737 }
1738
1739 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
1741 if let Some(slot) = self.nodes.get_mut(node) {
1742 if *slot != attrs {
1743 *slot = attrs;
1744 self.revision = self.revision.saturating_add(1);
1745 }
1746 true
1747 } else {
1748 false
1749 }
1750 }
1751
1752 pub fn remove_edge(&mut self, source: &str, target: &str) -> bool {
1754 let Some(pair) = self.edge_pair_key(source, target) else {
1755 return false;
1756 };
1757 let removed = self.edges.swap_remove(&pair).is_some();
1769 if removed {
1770 if let (Some(s_idx), Some(t_idx)) = (
1771 self.nodes.get_index_of(source),
1772 self.nodes.get_index_of(target),
1773 ) {
1774 self.succ_indices[s_idx].retain(|&i| i != t_idx);
1775 self.pred_indices[t_idx].retain(|&i| i != s_idx);
1776 }
1777 self.revision = self.revision.saturating_add(1);
1778 }
1779 removed
1780 }
1781
1782 pub fn remove_node(&mut self, node: &str) -> bool {
1784 if !self.nodes.contains_key(node) {
1785 return false;
1786 }
1787
1788 let node_idx = self
1800 .nodes
1801 .get_index_of(node)
1802 .expect("node existence checked above");
1803
1804 if node_idx + 1 == self.nodes.len()
1814 && self.succ_indices[node_idx].is_empty()
1815 && self.pred_indices[node_idx].is_empty()
1816 {
1817 self.nodes.shift_remove(node);
1818 self.succ_indices.remove(node_idx);
1819 self.pred_indices.remove(node_idx);
1820 self.revision = self.revision.saturating_add(1);
1821 return true;
1822 }
1823
1824 let targets: Vec<usize> = self.succ_indices[node_idx].clone();
1825 for t in targets {
1826 self.edges.swap_remove(&(node_idx, t));
1827 }
1828 let sources: Vec<usize> = self.pred_indices[node_idx].clone();
1829 for s in sources {
1830 self.edges.swap_remove(&(s, node_idx));
1831 }
1832
1833 let idx = self
1837 .nodes
1838 .get_index_of(node)
1839 .expect("node existence checked above");
1840 self.nodes.shift_remove(node);
1841 self.succ_indices.remove(idx);
1842 self.pred_indices.remove(idx);
1843 for rows in [&mut self.succ_indices, &mut self.pred_indices] {
1847 for row in rows.iter_mut() {
1848 let mut w = 0usize;
1849 for read in 0..row.len() {
1850 let e = row[read];
1851 if e == idx {
1852 continue;
1853 }
1854 row[w] = if e > idx { e - 1 } else { e };
1855 w += 1;
1856 }
1857 row.truncate(w);
1858 }
1859 }
1860 self.edges = std::mem::take(&mut self.edges)
1863 .into_iter()
1864 .map(|((s, t), attrs)| {
1865 (
1866 (
1867 if s > idx { s - 1 } else { s },
1868 if t > idx { t - 1 } else { t },
1869 ),
1870 attrs,
1871 )
1872 })
1873 .collect();
1874 self.revision = self.revision.saturating_add(1);
1875 true
1876 }
1877
1878 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
1879 where
1880 I: IntoIterator<Item = &'a str>,
1881 {
1882 let remove_set: std::collections::HashSet<&str> = nodes
1883 .into_iter()
1884 .filter(|node| self.nodes.contains_key(*node))
1885 .collect();
1886 if remove_set.is_empty() {
1887 return (0, 0);
1888 }
1889
1890 let old_node_count = self.nodes.len();
1891 let old_edge_count = self.edges.len();
1892
1893 let removed_mask: Vec<bool> = self
1897 .nodes
1898 .keys()
1899 .map(|k| remove_set.contains(k.as_str()))
1900 .collect();
1901 let mut remap: Vec<usize> = Vec::with_capacity(old_node_count);
1902 let mut next = 0usize;
1903 for &gone in &removed_mask {
1904 remap.push(next);
1905 if !gone {
1906 next += 1;
1907 }
1908 }
1909 for rows in [&mut self.succ_indices, &mut self.pred_indices] {
1910 let old_rows = std::mem::take(rows);
1911 *rows = old_rows
1912 .into_iter()
1913 .enumerate()
1914 .filter(|(old_idx, _)| !removed_mask[*old_idx])
1915 .map(|(_, row)| {
1916 row.into_iter()
1917 .filter(|&nbr| !removed_mask[nbr])
1918 .map(|nbr| remap[nbr])
1919 .collect()
1920 })
1921 .collect();
1922 }
1923 self.nodes
1924 .retain(|node, _| !remove_set.contains(node.as_str()));
1925 self.edges
1926 .retain(|&(s, t), _| !removed_mask[s] && !removed_mask[t]);
1927 self.edges = std::mem::take(&mut self.edges)
1929 .into_iter()
1930 .map(|((s, t), attrs)| ((remap[s], remap[t]), attrs))
1931 .collect();
1932
1933 let removed_nodes = old_node_count - self.nodes.len();
1934 let removed_edges = old_edge_count - self.edges.len();
1935 self.revision = self
1936 .revision
1937 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
1938 (removed_nodes, removed_edges)
1939 }
1940
1941 #[must_use]
1948 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
1949 let mut ordered = Vec::with_capacity(self.edges.len());
1950
1951 for (u, row) in self.succ_indices.iter().enumerate() {
1952 let node = self.nodes.get_index(u).expect("valid index").0;
1953 for &t in row {
1954 let target = self.nodes.get_index(t).expect("valid index").0;
1955 if let Some(attrs) = self.edges.get(&(u, t)) {
1956 ordered.push(EdgeSnapshot {
1957 left: node.clone(),
1958 right: target.clone(),
1959 attrs: attrs.clone(),
1960 });
1961 }
1962 }
1963 }
1964
1965 ordered
1966 }
1967
1968 #[must_use]
1969 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
1970 let mut ordered = Vec::with_capacity(self.edges.len());
1971
1972 for (u, row) in self.succ_indices.iter().enumerate() {
1973 let node = self.nodes.get_index(u).expect("valid index").0;
1974 for &t in row {
1975 let target = self.nodes.get_index(t).expect("valid index").0;
1976 if let Some(attrs) = self.edges.get(&(u, t)) {
1977 ordered.push((node.as_str(), target.as_str(), attrs));
1978 }
1979 }
1980 }
1981
1982 ordered
1983 }
1984
1985 #[must_use]
1993 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
1994 let mut total: i128 = 0;
1995 for attrs in self.edges.values() {
1996 let value = match attrs.get(weight) {
1997 Some(CgseValue::Int(v)) => i128::from(*v),
1998 Some(_) => return None,
1999 None => 1,
2000 };
2001 total = total.checked_add(value)?;
2002 }
2003 Some(total)
2004 }
2005
2006 #[must_use]
2021 pub fn reversed(&self) -> Self {
2022 let n = self.nodes.len();
2023 let mut succ_indices: Vec<Vec<usize>> = vec![Vec::new(); n];
2024 let mut pred_indices: Vec<Vec<usize>> = vec![Vec::new(); n];
2025 for (i, (succ_row, pred_row)) in succ_indices
2026 .iter_mut()
2027 .zip(pred_indices.iter_mut())
2028 .enumerate()
2029 {
2030 succ_row.reserve(self.pred_indices[i].len());
2033 pred_row.reserve(self.succ_indices[i].len());
2034 }
2035 let mut edges: crate::FxIndexMap<(usize, usize), AttrMap> =
2036 crate::FxIndexMap::with_capacity_and_hasher(
2037 self.edges.len(),
2038 rustc_hash::FxBuildHasher,
2039 );
2040 for (u, (source_succs, reversed_preds)) in self
2041 .succ_indices
2042 .iter()
2043 .zip(pred_indices.iter_mut())
2044 .enumerate()
2045 {
2046 for &t in source_succs {
2047 succ_indices[t].push(u);
2049 reversed_preds.push(t);
2050 if let Some(attrs) = self.edges.get(&(u, t)) {
2051 edges.insert((t, u), attrs.clone());
2052 }
2053 }
2054 }
2055 Self {
2056 mode: self.mode,
2057 revision: 0,
2058 nodes: self.nodes.clone(),
2059 succ_indices,
2060 pred_indices,
2061 edges,
2062 runtime_policy: self.runtime_policy.clone(),
2063 csr_cache: std::sync::Arc::default(),
2064 all_int_cache: std::sync::Arc::default(),
2065 }
2066 }
2067
2068 #[must_use]
2070 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
2071 let mut ordered = Vec::with_capacity(self.edges.len());
2072
2073 for (u, row) in self.succ_indices.iter().enumerate() {
2074 for &t in row {
2075 if self.edges.contains_key(&(u, t)) {
2076 ordered.push((u, t));
2077 }
2078 }
2079 }
2080
2081 ordered
2082 }
2083
2084 #[must_use]
2085 pub fn snapshot(&self) -> DiGraphSnapshot {
2086 let node_attrs: std::collections::BTreeMap<String, AttrMap> = self
2087 .nodes
2088 .iter()
2089 .filter(|(_, attrs)| !attrs.is_empty())
2090 .map(|(name, attrs)| (name.clone(), attrs.clone()))
2091 .collect();
2092 DiGraphSnapshot {
2093 mode: self.mode,
2094 nodes: self.nodes.keys().cloned().collect(),
2095 node_attrs,
2096 edges: self.edges_ordered(),
2097 }
2098 }
2099
2100 #[must_use]
2104 pub fn to_undirected(&self) -> crate::Graph {
2105 let mut g = crate::Graph::with_runtime_policy(self.runtime_policy.clone());
2106 for (node, attrs) in &self.nodes {
2107 g.add_node_with_attrs(node.clone(), attrs.clone());
2108 }
2109 let edges: Vec<(String, String, AttrMap)> = self
2123 .edges_ordered()
2124 .into_iter()
2125 .map(|snap| (snap.left, snap.right, snap.attrs))
2126 .collect();
2127 let _ = g.extend_edges_with_attrs_unrecorded(edges);
2128 g
2129 }
2130
2131 fn record_decision(
2136 &mut self,
2137 operation: &'static str,
2138 incompatibility_probability: f64,
2139 unknown_incompatible_feature: bool,
2140 evidence: Vec<EvidenceTerm>,
2141 ) -> DecisionAction {
2142 let action = self
2143 .runtime_policy
2144 .action_for(incompatibility_probability, unknown_incompatible_feature);
2145 self.runtime_policy.record(
2146 operation,
2147 action,
2148 incompatibility_probability,
2149 "argmin expected loss over {allow,full_validate,fail_closed}",
2150 evidence,
2151 );
2152 action
2153 }
2154}
2155
2156#[derive(Debug, Clone)]
2157pub struct MultiDiGraph {
2158 mode: CompatibilityMode,
2159 revision: u64,
2160 nodes: crate::FxIndexMap<String, AttrMap>,
2161 successors: crate::FxIndexMap<String, crate::FxIndexMap<String, crate::FxIndexSet<usize>>>,
2162 predecessors: crate::FxIndexMap<String, crate::FxIndexMap<String, crate::FxIndexSet<usize>>>,
2163 edges: crate::FxIndexMap<DirectedEdgeKey, IndexMap<usize, AttrMap>>,
2164 runtime_policy: RuntimePolicy,
2165 edge_count: usize,
2166 csr_cache: MultiDiCsrCache,
2167 edge_pair_index: EdgePairIndex,
2191}
2192
2193impl MultiDiGraph {
2194 #[must_use]
2200 pub fn get_node_index(&self, node: &str) -> Option<usize> {
2201 self.nodes.get_index_of(node)
2202 }
2203
2204 #[must_use]
2207 pub fn clone_with_fresh_policy(&self) -> Self {
2208 Self {
2209 mode: self.mode,
2210 revision: self.revision,
2211 nodes: self.nodes.clone(),
2212 successors: self.successors.clone(),
2213 predecessors: self.predecessors.clone(),
2214 edges: self.edges.clone(),
2215 runtime_policy: RuntimePolicy::new(self.mode),
2216 edge_count: self.edge_count,
2217 csr_cache: self.csr_cache.clone(),
2218 edge_pair_index: EdgePairIndex::default(),
2219 }
2220 }
2221
2222 pub fn reorder_pred_rows_for_nx_copy_walk(&mut self) {
2228 let mut order_map: IndexMap<String, Vec<String>> =
2236 IndexMap::with_capacity(self.predecessors.len());
2237 for (u, succ_row) in &self.successors {
2238 for v in succ_row.keys() {
2239 order_map.entry(v.clone()).or_default().push(u.clone());
2240 }
2241 }
2242 let orders: Vec<(String, Vec<String>)> = order_map.into_iter().collect();
2243 self.apply_row_orders(&orders, true);
2244 }
2245
2246 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)], pred: bool) {
2250 let map = if pred {
2251 &mut self.predecessors
2252 } else {
2253 &mut self.successors
2254 };
2255 for (node, order) in orders {
2256 let Some(row) = map.get_mut(node.as_str()) else {
2257 continue;
2258 };
2259 let mut old = std::mem::take(row);
2260 let mut new_row =
2261 crate::FxIndexMap::with_capacity_and_hasher(old.len(), rustc_hash::FxBuildHasher);
2262 for v in order {
2263 if let Some((k, val)) = old.shift_remove_entry(v.as_str()) {
2264 new_row.insert(k, val);
2265 }
2266 }
2267 for (k, val) in old {
2268 new_row.insert(k, val);
2269 }
2270 *row = new_row;
2271 }
2272 }
2273
2274 #[must_use]
2275 pub fn new(mode: CompatibilityMode) -> Self {
2276 Self {
2277 mode,
2278 revision: 0,
2279 nodes: crate::FxIndexMap::default(),
2280 successors: crate::FxIndexMap::default(),
2281 predecessors: crate::FxIndexMap::default(),
2282 edges: crate::FxIndexMap::default(),
2283 runtime_policy: RuntimePolicy::new(mode),
2284 edge_count: 0,
2285 csr_cache: std::sync::Arc::default(),
2286 edge_pair_index: EdgePairIndex::default(),
2287 }
2288 }
2289
2290 #[must_use]
2291 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
2292 let mode = runtime_policy.mode();
2293 Self {
2294 mode,
2295 revision: 0,
2296 nodes: crate::FxIndexMap::default(),
2297 successors: crate::FxIndexMap::default(),
2298 predecessors: crate::FxIndexMap::default(),
2299 edges: crate::FxIndexMap::default(),
2300 runtime_policy,
2301 edge_count: 0,
2302 csr_cache: std::sync::Arc::default(),
2303 edge_pair_index: EdgePairIndex::default(),
2304 }
2305 }
2306
2307 #[must_use]
2308 pub fn strict() -> Self {
2309 Self::new(CompatibilityMode::Strict)
2310 }
2311
2312 #[must_use]
2313 pub fn hardened() -> Self {
2314 Self::new(CompatibilityMode::Hardened)
2315 }
2316
2317 #[must_use]
2318 pub fn mode(&self) -> CompatibilityMode {
2319 self.mode
2320 }
2321
2322 #[must_use]
2323 pub fn node_count(&self) -> usize {
2324 self.nodes.len()
2325 }
2326
2327 #[must_use]
2328 pub fn edge_count(&self) -> usize {
2329 self.edge_count
2330 }
2331
2332 #[must_use]
2333 pub fn number_of_selfloops(&self) -> usize {
2334 self.edges
2335 .iter()
2336 .filter(|(edge_key, _)| edge_key.source == edge_key.target)
2337 .map(|(_, edge_bucket)| edge_bucket.len())
2338 .sum()
2339 }
2340
2341 #[must_use]
2344 pub fn edge_keys(&self, source: &str, target: &str) -> Option<Vec<usize>> {
2345 self.successors
2346 .get(source)?
2347 .get(target)
2348 .map(|keys| keys.iter().copied().collect())
2349 }
2350
2351 pub fn edge_keys_iter(
2353 &self,
2354 source: &str,
2355 target: &str,
2356 ) -> Option<impl Iterator<Item = &usize>> {
2357 self.successors
2358 .get(source)?
2359 .get(target)
2360 .map(|keys| keys.iter())
2361 }
2362
2363 #[must_use]
2364 pub fn revision(&self) -> u64 {
2365 self.revision
2366 }
2367
2368 #[must_use]
2372 pub fn csr(&self) -> std::sync::Arc<MultiDiCsr> {
2373 if let Ok(guard) = self.csr_cache.read()
2374 && let Some((rev, csr)) = guard.as_ref()
2375 && *rev == self.revision
2376 {
2377 return csr.clone();
2378 }
2379 let built = std::sync::Arc::new(self.build_csr());
2380 if let Ok(mut guard) = self.csr_cache.write() {
2381 *guard = Some((self.revision, built.clone()));
2382 }
2383 built
2384 }
2385
2386 fn build_csr(&self) -> MultiDiCsr {
2387 let n = self.nodes.len();
2388 let distinct_succ_edges: usize = self.successors.values().map(IndexMap::len).sum();
2389 let distinct_pred_edges: usize = self.predecessors.values().map(IndexMap::len).sum();
2390 let mut succ_offsets = Vec::with_capacity(n + 1);
2391 let mut pred_offsets = Vec::with_capacity(n + 1);
2392 let mut succ_targets = Vec::with_capacity(distinct_succ_edges);
2393 let mut pred_targets = Vec::with_capacity(distinct_pred_edges);
2394 succ_offsets.push(0);
2395 pred_offsets.push(0);
2396 for node in self.nodes.keys() {
2397 if let Some(row) = self.successors.get(node.as_str()) {
2398 succ_targets.extend(row.keys().filter_map(|target| {
2399 self.nodes
2400 .get_index_of(target.as_str())
2401 .map(|idx| u32::try_from(idx).unwrap_or(u32::MAX))
2402 }));
2403 }
2404 succ_offsets.push(succ_targets.len());
2405
2406 if let Some(row) = self.predecessors.get(node.as_str()) {
2407 pred_targets.extend(row.keys().filter_map(|source| {
2408 self.nodes
2409 .get_index_of(source.as_str())
2410 .map(|idx| u32::try_from(idx).unwrap_or(u32::MAX))
2411 }));
2412 }
2413 pred_offsets.push(pred_targets.len());
2414 }
2415 MultiDiCsr {
2416 succ_offsets,
2417 succ_targets,
2418 pred_offsets,
2419 pred_targets,
2420 }
2421 }
2422
2423 #[must_use]
2424 pub fn has_node(&self, node: &str) -> bool {
2425 self.nodes.contains_key(node)
2426 }
2427
2428 #[must_use]
2429 pub fn has_edge(&self, source: &str, target: &str) -> bool {
2430 self.edges
2431 .get(&DirectedEdgeKeyRef::new(source, target))
2432 .is_some_and(|edge_bucket| !edge_bucket.is_empty())
2433 }
2434
2435 fn build_edge_pair_index(&self) -> EdgePairMap {
2443 let mut built =
2444 EdgePairMap::with_capacity_and_hasher(self.edges.len(), rustc_hash::FxBuildHasher);
2445 for (position, edge_key) in self.edges.keys().enumerate() {
2446 if let Some(source_idx) = self.nodes.get_index_of(edge_key.source.as_str())
2447 && let Some(target_idx) = self.nodes.get_index_of(edge_key.target.as_str())
2448 {
2449 built.insert((source_idx, target_idx), position);
2450 }
2451 }
2452 built
2453 }
2454
2455 fn edge_pair_position(&self, source_idx: usize, target_idx: usize) -> Option<Option<usize>> {
2462 use std::sync::atomic::Ordering;
2463 if let Ok(guard) = self.edge_pair_index.map.read()
2464 && let Some((stamp, map)) = guard.as_ref()
2465 && *stamp == self.revision
2466 {
2467 return Some(map.get(&(source_idx, target_idx)).copied());
2468 }
2469 let revision = self.revision;
2470 if self.edge_pair_index.probed_revision.load(Ordering::Relaxed) != revision {
2471 self.edge_pair_index
2472 .probed_revision
2473 .store(revision, Ordering::Relaxed);
2474 self.edge_pair_index
2475 .probes_since_build
2476 .store(1, Ordering::Relaxed);
2477 return None;
2478 }
2479 let seen = self
2480 .edge_pair_index
2481 .probes_since_build
2482 .fetch_add(1, Ordering::Relaxed)
2483 + 1;
2484 if seen < pair_index_warmup(self.edges.len()) {
2485 return None;
2486 }
2487 let built = self.build_edge_pair_index();
2488 let found = built.get(&(source_idx, target_idx)).copied();
2489 if let Ok(mut guard) = self.edge_pair_index.map.write() {
2490 *guard = Some((revision, built));
2491 }
2492 Some(found)
2493 }
2494
2495 fn pair_index_note_edge_added(
2507 &mut self,
2508 previous_revision: u64,
2509 source: &str,
2510 target: &str,
2511 new_pair_position: Option<usize>,
2512 ) {
2513 match self.edge_pair_index.map.get_mut() {
2514 Ok(Some((stamp, _))) if *stamp == previous_revision => {}
2515 _ => return,
2516 }
2517 let resolved = match new_pair_position {
2518 Some(position) => match (
2519 self.nodes.get_index_of(source),
2520 self.nodes.get_index_of(target),
2521 ) {
2522 (Some(source_idx), Some(target_idx)) => Some(((source_idx, target_idx), position)),
2523 _ => {
2525 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2526 *slot = None;
2527 }
2528 return;
2529 }
2530 },
2531 None => None,
2532 };
2533 let current = self.revision;
2534 if let Ok(Some((stamp, map))) = self.edge_pair_index.map.get_mut() {
2535 if let Some((pair, position)) = resolved {
2536 map.insert(pair, position);
2537 }
2538 *stamp = current;
2539 }
2540 }
2541
2542 fn edge_pair_index_is_live(&self, at_revision: u64) -> bool {
2545 matches!(
2546 self.edge_pair_index.map.read().as_deref(),
2547 Ok(Some((stamp, _))) if *stamp == at_revision
2548 )
2549 }
2550
2551 fn pair_index_note_edge_removed(
2563 &mut self,
2564 previous_revision: u64,
2565 source: &str,
2566 target: &str,
2567 dropped_position: Option<usize>,
2568 ) {
2569 match self.edge_pair_index.map.get_mut() {
2570 Ok(Some((stamp, _))) if *stamp == previous_revision => {}
2571 _ => return,
2572 }
2573 let removed_pair = match dropped_position {
2574 Some(_) => match (
2575 self.nodes.get_index_of(source),
2576 self.nodes.get_index_of(target),
2577 ) {
2578 (Some(source_idx), Some(target_idx)) => Some((source_idx, target_idx)),
2579 _ => {
2580 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2581 *slot = None;
2582 }
2583 return;
2584 }
2585 },
2586 None => None,
2587 };
2588 let relocated = match dropped_position {
2590 Some(position) if position < self.edges.len() => match self.edges.get_index(position) {
2591 Some((moved_key, _)) => {
2592 let moved_source = moved_key.source.clone();
2593 let moved_target = moved_key.target.clone();
2594 match (
2595 self.nodes.get_index_of(moved_source.as_str()),
2596 self.nodes.get_index_of(moved_target.as_str()),
2597 ) {
2598 (Some(source_idx), Some(target_idx)) => {
2599 Some(((source_idx, target_idx), position))
2600 }
2601 _ => {
2602 if let Ok(slot) = self.edge_pair_index.map.get_mut() {
2603 *slot = None;
2604 }
2605 return;
2606 }
2607 }
2608 }
2609 None => None,
2610 },
2611 _ => None,
2612 };
2613 let current = self.revision;
2614 if let Ok(Some((stamp, map))) = self.edge_pair_index.map.get_mut() {
2615 if let Some(pair) = removed_pair {
2616 map.remove(&pair);
2617 }
2618 if let Some((pair, position)) = relocated {
2619 map.insert(pair, position);
2620 }
2621 *stamp = current;
2622 }
2623 }
2624
2625 #[must_use]
2635 pub fn has_edge_by_indices(&self, source_idx: usize, target_idx: usize) -> bool {
2636 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2637 return position.is_some();
2638 }
2639 match (
2640 self.get_node_name(source_idx),
2641 self.get_node_name(target_idx),
2642 ) {
2643 (Some(source), Some(target)) => self.has_edge(source, target),
2644 _ => false,
2645 }
2646 }
2647
2648 #[must_use]
2657 pub fn edge_key_count_by_indices(&self, source_idx: usize, target_idx: usize) -> usize {
2658 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2659 return position
2660 .and_then(|found| self.edges.get_index(found))
2661 .map_or(0, |(_, keyed)| keyed.len());
2662 }
2663 match (
2664 self.get_node_name(source_idx),
2665 self.get_node_name(target_idx),
2666 ) {
2667 (Some(source), Some(target)) => self
2668 .edges
2669 .get(&DirectedEdgeKeyRef::new(source, target))
2670 .map_or(0, IndexMap::len),
2671 _ => 0,
2672 }
2673 }
2674
2675 #[must_use]
2680 pub fn edge_attrs_by_indices(
2681 &self,
2682 source_idx: usize,
2683 target_idx: usize,
2684 key: usize,
2685 ) -> Option<&AttrMap> {
2686 if let Some(position) = self.edge_pair_position(source_idx, target_idx) {
2687 let (_, keyed) = self.edges.get_index(position?)?;
2688 return keyed.get(&key);
2689 }
2690 let source = self.get_node_name(source_idx)?;
2691 let target = self.get_node_name(target_idx)?;
2692 self.edges
2693 .get(&DirectedEdgeKeyRef::new(source, target))
2694 .and_then(|bucket| bucket.get(&key))
2695 }
2696
2697 #[must_use]
2698 pub fn nodes_ordered(&self) -> Vec<&str> {
2699 self.nodes.keys().map(String::as_str).collect()
2700 }
2701
2702 #[must_use]
2704 #[inline]
2705 pub fn get_node_name(&self, index: usize) -> Option<&str> {
2706 self.nodes.get_index(index).map(|(name, _)| name.as_str())
2707 }
2708
2709 #[must_use]
2712 pub fn node_index_matches_int(&self, idx: usize) -> bool {
2713 self.nodes
2714 .get_index(idx)
2715 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
2716 }
2717
2718 #[must_use]
2719 pub fn successors(&self, node: &str) -> Option<Vec<&str>> {
2720 self.successors
2721 .get(node)
2722 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
2723 }
2724
2725 #[must_use]
2726 pub fn successors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
2727 self.successors
2728 .get(node)
2729 .map(|neighbors| neighbors.keys().map(String::as_str))
2730 }
2731
2732 #[must_use]
2733 pub fn predecessors(&self, node: &str) -> Option<Vec<&str>> {
2734 self.predecessors
2735 .get(node)
2736 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
2737 }
2738
2739 #[must_use]
2740 pub fn predecessors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
2741 self.predecessors
2742 .get(node)
2743 .map(|neighbors| neighbors.keys().map(String::as_str))
2744 }
2745
2746 #[must_use]
2747 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
2748 self.successors(node)
2749 }
2750
2751 #[must_use]
2752 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
2753 self.successors_iter(node)
2754 }
2755
2756 #[must_use]
2757 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
2758 self.nodes.get(node)
2759 }
2760
2761 #[must_use]
2762 pub fn edge_attrs(&self, source: &str, target: &str, key: usize) -> Option<&AttrMap> {
2763 self.edges
2764 .get(&DirectedEdgeKeyRef::new(source, target))
2765 .and_then(|edge_bucket| edge_bucket.get(&key))
2766 }
2767
2768 #[must_use]
2769 pub fn edge_attr_values(
2770 &self,
2771 source: &str,
2772 target: &str,
2773 ) -> Option<impl Iterator<Item = &AttrMap> + '_> {
2774 self.edges
2775 .get(&DirectedEdgeKeyRef::new(source, target))
2776 .map(|edge_bucket| edge_bucket.values())
2777 }
2778
2779 #[must_use]
2780 pub fn evidence_ledger(&self) -> &EvidenceLedger {
2781 self.runtime_policy.decision_log()
2782 }
2783
2784 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
2785 self.runtime_policy.drain_decision_records()
2786 }
2787
2788 #[must_use]
2789 pub fn runtime_policy(&self) -> &RuntimePolicy {
2790 &self.runtime_policy
2791 }
2792
2793 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
2794 self.mode = runtime_policy.mode();
2795 self.runtime_policy = runtime_policy;
2796 }
2797
2798 #[must_use]
2799 pub fn is_directed(&self) -> bool {
2800 true
2801 }
2802
2803 #[must_use]
2804 pub fn is_multigraph(&self) -> bool {
2805 true
2806 }
2807
2808 #[must_use]
2810 pub fn out_degree(&self, node: &str) -> usize {
2811 self.successors
2812 .get(node)
2813 .map_or(0, |succs| succs.values().map(IndexSet::len).sum())
2814 }
2815
2816 #[must_use]
2818 pub fn in_degree(&self, node: &str) -> usize {
2819 self.predecessors
2820 .get(node)
2821 .map_or(0, |preds| preds.values().map(IndexSet::len).sum())
2822 }
2823
2824 #[must_use]
2826 pub fn degree(&self, node: &str) -> usize {
2827 self.in_degree(node) + self.out_degree(node)
2828 }
2829
2830 #[must_use]
2838 pub fn isolates(&self) -> Vec<String> {
2839 self.nodes
2840 .keys()
2841 .filter(|node| self.is_isolate(node.as_str()))
2842 .cloned()
2843 .collect()
2844 }
2845
2846 #[must_use]
2848 pub fn number_of_isolates(&self) -> usize {
2849 self.nodes
2850 .keys()
2851 .filter(|node| self.is_isolate(node.as_str()))
2852 .count()
2853 }
2854
2855 #[must_use]
2858 pub fn is_isolate(&self, node: &str) -> bool {
2859 self.nodes.contains_key(node)
2860 && self.successors.get(node).is_none_or(IndexMap::is_empty)
2861 && self.predecessors.get(node).is_none_or(IndexMap::is_empty)
2862 }
2863
2864 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
2865 self.add_node_with_attrs(node, AttrMap::new())
2866 }
2867
2868 fn add_node_with_attrs_unrecorded(
2886 &mut self,
2887 node: impl AsRef<str> + Into<String>,
2888 attrs: AttrMap,
2889 ) -> (bool, bool, usize) {
2890 let existed = self.nodes.contains_key(node.as_ref());
2891 let mut changed = !existed;
2892 let attrs_count = {
2893 let bucket = if existed {
2894 self.nodes
2895 .get_mut(node.as_ref())
2896 .expect("contains_key reported this node present")
2897 } else {
2898 self.nodes.entry(node.as_ref().to_owned()).or_default()
2899 };
2900 if !attrs.is_empty()
2901 && attrs
2902 .iter()
2903 .any(|(key, value)| bucket.get(key) != Some(value))
2904 {
2905 changed = true;
2906 }
2907 bucket.extend(attrs);
2908 bucket.len()
2909 };
2910 if !self.successors.contains_key(node.as_ref()) {
2911 self.successors
2912 .insert(node.as_ref().to_owned(), IndexMap::default());
2913 }
2914 if !self.predecessors.contains_key(node.as_ref()) {
2915 self.predecessors.insert(node.into(), IndexMap::default());
2916 }
2917 if changed {
2918 self.revision = self.revision.saturating_add(1);
2919 }
2920 (changed, existed, attrs_count)
2921 }
2922
2923 pub fn add_node_with_attrs(
2924 &mut self,
2925 node: impl AsRef<str> + Into<String>,
2926 attrs: AttrMap,
2927 ) -> bool {
2928 let (changed, existed, attrs_count) = self.add_node_with_attrs_unrecorded(node, attrs);
2929 self.record_decision(
2930 "add_node",
2931 0.0,
2932 false,
2933 vec![
2934 EvidenceTerm {
2935 signal: "node_preexisting".into(),
2936 observed_value: bool_evidence(existed),
2937 log_likelihood_ratio: -3.0,
2938 },
2939 EvidenceTerm {
2940 signal: "attrs_count".into(),
2941 observed_value: count_evidence(attrs_count),
2942 log_likelihood_ratio: -1.0,
2943 },
2944 ],
2945 );
2946 changed
2947 }
2948
2949 pub fn add_edge(
2950 &mut self,
2951 source: impl Into<String>,
2952 target: impl Into<String>,
2953 ) -> Result<usize, GraphError> {
2954 self.add_edge_impl(source, target, None, AttrMap::new())
2955 }
2956
2957 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
2961 where
2962 I: IntoIterator<Item = (String, AttrMap)>,
2963 {
2964 let mut inserted = 0usize;
2965 for (node, attrs) in nodes {
2966 if let Some(existing) = self.nodes.get_mut(&node) {
2967 existing.extend(attrs);
2968 continue;
2969 }
2970 self.nodes.insert(node.clone(), attrs);
2971 self.successors.entry(node.clone()).or_default();
2972 self.predecessors.entry(node).or_default();
2973 inserted += 1;
2974 }
2975 if inserted > 0 {
2976 self.revision = self
2977 .revision
2978 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2979 self.record_decision(
2980 "extend_nodes_with_attrs_unrecorded",
2981 0.0,
2982 false,
2983 vec![EvidenceTerm {
2984 signal: "batch_node_count".into(),
2985 observed_value: count_evidence(inserted),
2986 log_likelihood_ratio: -1.0,
2987 }],
2988 );
2989 }
2990 inserted
2991 }
2992
2993 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
3000 where
3001 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
3002 {
3003 let mut inserted = 0usize;
3004 for (source, target, key, attrs) in edges {
3005 if !self.nodes.contains_key(&source) {
3006 self.nodes.insert(source.clone(), AttrMap::new());
3007 self.successors.entry(source.clone()).or_default();
3008 self.predecessors.entry(source.clone()).or_default();
3009 }
3010 if source != target && !self.nodes.contains_key(&target) {
3011 self.nodes.insert(target.clone(), AttrMap::new());
3012 self.successors.entry(target.clone()).or_default();
3013 self.predecessors.entry(target.clone()).or_default();
3014 }
3015 let edge_key = DirectedEdgeKey::new(&source, &target);
3016 let bucket = self.edges.entry(edge_key).or_default();
3017 if !bucket.contains_key(&key) {
3018 self.edge_count += 1;
3019 inserted += 1;
3020 }
3021 bucket.entry(key).or_default().extend(attrs);
3022 self.successors
3023 .entry(source.clone())
3024 .or_default()
3025 .entry(target.clone())
3026 .or_default()
3027 .insert(key);
3028 self.predecessors
3029 .entry(target)
3030 .or_default()
3031 .entry(source)
3032 .or_default()
3033 .insert(key);
3034 }
3035 if inserted > 0 {
3036 self.revision = self
3037 .revision
3038 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
3039 self.record_decision(
3040 "extend_keyed_edges_with_attrs_unrecorded",
3041 0.0,
3042 false,
3043 vec![EvidenceTerm {
3044 signal: "batch_edge_count".into(),
3045 observed_value: count_evidence(inserted),
3046 log_likelihood_ratio: -1.0,
3047 }],
3048 );
3049 }
3050 inserted
3051 }
3052
3053 #[must_use]
3058 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
3059 &mut self,
3060 nodes: N,
3061 edges: I,
3062 ) -> usize
3063 where
3064 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
3065 N: IntoIterator<Item = String>,
3066 {
3067 if !self.nodes.is_empty()
3068 || !self.successors.is_empty()
3069 || !self.predecessors.is_empty()
3070 || !self.edges.is_empty()
3071 {
3072 return 0;
3073 }
3074
3075 let node_labels: Vec<String> = nodes.into_iter().collect();
3076 for node in &node_labels {
3077 self.nodes.insert(node.clone(), AttrMap::new());
3078 self.successors
3079 .insert(node.clone(), crate::FxIndexMap::default());
3080 self.predecessors
3081 .insert(node.clone(), crate::FxIndexMap::default());
3082 }
3083
3084 let node_count = node_labels.len();
3085 let mut inserted = 0usize;
3086 let mut merged_changed = false;
3087 for (source_idx, target_idx, key, attrs) in edges {
3088 let Some(source) = node_labels.get(source_idx) else {
3089 continue;
3090 };
3091 let Some(target) = node_labels.get(target_idx) else {
3092 continue;
3093 };
3094 let edge_key = DirectedEdgeKey::new(source, target);
3095 let bucket = self.edges.entry(edge_key).or_default();
3096 if !bucket.contains_key(&key) {
3097 self.edge_count += 1;
3098 inserted += 1;
3099 }
3100 let edge_attrs = bucket.entry(key).or_default();
3101 if !attrs.is_empty()
3102 && attrs
3103 .iter()
3104 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
3105 {
3106 merged_changed = true;
3107 }
3108 edge_attrs.extend(attrs);
3109 self.successors
3110 .get_mut(source.as_str())
3111 .expect("fresh node row exists")
3112 .entry(target.clone())
3113 .or_default()
3114 .insert(key);
3115 self.predecessors
3116 .get_mut(target.as_str())
3117 .expect("fresh node row exists")
3118 .entry(source.clone())
3119 .or_default()
3120 .insert(key);
3121 }
3122
3123 if node_count > 0 || inserted > 0 || merged_changed {
3124 self.revision = self.revision.saturating_add(
3125 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
3126 );
3127 self.record_decision(
3128 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
3129 0.0,
3130 false,
3131 vec![
3132 EvidenceTerm {
3133 signal: "batch_node_count".into(),
3134 observed_value: count_evidence(node_count),
3135 log_likelihood_ratio: -1.0,
3136 },
3137 EvidenceTerm {
3138 signal: "batch_edge_count".into(),
3139 observed_value: count_evidence(inserted),
3140 log_likelihood_ratio: -1.0,
3141 },
3142 ],
3143 );
3144 }
3145
3146 inserted
3147 }
3148
3149 pub fn add_edge_with_attrs(
3150 &mut self,
3151 source: impl Into<String>,
3152 target: impl Into<String>,
3153 attrs: AttrMap,
3154 ) -> Result<usize, GraphError> {
3155 self.add_edge_impl(source, target, None, attrs)
3156 }
3157
3158 pub fn add_edge_with_key_and_attrs(
3159 &mut self,
3160 source: impl Into<String>,
3161 target: impl Into<String>,
3162 key: usize,
3163 attrs: AttrMap,
3164 ) -> Result<usize, GraphError> {
3165 self.add_edge_impl(source, target, Some(key), attrs)
3166 }
3167
3168 fn add_edge_impl(
3169 &mut self,
3170 source: impl Into<String>,
3171 target: impl Into<String>,
3172 explicit_key: Option<usize>,
3173 attrs: AttrMap,
3174 ) -> Result<usize, GraphError> {
3175 let source = source.into();
3176 let target = target.into();
3177 let previous_revision = self.revision;
3184
3185 let unknown_feature = attrs
3186 .keys()
3187 .any(|key| key.starts_with("__fnx_incompatible"));
3188 let self_loop = source == target;
3189 let incompatibility_probability = if unknown_feature {
3190 1.0
3191 } else if self_loop {
3192 0.22
3193 } else {
3194 0.08
3195 };
3196
3197 let action = self
3205 .runtime_policy
3206 .action_for(incompatibility_probability, unknown_feature);
3207
3208 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
3209 self.record_decision(
3210 "add_edge",
3211 incompatibility_probability,
3212 unknown_feature,
3213 vec![EvidenceTerm {
3214 signal: "unknown_incompatible_feature".into(),
3215 observed_value: bool_evidence(unknown_feature),
3216 log_likelihood_ratio: 12.0,
3217 }],
3218 );
3219 return Err(GraphError::FailClosed {
3220 operation: "add_edge",
3221 reason: "incompatible edge metadata".to_owned(),
3222 });
3223 }
3224
3225 let mut source_autocreated = false;
3228 if !self.nodes.contains_key(&source) {
3229 let _ = self.add_node_with_attrs_unrecorded(source.clone(), AttrMap::new());
3230 source_autocreated = true;
3231 }
3232 let mut target_autocreated = false;
3233 if self_loop {
3234 target_autocreated = source_autocreated;
3235 } else if !self.nodes.contains_key(&target) {
3236 let _ = self.add_node_with_attrs_unrecorded(target.clone(), AttrMap::new());
3237 target_autocreated = true;
3238 }
3239
3240 let edge_key = DirectedEdgeKey::new(&source, &target);
3241 let key = explicit_key.unwrap_or_else(|| {
3242 let edge_bucket = self.edges.get(&edge_key);
3243 let mut k = edge_bucket.map_or(0, |b| b.len());
3244 if let Some(b) = edge_bucket {
3245 while b.contains_key(&k) {
3246 k += 1;
3247 }
3248 }
3249 k
3250 });
3251 let edges_len_before = self.edges.len();
3255 let mut changed;
3256 let edge_attr_count = {
3257 let edge_bucket = self.edges.entry(edge_key.clone()).or_default();
3258 let is_new = !edge_bucket.contains_key(&key);
3259 if is_new {
3260 self.edge_count += 1;
3261 }
3262 changed = is_new;
3263 let edge_attrs = edge_bucket.entry(key).or_default();
3264 if edge_attrs.is_empty() {
3265 if !attrs.is_empty() {
3270 changed = true;
3271 }
3272 *edge_attrs = attrs;
3273 } else {
3274 if !attrs.is_empty()
3275 && attrs
3276 .iter()
3277 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
3278 {
3279 changed = true;
3280 }
3281 edge_attrs.extend(attrs);
3282 }
3283 edge_bucket.len()
3284 };
3285
3286 self.successors
3287 .entry(source.clone())
3288 .or_default()
3289 .entry(target.clone())
3290 .or_default()
3291 .insert(key);
3292 self.predecessors
3293 .entry(target.clone())
3294 .or_default()
3295 .entry(source.clone())
3296 .or_default()
3297 .insert(key);
3298 if changed {
3299 self.revision = self.revision.saturating_add(1);
3300 }
3301 let new_pair_position = if self.edges.len() > edges_len_before {
3304 Some(self.edges.len() - 1)
3305 } else {
3306 None
3307 };
3308 self.pair_index_note_edge_added(previous_revision, &source, &target, new_pair_position);
3309
3310 self.record_decision(
3311 "add_edge",
3312 incompatibility_probability,
3313 unknown_feature,
3314 vec![
3315 EvidenceTerm {
3316 signal: "edge_key".into(),
3317 observed_value: key.to_string().into(),
3318 log_likelihood_ratio: -2.0,
3319 },
3320 EvidenceTerm {
3321 signal: "edge_attr_count".into(),
3322 observed_value: count_evidence(edge_attr_count),
3323 log_likelihood_ratio: -2.0,
3324 },
3325 EvidenceTerm {
3326 signal: "source_autocreated".into(),
3327 observed_value: bool_evidence(source_autocreated),
3328 log_likelihood_ratio: -1.25,
3329 },
3330 EvidenceTerm {
3331 signal: "target_autocreated".into(),
3332 observed_value: bool_evidence(target_autocreated),
3333 log_likelihood_ratio: -1.25,
3334 },
3335 ],
3336 );
3337
3338 Ok(key)
3339 }
3340
3341 pub fn replace_edge_attrs(
3344 &mut self,
3345 source: &str,
3346 target: &str,
3347 key: usize,
3348 attrs: AttrMap,
3349 ) -> bool {
3350 let edge_key = DirectedEdgeKey::new(source, target);
3351 if let Some(bucket) = self.edges.get_mut(&edge_key)
3352 && let Some(slot) = bucket.get_mut(&key)
3353 {
3354 if *slot != attrs {
3355 *slot = attrs;
3356 self.revision = self.revision.saturating_add(1);
3357 }
3358 return true;
3359 }
3360 false
3361 }
3362
3363 pub fn set_edge_attr_value(
3367 &mut self,
3368 source: &str,
3369 target: &str,
3370 key: usize,
3371 attr_key: &str,
3372 value: CgseValue,
3373 ) -> bool {
3374 let edge_key = DirectedEdgeKey::new(source, target);
3375 if let Some(bucket) = self.edges.get_mut(&edge_key)
3376 && let Some(attrs) = bucket.get_mut(&key)
3377 {
3378 if attrs.get(attr_key) != Some(&value) {
3379 attrs.insert(attr_key.to_owned(), value);
3380 self.revision = self.revision.saturating_add(1);
3381 }
3382 return true;
3383 }
3384 false
3385 }
3386
3387 pub fn set_ordered_edge_attr_values<I>(&mut self, attr_key: &str, values: I) -> bool
3390 where
3391 I: IntoIterator<Item = CgseValue>,
3392 {
3393 let mut values = values.into_iter();
3394 let mut changed = false;
3395 let mut visited = 0usize;
3396
3397 for node in self.nodes.keys() {
3398 if let Some(neighbors) = self.successors.get(node) {
3399 for target in neighbors.keys() {
3400 let pair = DirectedEdgeKeyRef::new(node, target);
3401 if let Some(edge_bucket) = self.edges.get_mut(&pair) {
3402 for attrs in edge_bucket.values_mut() {
3403 let Some(value) = values.next() else {
3404 return false;
3405 };
3406 if attrs.get(attr_key) != Some(&value) {
3407 attrs.insert(attr_key.to_owned(), value);
3408 changed = true;
3409 }
3410 visited = visited.saturating_add(1);
3411 }
3412 }
3413 }
3414 }
3415 }
3416
3417 if values.next().is_some() || visited != self.edge_count {
3418 return false;
3419 }
3420 if changed {
3421 self.revision = self.revision.saturating_add(1);
3422 }
3423 true
3424 }
3425
3426 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
3428 if let Some(slot) = self.nodes.get_mut(node) {
3429 if *slot != attrs {
3430 *slot = attrs;
3431 self.revision = self.revision.saturating_add(1);
3432 }
3433 true
3434 } else {
3435 false
3436 }
3437 }
3438
3439 pub fn remove_edge(&mut self, source: &str, target: &str, key: Option<usize>) -> bool {
3440 let previous_revision = self.revision;
3442 let edge_key = DirectedEdgeKeyRef::new(source, target);
3443 let removal_key = key.or_else(|| {
3444 self.edges
3445 .get(&edge_key)
3446 .and_then(|edge_bucket| edge_bucket.keys().next_back().copied())
3447 });
3448
3449 let Some(removal_key) = removal_key else {
3450 return false;
3451 };
3452
3453 let removed = self
3454 .edges
3455 .get_mut(&edge_key)
3456 .is_some_and(|edge_bucket| edge_bucket.shift_remove(&removal_key).is_some());
3457 if !removed {
3458 return false;
3459 }
3460
3461 self.edge_count -= 1;
3462
3463 let should_drop_bucket = self.edges.get(&edge_key).is_some_and(IndexMap::is_empty);
3464 let dropped_position =
3467 if should_drop_bucket && self.edge_pair_index_is_live(previous_revision) {
3468 self.edges.get_index_of(&edge_key)
3469 } else {
3470 None
3471 };
3472 if should_drop_bucket {
3473 self.edges.swap_remove(&edge_key);
3480 }
3481
3482 self.remove_successor_key(source, target, removal_key);
3483 self.remove_predecessor_key(target, source, removal_key);
3484 self.revision = self.revision.saturating_add(1);
3485 self.pair_index_note_edge_removed(previous_revision, source, target, dropped_position);
3486 true
3487 }
3488
3489 pub fn clear_edges(&mut self) {
3490 if self.edge_count == 0 {
3491 return;
3492 }
3493
3494 self.edges.clear();
3495 for row in self.successors.values_mut() {
3496 row.clear();
3497 }
3498 for row in self.predecessors.values_mut() {
3499 row.clear();
3500 }
3501 self.edge_count = 0;
3502 self.revision = self.revision.saturating_add(1);
3503 }
3504
3505 pub fn remove_node(&mut self, node: &str) -> bool {
3506 if !self.nodes.contains_key(node) {
3507 return false;
3508 }
3509
3510 let mut removed_count = 0usize;
3519 if let Some(succs) = self.successors.get(node) {
3520 let targets: Vec<String> = succs.keys().cloned().collect();
3521 for target in targets {
3522 if target != node
3523 && let Some(preds) = self.predecessors.get_mut(&target)
3524 {
3525 preds.shift_remove(node);
3526 }
3527 if let Some(bucket) = self
3528 .edges
3529 .swap_remove(&DirectedEdgeKeyRef::new(node, &target))
3530 {
3531 removed_count += bucket.len();
3532 }
3533 }
3534 }
3535 if let Some(preds) = self.predecessors.get(node) {
3536 let sources: Vec<String> = preds.keys().cloned().collect();
3537 for source in sources {
3538 if source != node
3539 && let Some(succs) = self.successors.get_mut(&source)
3540 {
3541 succs.shift_remove(node);
3542 }
3543 if let Some(bucket) = self
3544 .edges
3545 .swap_remove(&DirectedEdgeKeyRef::new(&source, node))
3546 {
3547 removed_count += bucket.len();
3548 }
3549 }
3550 }
3551 self.edge_count -= removed_count;
3552
3553 self.successors.shift_remove(node);
3554 self.predecessors.shift_remove(node);
3555 self.nodes.shift_remove(node);
3556 self.revision = self.revision.saturating_add(1);
3557 true
3558 }
3559
3560 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
3568 where
3569 I: IntoIterator<Item = &'a str>,
3570 {
3571 let remove_set: rustc_hash::FxHashSet<&str> = nodes
3572 .into_iter()
3573 .filter(|node| self.nodes.contains_key(*node))
3574 .collect();
3575 if remove_set.is_empty() {
3576 return (0, 0);
3577 }
3578
3579 let old_node_count = self.nodes.len();
3580 let old_edge_count = self.edge_count;
3581
3582 let mut removed_instances = 0usize;
3589 if remove_set.len().saturating_mul(4) <= old_node_count {
3590 for &rn in &remove_set {
3591 let targets: Vec<String> = match self.successors.get(rn) {
3593 Some(row) => row.keys().cloned().collect(),
3594 None => Vec::new(),
3595 };
3596 for t in &targets {
3597 if t.as_str() != rn
3598 && !remove_set.contains(t.as_str())
3599 && let Some(preds) = self.predecessors.get_mut(t)
3600 {
3601 preds.shift_remove(rn);
3602 }
3603 if let Some(bucket) = self.edges.swap_remove(&DirectedEdgeKeyRef::new(rn, t)) {
3604 removed_instances += bucket.len();
3605 }
3606 }
3607 let sources: Vec<String> = match self.predecessors.get(rn) {
3609 Some(row) => row.keys().cloned().collect(),
3610 None => Vec::new(),
3611 };
3612 for s in &sources {
3613 if s.as_str() != rn
3614 && !remove_set.contains(s.as_str())
3615 && let Some(succs) = self.successors.get_mut(s)
3616 {
3617 succs.shift_remove(rn);
3618 }
3619 if let Some(bucket) = self.edges.swap_remove(&DirectedEdgeKeyRef::new(s, rn)) {
3622 removed_instances += bucket.len();
3623 }
3624 }
3625 }
3626 self.edge_count -= removed_instances;
3627 self.successors
3628 .retain(|node, _| !remove_set.contains(node.as_str()));
3629 self.predecessors
3630 .retain(|node, _| !remove_set.contains(node.as_str()));
3631 self.nodes
3632 .retain(|node, _| !remove_set.contains(node.as_str()));
3633 let removed_nodes = old_node_count - self.nodes.len();
3634 let removed_edges = old_edge_count - self.edge_count;
3635 self.revision = self
3636 .revision
3637 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
3638 return (removed_nodes, removed_edges);
3639 }
3640
3641 self.edges.retain(|key, bucket| {
3647 let keep = !remove_set.contains(key.source.as_str())
3648 && !remove_set.contains(key.target.as_str());
3649 if !keep {
3650 removed_instances += bucket.len();
3651 }
3652 keep
3653 });
3654 self.edge_count -= removed_instances;
3655
3656 self.successors
3660 .retain(|node, _| !remove_set.contains(node.as_str()));
3661 for row in self.successors.values_mut() {
3662 row.retain(|target, _| !remove_set.contains(target.as_str()));
3663 }
3664 self.predecessors
3665 .retain(|node, _| !remove_set.contains(node.as_str()));
3666 for row in self.predecessors.values_mut() {
3667 row.retain(|source, _| !remove_set.contains(source.as_str()));
3668 }
3669 self.nodes
3670 .retain(|node, _| !remove_set.contains(node.as_str()));
3671
3672 let removed_nodes = old_node_count - self.nodes.len();
3673 let removed_edges = old_edge_count - self.edge_count;
3674 self.revision = self
3675 .revision
3676 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
3677 (removed_nodes, removed_edges)
3678 }
3679
3680 #[must_use]
3681 pub fn edges_ordered(&self) -> Vec<MultiDiEdgeSnapshot> {
3682 let mut ordered = Vec::with_capacity(self.edge_count());
3683
3684 for node in self.nodes.keys() {
3685 if let Some(neighbors) = self.successors.get(node) {
3686 for target in neighbors.keys() {
3687 let pair = DirectedEdgeKey::new(node, target);
3688 if let Some(edge_bucket) = self.edges.get(&pair) {
3689 for (key, attrs) in edge_bucket {
3690 ordered.push(MultiDiEdgeSnapshot {
3691 source: node.clone(),
3692 target: target.clone(),
3693 key: *key,
3694 attrs: attrs.clone(),
3695 });
3696 }
3697 }
3698 }
3699 }
3700 }
3701
3702 ordered
3703 }
3704
3705 #[must_use]
3706 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
3707 let mut ordered = Vec::with_capacity(self.edge_count());
3708
3709 for node in self.nodes.keys() {
3710 if let Some(neighbors) = self.successors.get(node) {
3711 for target in neighbors.keys() {
3712 let pair = DirectedEdgeKeyRef::new(node, target);
3713 if let Some(edge_bucket) = self.edges.get(&pair) {
3714 for (key, attrs) in edge_bucket {
3715 ordered.push((node.as_str(), target.as_str(), *key, attrs));
3716 }
3717 }
3718 }
3719 }
3720 }
3721
3722 ordered
3723 }
3724
3725 #[must_use]
3731 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
3732 let mut total: i128 = 0;
3733 for bucket in self.edges.values() {
3734 for attrs in bucket.values() {
3735 let value = match attrs.get(weight) {
3736 Some(CgseValue::Int(v)) => i128::from(*v),
3737 Some(_) => return None,
3738 None => 1,
3739 };
3740 total = total.checked_add(value)?;
3741 }
3742 }
3743 Some(total)
3744 }
3745
3746 pub fn try_for_each_edge_ordered_borrowed<E>(
3747 &self,
3748 mut visit: impl FnMut(&str, &str, usize, &AttrMap) -> Result<(), E>,
3749 ) -> Result<(), E> {
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(&pair) {
3755 for (key, attrs) in edge_bucket {
3756 visit(node.as_str(), target.as_str(), *key, attrs)?;
3757 }
3758 }
3759 }
3760 }
3761 }
3762
3763 Ok(())
3764 }
3765
3766 pub fn try_for_each_indexed_edge_ordered_borrowed<E>(
3767 &self,
3768 mut visit: impl FnMut(usize, usize, &str, &str, usize, &AttrMap) -> Result<(), E>,
3769 ) -> Result<(), E> {
3770 for (source_index, node) in self.nodes.keys().enumerate() {
3771 if let Some(neighbors) = self.successors.get(node) {
3772 for target in neighbors.keys() {
3773 let Some(target_index) = self.nodes.get_index_of(target.as_str()) else {
3774 continue;
3775 };
3776 let pair = DirectedEdgeKeyRef::new(node, target);
3777 if let Some(edge_bucket) = self.edges.get(&pair) {
3778 for (key, attrs) in edge_bucket {
3779 visit(
3780 source_index,
3781 target_index,
3782 node.as_str(),
3783 target.as_str(),
3784 *key,
3785 attrs,
3786 )?;
3787 }
3788 }
3789 }
3790 }
3791 }
3792
3793 Ok(())
3794 }
3795
3796 #[must_use]
3807 pub fn reversed(&self) -> Self {
3808 let mut successors: crate::FxIndexMap<
3809 String,
3810 crate::FxIndexMap<String, crate::FxIndexSet<usize>>,
3811 > = crate::FxIndexMap::with_capacity_and_hasher(
3812 self.nodes.len(),
3813 rustc_hash::FxBuildHasher,
3814 );
3815 let mut predecessors: crate::FxIndexMap<
3816 String,
3817 crate::FxIndexMap<String, crate::FxIndexSet<usize>>,
3818 > = crate::FxIndexMap::with_capacity_and_hasher(
3819 self.nodes.len(),
3820 rustc_hash::FxBuildHasher,
3821 );
3822 for node in self.nodes.keys() {
3823 successors.insert(node.clone(), crate::FxIndexMap::default());
3824 predecessors.insert(node.clone(), crate::FxIndexMap::default());
3825 }
3826
3827 let mut edges: crate::FxIndexMap<DirectedEdgeKey, IndexMap<usize, AttrMap>> =
3828 crate::FxIndexMap::with_capacity_and_hasher(
3829 self.edges.len(),
3830 rustc_hash::FxBuildHasher,
3831 );
3832 let mut edge_count = 0usize;
3833 for source in self.nodes.keys() {
3834 let Some(neighbors) = self.successors.get(source) else {
3835 continue;
3836 };
3837 for target in neighbors.keys() {
3838 let pair = DirectedEdgeKeyRef::new(source, target);
3839 let Some(edge_bucket) = self.edges.get(&pair) else {
3840 continue;
3841 };
3842 let reversed_pair = DirectedEdgeKey::new(target, source);
3843 let reversed_bucket = edges.entry(reversed_pair).or_default();
3844 let reversed_succ_keys = successors
3845 .get_mut(target.as_str())
3846 .expect("target row exists")
3847 .entry(source.clone())
3848 .or_default();
3849 let reversed_pred_keys = predecessors
3850 .get_mut(source.as_str())
3851 .expect("source row exists")
3852 .entry(target.clone())
3853 .or_default();
3854 for (key, attrs) in edge_bucket {
3855 reversed_bucket.insert(*key, attrs.clone());
3856 reversed_succ_keys.insert(*key);
3857 reversed_pred_keys.insert(*key);
3858 edge_count += 1;
3859 }
3860 }
3861 }
3862
3863 Self {
3864 mode: self.mode,
3865 revision: 0,
3866 nodes: self.nodes.clone(),
3867 successors,
3868 predecessors,
3869 edges,
3870 runtime_policy: self.runtime_policy.clone(),
3871 edge_count,
3872 csr_cache: std::sync::Arc::default(),
3873 edge_pair_index: EdgePairIndex::default(),
3874 }
3875 }
3876
3877 #[must_use]
3885 pub fn out_edges_ordered_borrowed<'a>(
3886 &'a self,
3887 node: &str,
3888 ) -> Vec<(&'a str, &'a str, usize, &'a AttrMap)> {
3889 let mut ordered = Vec::new();
3890 if let Some((src, neighbors)) = self.successors.get_key_value(node) {
3891 for target in neighbors.keys() {
3892 let pair = DirectedEdgeKeyRef::new(src, target);
3893 if let Some(edge_bucket) = self.edges.get(&pair) {
3894 for (key, attrs) in edge_bucket {
3895 ordered.push((src.as_str(), target.as_str(), *key, attrs));
3896 }
3897 }
3898 }
3899 }
3900 ordered
3901 }
3902
3903 #[must_use]
3904 pub fn snapshot(&self) -> MultiDiGraphSnapshot {
3905 let node_attrs: std::collections::BTreeMap<String, AttrMap> = self
3906 .nodes
3907 .iter()
3908 .filter(|(_, attrs)| !attrs.is_empty())
3909 .map(|(name, attrs)| (name.clone(), attrs.clone()))
3910 .collect();
3911 MultiDiGraphSnapshot {
3912 mode: self.mode,
3913 nodes: self.nodes.keys().cloned().collect(),
3914 node_attrs,
3915 edges: self.edges_ordered(),
3916 }
3917 }
3918
3919 fn remove_successor_key(&mut self, source: &str, target: &str, key: usize) {
3920 let mut drop_neighbor = false;
3921 if let Some(neighbors) = self.successors.get_mut(source)
3922 && let Some(keys) = neighbors.get_mut(target)
3923 {
3924 keys.shift_remove(&key);
3925 drop_neighbor = keys.is_empty();
3926 }
3927 if drop_neighbor && let Some(neighbors) = self.successors.get_mut(source) {
3928 neighbors.shift_remove(target);
3929 }
3930 }
3931
3932 fn remove_predecessor_key(&mut self, target: &str, source: &str, key: usize) {
3933 let mut drop_neighbor = false;
3934 if let Some(neighbors) = self.predecessors.get_mut(target)
3935 && let Some(keys) = neighbors.get_mut(source)
3936 {
3937 keys.shift_remove(&key);
3938 drop_neighbor = keys.is_empty();
3939 }
3940 if drop_neighbor && let Some(neighbors) = self.predecessors.get_mut(target) {
3941 neighbors.shift_remove(source);
3942 }
3943 }
3944
3945 fn record_decision(
3946 &mut self,
3947 operation: &'static str,
3948 incompatibility_probability: f64,
3949 unknown_incompatible_feature: bool,
3950 evidence: Vec<EvidenceTerm>,
3951 ) -> DecisionAction {
3952 let action = self
3953 .runtime_policy
3954 .action_for(incompatibility_probability, unknown_incompatible_feature);
3955 self.runtime_policy.record(
3956 operation,
3957 action,
3958 incompatibility_probability,
3959 "argmin expected loss over {allow,full_validate,fail_closed}",
3960 evidence,
3961 );
3962 action
3963 }
3964}
3965
3966#[cfg(test)]
3971mod tests {
3972 use super::*;
3973 use fnx_runtime::{
3974 CgseValue, CompatibilityMode, DecisionAction, DecisionRecord, RuntimePolicy,
3975 };
3976 use proptest::prelude::*;
3977
3978 fn node_name(id: u8) -> String {
3979 format!("n{}", id % 8)
3980 }
3981
3982 fn single_attr(key: &str, value: &str) -> AttrMap {
3983 let mut attrs = AttrMap::new();
3984 attrs.insert(key.to_owned(), CgseValue::from(value));
3985 attrs
3986 }
3987
3988 fn assert_runtime_policy_preserved(source: &RuntimePolicy, result: &RuntimePolicy) {
3989 assert_eq!(result.mode(), source.mode());
3990 assert_eq!(result.allowlist(), source.allowlist());
3991 assert_eq!(result.loss_matrix(), source.loss_matrix());
3992 assert!(result.posterior().observation_count >= source.posterior().observation_count);
3993 assert!(
3994 result
3995 .decision_log()
3996 .records()
3997 .starts_with(source.decision_log().records())
3998 );
3999 }
4000
4001 fn assert_digraph_core_invariants(g: &DiGraph) {
4004 for (&(source_idx, target_idx), _attrs) in &g.edges {
4006 let source = g
4007 .nodes
4008 .get_index(source_idx)
4009 .map(|(name, _)| name.as_str())
4010 .expect("edge source index should be a node");
4011 let target = g
4012 .nodes
4013 .get_index(target_idx)
4014 .map(|(name, _)| name.as_str())
4015 .expect("edge target index should be a node");
4016 assert!(
4017 g.has_node(source),
4018 "edge source {} should be a node",
4019 source
4020 );
4021 assert!(
4022 g.has_node(target),
4023 "edge target {} should be a node",
4024 target
4025 );
4026 let succs = g
4027 .successors(source)
4028 .expect("source should have successors bucket");
4029 assert!(
4030 succs.contains(&target),
4031 "{} should be in successors of {}",
4032 target,
4033 source
4034 );
4035 let preds = g
4036 .predecessors(target)
4037 .expect("target should have predecessors bucket");
4038 assert!(
4039 preds.contains(&source),
4040 "{} should be in predecessors of {}",
4041 source,
4042 target
4043 );
4044 }
4045
4046 let mut edge_count_from_adj = 0usize;
4048 for node in g.nodes_ordered() {
4049 let succs = g
4050 .successors(node)
4051 .expect("node should have successors bucket");
4052 for s in &succs {
4053 assert!(
4054 g.has_edge(node, s),
4055 "successor {} of {} should have directed edge",
4056 s,
4057 node
4058 );
4059 edge_count_from_adj += 1;
4060 }
4061 let preds = g
4063 .predecessors(node)
4064 .expect("node should have predecessors bucket");
4065 for p in &preds {
4066 assert!(
4067 g.has_edge(p, node),
4068 "predecessor {} of {} should have directed edge",
4069 p,
4070 node
4071 );
4072 }
4073 }
4074 assert_eq!(g.edge_count(), edge_count_from_adj);
4075 }
4076
4077 #[test]
4078 fn add_node_with_attrs_reports_change_on_existing_node() {
4079 let mut graph = DiGraph::strict();
4080 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
4081 let r1 = graph.revision();
4082 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
4083 let r2 = graph.revision();
4084 assert!(r2 > r1);
4085 let expected = CgseValue::from("blue");
4086 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
4087 }
4088
4089 #[test]
4090 fn multidigraph_add_node_with_attrs_reports_change_on_existing_node() {
4091 let mut graph = MultiDiGraph::strict();
4092 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
4093 let r1 = graph.revision();
4094 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
4095 let r2 = graph.revision();
4096 assert!(r2 > r1);
4097 let expected = CgseValue::from("blue");
4098 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
4099 }
4100
4101 fn assert_multidigraph_core_invariants(g: &MultiDiGraph) {
4102 let mut edge_instances = std::collections::BTreeSet::new();
4103 for node in g.nodes_ordered() {
4104 let successors = g
4105 .successors(node)
4106 .expect("multidigraph nodes should always have successor buckets");
4107 for target in successors {
4108 assert!(g.has_node(target));
4109 assert!(g.has_edge(node, target));
4110 let preds = g
4111 .predecessors(target)
4112 .expect("target should always have predecessor buckets");
4113 assert!(preds.contains(&node));
4114 let keys = g
4115 .edge_keys(node, target)
4116 .expect("parallel directed edge bucket should exist");
4117 for key in keys {
4118 edge_instances.insert((node.to_owned(), target.to_owned(), key));
4119 }
4120 }
4121 }
4122 assert_eq!(g.edge_count(), edge_instances.len());
4123 }
4124
4125 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
4126 assert!(record.ts_unix_ms > 0);
4127 assert!(!record.operation.trim().is_empty());
4128 assert_eq!(record.mode, expected_mode);
4129 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
4130 assert!(!record.rationale.trim().is_empty());
4131 assert!(!record.evidence.is_empty());
4132 for term in &record.evidence {
4133 assert!(!term.signal.trim().is_empty());
4134 assert!(!term.observed_value.trim().is_empty());
4135 }
4136 }
4137
4138 #[test]
4141 fn add_directed_edge_autocreates_nodes() {
4142 let mut g = DiGraph::strict();
4143 g.add_edge("a", "b").unwrap();
4144
4145 assert_eq!(g.node_count(), 2);
4146 assert_eq!(g.edge_count(), 1);
4147 assert!(g.has_edge("a", "b"));
4148 assert!(!g.has_edge("b", "a")); assert_digraph_core_invariants(&g);
4150 }
4151
4152 #[test]
4153 fn directed_edge_asymmetry() {
4154 let mut g = DiGraph::strict();
4155 g.add_edge("a", "b").unwrap();
4156 g.add_edge("b", "a").unwrap();
4157
4158 assert_eq!(g.edge_count(), 2);
4159 assert!(g.has_edge("a", "b"));
4160 assert!(g.has_edge("b", "a"));
4161 assert_digraph_core_invariants(&g);
4162 }
4163
4164 #[test]
4165 fn successors_and_predecessors() {
4166 let mut g = DiGraph::strict();
4167 g.add_edge("a", "b").unwrap();
4168 g.add_edge("a", "c").unwrap();
4169 g.add_edge("d", "a").unwrap();
4170
4171 assert_eq!(g.successors("a"), Some(vec!["b", "c"]));
4172 assert_eq!(g.predecessors("a"), Some(vec!["d"]));
4173 assert_eq!(g.out_degree("a"), 2);
4174 assert_eq!(g.in_degree("a"), 1);
4175 assert_eq!(g.degree("a"), 3);
4176 assert_digraph_core_invariants(&g);
4177 }
4178
4179 #[test]
4180 fn degree_single_lookup_preserves_missing_and_self_loop_semantics() {
4181 let mut g = DiGraph::strict();
4182 g.add_edge("a", "a").expect("self-loop add should succeed");
4183 g.add_edge("a", "b").expect("edge add should succeed");
4184 g.add_edge("c", "a").expect("edge add should succeed");
4185
4186 for node in ["a", "b", "c", "missing"] {
4187 assert_eq!(g.degree(node), g.in_degree(node) + g.out_degree(node));
4188 }
4189 assert_eq!(g.degree("a"), 4);
4190 assert_eq!(g.degree("missing"), 0);
4191 }
4192
4193 #[test]
4194 fn neighbors_returns_successors() {
4195 let mut g = DiGraph::strict();
4196 g.add_edge("a", "b").unwrap();
4197 g.add_edge("c", "a").unwrap();
4198
4199 assert_eq!(g.neighbors("a"), Some(vec!["b"]));
4201 assert_digraph_core_invariants(&g);
4202 }
4203
4204 #[test]
4205 fn in_edges_and_out_edges() {
4206 let mut g = DiGraph::strict();
4207 g.add_edge("x", "y").unwrap();
4208 g.add_edge("z", "y").unwrap();
4209 g.add_edge("y", "w").unwrap();
4210
4211 assert_eq!(g.out_edges("y"), vec![("y", "w")]);
4212 assert_eq!(g.in_edges("y"), vec![("x", "y"), ("z", "y")]);
4213 assert_digraph_core_invariants(&g);
4214 }
4215
4216 #[test]
4217 fn remove_directed_edge() {
4218 let mut g = DiGraph::strict();
4219 g.add_edge("a", "b").unwrap();
4220 g.add_edge("b", "a").unwrap();
4221
4222 assert!(g.remove_edge("a", "b"));
4223 assert!(!g.has_edge("a", "b"));
4224 assert!(g.has_edge("b", "a")); assert_eq!(g.edge_count(), 1);
4226 assert_digraph_core_invariants(&g);
4227 }
4228
4229 #[test]
4230 fn remove_node_removes_all_incident_directed_edges() {
4231 let mut g = DiGraph::strict();
4232 g.add_edge("a", "b").unwrap();
4233 g.add_edge("b", "c").unwrap();
4234 g.add_edge("c", "a").unwrap();
4235 g.add_edge("d", "b").unwrap();
4236
4237 assert!(g.remove_node("b"));
4238 assert_eq!(g.node_count(), 3);
4239 assert!(!g.has_edge("a", "b"));
4240 assert!(!g.has_edge("b", "c"));
4241 assert!(!g.has_edge("d", "b"));
4242 assert!(g.has_edge("c", "a")); assert_digraph_core_invariants(&g);
4244 }
4245
4246 #[test]
4247 fn multidigraph_counts_parallel_selfloops_only() {
4248 let mut g = MultiDiGraph::strict();
4249 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
4250 let _ = g.add_edge("a", "a").expect("self-loop add should succeed");
4251 let _ = g.add_edge("b", "b").expect("self-loop add should succeed");
4252 let _ = g.add_edge("a", "b").expect("edge add should succeed");
4253 let _ = g.add_edge("b", "a").expect("edge add should succeed");
4254
4255 assert_eq!(g.number_of_selfloops(), 3);
4256 assert_multidigraph_core_invariants(&g);
4257 }
4258
4259 #[test]
4265 #[ignore]
4266 fn ab_bench_multidigraph_remove_node() {
4267 use std::time::Instant;
4268 const N: usize = 1000;
4269 const M: usize = 8000;
4270 const ITERS: usize = 500;
4271 let build = || {
4272 let mut g = MultiDiGraph::new(CompatibilityMode::Strict);
4273 for i in 0..N {
4274 let _ = g.add_node(i.to_string());
4275 }
4276 let mut s: u64 = 0x9E3779B97F4A7C15;
4277 let mut next = || {
4278 s = s
4279 .wrapping_mul(6364136223846793005)
4280 .wrapping_add(1442695040888963407);
4281 (s >> 33) as usize % N
4282 };
4283 for _ in 0..M {
4284 let a = next();
4285 let b = next();
4286 let _ = g.add_edge(a.to_string(), b.to_string());
4287 }
4288 g
4289 };
4290
4291 let mut gnew = build();
4292 let victims: Vec<String> = gnew.nodes.keys().take(ITERS).cloned().collect();
4293 let t = Instant::now();
4294 for node in &victims {
4295 gnew.remove_node(node);
4296 }
4297 let new_t = t.elapsed();
4298
4299 let mut gold = build();
4301 let t = Instant::now();
4302 for node in &victims {
4303 if !gold.nodes.contains_key(node) {
4304 continue;
4305 }
4306 if let Some(succs) = gold.successors.get(node) {
4307 let targets: Vec<String> = succs.keys().cloned().collect();
4308 for target in targets {
4309 if target != *node
4310 && let Some(preds) = gold.predecessors.get_mut(&target)
4311 {
4312 preds.shift_remove(node.as_str());
4313 }
4314 }
4315 }
4316 if let Some(preds) = gold.predecessors.get(node) {
4317 let sources: Vec<String> = preds.keys().cloned().collect();
4318 for source in sources {
4319 if source != *node
4320 && let Some(succs) = gold.successors.get_mut(&source)
4321 {
4322 succs.shift_remove(node.as_str());
4323 }
4324 }
4325 }
4326 let mut rc = 0usize;
4327 let nd = node.clone();
4328 gold.edges.retain(|k, bucket| {
4329 let keep = k.source != nd && k.target != nd;
4330 if !keep {
4331 rc += bucket.len();
4332 }
4333 keep
4334 });
4335 gold.edge_count -= rc;
4336 gold.successors.shift_remove(node);
4337 gold.predecessors.shift_remove(node);
4338 gold.nodes.shift_remove(node);
4339 }
4340 let old_t = t.elapsed();
4341
4342 assert_eq!(gnew.node_count(), gold.node_count());
4343 assert_eq!(gnew.edge_count(), gold.edge_count());
4344 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
4345 eprintln!(
4346 "MultiDiGraph remove_node x{ITERS}: retain {old_t:?} -> swap_remove {new_t:?} = {:.2}x",
4347 old_t.as_secs_f64() / new_t.as_secs_f64()
4348 );
4349 }
4350
4351 #[test]
4352 fn self_loop_directed() {
4353 let mut g = DiGraph::strict();
4354 g.add_edge("a", "a").unwrap();
4355
4356 assert_eq!(g.edge_count(), 1);
4357 assert!(g.has_edge("a", "a"));
4358 assert_eq!(g.out_degree("a"), 1);
4359 assert_eq!(g.in_degree("a"), 1);
4360 assert_digraph_core_invariants(&g);
4361 }
4362
4363 #[test]
4364 fn edge_attrs_directed() {
4365 let mut g = DiGraph::strict();
4366 let mut attrs = AttrMap::new();
4367 attrs.insert("weight".to_owned(), "5".into());
4368 g.add_edge_with_attrs("a", "b", attrs).unwrap();
4369
4370 assert_eq!(
4371 g.edge_attrs("a", "b").unwrap().get("weight"),
4372 Some(&CgseValue::String("5".to_owned()))
4373 );
4374 assert!(g.edge_attrs("b", "a").is_none()); }
4376
4377 #[test]
4378 fn extend_edges_unrecorded_preserves_direction_and_records_once() {
4379 let mut g = DiGraph::strict();
4380 g.add_node("a");
4381 g.add_node("b");
4382 g.add_node("c");
4383 let before = g.evidence_ledger().records().len();
4384
4385 let inserted = g.extend_edges_unrecorded([("a", "b"), ("b", "a"), ("a", "b")]);
4386
4387 assert_eq!(inserted, 2);
4388 assert_eq!(g.edge_count(), 2);
4389 assert!(g.has_edge("a", "b"));
4390 assert!(g.has_edge("b", "a"));
4391 assert!(!g.has_edge("a", "c"));
4392 let records = g.evidence_ledger().records();
4393 assert_eq!(records.len(), before + 1);
4394 assert_eq!(
4395 records.last().map(|record| record.operation.as_ref()),
4396 Some("extend_edges_unrecorded")
4397 );
4398 }
4399
4400 #[test]
4401 fn row_staged_attr_edges_preserve_orders_and_duplicate_merges() {
4402 let mut g = DiGraph::strict();
4403 let before = g.evidence_ledger().records().len();
4404
4405 let inserted = g.extend_prepared_edges_with_attrs_row_staged_unrecorded(
4406 ["b", "x", "a", "t"]
4407 .into_iter()
4408 .map(std::borrow::ToOwned::to_owned),
4409 vec![
4410 ("b".to_owned(), "x".to_owned(), single_attr("first", "bx")),
4411 ("a".to_owned(), "t".to_owned(), single_attr("first", "at")),
4412 ("b".to_owned(), "t".to_owned(), single_attr("first", "bt")),
4413 (
4414 "a".to_owned(),
4415 "t".to_owned(),
4416 single_attr("second", "merge"),
4417 ),
4418 ("t".to_owned(), "t".to_owned(), single_attr("loop", "yes")),
4419 ],
4420 );
4421
4422 assert_eq!(inserted, 4);
4423 assert_eq!(g.nodes_ordered(), vec!["b", "x", "a", "t"]);
4424 assert_eq!(
4425 g.edges_ordered_borrowed()
4426 .into_iter()
4427 .map(|(u, v, _)| (u, v))
4428 .collect::<Vec<_>>(),
4429 vec![("b", "x"), ("b", "t"), ("a", "t"), ("t", "t")]
4430 );
4431 assert_eq!(
4432 g.edges_ordered_indices(),
4433 vec![(0, 1), (0, 3), (2, 3), (3, 3)]
4434 );
4435 assert_eq!(g.successors("b"), Some(vec!["x", "t"]));
4436 assert_eq!(g.predecessors("t"), Some(vec!["a", "b", "t"]));
4437 let merged = g.edge_attrs("a", "t").expect("merged edge should exist");
4438 assert_eq!(merged.get("first"), Some(&CgseValue::from("at")));
4439 assert_eq!(merged.get("second"), Some(&CgseValue::from("merge")));
4440 assert_digraph_core_invariants(&g);
4441 let records = g.evidence_ledger().records();
4442 assert_eq!(records.len(), before + 1);
4443 assert_eq!(
4444 records.last().map(|record| record.operation.as_ref()),
4445 Some("extend_edges_unrecorded")
4446 );
4447 }
4448
4449 #[test]
4450 fn repeated_edge_merges_attrs() {
4451 let mut g = DiGraph::strict();
4452 let mut first = AttrMap::new();
4453 first.insert("weight".to_owned(), "1".into());
4454 g.add_edge_with_attrs("a", "b", first).unwrap();
4455
4456 let mut second = AttrMap::new();
4457 second.insert("color".to_owned(), "red".into());
4458 g.add_edge_with_attrs("a", "b", second).unwrap();
4459
4460 assert_eq!(g.edge_count(), 1);
4461 let attrs = g.edge_attrs("a", "b").unwrap();
4462 assert_eq!(
4463 attrs.get("weight"),
4464 Some(&CgseValue::String("1".to_owned()))
4465 );
4466 assert_eq!(
4467 attrs.get("color"),
4468 Some(&CgseValue::String("red".to_owned()))
4469 );
4470 }
4471
4472 #[test]
4473 fn edges_ordered_preserves_direction() {
4474 let mut g = DiGraph::strict();
4475 g.add_edge("b", "a").unwrap();
4476 g.add_edge("a", "c").unwrap();
4477
4478 let pairs: Vec<(String, String)> = g
4479 .edges_ordered()
4480 .into_iter()
4481 .map(|e| (e.left, e.right))
4482 .collect();
4483 assert_eq!(
4485 pairs,
4486 vec![
4487 ("b".to_owned(), "a".to_owned()),
4488 ("a".to_owned(), "c".to_owned()),
4489 ]
4490 );
4491 }
4492
4493 #[test]
4494 fn type_identity() {
4495 let g = DiGraph::strict();
4496 assert!(g.is_directed());
4497 assert!(!g.is_multigraph());
4498 }
4499
4500 #[test]
4501 fn to_undirected_merges_edges() {
4502 let mut g = DiGraph::strict();
4503 g.add_edge("a", "b").unwrap();
4504 g.add_edge("b", "a").unwrap();
4505 g.add_edge("b", "c").unwrap();
4506
4507 let ug = g.to_undirected();
4508 assert_eq!(ug.node_count(), 3);
4509 assert_eq!(ug.edge_count(), 2); assert!(ug.has_edge("a", "b"));
4511 assert!(ug.has_edge("b", "a")); assert!(ug.has_edge("b", "c"));
4513 }
4514
4515 #[test]
4516 fn to_undirected_preserves_runtime_policy() {
4517 let mut g = DiGraph::hardened();
4518 g.add_edge("a", "b").unwrap();
4519 g.add_edge("b", "a").unwrap();
4520 let expected_policy = g.runtime_policy().clone();
4521
4522 let ug = g.to_undirected();
4523
4524 assert_runtime_policy_preserved(&expected_policy, ug.runtime_policy());
4525 }
4526
4527 #[test]
4535 #[ignore = "measurement; run with --release --ignored --nocapture"]
4536 fn to_undirected_batch_ab() {
4537 use std::hint::black_box;
4538 use std::time::Instant;
4539
4540 let mut input = DiGraph::strict();
4541 for i in 0..200 {
4542 input.add_node(format!("{i}"));
4543 }
4544 for a in 0..200 {
4545 for b in 0..200 {
4546 if a != b {
4547 input.add_edge(format!("{a}"), format!("{b}")).unwrap();
4548 }
4549 }
4550 }
4551
4552 let build = |g: &DiGraph, batch: bool| -> crate::Graph {
4553 let mut out = crate::Graph::with_runtime_policy(g.runtime_policy.clone());
4554 for (node, attrs) in &g.nodes {
4555 out.add_node_with_attrs(node.clone(), attrs.clone());
4556 }
4557 if batch {
4558 let edges: Vec<(String, String, AttrMap)> = g
4559 .edges_ordered()
4560 .into_iter()
4561 .map(|snap| (snap.left, snap.right, snap.attrs))
4562 .collect();
4563 let _ = out.extend_edges_with_attrs_unrecorded(edges);
4564 } else {
4565 for snap in g.edges_ordered() {
4566 let _ = out.add_edge_with_attrs(snap.left, snap.right, snap.attrs);
4567 }
4568 }
4569 out
4570 };
4571
4572 let old = build(&input, false);
4573 let new = build(&input, true);
4574 assert_eq!(
4575 old.edges_ordered_borrowed(),
4576 new.edges_ordered_borrowed(),
4577 "batch to_undirected must equal the per-edge baseline"
4578 );
4579 assert_eq!(old.nodes_ordered(), new.nodes_ordered());
4580
4581 let time = |batch: bool| -> f64 {
4582 let t0 = Instant::now();
4583 black_box(build(&input, batch));
4584 t0.elapsed().as_secs_f64()
4585 };
4586 for _ in 0..3 {
4587 black_box(time(true));
4588 black_box(time(false));
4589 }
4590 let median = |v: &[f64]| {
4591 let mut s = v.to_vec();
4592 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
4593 s[s.len() / 2]
4594 };
4595 let rounds = 61usize;
4596 let paired = |cand: bool, base_arm: bool| -> Vec<f64> {
4597 let mut v = Vec::with_capacity(rounds);
4598 for round in 0..rounds {
4599 let (tb, tc) = if round.is_multiple_of(2) {
4600 let bt = time(base_arm);
4601 let ct = time(cand);
4602 (bt, ct)
4603 } else {
4604 let ct = time(cand);
4605 let bt = time(base_arm);
4606 (bt, ct)
4607 };
4608 v.push(tb / tc);
4609 }
4610 v
4611 };
4612 let report = |name: &str, ratios: &[f64]| {
4613 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
4614 let mut sorted = ratios.to_vec();
4615 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
4616 println!(
4617 "TOUNDIR_BATCH_AB {name}: median={:.4}x win_rate={wins}/{rounds} \
4618 p5_p95=[{:.4},{:.4}]",
4619 median(ratios),
4620 sorted[rounds * 5 / 100],
4621 sorted[rounds * 95 / 100],
4622 );
4623 };
4624 println!("TOUNDIR_BATCH_AB K200_complete rounds={rounds} (>1 = batch faster)");
4625 report("BATCH_vs_string", &paired(true, false));
4626 report("NULL_batch_vs_batch", &paired(true, true));
4627 }
4628
4629 #[test]
4630 fn snapshot_roundtrip() {
4631 let mut g = DiGraph::strict();
4632 let mut attrs = AttrMap::new();
4633 attrs.insert("weight".to_owned(), "3".into());
4634 g.add_edge_with_attrs("a", "b", attrs).unwrap();
4635 g.add_edge("b", "c").unwrap();
4636 g.add_edge("c", "a").unwrap();
4637
4638 let snap = g.snapshot();
4639 let mut replayed = DiGraph::new(snap.mode);
4640 for node in &snap.nodes {
4641 let _ = replayed.add_node(node.clone());
4642 }
4643 for edge in &snap.edges {
4644 replayed
4645 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
4646 .unwrap();
4647 }
4648
4649 assert_eq!(replayed.snapshot(), snap);
4650 assert_digraph_core_invariants(&replayed);
4651 }
4652
4653 #[test]
4654 fn multidigraph_tracks_parallel_edges_with_distinct_keys() {
4655 let mut graph = MultiDiGraph::strict();
4656 let first = graph.add_edge("a", "b").expect("edge add should succeed");
4657 let second = graph.add_edge("a", "b").expect("edge add should succeed");
4658
4659 assert_ne!(first, second);
4660 assert_eq!(graph.edge_count(), 2);
4661 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
4662 assert_multidigraph_core_invariants(&graph);
4663 }
4664
4665 #[test]
4666 fn multidigraph_remove_node_clears_incoming_and_outgoing_parallel_edges() {
4667 let mut graph = MultiDiGraph::strict();
4668 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
4669 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
4670 let _ = graph.add_edge("b", "a").expect("edge add should succeed");
4671 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
4672
4673 assert!(graph.remove_node("b"));
4674 assert_eq!(graph.edge_count(), 0);
4675 assert!(!graph.has_node("b"));
4676 assert_eq!(graph.successors("a"), Some(vec![]));
4677 assert_eq!(graph.predecessors("a"), Some(vec![]));
4678 assert_multidigraph_core_invariants(&graph);
4679 }
4680
4681 #[test]
4682 fn multidigraph_clear_edges_preserves_nodes_attrs_and_rows() {
4683 let mut graph = MultiDiGraph::strict();
4684 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
4685 assert!(graph.add_node_with_attrs("b", single_attr("color", "blue")));
4686 let _ = graph
4687 .add_edge_with_key_and_attrs("a", "b", 7, single_attr("weight", "3"))
4688 .expect("edge add should succeed");
4689 let _ = graph
4690 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("weight", "5"))
4691 .expect("edge add should succeed");
4692 let before_revision = graph.revision();
4693
4694 graph.clear_edges();
4695
4696 assert_eq!(graph.node_count(), 2);
4697 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
4698 assert_eq!(graph.edge_count(), 0);
4699 assert_eq!(graph.edges_ordered(), Vec::<MultiDiEdgeSnapshot>::new());
4700 assert_eq!(graph.successors("a"), Some(vec![]));
4701 assert_eq!(graph.predecessors("a"), Some(vec![]));
4702 assert_eq!(
4703 graph.node_attrs("a").unwrap().get("color"),
4704 Some(&CgseValue::from("red"))
4705 );
4706 assert!(graph.revision() > before_revision);
4707 assert_multidigraph_core_invariants(&graph);
4708 }
4709
4710 #[test]
4711 fn multidigraph_roundtrips_sparse_snapshot_keys() {
4712 let mut graph = MultiDiGraph::strict();
4713 assert_eq!(
4714 graph.add_edge("a", "b").expect("edge add should succeed"),
4715 0
4716 );
4717 assert_eq!(
4718 graph.add_edge("a", "b").expect("edge add should succeed"),
4719 1
4720 );
4721 assert_eq!(
4722 graph.add_edge("a", "b").expect("edge add should succeed"),
4723 2
4724 );
4725 assert!(graph.remove_edge("a", "b", Some(1)));
4726 assert_eq!(
4727 graph.add_edge("a", "b").expect("edge add should succeed"),
4728 3
4729 );
4730
4731 let snapshot = graph.snapshot();
4732 assert_eq!(
4733 snapshot
4734 .edges
4735 .iter()
4736 .map(|edge| edge.key)
4737 .collect::<Vec<_>>(),
4738 vec![0, 2, 3]
4739 );
4740
4741 let mut replayed = MultiDiGraph::new(snapshot.mode);
4742 for node in &snapshot.nodes {
4743 let _ = replayed.add_node(node.clone());
4744 }
4745 for edge in &snapshot.edges {
4746 replayed
4747 .add_edge_with_key_and_attrs(
4748 edge.source.clone(),
4749 edge.target.clone(),
4750 edge.key,
4751 edge.attrs.clone(),
4752 )
4753 .expect("snapshot replay should preserve explicit keys");
4754 }
4755
4756 assert_eq!(replayed.snapshot(), snapshot);
4757 assert_multidigraph_core_invariants(&replayed);
4758 }
4759
4760 #[test]
4761 fn runtime_policy_tracks_hardened_multidigraph_recovery() {
4762 let mut g = MultiDiGraph::hardened();
4763 g.add_edge("a", "b")
4764 .expect("hardened multidigraph edge add should succeed");
4765
4766 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
4767 assert!(!g.runtime_policy().decision_log().records().is_empty());
4768 assert!(g.runtime_policy().posterior().observation_count >= 1);
4769 }
4770
4771 fn add_edge_evidence_signals(records: &[DecisionRecord]) -> Vec<Vec<&str>> {
4777 records
4778 .iter()
4779 .filter(|record| record.operation == "add_edge")
4780 .map(|record| {
4781 record
4782 .evidence
4783 .iter()
4784 .map(|term| term.signal.as_ref())
4785 .collect()
4786 })
4787 .collect()
4788 }
4789
4790 #[test]
4798 fn multidigraph_adjacency_iterates_in_insertion_order_under_fxhash() {
4799 let mut graph = MultiDiGraph::strict();
4800 for (source, target) in [
4801 ("zeta", "mid"),
4802 ("zeta", "alpha"),
4803 ("zeta", "omega"),
4804 ("zeta", "beta"),
4805 ("alpha", "zeta"),
4806 ] {
4807 graph
4808 .add_edge_with_attrs(source, target, AttrMap::new())
4809 .expect("edge is allowed");
4810 }
4811 graph
4813 .add_edge_with_attrs("zeta", "alpha", AttrMap::new())
4814 .expect("parallel edge is allowed");
4815
4816 let observed: Vec<(String, String, usize)> = graph
4817 .edges_ordered_borrowed()
4818 .into_iter()
4819 .map(|(source, target, key, _)| (source.to_owned(), target.to_owned(), key))
4820 .collect();
4821 assert_eq!(
4822 observed,
4823 vec![
4824 ("zeta".to_owned(), "mid".to_owned(), 0),
4825 ("zeta".to_owned(), "alpha".to_owned(), 0),
4826 ("zeta".to_owned(), "alpha".to_owned(), 1),
4827 ("zeta".to_owned(), "omega".to_owned(), 0),
4828 ("zeta".to_owned(), "beta".to_owned(), 0),
4829 ("alpha".to_owned(), "zeta".to_owned(), 0),
4830 ],
4831 "adjacency rows must stay insertion-ordered regardless of hasher"
4832 );
4833 }
4834
4835 #[test]
4840 fn multidigraph_fresh_bucket_take_matches_merge_semantics() {
4841 fn weight(value: i64) -> AttrMap {
4842 let mut attrs = AttrMap::new();
4843 attrs.insert("w".to_owned(), CgseValue::Int(value));
4844 attrs
4845 }
4846
4847 let mut graph = MultiDiGraph::strict();
4848 let k0 = graph
4849 .add_edge_with_attrs("a", "b", weight(1))
4850 .expect("edge is allowed");
4851 let k1 = graph
4853 .add_edge_with_attrs("a", "b", weight(2))
4854 .expect("parallel edge is allowed");
4855 assert_ne!(k0, k1);
4856 assert_eq!(
4857 graph.edge_attrs("a", "b", k0).and_then(|a| a.get("w")),
4858 Some(&CgseValue::Int(1))
4859 );
4860 assert_eq!(
4861 graph.edge_attrs("a", "b", k1).and_then(|a| a.get("w")),
4862 Some(&CgseValue::Int(2))
4863 );
4864
4865 let k2 = graph
4868 .add_edge_with_attrs("c", "d", AttrMap::new())
4869 .expect("edge is allowed");
4870 assert!(
4871 graph
4872 .edge_attrs("c", "d", k2)
4873 .expect("edge exists")
4874 .is_empty()
4875 );
4876 graph
4877 .add_edge_with_key_and_attrs("c", "d", k2, weight(7))
4878 .expect("edge is allowed");
4879 assert_eq!(
4880 graph.edge_attrs("c", "d", k2).and_then(|a| a.get("w")),
4881 Some(&CgseValue::Int(7))
4882 );
4883
4884 let mut extra = AttrMap::new();
4886 extra.insert("k".to_owned(), CgseValue::Int(3));
4887 graph
4888 .add_edge_with_key_and_attrs("c", "d", k2, extra)
4889 .expect("edge is allowed");
4890 let stored = graph.edge_attrs("c", "d", k2).expect("edge exists");
4891 assert_eq!(stored.get("w"), Some(&CgseValue::Int(7)));
4892 assert_eq!(stored.get("k"), Some(&CgseValue::Int(3)));
4893 }
4894
4895 #[test]
4903 fn multidigraph_add_node_creates_and_preserves_adjacency_rows() {
4904 let mut graph = MultiDiGraph::strict();
4905
4906 graph.add_node("x");
4909 graph
4910 .add_edge_with_attrs("x", "y", AttrMap::new())
4911 .expect("edge is allowed");
4912 graph
4913 .add_edge_with_attrs("w", "x", AttrMap::new())
4914 .expect("edge is allowed");
4915 assert_eq!(graph.out_degree("x"), 1);
4916 assert_eq!(graph.in_degree("x"), 1);
4917
4918 for _ in 0..5 {
4921 graph.add_node("x");
4922 }
4923 assert_eq!(graph.out_degree("x"), 1);
4924 assert_eq!(graph.in_degree("x"), 1);
4925 assert_eq!(graph.node_count(), 3);
4926
4927 graph
4929 .add_edge_with_attrs("x", "y", AttrMap::new())
4930 .expect("parallel edge is allowed");
4931 assert_eq!(graph.out_degree("x"), 2);
4932 }
4933
4934 #[test]
4942 fn digraph_add_edge_endpoint_resolution_preserves_autocreation_and_direction() {
4943 fn autocreated(graph: &DiGraph) -> (String, String) {
4944 let record = graph
4945 .evidence_ledger()
4946 .records()
4947 .last()
4948 .expect("add_edge files an outcome record");
4949 let read = |signal: &str| {
4950 record
4951 .evidence
4952 .iter()
4953 .find(|term| term.signal == signal)
4954 .map(|term| term.observed_value.to_string())
4955 .unwrap_or_else(|| panic!("{signal} must be present"))
4956 };
4957 (read("source_autocreated"), read("target_autocreated"))
4958 }
4959
4960 let mut graph = DiGraph::strict();
4962 graph
4963 .add_edge_with_attrs("s", "s", AttrMap::new())
4964 .expect("self-loop is allowed");
4965 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
4966 assert_eq!(graph.node_count(), 1);
4967
4968 graph
4970 .add_edge_with_attrs("s", "s", AttrMap::new())
4971 .expect("re-adding a self-loop is allowed");
4972 assert_eq!(
4973 autocreated(&graph),
4974 ("false".to_owned(), "false".to_owned())
4975 );
4976 assert_eq!(graph.node_count(), 1);
4977
4978 graph
4980 .add_edge_with_attrs("s", "t", AttrMap::new())
4981 .expect("edge is allowed");
4982 assert_eq!(autocreated(&graph), ("false".to_owned(), "true".to_owned()));
4983
4984 graph
4987 .add_edge_with_attrs("z", "a", AttrMap::new())
4988 .expect("edge is allowed");
4989 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
4990 assert_eq!(graph.get_node_index("s"), Some(0));
4991 assert_eq!(graph.get_node_index("t"), Some(1));
4992 assert_eq!(graph.get_node_index("z"), Some(2));
4993 assert_eq!(graph.get_node_index("a"), Some(3));
4994
4995 assert!(graph.has_edge("z", "a"));
4997 assert!(!graph.has_edge("a", "z"));
4998 assert!(graph.has_edge("s", "t"));
4999 assert!(!graph.has_edge("t", "s"));
5000 }
5001
5002 #[test]
5003 fn allowed_digraph_add_edge_records_only_the_outcome_decision() {
5004 let mut g = DiGraph::strict();
5005 g.add_edge_with_attrs("a", "b", AttrMap::new())
5006 .expect("edge add should succeed");
5007
5008 assert_eq!(
5009 add_edge_evidence_signals(g.evidence_ledger().records()),
5010 vec![vec![
5011 "self_loop",
5012 "edge_attr_count",
5013 "source_autocreated",
5014 "target_autocreated",
5015 ]]
5016 );
5017 assert_eq!(
5018 g.evidence_ledger()
5019 .records()
5020 .iter()
5021 .filter(|record| record.operation == "add_edge")
5022 .map(|record| record.action)
5023 .collect::<Vec<_>>(),
5024 vec![DecisionAction::Allow]
5025 );
5026 }
5027
5028 #[test]
5029 fn allowed_multidigraph_add_edge_records_only_the_outcome_decision() {
5030 let mut g = MultiDiGraph::strict();
5031 let _ = g
5032 .add_edge_with_attrs("a", "b", AttrMap::new())
5033 .expect("edge add should succeed");
5034
5035 assert_eq!(
5036 add_edge_evidence_signals(g.evidence_ledger().records()),
5037 vec![vec![
5038 "edge_key",
5039 "edge_attr_count",
5040 "source_autocreated",
5041 "target_autocreated",
5042 ]]
5043 );
5044 }
5045
5046 #[test]
5047 fn refused_multidigraph_add_edge_still_records_its_gate_decision() {
5048 let mut g = MultiDiGraph::strict();
5049 let mut attrs = AttrMap::new();
5050 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
5051 let err = g
5052 .add_edge_with_attrs("a", "b", attrs)
5053 .expect_err("strict mode should fail closed");
5054 assert_eq!(
5055 err,
5056 GraphError::FailClosed {
5057 operation: "add_edge",
5058 reason: "incompatible edge metadata".to_owned(),
5059 }
5060 );
5061
5062 assert_eq!(
5063 add_edge_evidence_signals(g.evidence_ledger().records()),
5064 vec![vec!["unknown_incompatible_feature"]]
5065 );
5066 assert_eq!(
5067 g.evidence_ledger().records().last().map(|r| r.action),
5068 Some(DecisionAction::FailClosed)
5069 );
5070 }
5071
5072 #[test]
5073 fn strict_mode_fails_closed_for_incompatible_attrs() {
5074 let mut g = DiGraph::strict();
5075 let mut attrs = AttrMap::new();
5076 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
5077 let err = g
5078 .add_edge_with_attrs("a", "b", attrs)
5079 .expect_err("strict mode should fail closed");
5080
5081 assert_eq!(
5082 err,
5083 GraphError::FailClosed {
5084 operation: "add_edge",
5085 reason: "incompatible edge metadata".to_owned(),
5086 }
5087 );
5088 }
5089
5090 #[test]
5091 fn revision_increments_on_mutations() {
5092 let mut g = DiGraph::strict();
5093 let r0 = g.revision();
5094 let _ = g.add_node("a");
5095 let r1 = g.revision();
5096 assert!(r1 > r0);
5097
5098 g.add_edge("a", "b").unwrap();
5099 let r2 = g.revision();
5100 assert!(r2 > r1);
5101
5102 let _ = g.remove_edge("a", "b");
5103 let r3 = g.revision();
5104 assert!(r3 > r2);
5105 }
5106
5107 #[test]
5108 fn hardened_self_loop_records_allow() {
5109 let mut g = DiGraph::hardened();
5110 g.add_edge("loop", "loop").unwrap();
5111
5112 let record = g
5113 .evidence_ledger()
5114 .records()
5115 .iter()
5116 .rev()
5117 .find(|r| r.operation == "add_edge")
5118 .expect("add_edge should emit ledger row");
5119 assert_decision_record_schema(record, CompatibilityMode::Hardened);
5120 assert_eq!(record.action, DecisionAction::Allow);
5121 }
5122
5123 #[test]
5124 fn runtime_policy_tracks_hardened_digraph_recovery() {
5125 let mut g = DiGraph::hardened();
5126 g.add_edge("a", "b")
5127 .expect("hardened digraph edge add should succeed");
5128
5129 assert_eq!(g.runtime_policy().mode(), CompatibilityMode::Hardened);
5130 assert!(!g.runtime_policy().decision_log().records().is_empty());
5131 assert!(g.runtime_policy().posterior().observation_count >= 1);
5132 }
5133
5134 #[test]
5138 fn has_edge_by_indices_is_oriented_and_range_checked() {
5139 let mut g = DiGraph::strict();
5140 g.add_edge("tail", "head").expect("edge add should succeed");
5141 g.add_edge("head", "third")
5142 .expect("edge add should succeed");
5143
5144 let (tail, head, third) = (
5145 g.get_node_index("tail").expect("tail index"),
5146 g.get_node_index("head").expect("head index"),
5147 g.get_node_index("third").expect("third index"),
5148 );
5149 assert!(g.has_edge_by_indices(tail, head));
5150 assert!(!g.has_edge_by_indices(head, tail));
5151 assert!(!g.has_edge_by_indices(tail, third));
5152 assert!(!g.has_edge_by_indices(third, g.node_count() + 7));
5153 assert!(!g.has_edge_by_indices(g.node_count() + 7, third));
5154
5155 assert!(g.remove_node("tail"), "node removal should succeed");
5158 let head_after = g.get_node_index("head").expect("head index after removal");
5159 let third_after = g
5160 .get_node_index("third")
5161 .expect("third index after removal");
5162 assert!(g.has_edge_by_indices(head_after, third_after));
5163 assert!(!g.has_edge_by_indices(third_after, head_after));
5164 assert!(!g.has_edge_by_indices(head_after, g.node_count()));
5165 }
5166
5167 proptest! {
5170 #[test]
5174 fn prop_has_edge_by_indices_matches_the_name_path(
5175 ops in prop::collection::vec((0_u8..6, 0_u8..6, 0_u8..10), 1..60),
5176 ) {
5177 let mut g = DiGraph::strict();
5178 for (src_id, tgt_id, action) in ops {
5179 let (source, target) = (node_name(src_id), node_name(tgt_id));
5180 match action {
5181 0..=6 => {
5182 prop_assert!(g.add_edge(source, target).is_ok());
5183 }
5184 7 | 8 => {
5185 let _ = g.remove_edge(&source, &target);
5186 }
5187 _ => {
5188 let _ = g.remove_node(&source);
5189 }
5190 }
5191 }
5192
5193 let names: Vec<String> = g.nodes_ordered().iter().map(|n| (*n).to_owned()).collect();
5194 for (i, source) in names.iter().enumerate() {
5195 for (j, target) in names.iter().enumerate() {
5196 prop_assert_eq!(
5197 g.has_edge_by_indices(i, j),
5198 g.has_edge(source, target),
5199 "index pair ({}, {}) disagrees with ({}, {})",
5200 i, j, source, target
5201 );
5202 }
5203 prop_assert!(!g.has_edge_by_indices(i, names.len()));
5204 prop_assert!(!g.has_edge_by_indices(names.len(), i));
5205 }
5206 }
5207
5208 #[test]
5209 fn prop_digraph_invariants_under_mixed_mutations(
5210 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
5211 ) {
5212 let mut g = DiGraph::strict();
5213 let mut last_rev = g.revision();
5214
5215 for (src_id, tgt_id, is_add) in ops {
5216 let src = node_name(src_id);
5217 let tgt = node_name(tgt_id);
5218 if is_add {
5219 prop_assert!(g.add_edge(src, tgt).is_ok());
5220 } else {
5221 let _ = g.remove_edge(&src, &tgt);
5222 }
5223 let rev = g.revision();
5224 prop_assert!(rev >= last_rev);
5225 last_rev = rev;
5226 assert_digraph_core_invariants(&g);
5227 }
5228 }
5229
5230 #[test]
5231 fn prop_digraph_snapshot_deterministic(
5232 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
5233 ) {
5234 let mut g1 = DiGraph::hardened();
5235 let mut g2 = DiGraph::hardened();
5236
5237 for (src_id, tgt_id, attrs_variant) in ops {
5238 let src = node_name(src_id);
5239 let tgt = node_name(tgt_id);
5240 let mut attrs = AttrMap::new();
5241 if attrs_variant == 1 {
5242 attrs.insert("weight".to_owned(), (src_id % 5).to_string().into());
5243 } else if attrs_variant == 2 {
5244 attrs.insert("tag".to_owned(), format!("k{}", tgt_id % 4).into());
5245 }
5246 prop_assert!(g1.add_edge_with_attrs(src.clone(), tgt.clone(), attrs.clone()).is_ok());
5247 prop_assert!(g2.add_edge_with_attrs(src, tgt, attrs).is_ok());
5248 }
5249
5250 prop_assert_eq!(g1.snapshot(), g2.snapshot());
5251 }
5252
5253 #[test]
5254 fn prop_remove_node_clears_all_directed_edges(
5255 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
5256 target_id in 0_u8..8,
5257 ) {
5258 let mut g = DiGraph::strict();
5259 for (src_id, tgt_id) in ops {
5260 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
5261 }
5262
5263 let target = node_name(target_id);
5264 let removed = g.remove_node(&target);
5265 if removed {
5266 prop_assert!(!g.has_node(&target));
5267 for node in g.nodes_ordered() {
5268 prop_assert!(!g.has_edge(node, &target));
5269 prop_assert!(!g.has_edge(&target, node));
5270 }
5271 }
5272 assert_digraph_core_invariants(&g);
5273 }
5274
5275 #[test]
5276 fn prop_directed_edge_count_equals_successor_sum(
5277 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
5278 ) {
5279 let mut g = DiGraph::strict();
5280 for (src_id, tgt_id) in ops {
5281 prop_assert!(g.add_edge(node_name(src_id), node_name(tgt_id)).is_ok());
5282 }
5283
5284 let total_out: usize = g.nodes_ordered().iter().map(|n| g.out_degree(n)).sum();
5285 let total_in: usize = g.nodes_ordered().iter().map(|n| g.in_degree(n)).sum();
5286 prop_assert_eq!(g.edge_count(), total_out);
5287 prop_assert_eq!(g.edge_count(), total_in);
5288 }
5289 }
5290}