1#![forbid(unsafe_code)]
2
3pub mod digraph;
4
5use fnx_runtime::{
6 CgseValue, CompatibilityMode, DecisionAction, EvidenceLedger, EvidenceTerm, RuntimePolicy,
7};
8use indexmap::{IndexMap, IndexSet};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::collections::HashSet;
12use std::fmt;
13
14pub type AttrMap = BTreeMap<String, CgseValue>;
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17struct EdgeKey {
18 left: String,
19 right: String,
20}
21
22impl EdgeKey {
23 fn new(left: &str, right: &str) -> Self {
24 if left <= right {
25 Self {
26 left: left.to_owned(),
27 right: right.to_owned(),
28 }
29 } else {
30 Self {
31 left: right.to_owned(),
32 right: left.to_owned(),
33 }
34 }
35 }
36}
37
38#[derive(Hash, PartialEq, Eq, Clone, Copy)]
39struct EdgeKeyRef<'a> {
40 left: &'a str,
41 right: &'a str,
42}
43
44impl<'a> EdgeKeyRef<'a> {
45 fn new(left: &'a str, right: &'a str) -> Self {
46 if left <= right {
47 Self { left, right }
48 } else {
49 Self {
50 left: right,
51 right: left,
52 }
53 }
54 }
55}
56
57impl<'a> indexmap::Equivalent<EdgeKey> for EdgeKeyRef<'a> {
58 fn equivalent(&self, key: &EdgeKey) -> bool {
59 self.left == key.left && self.right == key.right
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum GraphError {
65 FailClosed {
66 operation: &'static str,
67 reason: String,
68 },
69}
70
71impl fmt::Display for GraphError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::FailClosed { operation, reason } => {
75 write!(f, "operation `{operation}` failed closed: {reason}")
76 }
77 }
78 }
79}
80
81impl std::error::Error for GraphError {}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct EdgeSnapshot {
85 pub left: String,
86 pub right: String,
87 pub attrs: AttrMap,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct GraphSnapshot {
92 pub mode: CompatibilityMode,
93 pub nodes: Vec<String>,
94 #[serde(default)]
100 pub node_attrs: BTreeMap<String, AttrMap>,
101 pub edges: Vec<EdgeSnapshot>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct MultiEdgeSnapshot {
106 pub left: String,
107 pub right: String,
108 pub key: usize,
109 pub attrs: AttrMap,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct MultiGraphSnapshot {
114 pub mode: CompatibilityMode,
115 pub nodes: Vec<String>,
116 #[serde(default)]
118 pub node_attrs: BTreeMap<String, AttrMap>,
119 pub edges: Vec<MultiEdgeSnapshot>,
120}
121
122#[derive(Debug, Clone)]
123pub struct Graph {
124 mode: CompatibilityMode,
125 revision: u64,
126 nodes: IndexMap<String, AttrMap>,
127 adj_indices: Vec<Vec<usize>>,
134 all_int_cache: std::sync::Arc<std::sync::RwLock<Option<(u64, String, bool)>>>,
137 edge_index_endpoints: Vec<(usize, usize)>,
138 edges: IndexMap<(usize, usize), AttrMap>,
144 runtime_policy: RuntimePolicy,
145}
146
147impl Graph {
148 #[must_use]
149 pub fn new(mode: CompatibilityMode) -> Self {
150 Self {
151 mode,
152 revision: 0,
153 nodes: IndexMap::new(),
154 adj_indices: Vec::new(),
155 all_int_cache: std::sync::Arc::default(),
156 edge_index_endpoints: Vec::new(),
157 edges: IndexMap::new(),
158 runtime_policy: RuntimePolicy::new(mode),
159 }
160 }
161
162 #[must_use]
163 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
164 let mode = runtime_policy.mode();
165 Self {
166 mode,
167 revision: 0,
168 nodes: IndexMap::new(),
169 adj_indices: Vec::new(),
170 all_int_cache: std::sync::Arc::default(),
171 edge_index_endpoints: Vec::new(),
172 edges: IndexMap::new(),
173 runtime_policy,
174 }
175 }
176
177 #[must_use]
178 pub fn strict() -> Self {
179 Self::new(CompatibilityMode::Strict)
180 }
181
182 #[must_use]
183 pub fn complete_graph(mode: CompatibilityMode, n: usize) -> Self {
184 let edge_capacity = n.saturating_mul(n.saturating_sub(1)) / 2;
185 let revision = u64::try_from(n)
186 .unwrap_or(u64::MAX)
187 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
188 let node_labels = (0..n).map(|node| node.to_string()).collect::<Vec<_>>();
189 let mut graph = Self {
190 mode,
191 revision,
192 nodes: IndexMap::with_capacity(n),
193 adj_indices: vec![Vec::with_capacity(n.saturating_sub(1)); n],
194 all_int_cache: std::sync::Arc::default(),
195 edge_index_endpoints: Vec::with_capacity(edge_capacity),
196 edges: IndexMap::with_capacity(edge_capacity),
197 runtime_policy: RuntimePolicy::new(mode),
198 };
199
200 for node in &node_labels {
201 graph.nodes.insert(node.clone(), AttrMap::new());
202 }
203
204 for left_index in 0..n {
205 let _left = &node_labels[left_index];
206 for (right_index, _right) in node_labels.iter().enumerate().skip(left_index + 1) {
207 graph.adj_indices[left_index].push(right_index);
209 graph.adj_indices[right_index].push(left_index);
210 graph.edge_index_endpoints.push((left_index, right_index));
211 graph
212 .edges
213 .insert(Graph::canon_pair(left_index, right_index), AttrMap::new());
214 }
215 }
216
217 graph.record_decision(
218 "complete_graph_bulk",
219 0.0,
220 false,
221 vec![EvidenceTerm {
222 signal: "nodes".to_owned(),
223 observed_value: n.to_string(),
224 log_likelihood_ratio: 0.0,
225 }],
226 );
227 graph
228 }
229
230 #[must_use]
234 pub fn grid_2d(mode: CompatibilityMode, m: usize, n: usize) -> Self {
235 let node_count = m.saturating_mul(n);
236 let edge_capacity = m
237 .saturating_sub(1)
238 .saturating_mul(n)
239 .saturating_add(n.saturating_sub(1).saturating_mul(m));
240 let revision = u64::try_from(node_count)
241 .unwrap_or(u64::MAX)
242 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
243 let label = |i: usize, j: usize| format!("({i}, {j})");
244 let idx = |i: usize, j: usize| i * n + j;
245 let mut graph = Self {
246 mode,
247 revision,
248 nodes: IndexMap::with_capacity(node_count),
249 adj_indices: vec![Vec::with_capacity(4); node_count],
250 all_int_cache: std::sync::Arc::default(),
251 edge_index_endpoints: Vec::with_capacity(edge_capacity),
252 edges: IndexMap::with_capacity(edge_capacity),
253 runtime_policy: RuntimePolicy::new(mode),
254 };
255 for i in 0..m {
256 for j in 0..n {
257 let key = label(i, j);
258 graph.nodes.insert(key.clone(), AttrMap::new());
259 }
260 }
261 fn add_grid_edge(graph: &mut Graph, _u: String, _v: String, u_idx: usize, v_idx: usize) {
262 graph.adj_indices[u_idx].push(v_idx);
264 graph.adj_indices[v_idx].push(u_idx);
265 graph.edge_index_endpoints.push((u_idx, v_idx));
266 graph
267 .edges
268 .insert(Graph::canon_pair(u_idx, v_idx), AttrMap::new());
269 }
270 for i in 1..m {
272 for j in 0..n {
273 add_grid_edge(
274 &mut graph,
275 label(i, j),
276 label(i - 1, j),
277 idx(i, j),
278 idx(i - 1, j),
279 );
280 }
281 }
282 for i in 0..m {
284 for j in 1..n {
285 add_grid_edge(
286 &mut graph,
287 label(i, j),
288 label(i, j - 1),
289 idx(i, j),
290 idx(i, j - 1),
291 );
292 }
293 }
294 graph.record_decision(
295 "grid_2d_bulk",
296 0.0,
297 false,
298 vec![EvidenceTerm {
299 signal: "nodes".to_owned(),
300 observed_value: node_count.to_string(),
301 log_likelihood_ratio: 0.0,
302 }],
303 );
304 graph
305 }
306
307 pub fn grid_nd(
313 mode: CompatibilityMode,
314 dimensions: &[usize],
315 periodic: &[bool],
316 ) -> Result<Self, GraphError> {
317 if dimensions.len() != periodic.len() {
318 return Err(GraphError::FailClosed {
319 operation: "grid_nd",
320 reason: format!(
321 "periodic flag count {} does not match dimension count {}",
322 periodic.len(),
323 dimensions.len()
324 ),
325 });
326 }
327 if dimensions.is_empty() {
328 return Ok(Self::new(mode));
329 }
330
331 #[derive(Clone)]
332 struct ProductState {
333 node_count: usize,
334 adjacency: Vec<Vec<usize>>,
335 edge_set: HashSet<(usize, usize)>,
336 }
337
338 impl ProductState {
339 fn empty() -> Self {
340 Self {
341 node_count: 0,
342 adjacency: Vec::new(),
343 edge_set: HashSet::new(),
344 }
345 }
346
347 fn axis(size: usize, periodic: bool) -> Self {
348 let mut state = Self {
349 node_count: size,
350 adjacency: vec![Vec::new(); size],
351 edge_set: HashSet::new(),
352 };
353 if size == 0 {
354 return state;
355 }
356 if periodic {
357 if size == 1 {
358 state.add_edge(0, 0);
359 } else {
360 for node in 0..(size - 1) {
361 state.add_edge(node, node + 1);
362 }
363 state.add_edge(size - 1, 0);
364 }
365 } else {
366 for node in 0..size.saturating_sub(1) {
367 state.add_edge(node, node + 1);
368 }
369 }
370 state
371 }
372
373 fn product(axis: &Self, old: &Self) -> Result<Self, GraphError> {
374 let Some(node_count) = axis.node_count.checked_mul(old.node_count) else {
375 return Err(GraphError::FailClosed {
376 operation: "grid_nd",
377 reason: "node count overflow during cartesian product".to_owned(),
378 });
379 };
380 if node_count == 0 {
381 return Ok(Self::empty());
382 }
383
384 let axis_edges = axis.edges_view();
385 let old_edges = old.edges_view();
386 let mut state = Self {
387 node_count,
388 adjacency: vec![Vec::new(); node_count],
389 edge_set: HashSet::with_capacity(
390 axis_edges
391 .len()
392 .saturating_mul(old.node_count)
393 .saturating_add(old_edges.len().saturating_mul(axis.node_count)),
394 ),
395 };
396
397 for (axis_left, axis_right) in axis_edges {
398 let left_base = axis_left * old.node_count;
399 let right_base = axis_right * old.node_count;
400 for old_index in 0..old.node_count {
401 state.add_edge(left_base + old_index, right_base + old_index);
402 }
403 }
404 for axis_index in 0..axis.node_count {
405 let base = axis_index * old.node_count;
406 for &(old_left, old_right) in &old_edges {
407 state.add_edge(base + old_left, base + old_right);
408 }
409 }
410 Ok(state)
411 }
412
413 fn add_edge(&mut self, left: usize, right: usize) {
414 let key = if left <= right {
415 (left, right)
416 } else {
417 (right, left)
418 };
419 if !self.edge_set.insert(key) {
420 return;
421 }
422 self.adjacency[left].push(right);
423 if left != right {
424 self.adjacency[right].push(left);
425 }
426 }
427
428 fn edges_view(&self) -> Vec<(usize, usize)> {
429 let mut edges = Vec::with_capacity(self.edge_set.len());
430 let mut seen = vec![false; self.node_count];
431 for left in 0..self.node_count {
432 for &right in &self.adjacency[left] {
433 if !seen[right] {
434 edges.push((left, right));
435 }
436 }
437 seen[left] = true;
438 }
439 edges
440 }
441 }
442
443 fn coords_from_index(mut index: usize, sizes: &[usize]) -> Vec<usize> {
444 let mut coords = vec![0; sizes.len()];
445 for axis in (0..sizes.len()).rev() {
446 let size = sizes[axis];
447 coords[axis] = index % size;
448 index /= size;
449 }
450 coords
451 }
452
453 fn tuple_canonical(coords: &[usize]) -> String {
454 if let [value] = coords {
455 return value.to_string();
456 }
457 let mut s = String::with_capacity(coords.len() * 6 + 2);
458 s.push('(');
459 for (i, value) in coords.iter().enumerate() {
460 if i > 0 {
461 s.push_str(", ");
462 }
463 s.push_str(&value.to_string());
464 }
465 s.push(')');
466 s
467 }
468
469 let mut state = ProductState::axis(dimensions[0], periodic[0]);
470 for (&size, &periodic_axis) in dimensions.iter().zip(periodic).skip(1) {
471 let axis = ProductState::axis(size, periodic_axis);
472 state = ProductState::product(&axis, &state)?;
473 }
474
475 let output_sizes = dimensions.iter().rev().copied().collect::<Vec<_>>();
476 let labels = (0..state.node_count)
477 .map(|index| tuple_canonical(&coords_from_index(index, &output_sizes)))
478 .collect::<Vec<_>>();
479 let edge_view = state.edges_view();
480 let mut final_adjacency = vec![Vec::new(); state.node_count];
481 for &(left, right) in &edge_view {
482 final_adjacency[left].push(right);
483 if left != right {
484 final_adjacency[right].push(left);
485 }
486 }
487 let revision = u64::try_from(state.node_count)
488 .unwrap_or(u64::MAX)
489 .saturating_add(u64::try_from(edge_view.len()).unwrap_or(u64::MAX));
490 let mut graph = Self {
491 mode,
492 revision,
493 nodes: IndexMap::with_capacity(state.node_count),
494 adj_indices: Vec::with_capacity(state.node_count),
495 all_int_cache: std::sync::Arc::default(),
496 edge_index_endpoints: Vec::with_capacity(edge_view.len()),
497 edges: IndexMap::with_capacity(edge_view.len()),
498 runtime_policy: RuntimePolicy::new(mode),
499 };
500
501 for (index, label) in labels.iter().enumerate() {
502 graph.nodes.insert(label.clone(), AttrMap::new());
503 graph.adj_indices.push(final_adjacency[index].clone());
504 }
505
506 for (left, right) in edge_view {
507 graph.edge_index_endpoints.push((left, right));
508 graph
509 .edges
510 .insert(Graph::canon_pair(left, right), AttrMap::new());
511 }
512
513 graph.record_decision(
514 "grid_nd_bulk",
515 0.0,
516 false,
517 vec![EvidenceTerm {
518 signal: "nodes".to_owned(),
519 observed_value: state.node_count.to_string(),
520 log_likelihood_ratio: 0.0,
521 }],
522 );
523 Ok(graph)
524 }
525
526 #[must_use]
539 pub fn kneser(mode: CompatibilityMode, n: usize, k: usize) -> Self {
540 fn tuple_canonical(c: &[usize]) -> String {
541 let mut s = String::with_capacity(c.len() * 6 + 2);
542 s.push('(');
543 for (i, v) in c.iter().enumerate() {
544 if i > 0 {
545 s.push_str(", ");
546 }
547 s.push_str(&v.to_string());
548 }
549 if c.len() == 1 {
550 s.push(',');
551 }
552 s.push(')');
553 s
554 }
555 fn lex_combinations(m: usize, k: usize) -> Vec<Vec<usize>> {
556 let mut out = Vec::new();
557 if k > m {
558 return out;
559 }
560 let mut cur: Vec<usize> = (0..k).collect();
561 loop {
562 out.push(cur.clone());
563 let mut advanced = false;
564 let mut i = k;
565 while i > 0 {
566 i -= 1;
567 if cur[i] != i + m - k {
568 cur[i] += 1;
569 for j in (i + 1)..k {
570 cur[j] = cur[j - 1] + 1;
571 }
572 advanced = true;
573 break;
574 }
575 }
576 if !advanced {
577 break;
578 }
579 }
580 out
581 }
582 let subsets = lex_combinations(n, k);
583 let canonicals: Vec<String> = subsets.iter().map(|c| tuple_canonical(c)).collect();
584 let total = subsets.len();
585 let mut graph = Self {
586 mode,
587 revision: u64::try_from(total).unwrap_or(u64::MAX),
588 nodes: IndexMap::with_capacity(total),
589 adj_indices: Vec::new(),
590 all_int_cache: std::sync::Arc::default(),
591 edge_index_endpoints: Vec::new(),
592 edges: IndexMap::new(),
593 runtime_policy: RuntimePolicy::new(mode),
594 };
595 if 2 * k > n {
596 for canonical in &canonicals {
598 graph.nodes.insert(canonical.clone(), AttrMap::new());
599 }
600 graph.adj_indices = vec![Vec::new(); total];
601 return graph;
602 }
603 let mut binom = vec![vec![0usize; k + 1]; n + 1];
605 for i in 0..=n {
606 binom[i][0] = 1;
607 for j in 1..=k.min(i) {
608 binom[i][j] = if i == j {
609 1
610 } else {
611 binom[i - 1][j - 1].saturating_add(binom[i - 1][j])
612 };
613 }
614 }
615 let lex_rank = |c: &[usize]| -> usize {
616 let mut r = 0usize;
617 let mut prev = 0usize;
618 for (i, &ci) in c.iter().enumerate() {
619 for j in prev..ci {
620 r = r.saturating_add(binom[n - 1 - j][k - 1 - i]);
621 }
622 prev = ci + 1;
623 }
624 r
625 };
626 let mut seen = HashSet::<(usize, usize)>::with_capacity(total * 2);
627 let mut node_idx: Vec<usize> = vec![usize::MAX; total];
629 let mut in_s = vec![false; n];
630 let m = n - k;
631 let picks = lex_combinations(m, k);
632 for (s_idx, s) in subsets.iter().enumerate() {
633 for &v in s {
634 in_s[v] = true;
635 }
636 let complement: Vec<usize> = (0..n).filter(|&v| !in_s[v]).collect();
637 for pick in &picks {
638 let t: Vec<usize> = pick.iter().map(|&i| complement[i]).collect();
639 let t_idx = lex_rank(&t);
640 let pair = (s_idx.min(t_idx), s_idx.max(t_idx));
641 if seen.insert(pair) {
642 for idx in [s_idx, t_idx] {
643 if node_idx[idx] == usize::MAX {
644 let entry = graph
645 .nodes
646 .insert_full(canonicals[idx].clone(), AttrMap::new())
647 .0;
648 node_idx[idx] = entry;
649 graph.adj_indices.push(Vec::new());
650 }
651 }
652 if !graph.adj_indices[node_idx[s_idx]].contains(&node_idx[t_idx]) {
653 graph.adj_indices[node_idx[s_idx]].push(node_idx[t_idx]);
654 }
655 if node_idx[s_idx] != node_idx[t_idx]
656 && !graph.adj_indices[node_idx[t_idx]].contains(&node_idx[s_idx])
657 {
658 graph.adj_indices[node_idx[t_idx]].push(node_idx[s_idx]);
659 }
660 graph
661 .edge_index_endpoints
662 .push((node_idx[s_idx], node_idx[t_idx]));
663 graph.edges.insert(
664 Graph::canon_pair(node_idx[s_idx], node_idx[t_idx]),
665 AttrMap::new(),
666 );
667 }
668 }
669 for &v in s {
670 in_s[v] = false;
671 }
672 }
673 graph.revision = u64::try_from(graph.nodes.len() + graph.edges.len()).unwrap_or(u64::MAX);
675 graph
676 }
677
678 #[must_use]
679 pub fn hardened() -> Self {
680 Self::new(CompatibilityMode::Hardened)
681 }
682
683 #[must_use]
684 pub fn mode(&self) -> CompatibilityMode {
685 self.mode
686 }
687
688 #[must_use]
689 pub fn node_count(&self) -> usize {
690 self.nodes.len()
691 }
692
693 #[must_use]
694 pub fn edge_count(&self) -> usize {
695 self.edges.len()
696 }
697
698 #[must_use]
699 pub fn revision(&self) -> u64 {
700 self.revision
701 }
702
703 #[must_use]
704 pub fn has_node(&self, node: &str) -> bool {
705 self.nodes.contains_key(node)
706 }
707
708 #[inline]
711 fn edge_pair_key(&self, left: &str, right: &str) -> Option<(usize, usize)> {
712 let l = self.nodes.get_index_of(left)?;
713 let r = self.nodes.get_index_of(right)?;
714 Some(if l <= r { (l, r) } else { (r, l) })
715 }
716
717 #[inline]
718 fn canon_pair(l: usize, r: usize) -> (usize, usize) {
719 if l <= r { (l, r) } else { (r, l) }
720 }
721
722 #[must_use]
723 pub fn has_edge(&self, left: &str, right: &str) -> bool {
724 self.edge_pair_key(left, right)
725 .is_some_and(|k| self.edges.contains_key(&k))
726 }
727
728 #[must_use]
729 pub fn nodes_ordered(&self) -> Vec<&str> {
730 self.nodes.keys().map(String::as_str).collect()
731 }
732
733 #[must_use]
734 pub fn get_node_index(&self, node: &str) -> Option<usize> {
735 self.nodes.get_index_of(node)
736 }
737
738 #[must_use]
739 pub fn get_node_name(&self, index: usize) -> Option<&str> {
740 self.nodes.get_index(index).map(|(k, _)| k.as_str())
741 }
742
743 #[must_use]
744 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
745 let idx = self.nodes.get_index_of(node)?;
752 Some(
753 self.adj_indices[idx]
754 .iter()
755 .map(|&i| {
756 self.nodes
757 .get_index(i)
758 .expect("adj_indices entries are valid node indices")
759 .0
760 .as_str()
761 })
762 .collect::<Vec<&str>>(),
763 )
764 }
765
766 #[must_use]
767 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
768 let idx = self.nodes.get_index_of(node)?;
770 Some(self.adj_indices[idx].iter().map(move |&i| {
771 self.nodes
772 .get_index(i)
773 .expect("adj_indices entries are valid node indices")
774 .0
775 .as_str()
776 }))
777 }
778
779 #[must_use]
782 #[inline]
783 pub fn neighbors_indices(&self, node_idx: usize) -> Option<&[usize]> {
784 self.adj_indices.get(node_idx).map(Vec::as_slice)
785 }
786
787 #[must_use]
792 pub fn clone_with_fresh_policy(&self) -> Self {
793 Self {
794 mode: self.mode,
795 revision: self.revision,
796 nodes: self.nodes.clone(),
797 adj_indices: self.adj_indices.clone(),
798 all_int_cache: std::sync::Arc::default(),
799 edge_index_endpoints: self.edge_index_endpoints.clone(),
800 edges: self.edges.clone(),
801 runtime_policy: RuntimePolicy::new(self.mode),
802 }
803 }
804
805 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
811 for (node, order) in orders {
813 let Some(idx) = self.nodes.get_index_of(node.as_str()) else {
814 continue;
815 };
816 let row = &self.adj_indices[idx];
817 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
818 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
819 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
820 for v in order {
821 if let Some(v_idx) = self.nodes.get_index_of(v.as_str())
822 && row_set.contains(&v_idx)
823 && placed.insert(v_idx)
824 {
825 new_row.push(v_idx);
826 }
827 }
828 for &v_idx in row {
829 if placed.insert(v_idx) {
830 new_row.push(v_idx);
831 }
832 }
833 self.adj_indices[idx] = new_row;
834 }
835 }
836
837 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
848 let n = self.adj_indices.len();
858 let mut early: Vec<Vec<usize>> = vec![Vec::new(); n];
859 for (v, row) in self.adj_indices.iter().enumerate() {
860 for &pu in row {
861 if pu > v {
862 early[pu].push(v);
863 }
864 }
865 }
866 let mut new_rows: Vec<Vec<usize>> = Vec::with_capacity(n);
867 for (pu, row) in self.adj_indices.iter().enumerate() {
868 let mut new_row = std::mem::take(&mut early[pu]);
869 new_row.reserve(row.len());
870 for &v in row {
871 if v >= pu {
872 new_row.push(v);
873 }
874 }
875 new_rows.push(new_row);
876 }
877 self.adj_indices = new_rows;
878 }
879
880 pub fn edges_storage_order_index_iter(
881 &self,
882 ) -> impl Iterator<Item = (usize, usize, &AttrMap)> + '_ {
883 self.edge_index_endpoints
884 .iter()
885 .copied()
886 .zip(self.edges.values())
887 .map(|((left, right), attrs)| (left, right, attrs))
888 }
889
890 #[must_use]
891 pub fn neighbor_count(&self, node: &str) -> usize {
892 self.nodes
894 .get_index_of(node)
895 .map_or(0, |idx| self.adj_indices[idx].len())
896 }
897
898 #[must_use]
901 pub fn degree(&self, node: &str) -> usize {
902 let count = self.neighbor_count(node);
903 if self.has_edge(node, node) {
905 count + 1
906 } else {
907 count
908 }
909 }
910
911 #[must_use]
916 pub fn degree_by_index(&self, idx: usize) -> usize {
917 let row = &self.adj_indices[idx];
918 let mut count = row.len();
919 if row.contains(&idx) {
920 count += 1; }
922 count
923 }
924
925 #[must_use]
926 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
927 self.nodes.get(node)
928 }
929
930 #[must_use]
931 pub fn edge_attrs(&self, left: &str, right: &str) -> Option<&AttrMap> {
932 self.edges.get(&self.edge_pair_key(left, right)?)
933 }
934
935 #[must_use]
939 #[inline]
940 pub fn edge_attrs_by_indices(&self, left_idx: usize, right_idx: usize) -> Option<&AttrMap> {
941 self.edges.get(&Self::canon_pair(left_idx, right_idx))
942 }
943
944 #[must_use]
945 pub fn nodes_are_contiguous_int_prefix(&self) -> bool {
946 self.nodes
947 .keys()
948 .enumerate()
949 .all(|(index, key)| Self::decimal_key_matches_index(key, index))
950 }
951
952 fn decimal_key_matches_index(key: &str, index: usize) -> bool {
953 let bytes = key.as_bytes();
954 if bytes.is_empty() || (bytes.len() > 1 && bytes[0] == b'0') {
955 return false;
956 }
957
958 let mut parsed = 0usize;
959 for &byte in bytes {
960 if !byte.is_ascii_digit() {
961 return false;
962 }
963 let digit = usize::from(byte - b'0');
964 let Some(next) = parsed
965 .checked_mul(10)
966 .and_then(|value| value.checked_add(digit))
967 else {
968 return false;
969 };
970 parsed = next;
971 }
972 parsed == index
973 }
974
975 #[must_use]
976 pub fn evidence_ledger(&self) -> &EvidenceLedger {
977 self.runtime_policy.decision_log()
978 }
979
980 #[must_use]
981 pub fn runtime_policy(&self) -> &RuntimePolicy {
982 &self.runtime_policy
983 }
984
985 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
986 self.mode = runtime_policy.mode();
987 self.runtime_policy = runtime_policy;
988 }
989
990 #[must_use]
992 pub fn is_directed(&self) -> bool {
993 false
994 }
995
996 #[must_use]
998 pub fn is_multigraph(&self) -> bool {
999 false
1000 }
1001
1002 pub fn add_node(&mut self, node: impl Into<String>) -> bool {
1003 self.add_node_with_attrs(node, AttrMap::new())
1004 }
1005
1006 pub fn add_node_with_attrs(&mut self, node: impl Into<String>, attrs: AttrMap) -> bool {
1007 let node = node.into();
1008 let existed = self.nodes.contains_key(&node);
1009 let mut changed = !existed;
1010 let attrs_count = {
1011 let bucket = self.nodes.entry(node.clone()).or_default();
1012 if !attrs.is_empty()
1013 && attrs
1014 .iter()
1015 .any(|(key, value)| bucket.get(key) != Some(value))
1016 {
1017 changed = true;
1018 }
1019 bucket.extend(attrs);
1020 bucket.len()
1021 };
1022 if !existed {
1024 self.adj_indices.push(Vec::new());
1025 }
1026 if changed {
1027 self.revision = self.revision.saturating_add(1);
1028 }
1029 self.record_decision(
1030 "add_node",
1031 0.0,
1032 false,
1033 vec![
1034 EvidenceTerm {
1035 signal: "node_preexisting".to_owned(),
1036 observed_value: existed.to_string(),
1037 log_likelihood_ratio: -3.0,
1038 },
1039 EvidenceTerm {
1040 signal: "attrs_count".to_owned(),
1041 observed_value: attrs_count.to_string(),
1042 log_likelihood_ratio: -1.0,
1043 },
1044 ],
1045 );
1046 changed
1047 }
1048
1049 #[must_use]
1052 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
1053 where
1054 I: IntoIterator<Item = N>,
1055 N: Into<String>,
1056 {
1057 let iterator = nodes.into_iter();
1058 let (lower_bound, _) = iterator.size_hint();
1059 self.nodes.reserve(lower_bound);
1060 self.adj_indices.reserve(lower_bound);
1061
1062 let mut inserted = 0usize;
1063 for node in iterator {
1064 let node = node.into();
1065 if self.nodes.contains_key(&node) {
1066 continue;
1067 }
1068 self.nodes.insert(node.clone(), AttrMap::new());
1069 self.adj_indices.push(Vec::new());
1070 inserted += 1;
1071 }
1072 if inserted > 0 {
1073 self.revision = self
1074 .revision
1075 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1076 self.record_decision(
1077 "extend_nodes_unrecorded",
1078 0.0,
1079 false,
1080 vec![EvidenceTerm {
1081 signal: "batch_node_count".to_owned(),
1082 observed_value: inserted.to_string(),
1083 log_likelihood_ratio: -1.0,
1084 }],
1085 );
1086 }
1087 inserted
1088 }
1089
1090 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
1094 where
1095 I: IntoIterator<Item = (String, AttrMap)>,
1096 {
1097 let iterator = nodes.into_iter();
1098 let (lower_bound, _) = iterator.size_hint();
1099 self.nodes.reserve(lower_bound);
1100 self.adj_indices.reserve(lower_bound);
1101
1102 let mut inserted = 0usize;
1103 for (node, attrs) in iterator {
1104 if let Some(existing) = self.nodes.get_mut(&node) {
1105 existing.extend(attrs);
1106 continue;
1107 }
1108 self.nodes.insert(node.clone(), attrs);
1109 self.adj_indices.push(Vec::new());
1110 inserted += 1;
1111 }
1112 if inserted > 0 {
1113 self.revision = self
1114 .revision
1115 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1116 self.record_decision(
1117 "extend_nodes_with_attrs_unrecorded",
1118 0.0,
1119 false,
1120 vec![EvidenceTerm {
1121 signal: "batch_node_count".to_owned(),
1122 observed_value: inserted.to_string(),
1123 log_likelihood_ratio: -1.0,
1124 }],
1125 );
1126 }
1127 inserted
1128 }
1129
1130 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
1131 if defaults.is_empty() {
1132 return false;
1133 }
1134 let mut inserted = 0usize;
1135 for attrs in self.nodes.values_mut() {
1136 for (key, value) in defaults {
1137 if !attrs.contains_key(key) {
1138 attrs.insert(key.clone(), value.clone());
1139 inserted += 1;
1140 }
1141 }
1142 }
1143 if inserted > 0 {
1144 self.revision = self.revision.saturating_add(1);
1145 self.record_decision(
1146 "apply_node_defaults",
1147 0.0,
1148 false,
1149 vec![EvidenceTerm {
1150 signal: "defaults_applied".to_owned(),
1151 observed_value: inserted.to_string(),
1152 log_likelihood_ratio: 0.0,
1153 }],
1154 );
1155 return true;
1156 }
1157 false
1158 }
1159
1160 pub fn add_edge(
1161 &mut self,
1162 left: impl Into<String>,
1163 right: impl Into<String>,
1164 ) -> Result<(), GraphError> {
1165 self.add_edge_with_attrs(left, right, AttrMap::new())
1166 }
1167
1168 #[must_use]
1183 pub fn extend_edges_unrecorded<I, L, R>(&mut self, edges: I) -> usize
1184 where
1185 I: IntoIterator<Item = (L, R)>,
1186 L: Into<String>,
1187 R: Into<String>,
1188 {
1189 let iterator = edges.into_iter();
1190 let (lower_bound, _) = iterator.size_hint();
1191 self.nodes.reserve(lower_bound);
1192 self.adj_indices.reserve(lower_bound);
1193 self.edges.reserve(lower_bound);
1194 self.edge_index_endpoints.reserve(lower_bound);
1195
1196 let mut inserted = 0usize;
1197 let mut nodes_added = false;
1198 for (left, right) in iterator {
1199 let left = left.into();
1200 let right = right.into();
1201 let left_idx = match self.nodes.get_index_of(&left) {
1202 Some(index) => index,
1203 None => {
1204 let index = self.nodes.len();
1205 self.nodes.insert(left.clone(), AttrMap::new());
1206 self.adj_indices.push(Vec::new());
1207 nodes_added = true;
1208 index
1209 }
1210 };
1211 let right_idx = if left == right {
1212 left_idx
1213 } else {
1214 match self.nodes.get_index_of(&right) {
1215 Some(index) => index,
1216 None => {
1217 let index = self.nodes.len();
1218 self.nodes.insert(right.clone(), AttrMap::new());
1219 self.adj_indices.push(Vec::new());
1220 nodes_added = true;
1221 index
1222 }
1223 }
1224 };
1225 let edge_key = Self::canon_pair(left_idx, right_idx);
1226 if self.edges.contains_key(&edge_key) {
1227 continue;
1228 }
1229 if left <= right {
1230 self.edge_index_endpoints.push((left_idx, right_idx));
1231 } else {
1232 self.edge_index_endpoints.push((right_idx, left_idx));
1233 }
1234 self.edges.insert(edge_key, AttrMap::new());
1235
1236 self.adj_indices[left_idx].push(right_idx);
1237 if left_idx != right_idx {
1238 self.adj_indices[right_idx].push(left_idx);
1239 }
1240 inserted += 1;
1241 }
1242 self.record_bulk_edge_summary(inserted, nodes_added);
1243 inserted
1244 }
1245
1246 #[must_use]
1252 pub fn extend_existing_index_edges_unrecorded<I>(&mut self, edges: I) -> usize
1253 where
1254 I: IntoIterator<Item = (usize, usize)>,
1255 {
1256 let iterator = edges.into_iter();
1257 let (lower_bound, _) = iterator.size_hint();
1258 self.edges.reserve(lower_bound);
1259 self.edge_index_endpoints.reserve(lower_bound);
1260
1261 let mut inserted = 0usize;
1262 for (left_idx, right_idx) in iterator {
1263 debug_assert!(left_idx < self.nodes.len());
1264 debug_assert!(right_idx < self.nodes.len());
1265
1266 let edge_key = Self::canon_pair(left_idx, right_idx);
1267 if self.edges.contains_key(&edge_key) {
1268 continue;
1269 }
1270
1271 let left_name = self
1272 .nodes
1273 .get_index(left_idx)
1274 .expect("validated left node index")
1275 .0
1276 .as_str();
1277 let right_name = self
1278 .nodes
1279 .get_index(right_idx)
1280 .expect("validated right node index")
1281 .0
1282 .as_str();
1283 if left_name <= right_name {
1284 self.edge_index_endpoints.push((left_idx, right_idx));
1285 } else {
1286 self.edge_index_endpoints.push((right_idx, left_idx));
1287 }
1288 self.edges.insert(edge_key, AttrMap::new());
1289
1290 self.adj_indices[left_idx].push(right_idx);
1291 if left_idx != right_idx {
1292 self.adj_indices[right_idx].push(left_idx);
1293 }
1294 inserted += 1;
1295 }
1296 self.record_bulk_edge_summary(inserted, false);
1297 inserted
1298 }
1299
1300 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1308 where
1309 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1310 {
1311 let iterator = edges.into_iter();
1312 let (lower_bound, _) = iterator.size_hint();
1313 self.edges.reserve(lower_bound);
1314 self.edge_index_endpoints.reserve(lower_bound);
1315
1316 let mut inserted = 0usize;
1317 for (left_idx, right_idx, attrs) in iterator {
1318 debug_assert!(left_idx < self.nodes.len());
1319 debug_assert!(right_idx < self.nodes.len());
1320
1321 let edge_key = Self::canon_pair(left_idx, right_idx);
1322 if let Some(existing) = self.edges.get_mut(&edge_key) {
1323 for (key, value) in attrs {
1325 existing.insert(key, value);
1326 }
1327 continue;
1328 }
1329
1330 let left_name = self
1331 .nodes
1332 .get_index(left_idx)
1333 .expect("validated left node index")
1334 .0
1335 .as_str();
1336 let right_name = self
1337 .nodes
1338 .get_index(right_idx)
1339 .expect("validated right node index")
1340 .0
1341 .as_str();
1342 if left_name <= right_name {
1343 self.edge_index_endpoints.push((left_idx, right_idx));
1344 } else {
1345 self.edge_index_endpoints.push((right_idx, left_idx));
1346 }
1347 self.edges.insert(edge_key, attrs);
1348
1349 self.adj_indices[left_idx].push(right_idx);
1350 if left_idx != right_idx {
1351 self.adj_indices[right_idx].push(left_idx);
1352 }
1353 inserted += 1;
1354 }
1355 self.record_bulk_edge_summary(inserted, false);
1356 inserted
1357 }
1358
1359 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1374 where
1375 I: IntoIterator<Item = (String, String, AttrMap)>,
1376 {
1377 let iterator = edges.into_iter();
1378 let (lower_bound, _) = iterator.size_hint();
1379 self.nodes.reserve(lower_bound);
1380 self.adj_indices.reserve(lower_bound);
1381 self.edges.reserve(lower_bound);
1382 self.edge_index_endpoints.reserve(lower_bound);
1383
1384 let mut inserted = 0usize;
1385 let mut nodes_added = false;
1386 let mut merged_changed = false;
1387 for (left, right, attrs) in iterator {
1388 let left_idx = match self.nodes.get_index_of(&left) {
1389 Some(index) => index,
1390 None => {
1391 let index = self.nodes.len();
1392 self.nodes.insert(left.clone(), AttrMap::new());
1393 self.adj_indices.push(Vec::new());
1394 nodes_added = true;
1395 index
1396 }
1397 };
1398 let right_idx = if left == right {
1399 left_idx
1400 } else {
1401 match self.nodes.get_index_of(&right) {
1402 Some(index) => index,
1403 None => {
1404 let index = self.nodes.len();
1405 self.nodes.insert(right.clone(), AttrMap::new());
1406 self.adj_indices.push(Vec::new());
1407 nodes_added = true;
1408 index
1409 }
1410 }
1411 };
1412 let edge_key = Self::canon_pair(left_idx, right_idx);
1413 if let Some(existing) = self.edges.get_mut(&edge_key) {
1414 if !attrs.is_empty()
1417 && attrs
1418 .iter()
1419 .any(|(key, value)| existing.get(key) != Some(value))
1420 {
1421 merged_changed = true;
1422 }
1423 existing.extend(attrs);
1424 continue;
1425 }
1426 if left <= right {
1427 self.edge_index_endpoints.push((left_idx, right_idx));
1428 } else {
1429 self.edge_index_endpoints.push((right_idx, left_idx));
1430 }
1431 self.edges.insert(edge_key, attrs);
1432
1433 self.adj_indices[left_idx].push(right_idx);
1434 if left_idx != right_idx {
1435 self.adj_indices[right_idx].push(left_idx);
1436 }
1437 inserted += 1;
1438 }
1439 if merged_changed {
1440 self.revision = self.revision.saturating_add(1);
1444 }
1445 self.record_bulk_edge_summary(inserted, nodes_added || merged_changed);
1446 inserted
1447 }
1448
1449 #[must_use]
1458 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
1459 &mut self,
1460 nodes: N,
1461 edges: I,
1462 ) -> usize
1463 where
1464 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1465 N: IntoIterator<Item = String>,
1466 {
1467 if !self.nodes.is_empty()
1468 || !self.adj_indices.is_empty()
1469 || !self.edges.is_empty()
1470 || !self.edge_index_endpoints.is_empty()
1471 {
1472 return 0;
1473 }
1474
1475 let node_labels: Vec<String> = nodes.into_iter().collect();
1476 self.nodes.reserve(node_labels.len());
1477 self.adj_indices = vec![Vec::new(); node_labels.len()];
1478 for node in &node_labels {
1479 self.nodes.insert(node.clone(), AttrMap::new());
1480 }
1481
1482 let iterator = edges.into_iter();
1483 let (lower_bound, _) = iterator.size_hint();
1484 self.edges.reserve(lower_bound);
1485 self.edge_index_endpoints.reserve(lower_bound);
1486
1487 let node_count = node_labels.len();
1488 let mut inserted = 0usize;
1489 let mut merged_changed = false;
1490 for (left_idx, right_idx, attrs) in iterator {
1491 if left_idx >= node_count || right_idx >= node_count {
1492 continue;
1493 }
1494
1495 let edge_key = Self::canon_pair(left_idx, right_idx);
1496 if let Some(existing) = self.edges.get_mut(&edge_key) {
1497 if !attrs.is_empty()
1498 && attrs
1499 .iter()
1500 .any(|(key, value)| existing.get(key) != Some(value))
1501 {
1502 merged_changed = true;
1503 }
1504 existing.extend(attrs);
1505 continue;
1506 }
1507
1508 if node_labels[left_idx] <= node_labels[right_idx] {
1509 self.edge_index_endpoints.push((left_idx, right_idx));
1510 } else {
1511 self.edge_index_endpoints.push((right_idx, left_idx));
1512 }
1513 self.edges.insert(edge_key, attrs);
1514 self.adj_indices[left_idx].push(right_idx);
1515 if left_idx != right_idx {
1516 self.adj_indices[right_idx].push(left_idx);
1517 }
1518 inserted += 1;
1519 }
1520
1521 self.record_bulk_edge_summary(inserted, node_count > 0 || merged_changed);
1522 inserted
1523 }
1524
1525 fn record_bulk_edge_summary(&mut self, inserted: usize, nodes_added: bool) {
1526 if inserted == 0 && !nodes_added {
1527 return;
1528 }
1529 self.revision = self
1530 .revision
1531 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1532 self.record_decision(
1536 "extend_edges_unrecorded",
1537 0.0,
1538 false,
1539 vec![EvidenceTerm {
1540 signal: "batch_edge_count".to_owned(),
1541 observed_value: inserted.to_string(),
1542 log_likelihood_ratio: -1.0,
1543 }],
1544 );
1545 }
1546
1547 pub fn add_edge_with_attrs(
1548 &mut self,
1549 left: impl Into<String>,
1550 right: impl Into<String>,
1551 attrs: AttrMap,
1552 ) -> Result<(), GraphError> {
1553 let left = left.into();
1554 let right = right.into();
1555
1556 let unknown_feature = attrs
1557 .keys()
1558 .any(|key| key.starts_with("__fnx_incompatible"));
1559 let incompatibility_probability = if unknown_feature {
1560 1.0
1561 } else if left == right {
1562 0.22
1563 } else {
1564 0.08
1565 };
1566
1567 let action = self.record_decision(
1568 "add_edge",
1569 incompatibility_probability,
1570 unknown_feature,
1571 vec![EvidenceTerm {
1572 signal: "unknown_incompatible_feature".to_owned(),
1573 observed_value: unknown_feature.to_string(),
1574 log_likelihood_ratio: 12.0,
1575 }],
1576 );
1577
1578 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
1579 return Err(GraphError::FailClosed {
1580 operation: "add_edge",
1581 reason: "incompatible edge metadata".to_owned(),
1582 });
1583 }
1584
1585 let mut left_autocreated = false;
1586 if !self.nodes.contains_key(&left) {
1587 let _ = self.add_node(left.clone());
1588 left_autocreated = true;
1589 }
1590 let mut right_autocreated = false;
1591 if left == right {
1592 right_autocreated = left_autocreated;
1593 } else if !self.nodes.contains_key(&right) {
1594 let _ = self.add_node(right.clone());
1595 right_autocreated = true;
1596 }
1597
1598 let left_key_idx = self.nodes.get_index_of(&left).expect("autocreated above");
1599 let right_key_idx = self.nodes.get_index_of(&right).expect("autocreated above");
1600 let edge_key = Self::canon_pair(left_key_idx, right_key_idx);
1601 let self_loop = left == right;
1602 let new_edge = !self.edges.contains_key(&edge_key);
1603 let mut changed = new_edge;
1604 let edge_attr_count = {
1605 let edge_attrs = self.edges.entry(edge_key).or_default();
1606 if !attrs.is_empty()
1607 && attrs
1608 .iter()
1609 .any(|(key, value)| edge_attrs.get(key) != Some(value))
1610 {
1611 changed = true;
1612 }
1613 edge_attrs.extend(attrs);
1614 edge_attrs.len()
1615 };
1616
1617 if changed {
1619 if let (Some(left_idx), Some(right_idx)) = (
1620 self.nodes.get_index_of(&left),
1621 self.nodes.get_index_of(&right),
1622 ) {
1623 if new_edge {
1624 if left <= right {
1625 self.edge_index_endpoints.push((left_idx, right_idx));
1626 } else {
1627 self.edge_index_endpoints.push((right_idx, left_idx));
1628 }
1629 }
1630 if !self.adj_indices[left_idx].contains(&right_idx) {
1631 self.adj_indices[left_idx].push(right_idx);
1632 }
1633 if left_idx != right_idx && !self.adj_indices[right_idx].contains(&left_idx) {
1634 self.adj_indices[right_idx].push(left_idx);
1635 }
1636 }
1637 self.revision = self.revision.saturating_add(1);
1638 }
1639
1640 self.record_decision(
1641 "add_edge",
1642 incompatibility_probability,
1643 unknown_feature,
1644 vec![
1645 EvidenceTerm {
1646 signal: "self_loop".to_owned(),
1647 observed_value: self_loop.to_string(),
1648 log_likelihood_ratio: -0.5,
1649 },
1650 EvidenceTerm {
1651 signal: "edge_attr_count".to_owned(),
1652 observed_value: edge_attr_count.to_string(),
1653 log_likelihood_ratio: -2.0,
1654 },
1655 EvidenceTerm {
1656 signal: "left_autocreated".to_owned(),
1657 observed_value: left_autocreated.to_string(),
1658 log_likelihood_ratio: -1.25,
1659 },
1660 EvidenceTerm {
1661 signal: "right_autocreated".to_owned(),
1662 observed_value: right_autocreated.to_string(),
1663 log_likelihood_ratio: -1.25,
1664 },
1665 ],
1666 );
1667
1668 Ok(())
1669 }
1670
1671 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
1672 if defaults.is_empty() {
1673 return false;
1674 }
1675 let mut inserted = 0usize;
1676 for attrs in self.edges.values_mut() {
1677 for (key, value) in defaults {
1678 if !attrs.contains_key(key) {
1679 attrs.insert(key.clone(), value.clone());
1680 inserted += 1;
1681 }
1682 }
1683 }
1684 if inserted > 0 {
1685 self.revision = self.revision.saturating_add(1);
1686 self.record_decision(
1687 "apply_edge_defaults",
1688 0.0,
1689 false,
1690 vec![EvidenceTerm {
1691 signal: "defaults_applied".to_owned(),
1692 observed_value: inserted.to_string(),
1693 log_likelihood_ratio: 0.0,
1694 }],
1695 );
1696 return true;
1697 }
1698 false
1699 }
1700
1701 pub fn replace_edge_attrs(&mut self, left: &str, right: &str, attrs: AttrMap) -> bool {
1706 let Some(edge_key) = self.edge_pair_key(left, right) else {
1707 return false;
1708 };
1709 if let Some(slot) = self.edges.get_mut(&edge_key) {
1710 if *slot != attrs {
1711 *slot = attrs;
1712 self.revision = self.revision.saturating_add(1);
1713 }
1714 true
1715 } else {
1716 false
1717 }
1718 }
1719
1720 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
1722 if let Some(slot) = self.nodes.get_mut(node) {
1723 if *slot != attrs {
1724 *slot = attrs;
1725 self.revision = self.revision.saturating_add(1);
1726 }
1727 true
1728 } else {
1729 false
1730 }
1731 }
1732
1733 pub fn remove_edge(&mut self, left: &str, right: &str) -> bool {
1734 let Some(pair) = self.edge_pair_key(left, right) else {
1743 return false;
1744 };
1745 let removed = self.edges.swap_remove_full(&pair);
1756 if let Some((edge_pos, _, _)) = removed {
1757 if let (Some(left_idx), Some(right_idx)) = (
1760 self.nodes.get_index_of(left),
1761 self.nodes.get_index_of(right),
1762 ) {
1763 self.adj_indices[left_idx].retain(|&i| i != right_idx);
1764 if left_idx != right_idx {
1765 self.adj_indices[right_idx].retain(|&i| i != left_idx);
1766 }
1767 }
1768 self.edge_index_endpoints.swap_remove(edge_pos);
1769 self.revision = self.revision.saturating_add(1);
1770 true
1771 } else {
1772 false
1773 }
1774 }
1775
1776 pub fn remove_node(&mut self, node: &str) -> bool {
1777 let Some(idx) = self.nodes.get_index_of(node) else {
1778 return false;
1779 };
1780
1781 let has_incident = self.adj_indices.get(idx).is_some_and(|row| !row.is_empty());
1802 if has_incident {
1803 let keep: Vec<bool> = self
1804 .edge_index_endpoints
1805 .iter()
1806 .map(|&(l, r)| l != idx && r != idx)
1807 .collect();
1808 let mut i = 0usize;
1809 self.edges.retain(|_, _| {
1810 let k = keep[i];
1811 i += 1;
1812 k
1813 });
1814 let mut j = 0usize;
1815 self.edge_index_endpoints.retain(|_| {
1816 let k = keep[j];
1817 j += 1;
1818 k
1819 });
1820 }
1821
1822 self.adj_indices.remove(idx);
1825 self.nodes.shift_remove(node);
1827 for nbrs in &mut self.adj_indices {
1830 nbrs.retain(|&e| e != idx);
1831 for e in nbrs.iter_mut() {
1832 if *e > idx {
1833 *e -= 1;
1834 }
1835 }
1836 }
1837 for (left, right) in &mut self.edge_index_endpoints {
1838 if *left > idx {
1839 *left -= 1;
1840 }
1841 if *right > idx {
1842 *right -= 1;
1843 }
1844 }
1845 self.edges = std::mem::take(&mut self.edges)
1848 .into_iter()
1849 .map(|((l, r), attrs)| {
1850 (
1851 (
1852 if l > idx { l - 1 } else { l },
1853 if r > idx { r - 1 } else { r },
1854 ),
1855 attrs,
1856 )
1857 })
1858 .collect();
1859
1860 self.revision = self.revision.saturating_add(1);
1861 true
1862 }
1863
1864 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
1865 where
1866 I: IntoIterator<Item = &'a str>,
1867 {
1868 let remove_set: HashSet<&str> = nodes
1869 .into_iter()
1870 .filter(|node| self.nodes.contains_key(*node))
1871 .collect();
1872 if remove_set.is_empty() {
1873 return (0, 0);
1874 }
1875
1876 let old_node_count = self.nodes.len();
1877 let old_edge_count = self.edges.len();
1878
1879 let removed_mask: Vec<bool> = self
1885 .nodes
1886 .keys()
1887 .map(|k| remove_set.contains(k.as_str()))
1888 .collect();
1889 let mut remap: Vec<usize> = Vec::with_capacity(old_node_count);
1890 let mut next = 0usize;
1891 for &gone in &removed_mask {
1892 remap.push(next);
1893 if !gone {
1894 next += 1;
1895 }
1896 }
1897 let keep_edges: Vec<bool> = self
1898 .edge_index_endpoints
1899 .iter()
1900 .map(|&(l, r)| !removed_mask[l] && !removed_mask[r])
1901 .collect();
1902
1903 self.nodes
1904 .retain(|node, _| !remove_set.contains(node.as_str()));
1905 let mut i = 0usize;
1906 self.edges.retain(|_, _| {
1907 let k = keep_edges[i];
1908 i += 1;
1909 k
1910 });
1911 let mut j = 0usize;
1912 self.edge_index_endpoints.retain(|_| {
1913 let k = keep_edges[j];
1914 j += 1;
1915 k
1916 });
1917 for endpoints in &mut self.edge_index_endpoints {
1918 endpoints.0 = remap[endpoints.0];
1919 endpoints.1 = remap[endpoints.1];
1920 }
1921 self.edges = std::mem::take(&mut self.edges)
1923 .into_iter()
1924 .map(|((l, r), attrs)| ((remap[l], remap[r]), attrs))
1925 .collect();
1926 let old_rows = std::mem::take(&mut self.adj_indices);
1927 self.adj_indices = old_rows
1928 .into_iter()
1929 .enumerate()
1930 .filter(|(old_idx, _)| !removed_mask[*old_idx])
1931 .map(|(_, row)| {
1932 row.into_iter()
1933 .filter(|&nbr| !removed_mask[nbr])
1934 .map(|nbr| remap[nbr])
1935 .collect()
1936 })
1937 .collect();
1938
1939 let removed_nodes = old_node_count - self.nodes.len();
1940 let removed_edges = old_edge_count - self.edges.len();
1941 self.revision = self
1942 .revision
1943 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
1944 (removed_nodes, removed_edges)
1945 }
1946
1947 #[allow(dead_code)]
1950 #[must_use]
1953 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
1954 if let Ok(guard) = self.all_int_cache.read()
1955 && let Some((rev, a, v)) = guard.as_ref()
1956 && *rev == self.revision
1957 && a == attr
1958 {
1959 return *v;
1960 }
1961 let v = self
1962 .edges
1963 .values()
1964 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
1965 if let Ok(mut guard) = self.all_int_cache.write() {
1966 *guard = Some((self.revision, attr.to_owned(), v));
1967 }
1968 v
1969 }
1970
1971 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
1972 let mut pair_attrs: std::collections::HashMap<(usize, usize), &AttrMap> =
1978 std::collections::HashMap::with_capacity(self.edges.len());
1979 for ((l, r), attrs) in self.edge_index_endpoints.iter().zip(self.edges.values()) {
1980 let pair = if l <= r { (*l, *r) } else { (*r, *l) };
1983 pair_attrs.insert(pair, attrs);
1984 }
1985 let mut ordered = Vec::with_capacity(self.edges.len());
1986 let mut seen = HashSet::<(usize, usize)>::with_capacity(self.edges.len());
1987 for (u, row) in self.adj_indices.iter().enumerate() {
1988 for &v in row {
1989 let pair = if u <= v { (u, v) } else { (v, u) };
1990 if !seen.insert(pair) {
1991 continue;
1992 }
1993 if let Some(attrs) = pair_attrs.get(&pair) {
1994 ordered.push(EdgeSnapshot {
1995 left: self.nodes.get_index(u).expect("valid node index").0.clone(),
1996 right: self.nodes.get_index(v).expect("valid node index").0.clone(),
1997 attrs: (*attrs).clone(),
1998 });
1999 }
2000 }
2001 }
2002
2003 ordered
2004 }
2005
2006 #[must_use]
2007 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
2008 let mut pair_attrs: std::collections::HashMap<(usize, usize), &AttrMap> =
2010 std::collections::HashMap::with_capacity(self.edges.len());
2011 for ((l, r), attrs) in self.edge_index_endpoints.iter().zip(self.edges.values()) {
2012 let pair = if l <= r { (*l, *r) } else { (*r, *l) };
2015 pair_attrs.insert(pair, attrs);
2016 }
2017 let mut ordered = Vec::with_capacity(self.edges.len());
2018 let mut seen_pairs = HashSet::<(usize, usize)>::with_capacity(self.edges.len());
2019 for (u, row) in self.adj_indices.iter().enumerate() {
2020 for &v in row {
2021 let pair = if u <= v { (u, v) } else { (v, u) };
2022 if !seen_pairs.insert(pair) {
2023 continue;
2024 }
2025 if let Some(attrs) = pair_attrs.get(&pair) {
2026 let left = self.nodes.get_index(u).expect("valid node index").0;
2027 let right = self.nodes.get_index(v).expect("valid node index").0;
2028 ordered.push((left.as_str(), right.as_str(), *attrs));
2029 }
2030 }
2031 }
2032
2033 if ordered.len() < self.edges.len() {
2034 for (&(l, r), attrs) in &self.edges {
2035 if seen_pairs.insert((l, r)) {
2036 let left = self.nodes.get_index(l).expect("valid index").0;
2037 let right = self.nodes.get_index(r).expect("valid index").0;
2038 ordered.push((left.as_str(), right.as_str(), attrs));
2039 }
2040 }
2041 }
2042
2043 ordered
2044 }
2045
2046 #[must_use]
2052 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
2053 let mut present: HashSet<(usize, usize)> = HashSet::with_capacity(self.edges.len());
2054 for &(l, r) in &self.edge_index_endpoints {
2055 present.insert(if l <= r { (l, r) } else { (r, l) });
2056 }
2057 let mut ordered = Vec::with_capacity(self.edges.len());
2058 let mut seen_pairs = HashSet::<(usize, usize)>::with_capacity(self.edges.len());
2059 for (u, row) in self.adj_indices.iter().enumerate() {
2060 for &v in row {
2061 let pair = if u <= v { (u, v) } else { (v, u) };
2062 if !seen_pairs.insert(pair) {
2063 continue;
2064 }
2065 if present.contains(&pair) {
2066 ordered.push((u, v));
2067 }
2068 }
2069 }
2070 if ordered.len() < self.edges.len() {
2071 for &(l, r) in self.edges.keys() {
2075 if seen_pairs.insert((l, r)) {
2076 ordered.push((l, r));
2077 }
2078 }
2079 }
2080 ordered
2081 }
2082
2083 #[must_use]
2084 pub fn edges_storage_order_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
2085 self.edge_index_endpoints
2088 .iter()
2089 .zip(self.edges.values())
2090 .map(|(&(l, r), attrs)| {
2091 (
2092 self.nodes.get_index(l).expect("valid index").0.as_str(),
2093 self.nodes.get_index(r).expect("valid index").0.as_str(),
2094 attrs,
2095 )
2096 })
2097 .collect()
2098 }
2099
2100 pub fn edges_storage_order_iter(&self) -> impl Iterator<Item = (&str, &str, &AttrMap)> + '_ {
2101 self.edge_index_endpoints
2102 .iter()
2103 .zip(self.edges.values())
2104 .map(|(&(l, r), attrs)| {
2105 (
2106 self.nodes.get_index(l).expect("valid index").0.as_str(),
2107 self.nodes.get_index(r).expect("valid index").0.as_str(),
2108 attrs,
2109 )
2110 })
2111 }
2112
2113 #[must_use]
2114 pub fn snapshot(&self) -> GraphSnapshot {
2115 let node_attrs: BTreeMap<String, AttrMap> = self
2119 .nodes
2120 .iter()
2121 .filter(|(_, attrs)| !attrs.is_empty())
2122 .map(|(name, attrs)| (name.clone(), attrs.clone()))
2123 .collect();
2124 GraphSnapshot {
2125 mode: self.mode,
2126 nodes: self.nodes.keys().cloned().collect(),
2127 node_attrs,
2128 edges: self.edges_ordered(),
2129 }
2130 }
2131
2132 fn record_decision(
2133 &mut self,
2134 operation: &'static str,
2135 incompatibility_probability: f64,
2136 unknown_incompatible_feature: bool,
2137 evidence: Vec<EvidenceTerm>,
2138 ) -> DecisionAction {
2139 let action = self
2140 .runtime_policy
2141 .action_for(incompatibility_probability, unknown_incompatible_feature);
2142 self.runtime_policy.record(
2143 operation,
2144 action,
2145 incompatibility_probability,
2146 "argmin expected loss over {allow,full_validate,fail_closed}",
2147 evidence,
2148 );
2149 action
2150 }
2151}
2152
2153#[derive(Debug, Clone)]
2154pub struct MultiGraph {
2155 mode: CompatibilityMode,
2156 revision: u64,
2157 nodes: IndexMap<String, AttrMap>,
2158 adjacency: IndexMap<String, IndexMap<String, IndexSet<usize>>>,
2159 edges: IndexMap<EdgeKey, IndexMap<usize, AttrMap>>,
2160 runtime_policy: RuntimePolicy,
2161 edge_count: usize,
2162}
2163
2164impl MultiGraph {
2165 #[must_use]
2168 pub fn clone_with_fresh_policy(&self) -> Self {
2169 Self {
2170 mode: self.mode,
2171 revision: self.revision,
2172 nodes: self.nodes.clone(),
2173 adjacency: self.adjacency.clone(),
2174 edges: self.edges.clone(),
2175 runtime_policy: RuntimePolicy::new(self.mode),
2176 edge_count: self.edge_count,
2177 }
2178 }
2179
2180 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
2187 let n = self.adjacency.len();
2195 let mut new_orders: Vec<Vec<String>> = Vec::with_capacity(n);
2196 for (pu, (u, row)) in self.adjacency.iter().enumerate() {
2197 let mut early: Vec<(usize, usize, String)> = Vec::new();
2198 let mut late: Vec<String> = Vec::new();
2199 for v in row.keys() {
2200 let pv = self
2201 .adjacency
2202 .get_index_of(v.as_str())
2203 .unwrap_or(usize::MAX);
2204 if pv < pu {
2205 let idx = self
2206 .adjacency
2207 .get(v.as_str())
2208 .and_then(|r| r.get_index_of(u.as_str()))
2209 .unwrap_or(usize::MAX);
2210 early.push((pv, idx, v.clone()));
2211 } else {
2212 late.push(v.clone());
2213 }
2214 }
2215 early.sort_unstable();
2216 let mut order: Vec<String> = early.into_iter().map(|(_, _, v)| v).collect();
2217 order.extend(late);
2218 new_orders.push(order);
2219 }
2220 let keys: Vec<String> = self.adjacency.keys().cloned().collect();
2221 let orders: Vec<(String, Vec<String>)> = keys.into_iter().zip(new_orders).collect();
2222 self.apply_row_orders(&orders);
2223 }
2224
2225 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
2229 for (node, order) in orders {
2230 let Some(row) = self.adjacency.get_mut(node.as_str()) else {
2231 continue;
2232 };
2233 let mut old = std::mem::take(row);
2234 let mut new_row = IndexMap::with_capacity(old.len());
2235 for v in order {
2236 if let Some((k, val)) = old.shift_remove_entry(v.as_str()) {
2237 new_row.insert(k, val);
2238 }
2239 }
2240 for (k, val) in old {
2241 new_row.insert(k, val);
2242 }
2243 *row = new_row;
2244 }
2245 }
2246
2247 #[must_use]
2248 pub fn new(mode: CompatibilityMode) -> Self {
2249 Self {
2250 mode,
2251 revision: 0,
2252 nodes: IndexMap::new(),
2253 adjacency: IndexMap::new(),
2254 edges: IndexMap::new(),
2255 runtime_policy: RuntimePolicy::new(mode),
2256 edge_count: 0,
2257 }
2258 }
2259
2260 #[must_use]
2261 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
2262 let mode = runtime_policy.mode();
2263 Self {
2264 mode,
2265 revision: 0,
2266 nodes: IndexMap::new(),
2267 adjacency: IndexMap::new(),
2268 edges: IndexMap::new(),
2269 runtime_policy,
2270 edge_count: 0,
2271 }
2272 }
2273
2274 #[must_use]
2275 pub fn strict() -> Self {
2276 Self::new(CompatibilityMode::Strict)
2277 }
2278
2279 #[must_use]
2280 pub fn hardened() -> Self {
2281 Self::new(CompatibilityMode::Hardened)
2282 }
2283
2284 #[must_use]
2285 pub fn mode(&self) -> CompatibilityMode {
2286 self.mode
2287 }
2288
2289 #[must_use]
2290 pub fn node_count(&self) -> usize {
2291 self.nodes.len()
2292 }
2293
2294 #[must_use]
2295 pub fn edge_count(&self) -> usize {
2296 self.edge_count
2297 }
2298
2299 #[must_use]
2300 pub fn number_of_selfloops(&self) -> usize {
2301 self.edges
2302 .iter()
2303 .filter(|(edge_key, _)| edge_key.left == edge_key.right)
2304 .map(|(_, edge_bucket)| edge_bucket.len())
2305 .sum()
2306 }
2307
2308 #[must_use]
2310 pub fn edge_keys(&self, left: &str, right: &str) -> Option<Vec<usize>> {
2311 self.adjacency
2312 .get(left)?
2313 .get(right)
2314 .map(|keys| keys.iter().copied().collect())
2315 }
2316
2317 pub fn edge_keys_iter(&self, left: &str, right: &str) -> Option<impl Iterator<Item = &usize>> {
2319 self.adjacency.get(left)?.get(right).map(|keys| keys.iter())
2320 }
2321
2322 #[must_use]
2323 pub fn revision(&self) -> u64 {
2324 self.revision
2325 }
2326
2327 #[must_use]
2328 pub fn has_node(&self, node: &str) -> bool {
2329 self.nodes.contains_key(node)
2330 }
2331
2332 #[must_use]
2333 pub fn has_edge(&self, left: &str, right: &str) -> bool {
2334 self.edges
2335 .get(&EdgeKeyRef::new(left, right))
2336 .is_some_and(|edge_bucket| !edge_bucket.is_empty())
2337 }
2338
2339 #[must_use]
2340 pub fn nodes_ordered(&self) -> Vec<&str> {
2341 self.nodes.keys().map(String::as_str).collect()
2342 }
2343
2344 #[must_use]
2345 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
2346 self.adjacency
2347 .get(node)
2348 .map(|neighbors| neighbors.keys().map(String::as_str).collect::<Vec<&str>>())
2349 }
2350
2351 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
2352 self.adjacency
2353 .get(node)
2354 .map(|neighbors| neighbors.keys().map(String::as_str))
2355 }
2356
2357 #[must_use]
2358 pub fn edge_keys_vec(&self, left: &str, right: &str) -> Vec<usize> {
2359 self.edges
2360 .get(&EdgeKeyRef::new(left, right))
2361 .map(|edge_bucket| edge_bucket.keys().copied().collect::<Vec<usize>>())
2362 .unwrap_or_default()
2363 }
2364
2365 #[must_use]
2366 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
2367 self.nodes.get(node)
2368 }
2369
2370 #[must_use]
2371 pub fn edge_attrs(&self, left: &str, right: &str, key: usize) -> Option<&AttrMap> {
2372 self.edges
2373 .get(&EdgeKeyRef::new(left, right))
2374 .and_then(|edge_bucket| edge_bucket.get(&key))
2375 }
2376
2377 #[must_use]
2378 pub fn evidence_ledger(&self) -> &EvidenceLedger {
2379 self.runtime_policy.decision_log()
2380 }
2381
2382 #[must_use]
2383 pub fn runtime_policy(&self) -> &RuntimePolicy {
2384 &self.runtime_policy
2385 }
2386
2387 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
2388 self.mode = runtime_policy.mode();
2389 self.runtime_policy = runtime_policy;
2390 }
2391
2392 #[must_use]
2393 pub fn is_directed(&self) -> bool {
2394 false
2395 }
2396
2397 #[must_use]
2398 pub fn is_multigraph(&self) -> bool {
2399 true
2400 }
2401
2402 #[must_use]
2405 pub fn degree(&self, node: &str) -> usize {
2406 self.adjacency.get(node).map_or(0, |neighbors| {
2407 let mut deg = 0;
2408 for (neighbor, keys) in neighbors {
2409 let count = keys.len();
2410 if neighbor == node {
2411 deg += count * 2;
2413 } else {
2414 deg += count;
2415 }
2416 }
2417 deg
2418 })
2419 }
2420
2421 pub fn add_node(&mut self, node: impl Into<String>) -> bool {
2422 self.add_node_with_attrs(node, AttrMap::new())
2423 }
2424
2425 pub fn add_node_with_attrs(&mut self, node: impl Into<String>, attrs: AttrMap) -> bool {
2426 let node = node.into();
2427 let existed = self.nodes.contains_key(&node);
2428 let mut changed = !existed;
2429 let attrs_count = {
2430 let bucket = self.nodes.entry(node.clone()).or_default();
2431 if !attrs.is_empty()
2432 && attrs
2433 .iter()
2434 .any(|(key, value)| bucket.get(key) != Some(value))
2435 {
2436 changed = true;
2437 }
2438 bucket.extend(attrs);
2439 bucket.len()
2440 };
2441 self.adjacency.entry(node.clone()).or_default();
2442 if changed {
2443 self.revision = self.revision.saturating_add(1);
2444 }
2445 self.record_decision(
2446 "add_node",
2447 0.0,
2448 false,
2449 vec![
2450 EvidenceTerm {
2451 signal: "node_preexisting".to_owned(),
2452 observed_value: existed.to_string(),
2453 log_likelihood_ratio: -3.0,
2454 },
2455 EvidenceTerm {
2456 signal: "attrs_count".to_owned(),
2457 observed_value: attrs_count.to_string(),
2458 log_likelihood_ratio: -1.0,
2459 },
2460 ],
2461 );
2462 changed
2463 }
2464
2465 pub fn add_edge(
2466 &mut self,
2467 left: impl Into<String>,
2468 right: impl Into<String>,
2469 ) -> Result<usize, GraphError> {
2470 self.add_edge_impl(left, right, None, AttrMap::new())
2471 }
2472
2473 pub fn add_edge_with_attrs(
2474 &mut self,
2475 left: impl Into<String>,
2476 right: impl Into<String>,
2477 attrs: AttrMap,
2478 ) -> Result<usize, GraphError> {
2479 self.add_edge_impl(left, right, None, attrs)
2480 }
2481
2482 pub fn add_edge_with_key_and_attrs(
2483 &mut self,
2484 left: impl Into<String>,
2485 right: impl Into<String>,
2486 key: usize,
2487 attrs: AttrMap,
2488 ) -> Result<usize, GraphError> {
2489 self.add_edge_impl(left, right, Some(key), attrs)
2490 }
2491
2492 #[must_use]
2495 pub fn add_fresh_edge_unrecorded(
2496 &mut self,
2497 left: impl Into<String>,
2498 right: impl Into<String>,
2499 ) -> Option<usize> {
2500 self.add_fresh_edge_with_key_unrecorded(left, right, 0)
2501 }
2502
2503 #[must_use]
2507 pub fn add_fresh_edge_with_key_unrecorded(
2508 &mut self,
2509 left: impl Into<String>,
2510 right: impl Into<String>,
2511 key: usize,
2512 ) -> Option<usize> {
2513 let left = left.into();
2514 let right = right.into();
2515 let edge_key = EdgeKey::new(&left, &right);
2516 if !self.nodes.contains_key(&left) {
2517 self.nodes.insert(left.clone(), AttrMap::new());
2518 self.adjacency.entry(left.clone()).or_default();
2519 }
2520 if left != right && !self.nodes.contains_key(&right) {
2521 self.nodes.insert(right.clone(), AttrMap::new());
2522 self.adjacency.entry(right.clone()).or_default();
2523 }
2524
2525 match self.edges.entry(edge_key) {
2526 indexmap::map::Entry::Occupied(mut edge_bucket) => {
2527 if !edge_bucket.get().is_empty() {
2528 return None;
2529 }
2530 edge_bucket.get_mut().insert(key, AttrMap::new());
2531 }
2532 indexmap::map::Entry::Vacant(edge_bucket) => {
2533 let mut bucket = IndexMap::new();
2534 bucket.insert(key, AttrMap::new());
2535 edge_bucket.insert(bucket);
2536 }
2537 }
2538 self.edge_count += 1;
2539 self.adjacency
2540 .entry(left.clone())
2541 .or_default()
2542 .entry(right.clone())
2543 .or_default()
2544 .insert(key);
2545 if left != right {
2546 self.adjacency
2547 .entry(right)
2548 .or_default()
2549 .entry(left)
2550 .or_default()
2551 .insert(key);
2552 }
2553 self.revision = self.revision.saturating_add(1);
2554 Some(key)
2555 }
2556
2557 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
2561 where
2562 I: IntoIterator<Item = (String, AttrMap)>,
2563 {
2564 let mut inserted = 0usize;
2565 for (node, attrs) in nodes {
2566 if let Some(existing) = self.nodes.get_mut(&node) {
2567 existing.extend(attrs);
2568 continue;
2569 }
2570 self.nodes.insert(node.clone(), attrs);
2571 inserted += 1;
2572 }
2573 if inserted > 0 {
2574 self.revision = self
2575 .revision
2576 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2577 self.record_decision(
2578 "extend_nodes_with_attrs_unrecorded",
2579 0.0,
2580 false,
2581 vec![EvidenceTerm {
2582 signal: "batch_node_count".to_owned(),
2583 observed_value: inserted.to_string(),
2584 log_likelihood_ratio: -1.0,
2585 }],
2586 );
2587 }
2588 inserted
2589 }
2590
2591 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
2598 where
2599 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
2600 {
2601 let mut inserted = 0usize;
2602 for (left, right, key, attrs) in edges {
2603 if !self.nodes.contains_key(&left) {
2604 self.nodes.insert(left.clone(), AttrMap::new());
2605 self.adjacency.entry(left.clone()).or_default();
2606 }
2607 if left != right && !self.nodes.contains_key(&right) {
2608 self.nodes.insert(right.clone(), AttrMap::new());
2609 self.adjacency.entry(right.clone()).or_default();
2610 }
2611 let edge_key = EdgeKey::new(&left, &right);
2612 let bucket = self.edges.entry(edge_key).or_default();
2613 if !bucket.contains_key(&key) {
2614 self.edge_count += 1;
2615 inserted += 1;
2616 }
2617 bucket.entry(key).or_default().extend(attrs);
2618 self.adjacency
2619 .entry(left.clone())
2620 .or_default()
2621 .entry(right.clone())
2622 .or_default()
2623 .insert(key);
2624 if left != right {
2625 self.adjacency
2626 .entry(right)
2627 .or_default()
2628 .entry(left)
2629 .or_default()
2630 .insert(key);
2631 }
2632 }
2633 if inserted > 0 {
2634 self.revision = self
2635 .revision
2636 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2637 self.record_decision(
2638 "extend_keyed_edges_with_attrs_unrecorded",
2639 0.0,
2640 false,
2641 vec![EvidenceTerm {
2642 signal: "batch_edge_count".to_owned(),
2643 observed_value: inserted.to_string(),
2644 log_likelihood_ratio: -1.0,
2645 }],
2646 );
2647 }
2648 inserted
2649 }
2650
2651 #[must_use]
2658 pub fn extend_fresh_int_prefix_keyed_edges_unrecorded<I>(
2659 &mut self,
2660 node_count: usize,
2661 edges: I,
2662 ) -> usize
2663 where
2664 I: IntoIterator<Item = (usize, usize, usize)>,
2665 {
2666 debug_assert_eq!(self.node_count(), 0);
2667 debug_assert_eq!(self.edge_count(), 0);
2668
2669 let iterator = edges.into_iter();
2670 let (lower_bound, _) = iterator.size_hint();
2671 self.nodes.reserve(node_count);
2672 self.adjacency.reserve(node_count);
2673 self.edges.reserve(lower_bound);
2674
2675 let node_names = (0..node_count)
2676 .map(|node| node.to_string())
2677 .collect::<Vec<_>>();
2678 for node in &node_names {
2679 self.nodes.insert(node.clone(), AttrMap::new());
2680 self.adjacency.insert(node.clone(), IndexMap::new());
2681 }
2682
2683 let mut inserted = 0usize;
2684 for (left_idx, right_idx, key) in iterator {
2685 debug_assert!(left_idx < node_names.len());
2686 debug_assert!(right_idx < node_names.len());
2687
2688 let left = &node_names[left_idx];
2689 let right = &node_names[right_idx];
2690 let edge_key = EdgeKey::new(left, right);
2691 self.edges
2692 .entry(edge_key)
2693 .or_default()
2694 .insert(key, AttrMap::new());
2695 self.adjacency
2696 .get_mut(left.as_str())
2697 .expect("integer-prefix left node should exist")
2698 .entry(right.clone())
2699 .or_default()
2700 .insert(key);
2701 if left_idx != right_idx {
2702 self.adjacency
2703 .get_mut(right.as_str())
2704 .expect("integer-prefix right node should exist")
2705 .entry(left.clone())
2706 .or_default()
2707 .insert(key);
2708 }
2709 self.edge_count += 1;
2710 inserted += 1;
2711 }
2712 if inserted > 0 || node_count > 0 {
2713 self.revision = self.revision.saturating_add(
2714 u64::try_from(inserted.saturating_add(node_count)).unwrap_or(u64::MAX),
2715 );
2716 self.record_decision(
2717 "extend_fresh_int_prefix_keyed_edges_unrecorded",
2718 0.0,
2719 false,
2720 vec![EvidenceTerm {
2721 signal: "batch_edge_count".to_owned(),
2722 observed_value: inserted.to_string(),
2723 log_likelihood_ratio: -1.0,
2724 }],
2725 );
2726 }
2727 inserted
2728 }
2729
2730 #[must_use]
2736 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
2737 &mut self,
2738 nodes: N,
2739 edges: I,
2740 ) -> usize
2741 where
2742 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
2743 N: IntoIterator<Item = String>,
2744 {
2745 if !self.nodes.is_empty() || !self.adjacency.is_empty() || !self.edges.is_empty() {
2746 return 0;
2747 }
2748
2749 let node_labels: Vec<String> = nodes.into_iter().collect();
2750 for node in &node_labels {
2751 self.nodes.insert(node.clone(), AttrMap::new());
2752 self.adjacency.insert(node.clone(), IndexMap::new());
2753 }
2754
2755 let node_count = node_labels.len();
2756 let mut inserted = 0usize;
2757 let mut merged_changed = false;
2758 for (left_idx, right_idx, key, attrs) in edges {
2759 let Some(left) = node_labels.get(left_idx) else {
2760 continue;
2761 };
2762 let Some(right) = node_labels.get(right_idx) else {
2763 continue;
2764 };
2765
2766 let edge_key = EdgeKey::new(left, right);
2767 let bucket = self.edges.entry(edge_key).or_default();
2768 if !bucket.contains_key(&key) {
2769 self.edge_count += 1;
2770 inserted += 1;
2771 }
2772 let edge_attrs = bucket.entry(key).or_default();
2773 if !attrs.is_empty()
2774 && attrs
2775 .iter()
2776 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
2777 {
2778 merged_changed = true;
2779 }
2780 edge_attrs.extend(attrs);
2781
2782 self.adjacency
2783 .get_mut(left.as_str())
2784 .expect("fresh left node row exists")
2785 .entry(right.clone())
2786 .or_default()
2787 .insert(key);
2788 if left_idx != right_idx {
2789 self.adjacency
2790 .get_mut(right.as_str())
2791 .expect("fresh right node row exists")
2792 .entry(left.clone())
2793 .or_default()
2794 .insert(key);
2795 }
2796 }
2797
2798 if node_count > 0 || inserted > 0 || merged_changed {
2799 self.revision = self.revision.saturating_add(
2800 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
2801 );
2802 self.record_decision(
2803 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
2804 0.0,
2805 false,
2806 vec![
2807 EvidenceTerm {
2808 signal: "batch_node_count".to_owned(),
2809 observed_value: node_count.to_string(),
2810 log_likelihood_ratio: -1.0,
2811 },
2812 EvidenceTerm {
2813 signal: "batch_edge_count".to_owned(),
2814 observed_value: inserted.to_string(),
2815 log_likelihood_ratio: -1.0,
2816 },
2817 ],
2818 );
2819 }
2820
2821 inserted
2822 }
2823
2824 fn add_edge_impl(
2825 &mut self,
2826 left: impl Into<String>,
2827 right: impl Into<String>,
2828 explicit_key: Option<usize>,
2829 attrs: AttrMap,
2830 ) -> Result<usize, GraphError> {
2831 let left = left.into();
2832 let right = right.into();
2833
2834 let unknown_feature = attrs
2835 .keys()
2836 .any(|key| key.starts_with("__fnx_incompatible"));
2837 let incompatibility_probability = if unknown_feature {
2838 1.0
2839 } else if left == right {
2840 0.22
2841 } else {
2842 0.08
2843 };
2844
2845 let action = self.record_decision(
2846 "add_edge",
2847 incompatibility_probability,
2848 unknown_feature,
2849 vec![EvidenceTerm {
2850 signal: "unknown_incompatible_feature".to_owned(),
2851 observed_value: unknown_feature.to_string(),
2852 log_likelihood_ratio: 12.0,
2853 }],
2854 );
2855
2856 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
2857 return Err(GraphError::FailClosed {
2858 operation: "add_edge",
2859 reason: "incompatible edge metadata".to_owned(),
2860 });
2861 }
2862
2863 let mut left_autocreated = false;
2864 if !self.nodes.contains_key(&left) {
2865 let _ = self.add_node(left.clone());
2866 left_autocreated = true;
2867 }
2868 let mut right_autocreated = false;
2869 if left == right {
2870 right_autocreated = left_autocreated;
2871 } else if !self.nodes.contains_key(&right) {
2872 let _ = self.add_node(right.clone());
2873 right_autocreated = true;
2874 }
2875
2876 let edge_key = EdgeKey::new(&left, &right);
2877 let key = explicit_key.unwrap_or_else(|| {
2878 let edge_bucket = self.edges.get(&edge_key);
2879 let mut k = edge_bucket.map_or(0, |b| b.len());
2880 if let Some(b) = edge_bucket {
2881 while b.contains_key(&k) {
2882 k += 1;
2883 }
2884 }
2885 k
2886 });
2887 let mut changed;
2888 let edge_attr_count = {
2889 let edge_bucket = self.edges.entry(edge_key.clone()).or_default();
2890 let is_new = !edge_bucket.contains_key(&key);
2891 if is_new {
2892 self.edge_count += 1;
2893 }
2894 changed = is_new;
2895 let edge_attrs = edge_bucket.entry(key).or_default();
2896 if !attrs.is_empty()
2897 && attrs
2898 .iter()
2899 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
2900 {
2901 changed = true;
2902 }
2903 edge_attrs.extend(attrs);
2904 edge_bucket.len()
2905 };
2906
2907 self.adjacency
2908 .entry(left.clone())
2909 .or_default()
2910 .entry(right.clone())
2911 .or_default()
2912 .insert(key);
2913 if left != right {
2914 self.adjacency
2915 .entry(right.clone())
2916 .or_default()
2917 .entry(left.clone())
2918 .or_default()
2919 .insert(key);
2920 }
2921 if changed {
2922 self.revision = self.revision.saturating_add(1);
2923 }
2924
2925 self.record_decision(
2926 "add_edge",
2927 incompatibility_probability,
2928 unknown_feature,
2929 vec![
2930 EvidenceTerm {
2931 signal: "edge_key".to_owned(),
2932 observed_value: key.to_string(),
2933 log_likelihood_ratio: -2.0,
2934 },
2935 EvidenceTerm {
2936 signal: "edge_attr_count".to_owned(),
2937 observed_value: edge_attr_count.to_string(),
2938 log_likelihood_ratio: -2.0,
2939 },
2940 EvidenceTerm {
2941 signal: "left_autocreated".to_owned(),
2942 observed_value: left_autocreated.to_string(),
2943 log_likelihood_ratio: -1.25,
2944 },
2945 EvidenceTerm {
2946 signal: "right_autocreated".to_owned(),
2947 observed_value: right_autocreated.to_string(),
2948 log_likelihood_ratio: -1.25,
2949 },
2950 ],
2951 );
2952
2953 Ok(key)
2954 }
2955
2956 pub fn replace_edge_attrs(
2959 &mut self,
2960 left: &str,
2961 right: &str,
2962 key: usize,
2963 attrs: AttrMap,
2964 ) -> bool {
2965 let edge_key = EdgeKey::new(left, right);
2966 if let Some(bucket) = self.edges.get_mut(&edge_key)
2967 && let Some(slot) = bucket.get_mut(&key)
2968 {
2969 if *slot != attrs {
2970 *slot = attrs;
2971 self.revision = self.revision.saturating_add(1);
2972 }
2973 return true;
2974 }
2975 false
2976 }
2977
2978 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
2980 if let Some(slot) = self.nodes.get_mut(node) {
2981 if *slot != attrs {
2982 *slot = attrs;
2983 self.revision = self.revision.saturating_add(1);
2984 }
2985 true
2986 } else {
2987 false
2988 }
2989 }
2990
2991 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
2992 let edge_key = EdgeKeyRef::new(left, right);
2993 let removal_key = key.or_else(|| {
2994 self.edges
2995 .get(&edge_key)
2996 .and_then(|edge_bucket| edge_bucket.keys().next_back().copied())
2997 });
2998
2999 let Some(removal_key) = removal_key else {
3000 return false;
3001 };
3002
3003 let removed = if let Some(edge_bucket) = self.edges.get_mut(&edge_key) {
3004 let was_removed = edge_bucket.shift_remove(&removal_key).is_some();
3005 if was_removed {
3006 self.edge_count -= 1;
3007 if edge_bucket.is_empty() {
3008 self.edges.shift_remove(&edge_key);
3009 }
3010 }
3011 was_removed
3012 } else {
3013 false
3014 };
3015
3016 if !removed {
3017 return false;
3018 }
3019 self.remove_adjacency_key(left, right, removal_key);
3020 if left != right {
3021 self.remove_adjacency_key(right, left, removal_key);
3022 }
3023 self.revision = self.revision.saturating_add(1);
3024 true
3025 }
3026
3027 pub fn remove_node(&mut self, node: &str) -> bool {
3028 if !self.nodes.contains_key(node) {
3029 return false;
3030 }
3031
3032 let mut removed_count = 0usize;
3040 if let Some(neighbors) = self.adjacency.get(node) {
3041 let neighbor_names: Vec<String> = neighbors.keys().cloned().collect();
3042 for neighbor in neighbor_names {
3043 if neighbor != node
3044 && let Some(remote_neighbors) = self.adjacency.get_mut(&neighbor)
3045 {
3046 remote_neighbors.shift_remove(node);
3047 }
3048 if let Some(bucket) = self.edges.swap_remove(&EdgeKeyRef::new(node, &neighbor)) {
3049 removed_count += bucket.len();
3050 }
3051 }
3052 }
3053 self.edge_count -= removed_count;
3054
3055 self.adjacency.shift_remove(node);
3057 self.nodes.shift_remove(node);
3058 self.revision = self.revision.saturating_add(1);
3059 true
3060 }
3061
3062 #[must_use]
3063 pub fn edges_ordered(&self) -> Vec<MultiEdgeSnapshot> {
3064 let mut ordered = Vec::with_capacity(self.edge_count());
3065 let mut seen = HashSet::<(String, String, usize)>::with_capacity(self.edge_count());
3066
3067 for node in self.nodes.keys() {
3068 if let Some(neighbors) = self.adjacency.get(node) {
3069 for neighbor in neighbors.keys() {
3070 let pair = EdgeKeyRef::new(node, neighbor);
3071 if let Some(edge_bucket) = self.edges.get(&pair) {
3072 for (key, attrs) in edge_bucket {
3073 let canonical_instance =
3075 (pair.left.to_owned(), pair.right.to_owned(), *key);
3076 if !seen.insert(canonical_instance) {
3077 continue;
3078 }
3079 ordered.push(MultiEdgeSnapshot {
3080 left: pair.left.to_owned(),
3081 right: pair.right.to_owned(),
3082 key: *key,
3083 attrs: attrs.clone(),
3084 });
3085 }
3086 }
3087 }
3088 }
3089 }
3090
3091 ordered
3092 }
3093
3094 #[must_use]
3095 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
3096 let mut ordered = Vec::with_capacity(self.edge_count());
3097 let mut seen = HashSet::<(EdgeKeyRef, usize)>::with_capacity(self.edge_count());
3098
3099 for node in self.nodes.keys() {
3100 if let Some(neighbors) = self.adjacency.get(node) {
3101 for neighbor in neighbors.keys() {
3102 let pair = EdgeKeyRef::new(node, neighbor);
3103 if let Some(edge_bucket) = self.edges.get(&pair) {
3104 for (key, attrs) in edge_bucket {
3105 if !seen.insert((pair, *key)) {
3106 continue;
3107 }
3108 ordered.push((pair.left, pair.right, *key, attrs));
3109 }
3110 }
3111 }
3112 }
3113 }
3114
3115 ordered
3116 }
3117
3118 #[must_use]
3119 pub fn snapshot(&self) -> MultiGraphSnapshot {
3120 let node_attrs: BTreeMap<String, AttrMap> = self
3122 .nodes
3123 .iter()
3124 .filter(|(_, attrs)| !attrs.is_empty())
3125 .map(|(name, attrs)| (name.clone(), attrs.clone()))
3126 .collect();
3127 MultiGraphSnapshot {
3128 mode: self.mode,
3129 nodes: self.nodes.keys().cloned().collect(),
3130 node_attrs,
3131 edges: self.edges_ordered(),
3132 }
3133 }
3134
3135 fn remove_adjacency_key(&mut self, source: &str, target: &str, key: usize) {
3136 let mut drop_neighbor = false;
3137 if let Some(neighbors) = self.adjacency.get_mut(source)
3138 && let Some(keys) = neighbors.get_mut(target)
3139 {
3140 keys.shift_remove(&key);
3141 drop_neighbor = keys.is_empty();
3142 }
3143 if drop_neighbor && let Some(neighbors) = self.adjacency.get_mut(source) {
3144 neighbors.shift_remove(target);
3145 }
3146 }
3147
3148 fn record_decision(
3149 &mut self,
3150 operation: &'static str,
3151 incompatibility_probability: f64,
3152 unknown_incompatible_feature: bool,
3153 evidence: Vec<EvidenceTerm>,
3154 ) -> DecisionAction {
3155 let action = self
3156 .runtime_policy
3157 .action_for(incompatibility_probability, unknown_incompatible_feature);
3158 self.runtime_policy.record(
3159 operation,
3160 action,
3161 incompatibility_probability,
3162 "argmin expected loss over {allow,full_validate,fail_closed}",
3163 evidence,
3164 );
3165 action
3166 }
3167}
3168
3169#[cfg(test)]
3170mod tests {
3171 use super::{AttrMap, Graph, GraphError, MultiGraph};
3172 use fnx_runtime::{CgseValue, CompatibilityMode, DecisionAction, DecisionRecord};
3173 use proptest::prelude::*;
3174 use std::collections::BTreeSet;
3175
3176 fn node_name(id: u8) -> String {
3177 format!("n{}", id % 8)
3178 }
3179
3180 fn canonical_edge(left: &str, right: &str) -> (String, String) {
3181 if left <= right {
3182 (left.to_owned(), right.to_owned())
3183 } else {
3184 (right.to_owned(), left.to_owned())
3185 }
3186 }
3187
3188 fn single_attr(key: &str, value: &str) -> AttrMap {
3189 let mut attrs = AttrMap::new();
3190 attrs.insert(key.to_owned(), CgseValue::from(value));
3191 attrs
3192 }
3193
3194 #[test]
3195 fn edges_ordered_tracks_node_and_neighbor_iteration_order() {
3196 let mut graph = Graph::strict();
3197 graph
3198 .add_edge_with_attrs("a", "b", AttrMap::new())
3199 .expect("edge add should succeed");
3200 graph
3201 .add_edge_with_attrs("b", "c", AttrMap::new())
3202 .expect("edge add should succeed");
3203 graph
3204 .add_edge_with_attrs("a", "c", AttrMap::new())
3205 .expect("edge add should succeed");
3206
3207 let pairs = graph
3208 .edges_ordered()
3209 .into_iter()
3210 .map(|edge| (edge.left, edge.right))
3211 .collect::<Vec<(String, String)>>();
3212 assert_eq!(
3213 pairs,
3214 vec![
3215 ("a".to_owned(), "b".to_owned()),
3216 ("a".to_owned(), "c".to_owned()),
3217 ("b".to_owned(), "c".to_owned()),
3218 ]
3219 );
3220 }
3221
3222 #[test]
3223 fn edges_ordered_preserves_traversed_endpoint_orientation() {
3224 let mut graph = Graph::strict();
3225 graph
3226 .add_edge_with_attrs("2", "1", AttrMap::new())
3227 .expect("edge add should succeed");
3228
3229 let pairs = graph
3230 .edges_ordered()
3231 .into_iter()
3232 .map(|edge| (edge.left, edge.right))
3233 .collect::<Vec<(String, String)>>();
3234 assert_eq!(pairs, vec![("2".to_owned(), "1".to_owned())]);
3235
3236 let borrowed_pairs = graph
3237 .edges_ordered_borrowed()
3238 .into_iter()
3239 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
3240 .collect::<Vec<(String, String)>>();
3241 assert_eq!(borrowed_pairs, vec![("2".to_owned(), "1".to_owned())]);
3242 }
3243
3244 #[test]
3245 fn edges_ordered_uses_node_order_for_preexisting_nodes() {
3246 let mut graph = Graph::strict();
3247 graph.add_node("1".to_owned());
3248 graph.add_node("2".to_owned());
3249 graph
3250 .add_edge_with_attrs("2", "1", AttrMap::new())
3251 .expect("edge add should succeed");
3252
3253 let pairs = graph
3254 .edges_ordered()
3255 .into_iter()
3256 .map(|edge| (edge.left, edge.right))
3257 .collect::<Vec<(String, String)>>();
3258 assert_eq!(pairs, vec![("1".to_owned(), "2".to_owned())]);
3259 }
3260
3261 #[test]
3262 fn edge_storage_order_index_iter_tracks_mutations() {
3263 let mut graph = Graph::strict();
3264 graph.add_node("c");
3265 graph.add_node("a");
3266 graph.add_node("b");
3267 graph
3268 .add_edge_with_attrs("c", "a", single_attr("weight", "1"))
3269 .expect("edge add should succeed");
3270 graph
3271 .add_edge_with_attrs("b", "c", single_attr("weight", "2"))
3272 .expect("edge add should succeed");
3273 graph
3274 .add_edge_with_attrs("a", "c", single_attr("color", "red"))
3275 .expect("duplicate edge update should succeed");
3276
3277 let indexed_edges = |graph: &Graph| {
3278 graph
3279 .edges_storage_order_index_iter()
3280 .map(|(left, right, attrs)| {
3281 (
3282 graph
3283 .get_node_name(left)
3284 .expect("left endpoint index should resolve")
3285 .to_owned(),
3286 graph
3287 .get_node_name(right)
3288 .expect("right endpoint index should resolve")
3289 .to_owned(),
3290 attrs.get("weight").cloned(),
3291 )
3292 })
3293 .collect::<Vec<_>>()
3294 };
3295
3296 assert_eq!(
3297 indexed_edges(&graph),
3298 vec![
3299 (
3300 "a".to_owned(),
3301 "c".to_owned(),
3302 Some(CgseValue::String("1".to_owned())),
3303 ),
3304 (
3305 "b".to_owned(),
3306 "c".to_owned(),
3307 Some(CgseValue::String("2".to_owned())),
3308 ),
3309 ]
3310 );
3311
3312 assert!(graph.remove_edge("a", "c"));
3313 assert_eq!(
3314 indexed_edges(&graph),
3315 vec![(
3316 "b".to_owned(),
3317 "c".to_owned(),
3318 Some(CgseValue::String("2".to_owned())),
3319 )]
3320 );
3321
3322 assert!(graph.remove_node("b"));
3323 assert!(indexed_edges(&graph).is_empty());
3324 }
3325
3326 fn assert_graph_core_invariants(graph: &Graph) {
3327 let mut unique_edges = BTreeSet::new();
3328 for node in graph.nodes_ordered() {
3329 let neighbors = graph
3330 .neighbors(node)
3331 .expect("graph nodes should always have adjacency buckets");
3332 for neighbor in neighbors {
3333 assert!(graph.has_node(neighbor));
3334 assert!(graph.has_edge(node, neighbor));
3335 let reverse_neighbors = graph
3336 .neighbors(neighbor)
3337 .expect("neighbor should always have adjacency bucket");
3338 assert!(reverse_neighbors.contains(&node));
3339 unique_edges.insert(canonical_edge(node, neighbor));
3340 }
3341 }
3342 assert_eq!(graph.edge_count(), unique_edges.len());
3343 }
3344
3345 fn assert_multigraph_core_invariants(graph: &MultiGraph) {
3346 let mut edge_instances = BTreeSet::new();
3347 for node in graph.nodes_ordered() {
3348 let neighbors = graph
3349 .neighbors(node)
3350 .expect("multigraph nodes should always have adjacency buckets");
3351 for neighbor in neighbors {
3352 assert!(graph.has_node(neighbor));
3353 assert!(graph.has_edge(node, neighbor));
3354 let reverse_neighbors = graph
3355 .neighbors(neighbor)
3356 .expect("neighbor should always have adjacency bucket");
3357 assert!(reverse_neighbors.contains(&node));
3358 let keys = graph
3359 .edge_keys(node, neighbor)
3360 .expect("parallel edge bucket should exist");
3361 for key in keys {
3362 let canonical = canonical_edge(node, neighbor);
3363 edge_instances.insert((canonical.0, canonical.1, key));
3364 }
3365 }
3366 }
3367 assert_eq!(graph.edge_count(), edge_instances.len());
3368 }
3369
3370 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
3371 assert!(record.ts_unix_ms > 0);
3372 assert!(!record.operation.trim().is_empty());
3373 assert_eq!(record.mode, expected_mode);
3374 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
3375 assert!(!record.rationale.trim().is_empty());
3376 assert!(!record.evidence.is_empty());
3377 for term in &record.evidence {
3378 assert!(!term.signal.trim().is_empty());
3379 assert!(!term.observed_value.trim().is_empty());
3380 }
3381 }
3382
3383 #[test]
3384 fn add_edge_autocreates_nodes_and_preserves_order() {
3385 let mut graph = Graph::strict();
3386 graph
3387 .add_edge_with_attrs("a", "b", AttrMap::new())
3388 .expect("edge insert should succeed");
3389 graph
3390 .add_edge_with_attrs("a", "c", AttrMap::new())
3391 .expect("edge insert should succeed");
3392
3393 assert_eq!(graph.node_count(), 3);
3394 assert_eq!(graph.edge_count(), 2);
3395 assert_eq!(graph.nodes_ordered(), vec!["a", "b", "c"]);
3396 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
3397 }
3398
3399 #[test]
3400 fn extend_edges_unrecorded_preserves_adjacency_and_records_once() {
3401 let mut graph = Graph::strict();
3402 graph.add_node("a");
3403 graph.add_node("b");
3404 graph.add_node("c");
3405 let before = graph.evidence_ledger().records().len();
3406
3407 let inserted = graph.extend_edges_unrecorded([("a", "b"), ("a", "c"), ("b", "a")]);
3408
3409 assert_eq!(inserted, 2);
3410 assert_eq!(graph.edge_count(), 2);
3411 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
3412 let records = graph.evidence_ledger().records();
3413 assert_eq!(records.len(), before + 1);
3414 assert_eq!(
3415 records.last().map(|record| record.operation.as_str()),
3416 Some("extend_edges_unrecorded")
3417 );
3418 }
3419
3420 #[test]
3421 fn extend_existing_index_edges_unrecorded_matches_string_batch_on_int_prefix() {
3422 let nodes = (0..12).map(|node| node.to_string()).collect::<Vec<_>>();
3423 let edge_indices = vec![(10usize, 2usize), (2, 10), (3, 3), (1, 11), (11, 1)];
3424 let edge_strings = edge_indices
3425 .iter()
3426 .map(|(left, right)| (left.to_string(), right.to_string()))
3427 .collect::<Vec<_>>();
3428
3429 let mut by_string = Graph::strict();
3430 let mut by_index = Graph::strict();
3431 let _ = by_string.extend_nodes_unrecorded(nodes.clone());
3432 let _ = by_index.extend_nodes_unrecorded(nodes);
3433
3434 assert!(by_index.nodes_are_contiguous_int_prefix());
3435 assert_eq!(by_string.extend_edges_unrecorded(edge_strings), 3);
3436 assert_eq!(
3437 by_index.extend_existing_index_edges_unrecorded(edge_indices),
3438 3
3439 );
3440
3441 assert_eq!(by_index.snapshot(), by_string.snapshot());
3442 assert_graph_core_invariants(&by_index);
3443 }
3444
3445 #[test]
3446 fn extend_edges_with_attrs_unrecorded_matches_add_edge_with_attrs() {
3447 let mut attrs1 = AttrMap::new();
3452 attrs1.insert("w".to_owned(), CgseValue::Int(1));
3453 let mut attrs2 = AttrMap::new();
3454 attrs2.insert("w".to_owned(), CgseValue::Int(7));
3455 attrs2.insert("c".to_owned(), CgseValue::String("x".to_owned()));
3456
3457 let mut reference = Graph::strict();
3458 reference
3459 .add_edge_with_attrs("a", "b", attrs1.clone())
3460 .unwrap();
3461 reference
3462 .add_edge_with_attrs("b", "c", AttrMap::new())
3463 .unwrap();
3464 reference
3466 .add_edge_with_attrs("b", "a", attrs2.clone())
3467 .unwrap();
3468 reference
3470 .add_edge_with_attrs("d", "d", attrs1.clone())
3471 .unwrap();
3472
3473 let mut bulk = Graph::strict();
3474 let before = bulk.evidence_ledger().records().len();
3475 let inserted = bulk.extend_edges_with_attrs_unrecorded([
3476 ("a".to_owned(), "b".to_owned(), attrs1.clone()),
3477 ("b".to_owned(), "c".to_owned(), AttrMap::new()),
3478 ("b".to_owned(), "a".to_owned(), attrs2.clone()),
3479 ("d".to_owned(), "d".to_owned(), attrs1.clone()),
3480 ]);
3481
3482 assert_eq!(inserted, 3);
3483 assert_eq!(bulk.edge_count(), reference.edge_count());
3484 assert_eq!(bulk.nodes_ordered(), reference.nodes_ordered());
3485 for node in bulk.nodes_ordered() {
3486 assert_eq!(bulk.neighbors(node), reference.neighbors(node));
3487 }
3488 assert_eq!(bulk.edge_attrs("a", "b"), reference.edge_attrs("a", "b"));
3489 assert_eq!(bulk.edge_attrs("d", "d"), reference.edge_attrs("d", "d"));
3490 assert_eq!(bulk.evidence_ledger().records().len(), before + 1);
3492 }
3493
3494 #[test]
3495 fn multigraph_int_prefix_keyed_edges_match_string_batch() {
3496 let indexed_edges = vec![
3497 (0usize, 1usize, 0usize),
3498 (1, 2, 0),
3499 (2, 0, 0),
3500 (0, 1, 1),
3501 (2, 2, 0),
3502 ];
3503 let string_edges = indexed_edges
3504 .iter()
3505 .map(|(left, right, key)| (left.to_string(), right.to_string(), *key, AttrMap::new()))
3506 .collect::<Vec<_>>();
3507
3508 let mut by_string = MultiGraph::strict();
3509 let mut by_index = MultiGraph::strict();
3510
3511 assert_eq!(
3512 by_string.extend_keyed_edges_with_attrs_unrecorded(string_edges),
3513 indexed_edges.len()
3514 );
3515 assert_eq!(
3516 by_index.extend_fresh_int_prefix_keyed_edges_unrecorded(3, indexed_edges),
3517 5
3518 );
3519
3520 assert_eq!(by_index.snapshot(), by_string.snapshot());
3521 assert_multigraph_core_invariants(&by_index);
3522 }
3523
3524 #[test]
3525 fn extend_nodes_unrecorded_preserves_order_and_records_once() {
3526 let mut graph = Graph::strict();
3527 graph.add_node("1");
3528 let before = graph.evidence_ledger().records().len();
3529
3530 let inserted =
3531 graph.extend_nodes_unrecorded(["0".to_owned(), "1".to_owned(), "2".to_owned()]);
3532
3533 assert_eq!(inserted, 2);
3534 assert_eq!(graph.node_count(), 3);
3535 assert_eq!(graph.nodes_ordered(), vec!["1", "0", "2"]);
3536 assert_eq!(graph.neighbor_count("0"), 0);
3537 assert_eq!(graph.neighbor_count("2"), 0);
3538 let records = graph.evidence_ledger().records();
3539 assert_eq!(records.len(), before + 1);
3540 assert_eq!(
3541 records.last().map(|record| record.operation.as_str()),
3542 Some("extend_nodes_unrecorded")
3543 );
3544 }
3545
3546 #[test]
3547 fn neighbors_iter_preserves_deterministic_order() {
3548 let mut graph = Graph::strict();
3549 graph.add_edge("a", "b").expect("edge add should succeed");
3550 graph.add_edge("a", "c").expect("edge add should succeed");
3551 graph.add_edge("a", "d").expect("edge add should succeed");
3552
3553 let neighbors = graph
3554 .neighbors_iter("a")
3555 .expect("neighbors should exist")
3556 .collect::<Vec<&str>>();
3557 assert_eq!(neighbors, vec!["b", "c", "d"]);
3558 }
3559
3560 #[test]
3561 fn neighbor_count_matches_neighbors_len() {
3562 let mut graph = Graph::strict();
3563 graph.add_edge("a", "b").expect("edge add should succeed");
3564 graph.add_edge("a", "c").expect("edge add should succeed");
3565 assert_eq!(graph.neighbor_count("a"), 2);
3566 assert_eq!(graph.neighbor_count("missing"), 0);
3567 }
3568
3569 #[test]
3570 fn repeated_edge_updates_attrs_in_place() {
3571 let mut graph = Graph::strict();
3572 let mut first = AttrMap::new();
3573 first.insert("weight".to_owned(), "1".into());
3574 graph
3575 .add_edge_with_attrs("a", "b", first)
3576 .expect("edge insert should succeed");
3577
3578 let mut second = AttrMap::new();
3579 second.insert("color".to_owned(), "blue".into());
3580 graph
3581 .add_edge_with_attrs("b", "a", second)
3582 .expect("edge update should succeed");
3583
3584 let attrs = graph
3585 .edge_attrs("a", "b")
3586 .expect("edge attrs should be present");
3587 assert_eq!(
3588 attrs.get("weight"),
3589 Some(&CgseValue::String("1".to_owned()))
3590 );
3591 assert_eq!(
3592 attrs.get("color"),
3593 Some(&CgseValue::String("blue".to_owned()))
3594 );
3595 assert_eq!(graph.edge_count(), 1);
3596 }
3597
3598 #[test]
3599 fn complete_graph_bulk_constructor_preserves_ordered_contract() {
3600 let graph = Graph::complete_graph(CompatibilityMode::Strict, 5);
3601
3602 assert_eq!(graph.node_count(), 5);
3603 assert_eq!(graph.edge_count(), 10);
3604 assert_eq!(graph.nodes_ordered(), vec!["0", "1", "2", "3", "4"]);
3605 assert_eq!(
3606 graph.neighbors("0").expect("node 0 should exist"),
3607 vec!["1", "2", "3", "4"]
3608 );
3609 assert_eq!(
3610 graph.neighbors("4").expect("node 4 should exist"),
3611 vec!["0", "1", "2", "3"]
3612 );
3613 assert_eq!(
3614 graph
3615 .edges_ordered()
3616 .into_iter()
3617 .map(|edge| (edge.left, edge.right))
3618 .collect::<Vec<_>>(),
3619 vec![
3620 ("0".to_owned(), "1".to_owned()),
3621 ("0".to_owned(), "2".to_owned()),
3622 ("0".to_owned(), "3".to_owned()),
3623 ("0".to_owned(), "4".to_owned()),
3624 ("1".to_owned(), "2".to_owned()),
3625 ("1".to_owned(), "3".to_owned()),
3626 ("1".to_owned(), "4".to_owned()),
3627 ("2".to_owned(), "3".to_owned()),
3628 ("2".to_owned(), "4".to_owned()),
3629 ("3".to_owned(), "4".to_owned()),
3630 ]
3631 );
3632
3633 let operations = graph
3634 .evidence_ledger()
3635 .records()
3636 .iter()
3637 .map(|record| record.operation.as_str())
3638 .collect::<Vec<_>>();
3639 assert_eq!(operations, vec!["complete_graph_bulk"]);
3640 }
3641
3642 #[test]
3643 fn remove_node_removes_incident_edges() {
3644 let mut graph = Graph::strict();
3645 graph.add_edge("a", "b").expect("edge add should succeed");
3646 graph.add_edge("b", "c").expect("edge add should succeed");
3647 assert!(graph.remove_node("b"));
3648 assert_eq!(graph.node_count(), 2);
3649 assert_eq!(graph.edge_count(), 0);
3650 }
3651
3652 #[test]
3658 #[ignore]
3659 fn ab_bench_multigraph_remove_node() {
3660 use std::time::Instant;
3661 const N: usize = 1000;
3662 const M: usize = 8000;
3663 const ITERS: usize = 500;
3664 let build = || {
3665 let mut g = MultiGraph::new(CompatibilityMode::Strict);
3666 for i in 0..N {
3667 let _ = g.add_node(i.to_string());
3668 }
3669 let mut s: u64 = 0x9E3779B97F4A7C15;
3670 let mut next = || {
3671 s = s
3672 .wrapping_mul(6364136223846793005)
3673 .wrapping_add(1442695040888963407);
3674 (s >> 33) as usize % N
3675 };
3676 for _ in 0..M {
3677 let a = next();
3678 let b = next();
3679 let _ = g.add_edge(a.to_string(), b.to_string());
3680 }
3681 g
3682 };
3683
3684 let mut gnew = build();
3685 let victims: Vec<String> = gnew.nodes.keys().take(ITERS).cloned().collect();
3686 let t = Instant::now();
3687 for node in &victims {
3688 gnew.remove_node(node);
3689 }
3690 let new_t = t.elapsed();
3691
3692 let mut gold = build();
3694 let t = Instant::now();
3695 for node in &victims {
3696 if !gold.nodes.contains_key(node) {
3697 continue;
3698 }
3699 if let Some(neighbors) = gold.adjacency.get(node) {
3700 let names: Vec<String> = neighbors.keys().cloned().collect();
3701 for nb in names {
3702 if nb != *node
3703 && let Some(rn) = gold.adjacency.get_mut(&nb)
3704 {
3705 rn.shift_remove(node.as_str());
3706 }
3707 }
3708 }
3709 let mut rc = 0usize;
3710 let nd = node.clone();
3711 gold.edges.retain(|k, bucket| {
3712 let keep = k.left != nd && k.right != nd;
3713 if !keep {
3714 rc += bucket.len();
3715 }
3716 keep
3717 });
3718 gold.edge_count -= rc;
3719 gold.adjacency.shift_remove(node);
3720 gold.nodes.shift_remove(node);
3721 }
3722 let old_t = t.elapsed();
3723
3724 assert_eq!(gnew.node_count(), gold.node_count());
3725 assert_eq!(gnew.edge_count(), gold.edge_count());
3726 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
3727 eprintln!(
3728 "MultiGraph remove_node x{ITERS}: retain {old_t:?} -> swap_remove {new_t:?} = {:.2}x",
3729 old_t.as_secs_f64() / new_t.as_secs_f64()
3730 );
3731 }
3732
3733 #[test]
3734 fn remove_nodes_from_matches_repeated_removal_and_rebuilds_indices() {
3735 let edges = [
3736 ("0", "1"),
3737 ("1", "2"),
3738 ("2", "3"),
3739 ("3", "4"),
3740 ("4", "5"),
3741 ("0", "5"),
3742 ("1", "4"),
3743 ("2", "5"),
3744 ];
3745 let mut batch = Graph::strict();
3746 let mut repeated = Graph::strict();
3747 for (left, right) in edges {
3748 batch.add_edge(left, right).expect("batch edge add");
3749 repeated.add_edge(left, right).expect("repeated edge add");
3750 }
3751
3752 let victims = ["1", "3", "99", "1"];
3753 let removed = batch.remove_nodes_from(victims);
3754 for victim in victims {
3755 let _ = repeated.remove_node(victim);
3756 }
3757
3758 assert_eq!(removed, (2, 5));
3759 assert_eq!(batch.snapshot(), repeated.snapshot());
3760 let ordered_nodes = batch
3761 .nodes_ordered()
3762 .into_iter()
3763 .map(str::to_owned)
3764 .collect::<Vec<_>>();
3765 for (node_index, node) in ordered_nodes.iter().enumerate() {
3766 let names_from_strings = batch.neighbors(node).expect("node should exist");
3767 let names_from_indices = batch
3768 .neighbors_indices(node_index)
3769 .expect("index should exist")
3770 .iter()
3771 .map(|index| batch.get_node_name(*index).expect("index should resolve"))
3772 .collect::<Vec<_>>();
3773 assert_eq!(names_from_indices, names_from_strings);
3774 }
3775 let names_from_edge_indices = batch
3776 .edges_storage_order_index_iter()
3777 .map(|(left, right, _)| {
3778 (
3779 batch
3780 .get_node_name(left)
3781 .expect("left endpoint should resolve")
3782 .to_owned(),
3783 batch
3784 .get_node_name(right)
3785 .expect("right endpoint should resolve")
3786 .to_owned(),
3787 )
3788 })
3789 .collect::<Vec<_>>();
3790 let names_from_edge_storage = batch
3791 .edges_storage_order_iter()
3792 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
3793 .collect::<Vec<_>>();
3794 assert_eq!(names_from_edge_indices, names_from_edge_storage);
3795 }
3796
3797 #[test]
3798 fn strict_mode_fails_closed_for_unknown_incompatible_feature() {
3799 let mut graph = Graph::strict();
3800 let mut attrs = AttrMap::new();
3801 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
3802 let err = graph
3803 .add_edge_with_attrs("a", "b", attrs)
3804 .expect_err("strict mode should fail closed");
3805
3806 assert_eq!(
3807 err,
3808 GraphError::FailClosed {
3809 operation: "add_edge",
3810 reason: "incompatible edge metadata".to_owned(),
3811 }
3812 );
3813
3814 let last_record = graph
3815 .evidence_ledger()
3816 .records()
3817 .last()
3818 .expect("strict fail-closed path should emit a ledger row");
3819 assert_decision_record_schema(last_record, CompatibilityMode::Strict);
3820 assert_eq!(last_record.operation, "add_edge");
3821 assert_eq!(last_record.action, DecisionAction::FailClosed);
3822 assert!(
3823 last_record
3824 .evidence
3825 .iter()
3826 .any(|term| term.signal == "unknown_incompatible_feature")
3827 );
3828 }
3829
3830 #[test]
3831 fn revision_increments_on_mutating_operations() {
3832 let mut graph = Graph::strict();
3833 let r0 = graph.revision();
3834 let _ = graph.add_node("a");
3835 let r1 = graph.revision();
3836 assert!(r1 > r0);
3837
3838 graph.add_edge("a", "b").expect("edge add should succeed");
3839 let r2 = graph.revision();
3840 assert!(r2 > r1);
3841
3842 let _ = graph.remove_edge("a", "b");
3843 let r3 = graph.revision();
3844 assert!(r3 > r2);
3845 }
3846
3847 #[test]
3848 fn add_node_with_attrs_reports_change_on_existing_node() {
3849 let mut graph = Graph::strict();
3850 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
3851 let r1 = graph.revision();
3852 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
3853 let r2 = graph.revision();
3854 assert!(r2 > r1);
3855 let expected = CgseValue::from("blue");
3856 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
3857 }
3858
3859 #[test]
3860 fn multigraph_add_node_with_attrs_reports_change_on_existing_node() {
3861 let mut graph = MultiGraph::strict();
3862 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
3863 let r1 = graph.revision();
3864 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
3865 let r2 = graph.revision();
3866 assert!(r2 > r1);
3867 let expected = CgseValue::from("blue");
3868 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
3869 }
3870
3871 #[test]
3872 fn hardened_self_loop_records_allow_decision() {
3873 let mut graph = Graph::hardened();
3874 graph
3875 .add_edge("loop", "loop")
3876 .expect("hardened self-loop edge should be accepted");
3877
3878 let add_edge_record = graph
3879 .evidence_ledger()
3880 .records()
3881 .iter()
3882 .rev()
3883 .find(|record| record.operation == "add_edge")
3884 .expect("add_edge operation should emit ledger row");
3885 assert_decision_record_schema(add_edge_record, CompatibilityMode::Hardened);
3886 assert_eq!(add_edge_record.action, DecisionAction::Allow);
3887 assert!(
3888 add_edge_record
3889 .evidence
3890 .iter()
3891 .any(|term| term.signal == "self_loop" && term.observed_value == "true")
3892 );
3893 }
3894
3895 #[test]
3896 fn runtime_policy_tracks_hardened_graph_recovery() {
3897 let mut graph = Graph::hardened();
3898 graph
3899 .add_edge("a", "b")
3900 .expect("hardened graph edge add should succeed");
3901
3902 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
3903 assert!(!graph.runtime_policy().decision_log().records().is_empty());
3904 assert!(graph.runtime_policy().posterior().observation_count >= 1);
3905 }
3906
3907 #[test]
3908 fn snapshot_roundtrip_replays_to_identical_state() {
3909 let mut graph = Graph::strict();
3910
3911 let mut first_attrs = AttrMap::new();
3912 first_attrs.insert("weight".to_owned(), "7".into());
3913 graph
3914 .add_edge_with_attrs("a", "b", first_attrs)
3915 .expect("edge insert should succeed");
3916
3917 let mut second_attrs = AttrMap::new();
3918 second_attrs.insert("color".to_owned(), "green".into());
3919 graph
3920 .add_edge_with_attrs("b", "c", second_attrs)
3921 .expect("edge insert should succeed");
3922
3923 let snapshot = graph.snapshot();
3924 let mut replayed = Graph::new(snapshot.mode);
3925 for node in &snapshot.nodes {
3926 let _ = replayed.add_node(node.clone());
3927 }
3928 for edge in &snapshot.edges {
3929 replayed
3930 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
3931 .expect("snapshot replay should be valid");
3932 }
3933
3934 assert_eq!(replayed.snapshot(), snapshot);
3935 assert_graph_core_invariants(&replayed);
3936 }
3937
3938 #[test]
3939 fn multigraph_tracks_parallel_edges_with_distinct_keys() {
3940 let mut graph = MultiGraph::strict();
3941 let first = graph.add_edge("a", "b").expect("edge add should succeed");
3942 let second = graph.add_edge("a", "b").expect("edge add should succeed");
3943
3944 assert_ne!(first, second);
3945 assert_eq!(graph.edge_count(), 2);
3946 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
3947 assert_multigraph_core_invariants(&graph);
3948 }
3949
3950 #[test]
3951 fn multigraph_counts_parallel_selfloops_only() {
3952 let mut graph = MultiGraph::strict();
3953 let _ = graph
3954 .add_edge("a", "a")
3955 .expect("self-loop add should succeed");
3956 let _ = graph
3957 .add_edge("a", "a")
3958 .expect("self-loop add should succeed");
3959 let _ = graph
3960 .add_edge("b", "b")
3961 .expect("self-loop add should succeed");
3962 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
3963 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
3964
3965 assert_eq!(graph.number_of_selfloops(), 3);
3966 assert_multigraph_core_invariants(&graph);
3967 }
3968
3969 #[test]
3970 fn multigraph_add_fresh_edge_unrecorded_rejects_occupied_bucket() {
3971 let mut graph = MultiGraph::strict();
3972 assert_eq!(graph.add_fresh_edge_unrecorded("a", "b"), Some(0));
3973 let revision = graph.revision();
3974 let edge_count = graph.edge_count();
3975
3976 assert_eq!(graph.add_fresh_edge_unrecorded("b", "a"), None);
3977
3978 assert_eq!(graph.revision(), revision);
3979 assert_eq!(graph.edge_count(), edge_count);
3980 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0]));
3981 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
3982 assert_multigraph_core_invariants(&graph);
3983 }
3984
3985 #[test]
3986 fn multigraph_add_fresh_edge_with_key_unrecorded_preserves_key() {
3987 let mut graph = MultiGraph::strict();
3988
3989 assert_eq!(
3990 graph.add_fresh_edge_with_key_unrecorded("a", "b", 42),
3991 Some(42)
3992 );
3993
3994 assert_eq!(graph.edge_count(), 1);
3995 assert_eq!(graph.edge_keys("a", "b"), Some(vec![42]));
3996 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
3997 assert_multigraph_core_invariants(&graph);
3998 }
3999
4000 #[test]
4001 fn multigraph_remove_edge_without_key_removes_latest_instance() {
4002 let mut graph = MultiGraph::strict();
4003 let first = graph.add_edge("a", "b").expect("edge add should succeed");
4004 let second = graph.add_edge("a", "b").expect("edge add should succeed");
4005
4006 assert!(graph.remove_edge("a", "b", None));
4007 assert_eq!(graph.edge_count(), 1);
4008 assert_eq!(graph.edge_keys("a", "b"), Some(vec![first]));
4009 assert_ne!(first, second);
4010 assert_multigraph_core_invariants(&graph);
4011 }
4012
4013 #[test]
4014 fn multigraph_remove_node_clears_parallel_incident_edges() {
4015 let mut graph = MultiGraph::strict();
4016 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
4017 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
4018 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
4019
4020 assert!(graph.remove_node("b"));
4021 assert_eq!(graph.edge_count(), 0);
4022 assert!(!graph.has_node("b"));
4023 assert_eq!(graph.neighbors("a"), Some(vec![]));
4024 assert_eq!(graph.neighbors("c"), Some(vec![]));
4025 assert_multigraph_core_invariants(&graph);
4026 }
4027
4028 #[test]
4029 fn runtime_policy_tracks_hardened_multigraph_recovery() {
4030 let mut graph = MultiGraph::hardened();
4031 graph
4032 .add_edge("a", "b")
4033 .expect("hardened multigraph edge add should succeed");
4034
4035 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
4036 assert!(!graph.runtime_policy().decision_log().records().is_empty());
4037 assert!(graph.runtime_policy().posterior().observation_count >= 1);
4038 }
4039
4040 #[test]
4041 fn multigraph_roundtrips_sparse_snapshot_keys() {
4042 let mut graph = MultiGraph::strict();
4043 assert_eq!(
4044 graph.add_edge("a", "b").expect("edge add should succeed"),
4045 0
4046 );
4047 assert_eq!(
4048 graph.add_edge("a", "b").expect("edge add should succeed"),
4049 1
4050 );
4051 assert_eq!(
4052 graph.add_edge("a", "b").expect("edge add should succeed"),
4053 2
4054 );
4055 assert!(graph.remove_edge("a", "b", Some(1)));
4056 assert_eq!(
4057 graph.add_edge("a", "b").expect("edge add should succeed"),
4058 3
4059 );
4060
4061 let snapshot = graph.snapshot();
4062 assert_eq!(
4063 snapshot
4064 .edges
4065 .iter()
4066 .map(|edge| edge.key)
4067 .collect::<Vec<_>>(),
4068 vec![0, 2, 3]
4069 );
4070
4071 let mut replayed = MultiGraph::new(snapshot.mode);
4072 for node in &snapshot.nodes {
4073 let _ = replayed.add_node(node.clone());
4074 }
4075 for edge in &snapshot.edges {
4076 replayed
4077 .add_edge_with_key_and_attrs(
4078 edge.left.clone(),
4079 edge.right.clone(),
4080 edge.key,
4081 edge.attrs.clone(),
4082 )
4083 .expect("snapshot replay should preserve explicit keys");
4084 }
4085
4086 assert_eq!(replayed.snapshot(), snapshot);
4087 assert_multigraph_core_invariants(&replayed);
4088 }
4089
4090 proptest! {
4091 #[test]
4092 fn prop_core_invariants_hold_for_mixed_edge_mutations(
4093 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
4094 ) {
4095 let mut graph = Graph::strict();
4096 let mut last_revision = graph.revision();
4097
4098 for (left_id, right_id, is_add) in ops {
4099 let left = node_name(left_id);
4100 let right = node_name(right_id);
4101 if is_add {
4102 prop_assert!(graph.add_edge(left.clone(), right.clone()).is_ok());
4103 } else {
4104 let _ = graph.remove_edge(&left, &right);
4105 }
4106 let revision = graph.revision();
4107 prop_assert!(revision >= last_revision);
4108 last_revision = revision;
4109 assert_graph_core_invariants(&graph);
4110 }
4111 }
4112
4113 #[test]
4114 fn prop_snapshot_is_deterministic_for_same_operation_stream(
4115 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
4116 ) {
4117 let mut graph_left = Graph::hardened();
4118 let mut graph_right = Graph::hardened();
4119
4120 for (left_id, right_id, attrs_variant) in ops {
4121 let left = node_name(left_id);
4122 let right = node_name(right_id);
4123 let mut attrs = AttrMap::new();
4124 if attrs_variant == 1 {
4125 attrs.insert("weight".to_owned(), (left_id % 5).to_string().into());
4126 } else if attrs_variant == 2 {
4127 attrs.insert("tag".to_owned(), format!("k{}", right_id % 4).into());
4128 }
4129 prop_assert!(
4130 graph_left
4131 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
4132 .is_ok()
4133 );
4134 prop_assert!(
4135 graph_right
4136 .add_edge_with_attrs(left, right, attrs)
4137 .is_ok()
4138 );
4139 }
4140
4141 prop_assert_eq!(graph_left.snapshot(), graph_right.snapshot());
4142 prop_assert_eq!(graph_left.snapshot(), graph_left.snapshot());
4143 }
4144
4145 #[test]
4146 fn prop_reapplying_identical_edge_attrs_is_revision_stable(
4147 left_id in 0_u8..8,
4148 right_id in 0_u8..8,
4149 weight in 0_u16..5000,
4150 ) {
4151 let mut graph = Graph::strict();
4152 let left = node_name(left_id);
4153 let right = node_name(right_id);
4154 let mut attrs = AttrMap::new();
4155 attrs.insert("weight".to_owned(), weight.to_string().into());
4156
4157 prop_assert!(
4158 graph
4159 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
4160 .is_ok()
4161 );
4162 let revision_after_first = graph.revision();
4163 prop_assert!(
4164 graph
4165 .add_edge_with_attrs(left, right, attrs)
4166 .is_ok()
4167 );
4168 prop_assert_eq!(graph.revision(), revision_after_first);
4169 }
4170
4171 #[test]
4172 fn prop_remove_node_clears_incident_edges(
4173 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
4174 target_id in 0_u8..8,
4175 ) {
4176 let mut graph = Graph::strict();
4177 for (left_id, right_id) in ops {
4178 let left = node_name(left_id);
4179 let right = node_name(right_id);
4180 prop_assert!(graph.add_edge(left, right).is_ok());
4181 }
4182
4183 let target = node_name(target_id);
4184 let removed = graph.remove_node(&target);
4185 if removed {
4186 prop_assert!(!graph.has_node(&target));
4187 for node in graph.nodes_ordered() {
4188 let neighbors = graph
4189 .neighbors(node)
4190 .expect("graph nodes should always have adjacency buckets");
4191 prop_assert!(!neighbors.contains(&target.as_str()));
4192 prop_assert!(!graph.has_edge(node, &target));
4193 }
4194 }
4195 assert_graph_core_invariants(&graph);
4196 }
4197
4198 #[test]
4199 fn prop_decision_ledger_records_follow_schema(
4200 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..4), 1..72),
4201 ) {
4202 let mut graph = Graph::strict();
4203 for (left_id, right_id, attrs_kind) in ops {
4204 let left = node_name(left_id);
4205 let right = node_name(right_id);
4206 let mut attrs = AttrMap::new();
4207 match attrs_kind {
4208 0 => {}
4209 1 => {
4210 attrs.insert("weight".to_owned(), (left_id % 9).to_string().into());
4211 }
4212 2 => {
4213 attrs.insert("color".to_owned(), format!("c{}", right_id % 6).into());
4214 }
4215 _ => {
4216 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
4217 }
4218 }
4219 let _ = graph.add_edge_with_attrs(left, right, attrs);
4220 }
4221
4222 let records = graph.evidence_ledger().records();
4223 prop_assert!(!records.is_empty());
4224 for record in records {
4225 assert_decision_record_schema(record, CompatibilityMode::Strict);
4226 if record.operation == "add_node" {
4227 prop_assert_eq!(record.action, DecisionAction::Allow);
4228 prop_assert!(record.evidence.iter().any(|term| term.signal == "node_preexisting"));
4229 prop_assert!(record.evidence.iter().any(|term| term.signal == "attrs_count"));
4230 } else {
4231 prop_assert_eq!(&record.operation, "add_edge");
4232 if record.action == DecisionAction::FailClosed {
4233 prop_assert!(
4234 record
4235 .evidence
4236 .iter()
4237 .any(|term| term.signal == "unknown_incompatible_feature")
4238 );
4239 } else {
4240 prop_assert_eq!(record.action, DecisionAction::Allow);
4241 let is_precheck = record
4245 .evidence
4246 .iter()
4247 .any(|term| term.signal == "unknown_incompatible_feature")
4248 && !record
4249 .evidence
4250 .iter()
4251 .any(|term| term.signal == "edge_attr_count");
4252 if !is_precheck {
4253 prop_assert!(record.evidence.iter().any(|term| term.signal == "edge_attr_count"));
4254 prop_assert!(record.evidence.iter().any(|term| term.signal == "self_loop"));
4255 }
4256 }
4257 }
4258 }
4259 }
4260 }
4261}