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}