Skip to main content

argumentation_values/
multi.rs

1//! Multi-audience consensus queries.
2//!
3//! When a scene involves multiple characters, each potentially with their
4//! own value ordering, the natural query becomes: "which arguments survive
5//! under *every* character's audience?" This is DiArg's AgreementScenario
6//! abstraction (Kampik 2020), simplified.
7
8use crate::error::Error;
9use crate::framework::ValueBasedFramework;
10use crate::types::Audience;
11use std::collections::HashSet;
12use std::hash::Hash;
13
14/// Query operations over a set of audiences.
15pub struct MultiAudience<'a> {
16    audiences: &'a [Audience],
17}
18
19impl<'a> MultiAudience<'a> {
20    /// Construct from a slice of audiences. Empty slice means "no audiences"
21    /// — every argument is trivially accepted under the empty universal
22    /// quantifier (`common_extensions` returns the union of arguments).
23    pub fn new(audiences: &'a [Audience]) -> Self {
24        Self { audiences }
25    }
26
27    /// Borrow the underlying audiences.
28    pub fn audiences(&self) -> &[Audience] {
29        self.audiences
30    }
31
32    /// Returns the set of arguments that are credulously accepted (i.e.,
33    /// in *some* preferred extension) under *every* audience in the set.
34    ///
35    /// This is the consensus answer: which proposals survive regardless
36    /// of which character's value ordering you adopt? Useful for council
37    /// / jury / cabinet narrative queries.
38    pub fn common_credulous<A>(
39        &self,
40        vaf: &ValueBasedFramework<A>,
41    ) -> Result<HashSet<A>, Error>
42    where
43        A: Clone + Eq + Hash + Ord + std::fmt::Debug,
44    {
45        if self.audiences.is_empty() {
46            return Ok(vaf.base().arguments().cloned().collect());
47        }
48
49        // Compute per-audience credulous sets.
50        let per_audience: Vec<HashSet<A>> = self
51            .audiences
52            .iter()
53            .map(|aud| -> Result<HashSet<A>, Error> {
54                let defeat = vaf.defeat_graph(aud)?;
55                let extensions = defeat.preferred_extensions().map_err(Error::from)?;
56                let mut credulous = HashSet::new();
57                for ext in extensions {
58                    for arg in ext {
59                        credulous.insert(arg);
60                    }
61                }
62                Ok(credulous)
63            })
64            .collect::<Result<_, _>>()?;
65
66        // Intersect.
67        let mut iter = per_audience.into_iter();
68        let Some(mut acc) = iter.next() else {
69            return Ok(HashSet::new());
70        };
71        for next in iter {
72            acc.retain(|a| next.contains(a));
73        }
74        Ok(acc)
75    }
76
77    /// Returns the set of arguments grounded under *every* audience in
78    /// the set. Strictly stronger than `common_credulous` — the consensus
79    /// among the most cautious answers from each character.
80    pub fn common_grounded<A>(
81        &self,
82        vaf: &ValueBasedFramework<A>,
83    ) -> Result<HashSet<A>, Error>
84    where
85        A: Clone + Eq + Hash + Ord + std::fmt::Debug,
86    {
87        if self.audiences.is_empty() {
88            return Ok(vaf.base().arguments().cloned().collect());
89        }
90
91        let per_audience: Vec<HashSet<A>> = self
92            .audiences
93            .iter()
94            .map(|aud| vaf.grounded_for(aud))
95            .collect::<Result<_, _>>()?;
96
97        let mut iter = per_audience.into_iter();
98        let Some(mut acc) = iter.next() else {
99            return Ok(HashSet::new());
100        };
101        for next in iter {
102            acc.retain(|a| next.contains(a));
103        }
104        Ok(acc)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::types::{Value, ValueAssignment};
112    use argumentation::ArgumentationFramework;
113
114    fn hal_carla() -> ValueBasedFramework<&'static str> {
115        let mut base = ArgumentationFramework::new();
116        for arg in ["h1", "c1", "h2", "c2"] {
117            base.add_argument(arg);
118        }
119        base.add_attack(&"h1", &"c1").unwrap();
120        base.add_attack(&"c1", &"h1").unwrap();
121        base.add_attack(&"c2", &"h2").unwrap();
122        base.add_attack(&"h2", &"c1").unwrap();
123
124        let mut values = ValueAssignment::new();
125        values.promote("h1", Value::new("life"));
126        values.promote("c1", Value::new("property"));
127        values.promote("h2", Value::new("fairness"));
128        values.promote("c2", Value::new("life"));
129
130        ValueBasedFramework::new(base, values)
131    }
132
133    #[test]
134    fn common_grounded_across_opposing_audiences_yields_only_unanimous_winners() {
135        let vaf = hal_carla();
136        let life_first = Audience::total([Value::new("life"), Value::new("property")]);
137        let property_first = Audience::total([Value::new("property"), Value::new("life")]);
138        let auds = [life_first, property_first];
139        let multi = MultiAudience::new(&auds);
140        let common = multi.common_grounded(&vaf).unwrap();
141        // c2 always grounded; nothing else survives both audiences.
142        assert!(common.contains("c2"));
143        assert!(!common.contains("h1"));
144        assert!(!common.contains("c1"));
145    }
146
147    #[test]
148    fn empty_audience_set_returns_all_arguments() {
149        let vaf = hal_carla();
150        let multi = MultiAudience::new(&[]);
151        let common = multi.common_grounded(&vaf).unwrap();
152        assert_eq!(common.len(), 4);
153    }
154
155    #[test]
156    fn common_credulous_is_superset_of_common_grounded() {
157        let vaf = hal_carla();
158        let life_first = Audience::total([Value::new("life"), Value::new("property")]);
159        let property_first = Audience::total([Value::new("property"), Value::new("life")]);
160        let auds = [life_first, property_first];
161        let multi = MultiAudience::new(&auds);
162        let credulous = multi.common_credulous(&vaf).unwrap();
163        let grounded = multi.common_grounded(&vaf).unwrap();
164        for arg in &grounded {
165            assert!(credulous.contains(arg), "common_grounded must subset common_credulous");
166        }
167    }
168}