encounter_argumentation/scoring.rs
1//! Scheme-strength action scoring.
2//!
3//! [`SchemeActionScorer`] wraps an inner [`ActionScorer`] and boosts scores
4//! for actions that have scheme backing in the supplied [`ArgumentKnowledge`].
5//! The boost scales with the scheme's [`SchemeStrength`]:
6//!
7//! | Strength | Multiplier |
8//! |----------|-----------|
9//! | Strong | 1.00 |
10//! | Moderate | 0.67 |
11//! | Weak | 0.33 |
12//!
13//! The best (highest) boost across all argument positions for an action is
14//! applied; positions without a matching registry entry are skipped.
15
16use crate::knowledge::{ArgumentKnowledge, ArgumentPosition};
17use argumentation_schemes::CatalogRegistry;
18use argumentation_schemes::types::SchemeStrength;
19use encounter::affordance::CatalogEntry;
20use encounter::scoring::{ActionScorer, ScoredAffordance};
21
22/// An [`ActionScorer`] that boosts scores for scheme-backed actions.
23///
24/// Wraps an inner scorer and, for each scored affordance, looks up argument
25/// positions from [`ArgumentKnowledge`]. If any position maps to a registered
26/// scheme, the affordance score is increased by:
27///
28/// ```text
29/// max_boost * strength_multiplier * preference_weight
30/// ```
31///
32/// The highest boost across all positions wins. The returned list is sorted
33/// descending by final score.
34pub struct SchemeActionScorer<K, S> {
35 knowledge: K,
36 registry: CatalogRegistry,
37 inner: S,
38 max_boost: f64,
39}
40
41impl<K: ArgumentKnowledge, S> SchemeActionScorer<K, S> {
42 /// Create a new `SchemeActionScorer`.
43 ///
44 /// # Parameters
45 /// - `knowledge` – provides per-character argument positions.
46 /// - `registry` – the scheme catalog used to resolve scheme strengths.
47 /// - `inner` – the base scorer whose results are boosted.
48 /// - `max_boost` – the maximum additive score increase for a Strong scheme
49 /// with `preference_weight = 1.0`.
50 pub fn new(knowledge: K, registry: CatalogRegistry, inner: S, max_boost: f64) -> Self {
51 Self {
52 knowledge,
53 registry,
54 inner,
55 max_boost,
56 }
57 }
58}
59
60impl<K, S, P> ActionScorer<P> for SchemeActionScorer<K, S>
61where
62 K: ArgumentKnowledge,
63 S: ActionScorer<P>,
64 P: Clone,
65{
66 /// Score available actions, boosting those with scheme backing.
67 ///
68 /// Delegates to the inner scorer, then for each result looks up argument
69 /// positions from the knowledge base and applies the best scheme boost.
70 /// Returns results sorted descending by final score.
71 fn score_actions(
72 &self,
73 actor: &str,
74 available: &[CatalogEntry<P>],
75 participants: &[String],
76 ) -> Vec<ScoredAffordance<P>> {
77 let mut scored = self.inner.score_actions(actor, available, participants);
78 for sa in &mut scored {
79 let positions =
80 self.knowledge
81 .arguments_for_action(actor, &sa.entry.spec.name, &sa.bindings);
82 let boost = best_scheme_boost(&positions, &self.registry, self.max_boost);
83 sa.score += boost;
84 }
85 scored.sort_by(|a, b| {
86 b.score
87 .partial_cmp(&a.score)
88 .unwrap_or(std::cmp::Ordering::Equal)
89 });
90 scored
91 }
92}
93
94/// Returns the multiplier for a given scheme strength.
95fn strength_multiplier(strength: SchemeStrength) -> f64 {
96 match strength {
97 SchemeStrength::Strong => 1.0,
98 SchemeStrength::Moderate => 0.67,
99 SchemeStrength::Weak => 0.33,
100 }
101}
102
103/// Computes the best (highest) scheme boost across all argument positions.
104///
105/// Positions whose `scheme_key` does not match any entry in the registry are
106/// silently skipped.
107fn best_scheme_boost(
108 positions: &[ArgumentPosition],
109 registry: &CatalogRegistry,
110 max_boost: f64,
111) -> f64 {
112 positions
113 .iter()
114 .filter_map(|pos| {
115 let scheme = registry.by_key(&pos.scheme_key)?;
116 let mult = strength_multiplier(scheme.metadata.strength);
117 Some(max_boost * mult * pos.preference_weight)
118 })
119 .fold(0.0_f64, f64::max)
120}