Skip to main content

argumentation_values/
framework.rs

1//! `ValueBasedFramework` — Dung framework + value assignment, with
2//! audience-conditioned defeat semantics.
3
4use crate::error::Error;
5use crate::types::{Audience, ValueAssignment};
6use argumentation::ArgumentationFramework;
7use std::collections::HashSet;
8use std::hash::Hash;
9
10/// A value-based argumentation framework: an underlying Dung framework
11/// plus a [`ValueAssignment`] mapping arguments to the values they promote.
12///
13/// Acceptance is computed *per audience* — there is no audience-independent
14/// notion of acceptance in a VAF. See [`Self::accepted_for`].
15///
16/// # Type parameter
17///
18/// `A` is the argument label type, matching the underlying
19/// [`ArgumentationFramework<A>`]. For encounter-bridge use, this is
20/// typically `argumentation::ArgumentId`; for standalone use `&'static str`
21/// or `String` work fine.
22#[derive(Debug, Clone)]
23pub struct ValueBasedFramework<A: Clone + Eq + Hash> {
24    base: ArgumentationFramework<A>,
25    values: ValueAssignment<A>,
26}
27
28impl<A: Clone + Eq + Hash + Ord + std::fmt::Debug> ValueBasedFramework<A> {
29    /// Construct from a Dung framework and value assignment.
30    pub fn new(base: ArgumentationFramework<A>, values: ValueAssignment<A>) -> Self {
31        Self { base, values }
32    }
33
34    /// Borrow the underlying Dung framework (unconditioned attacks).
35    pub fn base(&self) -> &ArgumentationFramework<A> {
36        &self.base
37    }
38
39    /// Borrow the value assignment.
40    pub fn value_assignment(&self) -> &ValueAssignment<A> {
41        &self.values
42    }
43
44    /// Build the audience-conditioned defeat graph as a fresh
45    /// [`ArgumentationFramework`].
46    ///
47    /// An attack `(attacker, target)` in [`Self::base`] becomes a defeat
48    /// in the result iff `defeats(attacker, target)` returns true under
49    /// this audience.
50    ///
51    /// # Defeat rule (Kaci & van der Torre 2008, Pareto-defeating)
52    ///
53    /// Given multi-value assignments, A defeats B iff for **every** value
54    /// `v_b` promoted by B, there is **some** value `v_a` promoted by A
55    /// such that `v_b` is *not strictly preferred* over `v_a` under the
56    /// audience. This degenerates to Bench-Capon (2003) when each
57    /// argument promotes exactly one value.
58    ///
59    /// # Special cases
60    ///
61    /// - Attacker promotes no value → A defeats B (unconditional).
62    /// - Target promotes no value → A defeats B (no preference can save B).
63    /// - Either value is unranked in the audience → considered incomparable
64    ///   (no strict preference); the attacker side wins ties.
65    pub fn defeat_graph(&self, audience: &Audience) -> Result<ArgumentationFramework<A>, Error> {
66        let mut result = ArgumentationFramework::new();
67        let args: Vec<A> = self.base.arguments().cloned().collect();
68        for arg in &args {
69            result.add_argument(arg.clone());
70        }
71        // Iterate attacks via the per-target attackers() accessor — the
72        // base framework doesn't expose a flat (attacker, target) iterator,
73        // so we walk the graph one target at a time.
74        for target in &args {
75            let attackers: Vec<A> = self.base.attackers(target).into_iter().cloned().collect();
76            for attacker in &attackers {
77                if self.defeats(attacker, target, audience) {
78                    result.add_attack(attacker, target)?;
79                }
80            }
81        }
82        Ok(result)
83    }
84
85    /// Returns true iff `attacker` defeats `target` under the audience.
86    /// Both arguments must already be in the underlying framework's attack
87    /// graph (i.e., `attacker` attacks `target` in the base); this method
88    /// only filters by value preference. Calling this with non-attacking
89    /// pairs is meaningless but not an error.
90    pub fn defeats(&self, attacker: &A, target: &A, audience: &Audience) -> bool {
91        let attacker_values = self.values.values(attacker);
92        let target_values = self.values.values(target);
93
94        // Null-promotion rule: if either side promotes no value, no value
95        // preference can intervene, so the attack stands as a defeat.
96        if attacker_values.is_empty() || target_values.is_empty() {
97            return true;
98        }
99
100        // Pareto-defeating: for every target value, attacker has at least
101        // one value that the target's value does not strictly outrank.
102        target_values.iter().all(|tv| {
103            attacker_values
104                .iter()
105                .any(|av| !audience.prefers(tv, av))
106        })
107    }
108
109    /// Audience-conditioned credulous acceptance under preferred semantics.
110    ///
111    /// Returns `Ok(true)` iff `arg` is in *some* preferred extension of the
112    /// audience-conditioned defeat graph.
113    pub fn accepted_for(&self, audience: &Audience, arg: &A) -> Result<bool, Error> {
114        let defeat = self.defeat_graph(audience)?;
115        let extensions = defeat.preferred_extensions().map_err(Error::from)?;
116        Ok(extensions.iter().any(|ext| ext.contains(arg)))
117    }
118
119    /// Audience-conditioned grounded extension (Dung 1995).
120    ///
121    /// Returns the unique skeptically-accepted set under the audience-conditioned
122    /// defeat graph. The grounded extension is computed by the upstream
123    /// `argumentation` crate via least-fixed-point of the characteristic
124    /// function (not via intersection of preferred extensions, which is the
125    /// ideal extension and may be a strict superset on non-well-founded
126    /// frameworks).
127    pub fn grounded_for(&self, audience: &Audience) -> Result<HashSet<A>, Error> {
128        let defeat = self.defeat_graph(audience)?;
129        Ok(defeat.grounded_extension())
130    }
131
132    /// Subjective acceptance — accepted by *some* total ordering of values
133    /// in this framework. See [`crate::acceptance::subjectively_accepted`].
134    pub fn subjectively_accepted(&self, arg: &A) -> Result<bool, Error> {
135        crate::acceptance::subjectively_accepted(self, arg)
136    }
137
138    /// Objective acceptance — accepted by *every* total ordering of values
139    /// in this framework. See [`crate::acceptance::objectively_accepted`].
140    pub fn objectively_accepted(&self, arg: &A) -> Result<bool, Error> {
141        crate::acceptance::objectively_accepted(self, arg)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::types::Value;
149
150    fn framework_two_args_mutual_attack() -> ValueBasedFramework<&'static str> {
151        let mut base = ArgumentationFramework::new();
152        base.add_argument("h1");
153        base.add_argument("c1");
154        base.add_attack(&"h1", &"c1").unwrap();
155        base.add_attack(&"c1", &"h1").unwrap();
156
157        let mut values = ValueAssignment::new();
158        values.promote("h1", Value::new("life"));
159        values.promote("c1", Value::new("property"));
160
161        ValueBasedFramework::new(base, values)
162    }
163
164    #[test]
165    fn life_audience_defeats_property_attack() {
166        let vaf = framework_two_args_mutual_attack();
167        let audience = Audience::total([Value::new("life"), Value::new("property")]);
168        // h1 attacks c1: target value (property) NOT preferred over attacker
169        // value (life), so h1 defeats c1.
170        assert!(vaf.defeats(&"h1", &"c1", &audience));
171        // c1 attacks h1: target value (life) IS strictly preferred over
172        // attacker value (property), so c1 does NOT defeat h1.
173        assert!(!vaf.defeats(&"c1", &"h1", &audience));
174    }
175
176    #[test]
177    fn property_audience_inverts_defeats() {
178        let vaf = framework_two_args_mutual_attack();
179        let audience = Audience::total([Value::new("property"), Value::new("life")]);
180        assert!(!vaf.defeats(&"h1", &"c1", &audience));
181        assert!(vaf.defeats(&"c1", &"h1", &audience));
182    }
183
184    #[test]
185    fn null_promotion_attacker_always_defeats() {
186        let mut base = ArgumentationFramework::new();
187        base.add_argument("a");
188        base.add_argument("b");
189        base.add_attack(&"a", &"b").unwrap();
190        let mut values = ValueAssignment::new();
191        values.promote("b", Value::new("life"));
192        // a promotes nothing.
193        let vaf = ValueBasedFramework::new(base, values);
194        let audience = Audience::total([Value::new("life")]);
195        assert!(vaf.defeats(&"a", &"b", &audience));
196    }
197
198    #[test]
199    fn empty_audience_preserves_all_attacks() {
200        let vaf = framework_two_args_mutual_attack();
201        let audience = Audience::new();
202        // No preferences → everything is incomparable → all attacks defeat.
203        assert!(vaf.defeats(&"h1", &"c1", &audience));
204        assert!(vaf.defeats(&"c1", &"h1", &audience));
205    }
206
207    #[test]
208    fn defeat_graph_filters_attacks() {
209        let vaf = framework_two_args_mutual_attack();
210        let audience = Audience::total([Value::new("life"), Value::new("property")]);
211        let defeat = vaf.defeat_graph(&audience).unwrap();
212        assert_eq!(defeat.attackers(&"h1").len(), 0);
213        assert_eq!(defeat.attackers(&"c1").len(), 1);
214    }
215}