Skip to main content

encounter_argumentation/
resolver.rs

1//! Core argument resolution via ASPIC+ extension semantics.
2
3use argumentation::aspic::{Literal, RuleId, StructuredSystem};
4use argumentation_schemes::CatalogRegistry;
5use argumentation_schemes::aspic::add_scheme_to_system;
6use argumentation_schemes::instance::SchemeInstance;
7use argumentation_schemes::types::SchemeStrength;
8
9/// Outcome of resolving an argument between proposer and responder.
10#[derive(Debug, Clone, PartialEq)]
11pub enum ArgumentOutcome {
12    /// The proposer's argument(s) survived in the preferred extension.
13    ProposerWins {
14        /// Fraction of proposer conclusions that survived (0.0-1.0).
15        survival_rate: f64,
16    },
17    /// The responder's counter-argument(s) defeated the proposer.
18    ResponderWins {
19        /// Fraction of proposer conclusions that were defeated (0.0-1.0).
20        defeat_rate: f64,
21    },
22    /// Neither side decisively won.
23    Undecided,
24}
25
26/// Look up a scheme instance's strength from the catalog registry.
27///
28/// Converts the instance's `scheme_name` to a snake_case key and looks it up.
29/// Falls back to `Moderate` when the name is not found in the registry.
30fn lookup_strength(instance: &SchemeInstance, registry: &CatalogRegistry) -> SchemeStrength {
31    let key = instance.scheme_name.to_lowercase().replace(' ', "_");
32    registry
33        .by_key(&key)
34        .map(|s| s.metadata.strength)
35        .unwrap_or(SchemeStrength::Moderate)
36}
37
38/// Numeric weight for a scheme strength (higher = stronger).
39fn strength_rank(strength: SchemeStrength) -> u8 {
40    crate::strength_rank(strength)
41}
42
43/// Resolve an argument between proposer and responder scheme instances.
44///
45/// Builds an ASPIC+ `StructuredSystem`, adds all scheme instances, sets
46/// preference ordering based on scheme strength, computes preferred
47/// extensions, and determines which side's conclusions survive.
48pub fn resolve_argument(
49    proposer_instances: &[SchemeInstance],
50    responder_instances: &[SchemeInstance],
51    registry: &CatalogRegistry,
52) -> ArgumentOutcome {
53    if proposer_instances.is_empty() {
54        return ArgumentOutcome::Undecided;
55    }
56
57    if responder_instances.is_empty() {
58        return ArgumentOutcome::ProposerWins { survival_rate: 1.0 };
59    }
60
61    let mut system = StructuredSystem::new();
62
63    // Add all proposer instances and record their rule IDs with strengths.
64    let proposer_rules: Vec<(RuleId, SchemeStrength)> = proposer_instances
65        .iter()
66        .map(|inst| {
67            let rule_id = add_scheme_to_system(inst, &mut system);
68            let strength = lookup_strength(inst, registry);
69            (rule_id, strength)
70        })
71        .collect();
72
73    // Add all responder instances and record their rule IDs with strengths.
74    let responder_rules: Vec<(RuleId, SchemeStrength)> = responder_instances
75        .iter()
76        .map(|inst| {
77            let rule_id = add_scheme_to_system(inst, &mut system);
78            let strength = lookup_strength(inst, registry);
79            (rule_id, strength)
80        })
81        .collect();
82
83    // Set preference ordering: stronger rules defeat weaker ones.
84    // For each proposer rule vs each responder rule, prefer the one with
85    // higher strength. Equal strength means no preference is set.
86    for &(p_rule, p_strength) in &proposer_rules {
87        for &(r_rule, r_strength) in &responder_rules {
88            let p_rank = strength_rank(p_strength);
89            let r_rank = strength_rank(r_strength);
90            if p_rank > r_rank {
91                // Proposer's rule is stronger; prefer it over responder's.
92                let _ = system.prefer_rule(p_rule, r_rule);
93            } else if r_rank > p_rank {
94                // Responder's rule is stronger; prefer it over proposer's.
95                let _ = system.prefer_rule(r_rule, p_rule);
96            }
97            // Equal strength: no preference — symmetric attack, both can survive.
98        }
99    }
100
101    // Build the framework and compute preferred extensions.
102    let built = match system.build_framework() {
103        Ok(b) => b,
104        Err(_) => return ArgumentOutcome::Undecided,
105    };
106
107    let extensions = match built.framework.preferred_extensions() {
108        Ok(exts) => exts,
109        Err(_) => return ArgumentOutcome::Undecided,
110    };
111
112    if extensions.is_empty() {
113        return ArgumentOutcome::Undecided;
114    }
115
116    // Collect the proposer's conclusions.
117    let proposer_conclusions: Vec<Literal> = proposer_instances
118        .iter()
119        .map(|inst| inst.conclusion.clone())
120        .collect();
121
122    // Count how many proposer conclusions survive across ALL preferred extensions.
123    // A conclusion "survives" when it appears in every preferred extension.
124    let total = proposer_conclusions.len();
125    let survived_count = proposer_conclusions
126        .iter()
127        .filter(|conclusion| {
128            // The conclusion survives if at least one argument supporting it
129            // is in every preferred extension.
130            extensions.iter().all(|ext| {
131                let ext_conclusions = built.conclusions_in(ext);
132                ext_conclusions.contains(conclusion)
133            })
134        })
135        .count();
136
137    let survival_rate = survived_count as f64 / total as f64;
138
139    if survival_rate > 0.5 {
140        ArgumentOutcome::ProposerWins { survival_rate }
141    } else if survival_rate == 0.0 {
142        // Check whether any responder conclusion survived.
143        let responder_conclusions: Vec<Literal> = responder_instances
144            .iter()
145            .map(|inst| inst.conclusion.clone())
146            .collect();
147        let responder_survived = responder_conclusions.iter().any(|conclusion| {
148            extensions.iter().all(|ext| {
149                let ext_conclusions = built.conclusions_in(ext);
150                ext_conclusions.contains(conclusion)
151            })
152        });
153        if responder_survived {
154            let defeat_rate = 1.0 - survival_rate;
155            ArgumentOutcome::ResponderWins { defeat_rate }
156        } else {
157            ArgumentOutcome::Undecided
158        }
159    } else {
160        ArgumentOutcome::Undecided
161    }
162}