Skip to main content

kanban_domain/dependencies/
dependency_graph.rs

1use kanban_core::{
2    Cascadable, DagGraph, Directed, EdgeSet, GraphError, Undirected, UndirectedGraph,
3};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use super::edges::{BlocksEdge, RelatesEdge, SpawnsEdge};
8use crate::error::DependencyError;
9use crate::{CardId, KanbanResult};
10
11/// Top-level container for all entity dependency graphs.
12///
13/// Three discrete sub-graphs, each with its own structural rules
14/// and its own concrete edge kind (carrying any per-kind metadata):
15///
16/// - `spawns: DagGraph<SpawnsEdge>` (no extra metadata today)
17/// - `blocks: DagGraph<BlocksEdge>` (carries [`Severity`])
18/// - `relates: UndirectedGraph<RelatesEdge>` (carries [`RelatesKind`])
19///
20/// Cross-cutting operations (`archive_node`, `unarchive_node`,
21/// `remove_node`) cascade across all three. Per-edge-type convenience
22/// methods delegate to the matching sub-graph and convert
23/// [`GraphError`] into the domain-level [`DependencyError`].
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
25pub struct DependencyGraph {
26    #[serde(default)]
27    spawns: DagGraph<SpawnsEdge>,
28    #[serde(default)]
29    blocks: DagGraph<BlocksEdge>,
30    #[serde(default)]
31    relates: UndirectedGraph<RelatesEdge>,
32}
33
34impl DependencyGraph {
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Borrow each component graph as `&mut dyn Cascadable<NodeId = Uuid>`
40    /// for node-level cascade operations. Order is `spawns`, `blocks`,
41    /// `relates`. A new component sub-graph only needs to be added to
42    /// this helper, not to every cascade method.
43    ///
44    /// The explicit `NodeId = Uuid` binding picks the kanban-domain
45    /// identity; the underlying `Cascadable` trait is generic over
46    /// `NodeId` so a heterogeneous-entity graph can pick its own
47    /// without touching this method.
48    fn cascadable_parts_mut(&mut self) -> [&mut dyn Cascadable<NodeId = Uuid>; 3] {
49        [&mut self.spawns, &mut self.blocks, &mut self.relates]
50    }
51
52    /// Borrow each component graph as `&dyn EdgeSet<NodeId = Uuid>`
53    /// for read-only edge-level queries.
54    fn edge_sets(&self) -> [&dyn EdgeSet<NodeId = Uuid>; 3] {
55        [&self.spawns, &self.blocks, &self.relates]
56    }
57
58    // --- Cross-cutting node cascades (Cascadable surface) ---
59
60    pub fn archive_node(&mut self, card: CardId) {
61        for sg in self.cascadable_parts_mut() {
62            sg.archive_node(card);
63        }
64    }
65
66    pub fn unarchive_node(&mut self, card: CardId) {
67        for sg in self.cascadable_parts_mut() {
68            sg.unarchive_node(card);
69        }
70    }
71
72    pub fn remove_node(&mut self, card: CardId) {
73        for sg in self.cascadable_parts_mut() {
74            sg.remove_node(card);
75        }
76    }
77
78    // --- Cross-cutting edge aggregates (EdgeSet surface) ---
79
80    pub fn len(&self) -> usize {
81        self.edge_sets().iter().map(|g| g.len()).sum()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.len() == 0
86    }
87
88    pub fn active_len(&self) -> usize {
89        self.edge_sets().iter().map(|g| g.active_len()).sum()
90    }
91
92    // --- Parent / child (Spawns) ---
93
94    pub fn set_parent(&mut self, child: CardId, parent: CardId) -> KanbanResult<()> {
95        self.spawns
96            .add_edge_with_metadata(SpawnsEdge::new(parent, child))
97            .map_err(dep_err)
98    }
99
100    /// Insert a parent->child Spawns edge already in the archived
101    /// state. Used by cascade-undo (`DeleteCard` / `DeleteCardEdges`)
102    /// to restore archived incident edges as archived, not active.
103    /// `DagGraph::add_edge_with_metadata` skips the duplicate and
104    /// cycle checks for archived edges, so this is structurally
105    /// equivalent to load-time rehydration of a historical edge.
106    pub fn add_archived_spawns(&mut self, parent: CardId, child: CardId) -> KanbanResult<()> {
107        use kanban_core::Edge as _;
108        let mut edge = SpawnsEdge::new(parent, child);
109        edge.archive();
110        self.spawns.add_edge_with_metadata(edge).map_err(dep_err)
111    }
112
113    pub fn remove_parent(&mut self, child: CardId, parent: CardId) -> KanbanResult<()> {
114        // Use the structural `Graph::remove_edge` via the trait so the
115        // sub-graph picks the right matching semantics (directed for
116        // DAG sub-graphs).
117        use kanban_core::Graph as _;
118        self.spawns.remove_edge(parent, child).map_err(dep_err)
119    }
120
121    pub fn children(&self, parent: CardId) -> Vec<CardId> {
122        self.spawns.outgoing(parent)
123    }
124
125    pub fn parents(&self, child: CardId) -> Vec<CardId> {
126        self.spawns.incoming(child)
127    }
128
129    pub fn ancestors(&self, child: CardId) -> Vec<CardId> {
130        self.spawns.ancestors(child)
131    }
132
133    pub fn descendants(&self, parent: CardId) -> Vec<CardId> {
134        self.spawns.descendants(parent)
135    }
136
137    pub fn child_count(&self, parent: CardId) -> usize {
138        self.spawns.outgoing(parent).len()
139    }
140
141    // --- Blocks ---
142
143    /// Add a `blocker -> blocked` edge with default
144    /// ([`Severity::Medium`]) severity.
145    pub fn set_block(&mut self, blocker: CardId, blocked: CardId) -> KanbanResult<()> {
146        self.set_block_with_severity(blocker, blocked, super::Severity::default())
147    }
148
149    /// Add a `blocker -> blocked` edge with an explicit severity.
150    pub fn set_block_with_severity(
151        &mut self,
152        blocker: CardId,
153        blocked: CardId,
154        severity: super::Severity,
155    ) -> KanbanResult<()> {
156        self.blocks
157            .add_edge_with_metadata(BlocksEdge::new(blocker, blocked, severity))
158            .map_err(dep_err)
159    }
160
161    /// Insert a blocker->blocked Blocks edge already in the archived
162    /// state, preserving the supplied `severity`. See
163    /// [`add_archived_spawns`] for the cascade-undo rationale.
164    pub fn add_archived_blocks(
165        &mut self,
166        blocker: CardId,
167        blocked: CardId,
168        severity: super::Severity,
169    ) -> KanbanResult<()> {
170        use kanban_core::Edge as _;
171        let mut edge = BlocksEdge::new(blocker, blocked, severity);
172        edge.archive();
173        self.blocks.add_edge_with_metadata(edge).map_err(dep_err)
174    }
175
176    pub fn unblock(&mut self, blocker: CardId, blocked: CardId) -> KanbanResult<()> {
177        use kanban_core::Graph as _;
178        self.blocks.remove_edge(blocker, blocked).map_err(dep_err)
179    }
180
181    pub fn blocked(&self, card: CardId) -> Vec<CardId> {
182        self.blocks.outgoing(card)
183    }
184
185    pub fn blockers(&self, card: CardId) -> Vec<CardId> {
186        self.blocks.incoming(card)
187    }
188
189    pub fn can_start<F>(&self, card: CardId, is_complete: F) -> bool
190    where
191        F: Fn(CardId) -> bool,
192    {
193        self.blockers(card).into_iter().all(is_complete)
194    }
195
196    // --- Relates ---
197
198    /// Add an undirected `a <-> b` relates edge with default
199    /// ([`RelatesKind::General`]) kind.
200    pub fn relate(&mut self, a: CardId, b: CardId) -> KanbanResult<()> {
201        self.relate_with_kind(a, b, super::RelatesKind::default())
202    }
203
204    /// Add an undirected `a <-> b` relates edge with an explicit
205    /// sub-kind.
206    pub fn relate_with_kind(
207        &mut self,
208        a: CardId,
209        b: CardId,
210        kind: super::RelatesKind,
211    ) -> KanbanResult<()> {
212        self.relates
213            .add_edge_with_metadata(RelatesEdge::new(a, b, kind))
214            .map_err(dep_err)
215    }
216
217    /// Insert an undirected RelatesTo edge already in the archived
218    /// state, preserving the supplied `kind`. See
219    /// [`add_archived_spawns`] for the cascade-undo rationale.
220    pub fn add_archived_relates(
221        &mut self,
222        a: CardId,
223        b: CardId,
224        kind: super::RelatesKind,
225    ) -> KanbanResult<()> {
226        use kanban_core::Edge as _;
227        let mut edge = RelatesEdge::new(a, b, kind);
228        edge.archive();
229        self.relates.add_edge_with_metadata(edge).map_err(dep_err)
230    }
231
232    pub fn dissociate(&mut self, a: CardId, b: CardId) -> KanbanResult<()> {
233        use kanban_core::Graph as _;
234        self.relates.remove_edge(a, b).map_err(dep_err)
235    }
236
237    pub fn related(&self, card: CardId) -> Vec<CardId> {
238        self.relates.neighbors(card)
239    }
240
241    /// True iff an **active** edge between `a` and `b` exists in any
242    /// sub-graph. Use this to ask "is there a current dependency
243    /// here?". Archived edges are not counted; use
244    /// `contains_archived` to consult both.
245    pub fn contains(&self, a: Uuid, b: Uuid) -> bool {
246        self.edge_sets().iter().any(|g| g.contains(a, b))
247    }
248
249    /// True iff any edge between `a` and `b` exists in any sub-graph,
250    /// including archived edges. Use when reasoning about edge
251    /// history.
252    pub fn contains_archived(&self, a: Uuid, b: Uuid) -> bool {
253        self.edge_sets().iter().any(|g| g.contains_archived(a, b))
254    }
255
256    // --- Persistence helpers ---
257
258    /// Per-kind raw edge accessors. Persistence backends and test
259    /// fixtures use these to round-trip metadata-bearing edges. The
260    /// previous kind-discriminated `edge_bases_of(kind)` was dropped:
261    /// callers either know which kind they want (per-kind accessor)
262    /// or want them all (iterate all three).
263    pub fn spawns_edges(&self) -> &[SpawnsEdge] {
264        self.spawns.edges()
265    }
266    pub fn blocks_edges(&self) -> &[BlocksEdge] {
267        self.blocks.edges()
268    }
269    pub fn relates_edges(&self) -> &[RelatesEdge] {
270        self.relates.edges()
271    }
272
273    /// Construct a graph from per-kind edge vectors with structural
274    /// validation. Each sub-graph independently checks its
275    /// self-reference / cycle / duplicate invariants on the way in; a
276    /// corrupted load fails up front instead of silently rehydrating
277    /// an invariant-violating graph.
278    ///
279    /// On failure the returned error names the offending edge kind
280    /// and endpoints so a user inspecting a load failure has enough
281    /// information to find the bad row in the source file (the
282    /// anonymous `DependencyError` variant alone would only say
283    /// "cycle detected" with no clue which kind or which edge).
284    pub fn from_validated_per_kind_edges(
285        spawns: Vec<SpawnsEdge>,
286        blocks: Vec<BlocksEdge>,
287        relates: Vec<RelatesEdge>,
288    ) -> KanbanResult<Self> {
289        use kanban_core::Edge as _;
290        let mut graph = Self::new();
291        for edge in spawns {
292            let (s, t) = (edge.source(), edge.target());
293            graph
294                .spawns
295                .add_edge_with_metadata(edge)
296                .map_err(|e| load_err_with_context(e, "spawns", s, t))?;
297        }
298        for edge in blocks {
299            let (s, t) = (edge.source(), edge.target());
300            graph
301                .blocks
302                .add_edge_with_metadata(edge)
303                .map_err(|e| load_err_with_context(e, "blocks", s, t))?;
304        }
305        for edge in relates {
306            let (s, t) = (edge.source(), edge.target());
307            graph
308                .relates
309                .add_edge_with_metadata(edge)
310                .map_err(|e| load_err_with_context(e, "relates", s, t))?;
311        }
312        Ok(graph)
313    }
314}
315
316/// Wrap a structural `GraphError` from a load path with the kind tag
317/// and offending endpoints, so a corrupt-file diagnostic identifies
318/// the bad row instead of just naming the invariant.
319fn load_err_with_context(
320    e: GraphError,
321    kind: &'static str,
322    source: Uuid,
323    target: Uuid,
324) -> crate::KanbanError {
325    crate::KanbanError::validation(format!(
326        "load failed on {kind} edge {source} -> {target}: {e}"
327    ))
328}
329
330fn dep_err(e: GraphError) -> crate::KanbanError {
331    let d: DependencyError = e.into();
332    d.into()
333}
334
335impl From<GraphError> for DependencyError {
336    fn from(e: GraphError) -> Self {
337        match e {
338            GraphError::Cycle => DependencyError::CycleDetected,
339            GraphError::SelfReference => DependencyError::SelfReference,
340            GraphError::EdgeNotFound => DependencyError::EdgeNotFound,
341            GraphError::Duplicate => DependencyError::DuplicateEdge,
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use uuid::Uuid;
350
351    fn ids() -> (Uuid, Uuid, Uuid) {
352        (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4())
353    }
354
355    #[test]
356    fn test_new_returns_empty_graph() {
357        let graph = DependencyGraph::new();
358        assert_eq!(graph.len(), 0);
359    }
360
361    #[test]
362    fn test_default_graph_round_trips_through_serde() {
363        let graph = DependencyGraph::new();
364        let json = serde_json::to_string(&graph).unwrap();
365        let deserialized: DependencyGraph = serde_json::from_str(&json).unwrap();
366        assert_eq!(deserialized.len(), 0);
367    }
368
369    #[test]
370    fn test_dependency_graph_serializes_spawns_bucket_key() {
371        // The on-disk JSON bucket name must match the domain
372        // vocabulary (SpawnsEdge, spawns_edges(), SQLite
373        // spawns_edges table). The historical Rust field name
374        // `parent_child` leaked into the wire format; this pins
375        // the rename to `spawns`.
376        let graph = DependencyGraph::new();
377        let json = serde_json::to_value(&graph).unwrap();
378        let obj = json.as_object().expect("graph serialises to a JSON object");
379        assert!(
380            obj.contains_key("spawns"),
381            "DependencyGraph must serialise the spawns bucket under key `spawns`; got keys {:?}",
382            obj.keys().collect::<Vec<_>>()
383        );
384        assert!(
385            !obj.contains_key("parent_child"),
386            "legacy key `parent_child` must be gone from the wire format; got keys {:?}",
387            obj.keys().collect::<Vec<_>>()
388        );
389    }
390
391    // --- Parent/child (Spawns) ---
392
393    // --- from_validated_per_kind_edges context ---
394    //
395    // A corrupt load (cycle, self-ref, duplicate) must surface enough
396    // information for a user to find the bad row in the source file.
397    // The bare DependencyError variants don't carry context; the load
398    // path wraps them with the kind tag and the offending endpoints.
399
400    #[test]
401    fn test_load_with_blocks_cycle_names_kind_and_endpoints() {
402        let (a, b, c) = ids();
403        // Construct a cycle by going through the validated path with
404        // a chain a->b, b->c, c->a in the blocks vector.
405        let err = DependencyGraph::from_validated_per_kind_edges(
406            Vec::new(),
407            vec![
408                super::super::BlocksEdge::new(a, b, super::super::Severity::Medium),
409                super::super::BlocksEdge::new(b, c, super::super::Severity::Medium),
410                super::super::BlocksEdge::new(c, a, super::super::Severity::Medium),
411            ],
412            Vec::new(),
413        )
414        .unwrap_err();
415        let msg = err.to_string();
416        assert!(
417            msg.contains("blocks"),
418            "load error must name the kind; got: {msg}"
419        );
420        assert!(
421            msg.contains(&c.to_string()) && msg.contains(&a.to_string()),
422            "load error must name the offending endpoints; got: {msg}"
423        );
424        assert!(
425            msg.to_lowercase().contains("cycle"),
426            "load error must name the invariant; got: {msg}"
427        );
428    }
429
430    #[test]
431    fn test_load_with_relates_self_reference_names_kind_and_endpoint() {
432        let a = Uuid::new_v4();
433        let err = DependencyGraph::from_validated_per_kind_edges(
434            Vec::new(),
435            Vec::new(),
436            vec![super::super::RelatesEdge::new(
437                a,
438                a,
439                super::super::RelatesKind::General,
440            )],
441        )
442        .unwrap_err();
443        let msg = err.to_string();
444        assert!(msg.contains("relates"), "kind name in message: {msg}");
445        assert!(msg.contains(&a.to_string()), "endpoint in message: {msg}");
446    }
447
448    #[test]
449    fn test_set_parent_creates_parent_child_edge() {
450        let (parent, child, _) = ids();
451        let mut g = DependencyGraph::new();
452        g.set_parent(child, parent).unwrap();
453        assert_eq!(g.children(parent), vec![child]);
454        assert_eq!(g.parents(child), vec![parent]);
455    }
456
457    #[test]
458    fn test_set_parent_self_reference_returns_error() {
459        let (a, _, _) = ids();
460        let mut g = DependencyGraph::new();
461        assert!(g.set_parent(a, a).unwrap_err().is_self_reference());
462    }
463
464    #[test]
465    fn test_set_parent_cycle_returns_error() {
466        let (a, b, c) = ids();
467        let mut g = DependencyGraph::new();
468        g.set_parent(b, a).unwrap();
469        g.set_parent(c, b).unwrap();
470        assert!(g.set_parent(a, c).unwrap_err().is_cycle_detected());
471    }
472
473    #[test]
474    fn test_remove_parent_existing_succeeds() {
475        let (parent, child, _) = ids();
476        let mut g = DependencyGraph::new();
477        g.set_parent(child, parent).unwrap();
478        g.remove_parent(child, parent).unwrap();
479        assert!(g.children(parent).is_empty());
480    }
481
482    #[test]
483    fn test_set_parent_duplicate_returns_duplicate_edge_error() {
484        let (parent, child, _) = ids();
485        let mut g = DependencyGraph::new();
486        g.set_parent(child, parent).unwrap();
487        assert!(g.set_parent(child, parent).unwrap_err().is_duplicate_edge());
488        assert_eq!(g.children(parent), vec![child], "no duplicate stored");
489    }
490
491    #[test]
492    fn test_remove_parent_missing_returns_edge_not_found() {
493        let (parent, child, _) = ids();
494        let mut g = DependencyGraph::new();
495        assert!(g
496            .remove_parent(child, parent)
497            .unwrap_err()
498            .is_edge_not_found());
499    }
500
501    #[test]
502    fn test_ancestors_returns_transitive_parents() {
503        let (gp, p, c) = ids();
504        let mut g = DependencyGraph::new();
505        g.set_parent(p, gp).unwrap();
506        g.set_parent(c, p).unwrap();
507        let mut anc = g.ancestors(c);
508        anc.sort();
509        let mut expected = vec![gp, p];
510        expected.sort();
511        assert_eq!(anc, expected);
512    }
513
514    #[test]
515    fn test_descendants_returns_transitive_children() {
516        let (parent, child, grandchild) = ids();
517        let mut g = DependencyGraph::new();
518        g.set_parent(child, parent).unwrap();
519        g.set_parent(grandchild, child).unwrap();
520        let mut desc = g.descendants(parent);
521        desc.sort();
522        let mut expected = vec![child, grandchild];
523        expected.sort();
524        assert_eq!(desc, expected);
525    }
526
527    #[test]
528    fn test_child_count_matches_children_len() {
529        let (parent, a, b) = ids();
530        let mut g = DependencyGraph::new();
531        assert_eq!(g.child_count(parent), 0);
532        g.set_parent(a, parent).unwrap();
533        g.set_parent(b, parent).unwrap();
534        assert_eq!(g.child_count(parent), 2);
535    }
536
537    // --- Blocks ---
538
539    #[test]
540    fn test_add_blocks_creates_directed_edge() {
541        let (a, b, _) = ids();
542        let mut g = DependencyGraph::new();
543        g.set_block(a, b).unwrap();
544        assert_eq!(g.blocked(a), vec![b]);
545        assert_eq!(g.blockers(b), vec![a]);
546    }
547
548    #[test]
549    fn test_set_block_with_severity_preserves_metadata() {
550        let (a, b, _) = ids();
551        let mut g = DependencyGraph::new();
552        g.set_block_with_severity(a, b, super::super::Severity::Critical)
553            .unwrap();
554        assert_eq!(
555            g.blocks_edges()[0].severity,
556            super::super::Severity::Critical
557        );
558    }
559
560    #[test]
561    fn test_set_block_default_is_medium_severity() {
562        let (a, b, _) = ids();
563        let mut g = DependencyGraph::new();
564        g.set_block(a, b).unwrap();
565        assert_eq!(g.blocks_edges()[0].severity, super::super::Severity::Medium);
566    }
567
568    #[test]
569    fn test_add_blocks_cycle_returns_error() {
570        let (a, b, c) = ids();
571        let mut g = DependencyGraph::new();
572        g.set_block(a, b).unwrap();
573        g.set_block(b, c).unwrap();
574        assert!(g.set_block(c, a).unwrap_err().is_cycle_detected());
575    }
576
577    #[test]
578    fn test_set_block_duplicate_returns_duplicate_edge_error() {
579        let (a, b, _) = ids();
580        let mut g = DependencyGraph::new();
581        g.set_block(a, b).unwrap();
582        assert!(g.set_block(a, b).unwrap_err().is_duplicate_edge());
583    }
584
585    #[test]
586    fn test_relate_duplicate_in_either_orientation_returns_duplicate_edge_error() {
587        let (a, b, _) = ids();
588        let mut g = DependencyGraph::new();
589        g.relate(a, b).unwrap();
590        assert!(g.relate(a, b).unwrap_err().is_duplicate_edge());
591        assert!(g.relate(b, a).unwrap_err().is_duplicate_edge());
592    }
593
594    #[test]
595    fn test_add_blocks_self_reference_returns_error() {
596        let (a, _, _) = ids();
597        let mut g = DependencyGraph::new();
598        assert!(g.set_block(a, a).unwrap_err().is_self_reference());
599    }
600
601    #[test]
602    fn test_can_start_returns_true_when_all_blockers_complete() {
603        let (a, b, c) = ids();
604        let mut g = DependencyGraph::new();
605        g.set_block(a, c).unwrap();
606        g.set_block(b, c).unwrap();
607        assert!(!g.can_start(c, |id| id == a));
608        assert!(g.can_start(c, |_| true));
609    }
610
611    // --- Relates ---
612
613    #[test]
614    fn test_add_relates_to_creates_bidirectional_edge() {
615        let (a, b, _) = ids();
616        let mut g = DependencyGraph::new();
617        g.relate(a, b).unwrap();
618        assert_eq!(g.related(a), vec![b]);
619        assert_eq!(g.related(b), vec![a]);
620    }
621
622    #[test]
623    fn test_relate_with_kind_preserves_metadata() {
624        let (a, b, _) = ids();
625        let mut g = DependencyGraph::new();
626        g.relate_with_kind(a, b, super::super::RelatesKind::Duplicates)
627            .unwrap();
628        assert_eq!(
629            g.relates_edges()[0].kind,
630            super::super::RelatesKind::Duplicates
631        );
632    }
633
634    #[test]
635    fn test_relate_default_is_general_kind() {
636        let (a, b, _) = ids();
637        let mut g = DependencyGraph::new();
638        g.relate(a, b).unwrap();
639        assert_eq!(
640            g.relates_edges()[0].kind,
641            super::super::RelatesKind::General
642        );
643    }
644
645    #[test]
646    fn test_add_relates_to_self_reference_returns_error() {
647        let (a, _, _) = ids();
648        let mut g = DependencyGraph::new();
649        assert!(g.relate(a, a).unwrap_err().is_self_reference());
650    }
651
652    #[test]
653    fn test_add_relates_permits_cycle() {
654        let (a, b, c) = ids();
655        let mut g = DependencyGraph::new();
656        g.relate(a, b).unwrap();
657        g.relate(b, c).unwrap();
658        assert!(g.relate(c, a).is_ok());
659    }
660
661    // --- Cross-cutting cascades ---
662
663    #[test]
664    fn test_archive_node_hides_edges_in_all_subgraphs() {
665        let (a, b, c) = ids();
666        let mut g = DependencyGraph::new();
667        g.set_parent(b, a).unwrap();
668        g.set_block(a, c).unwrap();
669        g.relate(a, c).unwrap();
670        g.archive_node(a);
671        assert!(g.children(a).is_empty());
672        assert!(g.blocked(a).is_empty());
673        assert!(g.related(a).is_empty());
674        assert_eq!(g.len(), 3);
675        assert_eq!(g.active_len(), 0);
676    }
677
678    #[test]
679    fn test_remove_node_removes_edges_in_all_subgraphs() {
680        let (a, b, c) = ids();
681        let mut g = DependencyGraph::new();
682        g.set_parent(b, a).unwrap();
683        g.set_block(a, c).unwrap();
684        g.relate(a, c).unwrap();
685        g.remove_node(a);
686        assert_eq!(g.len(), 0);
687    }
688
689    // --- Active-only contains vs contains_archived ---
690
691    #[test]
692    fn test_contains_returns_false_for_archived_only_edge() {
693        let (a, b, _) = ids();
694        let mut g = DependencyGraph::new();
695        g.set_block(a, b).unwrap();
696        g.archive_node(a);
697        assert!(!g.contains(a, b), "active contains() must skip archived");
698        assert!(
699            g.contains_archived(a, b),
700            "contains_archived() picks up the archived edge"
701        );
702    }
703
704    #[test]
705    fn test_contains_returns_true_for_active_edge() {
706        let (a, b, _) = ids();
707        let mut g = DependencyGraph::new();
708        g.set_block(a, b).unwrap();
709        assert!(g.contains(a, b));
710        assert!(g.contains_archived(a, b));
711    }
712
713    #[test]
714    fn test_parent_child_and_blocks_are_independent() {
715        let (a, b, c) = ids();
716        let mut g = DependencyGraph::new();
717        g.set_parent(b, a).unwrap();
718        g.set_block(b, c).unwrap();
719        assert_eq!(g.children(a), vec![b]);
720        assert_eq!(g.blocked(b), vec![c]);
721    }
722}