Skip to main content

argumentation_bipolar/
flatten.rs

1//! Flattening: convert a [`BipolarFramework`] into an equivalent
2//! [`argumentation::ArgumentationFramework`] whose attack relation is
3//! the closed attack set from [`crate::derived::closed_attacks`].
4//!
5//! The flattened framework has the same node set as the bipolar
6//! framework. Every direct attack and every derived attack (supported,
7//! secondary, mediated) appears as a single edge in the flattened
8//! framework's attack relation. Support edges are NOT represented in
9//! the flattened framework — they are handled at the semantics layer
10//! via the support-closure filter.
11//!
12//! This is the abstraction that lets the rest of the crate reuse the
13//! core Dung semantics without re-implementing fixed-point equations.
14
15use crate::derived::closed_attacks;
16use crate::framework::BipolarFramework;
17use argumentation::ArgumentationFramework;
18use std::fmt::Debug;
19use std::hash::Hash;
20
21/// Build a [`argumentation::ArgumentationFramework`] from a
22/// [`BipolarFramework`] whose attack relation is the closed attack set.
23///
24/// Propagates [`argumentation::Error`] from `add_attack` calls, but in
25/// practice this only fires if the argument universe is inconsistent
26/// (an edge references an argument that wasn't registered), which
27/// cannot happen here because `closed_attacks` only produces edges
28/// between arguments already in the framework.
29pub fn flatten<A>(
30    framework: &BipolarFramework<A>,
31) -> Result<ArgumentationFramework<A>, argumentation::Error>
32where
33    A: Clone + Eq + Hash + Debug,
34{
35    let mut af = ArgumentationFramework::new();
36    for arg in framework.arguments() {
37        af.add_argument(arg.clone());
38    }
39    for (attacker, target) in closed_attacks(framework) {
40        af.add_attack(&attacker, &target)?;
41    }
42    Ok(af)
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn empty_bipolar_flattens_to_empty_dung() {
51        let bf: BipolarFramework<&str> = BipolarFramework::new();
52        let af = flatten(&bf).unwrap();
53        assert_eq!(af.len(), 0);
54    }
55
56    #[test]
57    fn direct_attack_survives_flattening() {
58        let mut bf = BipolarFramework::new();
59        bf.add_attack("a", "b");
60        let af = flatten(&bf).unwrap();
61        assert_eq!(af.len(), 2);
62        assert_eq!(af.attackers(&"b").len(), 1);
63    }
64
65    #[test]
66    fn supported_attack_becomes_direct_in_flat_framework() {
67        // a supports x, x attacks b. Flattened: a → b and x → b.
68        let mut bf = BipolarFramework::new();
69        bf.add_support("a", "x").unwrap();
70        bf.add_attack("x", "b");
71        let af = flatten(&bf).unwrap();
72        let attackers_of_b: Vec<&&str> = af.attackers(&"b").into_iter().collect();
73        assert_eq!(attackers_of_b.len(), 2);
74        assert!(attackers_of_b.contains(&&"a"));
75        assert!(attackers_of_b.contains(&&"x"));
76    }
77
78    #[test]
79    fn unrelated_arguments_appear_in_flattened_framework() {
80        // Arguments with no edges still appear as isolated nodes.
81        let mut bf = BipolarFramework::new();
82        bf.add_argument("a");
83        bf.add_argument("b");
84        let af = flatten(&bf).unwrap();
85        assert_eq!(af.len(), 2);
86        assert!(af.attackers(&"a").is_empty());
87        assert!(af.attackers(&"b").is_empty());
88    }
89}