Skip to main content

argumentation_schemes/
scheme.rs

1//! `SchemeSpec`: the compile-time definition of one argumentation scheme.
2
3use crate::critical::CriticalQuestion;
4use crate::types::{SchemeCategory, SchemeId, SchemeStrength, SlotRole};
5
6/// A named premise slot in a scheme.
7///
8/// When instantiated with bindings, each slot maps to a concrete value
9/// (e.g., slot "expert" → "alice").
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct PremiseSlot {
12    /// Slot name (used as the binding key, e.g., "expert", "claim", "domain").
13    pub name: String,
14    /// Human-readable description of what this slot represents.
15    pub description: String,
16    /// What role this slot plays in the scheme.
17    pub role: SlotRole,
18}
19
20impl PremiseSlot {
21    /// Convenience constructor.
22    pub fn new(name: impl Into<String>, description: impl Into<String>, role: SlotRole) -> Self {
23        Self {
24            name: name.into(),
25            description: description.into(),
26            role,
27        }
28    }
29}
30
31/// Template for the scheme's conclusion.
32///
33/// `literal_template` is a string with `?slot` references that get resolved
34/// against the bindings at instantiation time. `is_negated` controls whether
35/// the resulting [`argumentation::aspic::Literal`] is constructed via
36/// `Literal::neg` (for rebut-concluding schemes like ad hominem) or
37/// `Literal::atom`.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct ConclusionTemplate {
40    /// Human-readable description (e.g., "?claim is plausibly true").
41    pub description: String,
42    /// The literal name template. Slot references prefixed with `?` are
43    /// replaced with bound values during instantiation.
44    pub literal_template: String,
45    /// If true, the conclusion is constructed as a negated literal.
46    /// Required for rebuttal-concluding schemes (ad hominem, argument
47    /// from negative consequences, slippery slope, etc.).
48    pub is_negated: bool,
49}
50
51impl ConclusionTemplate {
52    /// Convenience constructor for a positive (non-negated) conclusion.
53    pub fn positive(description: impl Into<String>, literal_template: impl Into<String>) -> Self {
54        Self {
55            description: description.into(),
56            literal_template: literal_template.into(),
57            is_negated: false,
58        }
59    }
60
61    /// Convenience constructor for a negated conclusion (e.g., ad hominem
62    /// concluding ¬claim).
63    pub fn negated(description: impl Into<String>, literal_template: impl Into<String>) -> Self {
64        Self {
65            description: description.into(),
66            literal_template: literal_template.into(),
67            is_negated: true,
68        }
69    }
70}
71
72/// Metadata about a scheme: citation, tags, strength.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct SchemeMetadata {
75    /// Citation (e.g., "Walton 2008 p.14").
76    pub citation: String,
77    /// Domain tags for filtering (e.g., ["epistemic", "authority"]).
78    pub domain_tags: Vec<String>,
79    /// Whether the scheme is presumptive (virtually all Walton schemes are).
80    pub presumptive: bool,
81    /// How strong the scheme's inference typically is.
82    pub strength: SchemeStrength,
83}
84
85/// The complete definition of one argumentation scheme.
86///
87/// A scheme is a recognisable pattern of reasoning with named premise slots,
88/// a conclusion template, and critical questions that probe its weak points.
89/// Schemes are compile-time data: each is constructed by a function in the
90/// [`crate::catalog`] module. Consumers instantiate schemes with concrete
91/// bindings via [`SchemeSpec::instantiate`] or [`crate::instance::instantiate`].
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct SchemeSpec {
94    /// Unique scheme id.
95    pub id: SchemeId,
96    /// Canonical name (e.g., "Argument from Expert Opinion").
97    pub name: String,
98    /// Scheme category for catalog filtering.
99    pub category: SchemeCategory,
100    /// Named premise slots. Order matters — the first N are the scheme's
101    /// "core premises" as defined by Walton.
102    pub premises: Vec<PremiseSlot>,
103    /// Conclusion template. References premise slot names via `?name` syntax.
104    pub conclusion: ConclusionTemplate,
105    /// Critical questions that probe the scheme's weak points.
106    pub critical_questions: Vec<CriticalQuestion>,
107    /// Bibliographic and classification metadata.
108    pub metadata: SchemeMetadata,
109}
110
111impl SchemeSpec {
112    /// Instantiate this scheme with concrete bindings. Convenience method
113    /// that delegates to [`crate::instance::instantiate`].
114    pub fn instantiate(
115        &self,
116        bindings: &std::collections::HashMap<String, String>,
117    ) -> Result<crate::instance::SchemeInstance, crate::Error> {
118        crate::instance::instantiate(self, bindings)
119    }
120
121    /// The scheme's canonical name as a snake_case identifier suitable
122    /// for lookup keys and affordance mapping.
123    pub fn key(&self) -> String {
124        self.name.to_lowercase().replace([' ', '-'], "_")
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn scheme_key_is_snake_case() {
134        let scheme = SchemeSpec {
135            id: SchemeId(1),
136            name: "Argument from Expert Opinion".into(),
137            category: SchemeCategory::Epistemic,
138            premises: vec![PremiseSlot::new("expert", "The expert", SlotRole::Agent)],
139            conclusion: ConclusionTemplate::positive("?claim is true", "?claim"),
140            critical_questions: vec![],
141            metadata: SchemeMetadata {
142                citation: "Walton 2008".into(),
143                domain_tags: vec!["epistemic".into()],
144                presumptive: true,
145                strength: SchemeStrength::Moderate,
146            },
147        };
148        assert_eq!(scheme.key(), "argument_from_expert_opinion");
149    }
150
151    #[test]
152    fn conclusion_template_positive_has_is_negated_false() {
153        let t = ConclusionTemplate::positive("desc", "?claim");
154        assert!(!t.is_negated);
155    }
156
157    #[test]
158    fn conclusion_template_negated_has_is_negated_true() {
159        let t = ConclusionTemplate::negated("desc", "?claim");
160        assert!(t.is_negated);
161    }
162}