Skip to main content

argumentation/aspic/
postulates.rs

1//! Caminada–Amgoud (2007) rationality postulates for structured
2//! argumentation.
3//!
4//! These postulates express properties that a "well-behaved" ASPIC+
5//! system should satisfy. They are:
6//!
7//! 1. **Sub-argument closure**: if an extension contains an argument `A`,
8//!    it contains every sub-argument of `A`.
9//! 2. **Closure under strict rules**: if `S` is the set of conclusions of
10//!    an extension, and `S ⊢ φ` via strict rules, then `φ` is also a
11//!    conclusion of some argument in the extension.
12//! 3. **Direct consistency**: no pair of contrary conclusions both appear
13//!    in the extension.
14//! 4. **Indirect consistency**: the closure of the extension's conclusions
15//!    under strict rules is consistent.
16//!
17//! This module provides [`PostulateReport`] and a check function that
18//! evaluates a given extension against all four postulates. Violations
19//! typically signal a bug in the user's rule set (e.g. missing
20//! transposition closure) rather than a bug in the crate.
21
22use super::argument::{Argument, ArgumentId};
23use super::language::Literal;
24use super::rules::{Rule, RuleKind};
25use std::collections::HashSet;
26
27/// A report from checking the Caminada-Amgoud postulates against an
28/// extension.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PostulateReport {
31    /// Violations found, if any. An empty vector means all postulates hold.
32    pub violations: Vec<PostulateViolation>,
33}
34
35impl PostulateReport {
36    /// Whether all four postulates hold.
37    pub fn is_clean(&self) -> bool {
38        self.violations.is_empty()
39    }
40}
41
42/// A specific postulate violation with a short human-readable description.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum PostulateViolation {
45    /// Argument `parent` is in the extension but its sub-argument `sub`
46    /// is not. Violates sub-argument closure.
47    SubArgumentNotInExtension {
48        /// Argument that IS in the extension.
49        parent: ArgumentId,
50        /// Sub-argument of `parent` that is NOT in the extension.
51        sub: ArgumentId,
52    },
53    /// The extension's conclusion set derives `literal` via strict rules
54    /// but no argument in the extension concludes `literal`. Violates
55    /// closure under strict rules.
56    StrictClosureViolation {
57        /// Literal derivable from the extension via strict rules but not
58        /// itself a conclusion of any extension member.
59        missing: Literal,
60    },
61    /// Both `literal` and its contrary appear in the extension's
62    /// conclusion set. Violates direct consistency.
63    DirectInconsistency {
64        /// One of the conflicting literals (the positive side, by
65        /// convention, to avoid duplicate reports).
66        literal: Literal,
67    },
68    /// The strict-rule closure of the extension's conclusions is
69    /// inconsistent (contains both a literal and its contrary).
70    IndirectInconsistency {
71        /// One of the conflicting literals.
72        literal: Literal,
73    },
74}
75
76/// Check all four rationality postulates against the given extension.
77pub fn check_postulates(
78    arguments: &[Argument],
79    rules: &[Rule],
80    extension: &HashSet<ArgumentId>,
81) -> PostulateReport {
82    let mut violations = Vec::new();
83
84    // Postulate 1: sub-argument closure.
85    for arg in arguments {
86        if !extension.contains(&arg.id) {
87            continue;
88        }
89        for sub_id in &arg.sub_arguments {
90            if !extension.contains(sub_id) {
91                violations.push(PostulateViolation::SubArgumentNotInExtension {
92                    parent: arg.id,
93                    sub: *sub_id,
94                });
95            }
96        }
97    }
98
99    // Collect the extension's conclusions.
100    let conclusions: HashSet<Literal> = arguments
101        .iter()
102        .filter(|a| extension.contains(&a.id))
103        .map(|a| a.conclusion.clone())
104        .collect();
105
106    // Postulate 3: direct consistency.
107    for lit in &conclusions {
108        if conclusions.contains(&lit.contrary()) && !matches!(lit, Literal::Neg(_)) {
109            violations.push(PostulateViolation::DirectInconsistency {
110                literal: lit.clone(),
111            });
112        }
113    }
114
115    // Postulate 2: closure under strict rules.
116    let closure = strict_closure(&conclusions, rules);
117    for lit in &closure {
118        if !conclusions.contains(lit) {
119            violations.push(PostulateViolation::StrictClosureViolation {
120                missing: lit.clone(),
121            });
122        }
123    }
124
125    // Postulate 4: indirect consistency.
126    for lit in &closure {
127        if closure.contains(&lit.contrary()) && !matches!(lit, Literal::Neg(_)) {
128            violations.push(PostulateViolation::IndirectInconsistency {
129                literal: lit.clone(),
130            });
131        }
132    }
133
134    PostulateReport { violations }
135}
136
137/// Compute the closure of `initial` under strict rules.
138fn strict_closure(initial: &HashSet<Literal>, rules: &[Rule]) -> HashSet<Literal> {
139    let mut closure = initial.clone();
140    loop {
141        let before = closure.len();
142        for rule in rules.iter().filter(|r| r.kind == RuleKind::Strict) {
143            if rule.premises.iter().all(|p| closure.contains(p)) {
144                closure.insert(rule.conclusion.clone());
145            }
146        }
147        if closure.len() == before {
148            return closure;
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::aspic::argument::construct_arguments;
157    use crate::aspic::kb::KnowledgeBase;
158    use crate::aspic::rules::{Rule, RuleId};
159
160    #[test]
161    fn clean_system_has_no_violations() {
162        let mut kb = KnowledgeBase::new();
163        kb.add_ordinary(Literal::atom("p"));
164        let rules = vec![Rule::defeasible(
165            RuleId(0),
166            vec![Literal::atom("p")],
167            Literal::atom("q"),
168        )];
169        let args = construct_arguments(&kb, &rules).unwrap();
170        let extension: HashSet<ArgumentId> = args.iter().map(|a| a.id).collect();
171        let report = check_postulates(&args, &rules, &extension);
172        assert!(
173            report.is_clean(),
174            "expected clean report, got {:?}",
175            report.violations
176        );
177    }
178
179    #[test]
180    fn direct_inconsistency_is_detected() {
181        let mut kb = KnowledgeBase::new();
182        kb.add_ordinary(Literal::atom("p"));
183        kb.add_ordinary(Literal::atom("r"));
184        let rules = vec![Rule::defeasible(
185            RuleId(0),
186            vec![Literal::atom("r")],
187            Literal::neg("p"),
188        )];
189        let args = construct_arguments(&kb, &rules).unwrap();
190        let extension: HashSet<ArgumentId> = args.iter().map(|a| a.id).collect();
191        let report = check_postulates(&args, &rules, &extension);
192        assert!(
193            report
194                .violations
195                .iter()
196                .any(|v| matches!(v, PostulateViolation::DirectInconsistency { .. })),
197            "expected direct inconsistency, got {:?}",
198            report.violations
199        );
200    }
201}