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}