Skip to main content

encounter_argumentation/
acceptance.rs

1//! Argument-backed acceptance evaluation.
2
3use crate::knowledge::{ArgumentKnowledge, ArgumentPosition};
4use crate::resolver::{ArgumentOutcome, resolve_argument};
5use argumentation_schemes::CatalogRegistry;
6use argumentation_schemes::instance::SchemeInstance;
7use encounter::scoring::{AcceptanceEval, ScoredAffordance};
8
9/// Acceptance evaluator that uses argumentation schemes to decide whether
10/// a responder accepts a proposed action.
11///
12/// When the responder has no counter-arguments, the action is accepted.
13/// When both sides have arguments, they are resolved via ASPIC+ extension
14/// semantics, and the action is accepted if the proposer's arguments survive.
15/// When ASPIC+ is inconclusive (non-conflicting conclusions), scheme strength
16/// is compared as a fallback — both sides are scoped to the same action by the
17/// `ArgumentKnowledge` trait, so the stronger authority basis wins.
18pub struct ArgumentAcceptanceEval<K> {
19    knowledge: K,
20    registry: CatalogRegistry,
21}
22
23impl<K: ArgumentKnowledge> ArgumentAcceptanceEval<K> {
24    /// Create a new evaluator with the given knowledge provider and scheme registry.
25    pub fn new(knowledge: K, registry: CatalogRegistry) -> Self {
26        Self {
27            knowledge,
28            registry,
29        }
30    }
31}
32
33impl<K: ArgumentKnowledge, P> AcceptanceEval<P> for ArgumentAcceptanceEval<K> {
34    fn evaluate(&self, responder: &str, action: &ScoredAffordance<P>) -> bool {
35        let action_name = &action.entry.spec.name;
36        let proposer = action
37            .bindings
38            .get("self")
39            .map(|s| s.as_str())
40            .unwrap_or("unknown");
41
42        let proposer_positions =
43            self.knowledge
44                .arguments_for_action(proposer, action_name, &action.bindings);
45
46        if proposer_positions.is_empty() {
47            return true; // No formal argument backing → accept by default
48        }
49
50        let responder_positions =
51            self.knowledge
52                .counter_arguments(responder, action_name, &proposer_positions);
53
54        if responder_positions.is_empty() {
55            return true;
56        }
57
58        let proposer_instances = instantiate_positions(&proposer_positions, &self.registry);
59        let responder_instances = instantiate_positions(&responder_positions, &self.registry);
60
61        let outcome = resolve_argument(&proposer_instances, &responder_instances, &self.registry);
62
63        match outcome {
64            ArgumentOutcome::ResponderWins { .. } => false,
65            ArgumentOutcome::ProposerWins { .. } | ArgumentOutcome::Undecided => {
66                // Strength fallback: both sides are scoped to the same action
67                // by the ArgumentKnowledge trait, so comparing strength ranks
68                // is always relevant — it's "how strong is your basis for
69                // supporting/opposing THIS action?" not unrelated topics.
70                // When ASPIC+ is inconclusive (non-conflicting conclusions),
71                // the stronger authority basis wins.
72                let proposer_rank = max_strength_rank(&proposer_positions, &self.registry);
73                let responder_rank = max_strength_rank(&responder_positions, &self.registry);
74                responder_rank <= proposer_rank
75            }
76        }
77    }
78}
79
80/// Return the maximum strength rank across a set of argument positions.
81fn max_strength_rank(positions: &[ArgumentPosition], registry: &CatalogRegistry) -> u8 {
82    positions
83        .iter()
84        .filter_map(|pos| {
85            registry
86                .by_key(&pos.scheme_key)
87                .map(|s| crate::strength_rank(s.metadata.strength))
88        })
89        .max()
90        .unwrap_or(0)
91}
92
93fn instantiate_positions(
94    positions: &[ArgumentPosition],
95    registry: &CatalogRegistry,
96) -> Vec<SchemeInstance> {
97    positions
98        .iter()
99        .filter_map(|pos| {
100            let scheme = registry.by_key(&pos.scheme_key)?;
101            scheme.instantiate(&pos.bindings).ok()
102        })
103        .collect()
104}