1use super::{ThinkToolContext, ThinkToolModule, ThinkToolModuleConfig, ThinkToolOutput};
40use crate::error::{Error, Result};
41use serde::{Deserialize, Serialize};
42use std::collections::HashSet;
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct LaserLogicConfig {
51 pub detect_fallacies: bool,
53 pub check_validity: bool,
55 pub check_soundness: bool,
57 pub max_premise_depth: usize,
59 pub analyze_syllogisms: bool,
61 pub detect_contradictions: bool,
63 pub confidence_threshold: f64,
65 pub verbose_output: bool,
67}
68
69impl Default for LaserLogicConfig {
70 fn default() -> Self {
71 Self {
72 detect_fallacies: true,
73 check_validity: true,
74 check_soundness: true,
75 max_premise_depth: 10,
76 analyze_syllogisms: true,
77 detect_contradictions: true,
78 confidence_threshold: 0.7,
79 verbose_output: false,
80 }
81 }
82}
83
84impl LaserLogicConfig {
85 pub fn quick() -> Self {
87 Self {
88 detect_fallacies: true,
89 check_validity: true,
90 check_soundness: false,
91 max_premise_depth: 5,
92 analyze_syllogisms: false,
93 detect_contradictions: false,
94 confidence_threshold: 0.6,
95 verbose_output: false,
96 }
97 }
98
99 pub fn deep() -> Self {
101 Self {
102 detect_fallacies: true,
103 check_validity: true,
104 check_soundness: true,
105 max_premise_depth: 20,
106 analyze_syllogisms: true,
107 detect_contradictions: true,
108 confidence_threshold: 0.8,
109 verbose_output: true,
110 }
111 }
112
113 pub fn paranoid() -> Self {
115 Self {
116 detect_fallacies: true,
117 check_validity: true,
118 check_soundness: true,
119 max_premise_depth: 50,
120 analyze_syllogisms: true,
121 detect_contradictions: true,
122 confidence_threshold: 0.9,
123 verbose_output: true,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct Argument {
135 pub premises: Vec<Premise>,
137 pub conclusion: String,
139 pub form: Option<ArgumentForm>,
141}
142
143impl Argument {
144 pub fn new(premises: Vec<&str>, conclusion: &str) -> Self {
146 Self {
147 premises: premises.iter().map(|p| Premise::new(p)).collect(),
148 conclusion: conclusion.to_string(),
149 form: None,
150 }
151 }
152
153 pub fn with_premises(premises: Vec<Premise>, conclusion: &str) -> Self {
155 Self {
156 premises,
157 conclusion: conclusion.to_string(),
158 form: None,
159 }
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct Premise {
166 pub statement: String,
168 pub premise_type: PremiseType,
170 pub confidence: f64,
172 pub evidence: Vec<String>,
174}
175
176impl Premise {
177 pub fn new(statement: &str) -> Self {
179 let premise_type = Self::infer_type(statement);
180 Self {
181 statement: statement.to_string(),
182 premise_type,
183 confidence: 1.0,
184 evidence: Vec::new(),
185 }
186 }
187
188 pub fn with_type(statement: &str, premise_type: PremiseType, confidence: f64) -> Self {
190 Self {
191 statement: statement.to_string(),
192 premise_type,
193 confidence,
194 evidence: Vec::new(),
195 }
196 }
197
198 fn infer_type(statement: &str) -> PremiseType {
200 let lower = statement.to_lowercase();
201
202 if lower.starts_with("all ")
204 || lower.starts_with("every ")
205 || lower.starts_with("each ")
206 || lower.contains(" always ")
207 || lower.starts_with("no ")
208 {
209 return PremiseType::Universal;
210 }
211
212 if lower.starts_with("some ")
214 || lower.starts_with("most ")
215 || lower.starts_with("many ")
216 || lower.starts_with("few ")
217 || lower.contains(" sometimes ")
218 {
219 return PremiseType::Particular;
220 }
221
222 if lower.starts_with("if ")
224 || lower.contains(" then ")
225 || lower.contains(" implies ")
226 || lower.contains(" only if ")
227 {
228 return PremiseType::Conditional;
229 }
230
231 if lower.contains(" or ") || lower.starts_with("either ") {
233 return PremiseType::Disjunctive;
234 }
235
236 if lower.starts_with("not ") || lower.contains(" not ") || lower.starts_with("no ") {
238 return PremiseType::Negative;
239 }
240
241 PremiseType::Singular
243 }
244
245 pub fn with_evidence(mut self, evidence: &str) -> Self {
247 self.evidence.push(evidence.to_string());
248 self
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254pub enum PremiseType {
255 Universal,
257 Particular,
259 Singular,
261 Conditional,
263 Disjunctive,
265 Negative,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271pub enum ArgumentForm {
272 ModusPonens,
274 ModusTollens,
276 HypotheticalSyllogism,
278 DisjunctiveSyllogism,
280 CategoricalSyllogism,
282 ConstructiveDilemma,
284 DestructiveDilemma,
286 ReductioAdAbsurdum,
288 Unknown,
290}
291
292impl ArgumentForm {
293 pub fn description(&self) -> &'static str {
295 match self {
296 Self::ModusPonens => "Modus Ponens (affirming the antecedent)",
297 Self::ModusTollens => "Modus Tollens (denying the consequent)",
298 Self::HypotheticalSyllogism => "Hypothetical Syllogism (chain reasoning)",
299 Self::DisjunctiveSyllogism => "Disjunctive Syllogism (process of elimination)",
300 Self::CategoricalSyllogism => "Categorical Syllogism (term-based reasoning)",
301 Self::ConstructiveDilemma => "Constructive Dilemma (complex conditional)",
302 Self::DestructiveDilemma => "Destructive Dilemma (complex conditional)",
303 Self::ReductioAdAbsurdum => "Reductio Ad Absurdum (proof by contradiction)",
304 Self::Unknown => "Unknown or complex argument form",
305 }
306 }
307
308 pub fn is_valid_form(&self) -> bool {
310 !matches!(self, Self::Unknown)
311 }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
320pub enum Fallacy {
321 AffirmingConsequent,
324 DenyingAntecedent,
326 UndistributedMiddle,
328 IllicitMajor,
330 IllicitMinor,
332 FourTerms,
334 ExistentialFallacy,
336 AffirmingDisjunct,
338
339 CircularReasoning,
342 NonSequitur,
344 Composition,
346 Division,
348
349 PostHoc,
352 SlipperySlope,
354 FalseCause,
356
357 StrawMan,
360 FalseDichotomy,
362 Equivocation,
364}
365
366impl Fallacy {
367 pub fn description(&self) -> &'static str {
369 match self {
370 Self::AffirmingConsequent => {
371 "Inferring P from P->Q and Q. Just because the outcome occurred doesn't mean the specific cause happened."
372 }
373 Self::DenyingAntecedent => {
374 "Inferring ~Q from P->Q and ~P. The consequent might be true for other reasons."
375 }
376 Self::UndistributedMiddle => {
377 "The middle term is not distributed in at least one premise of the syllogism."
378 }
379 Self::IllicitMajor => {
380 "The major term is distributed in the conclusion but not in the major premise."
381 }
382 Self::IllicitMinor => {
383 "The minor term is distributed in the conclusion but not in the minor premise."
384 }
385 Self::FourTerms => {
386 "Four distinct terms used where a syllogism requires exactly three."
387 }
388 Self::ExistentialFallacy => {
389 "Drawing an existential conclusion from purely universal premises."
390 }
391 Self::AffirmingDisjunct => {
392 "Concluding ~Q from (P v Q) and P. With inclusive OR, both can be true."
393 }
394 Self::CircularReasoning => {
395 "The conclusion is essentially restated in the premises."
396 }
397 Self::NonSequitur => {
398 "The conclusion does not logically follow from the premises."
399 }
400 Self::Composition => {
401 "Assuming what's true of parts must be true of the whole."
402 }
403 Self::Division => {
404 "Assuming what's true of the whole must be true of the parts."
405 }
406 Self::PostHoc => {
407 "Assuming A caused B simply because A preceded B."
408 }
409 Self::SlipperySlope => {
410 "Claiming that one event will inevitably lead to a chain of negative events."
411 }
412 Self::FalseCause => {
413 "Incorrectly identifying something as the cause."
414 }
415 Self::StrawMan => {
416 "Misrepresenting someone's argument to make it easier to attack."
417 }
418 Self::FalseDichotomy => {
419 "Presenting only two options when more exist."
420 }
421 Self::Equivocation => {
422 "Using the same term with different meanings in different parts of the argument."
423 }
424 }
425 }
426
427 pub fn pattern(&self) -> &'static str {
429 match self {
430 Self::AffirmingConsequent => "P->Q, Q |- P (INVALID)",
431 Self::DenyingAntecedent => "P->Q, ~P |- ~Q (INVALID)",
432 Self::UndistributedMiddle => "All A are B, All C are B |- All A are C (INVALID)",
433 Self::IllicitMajor => "Major term undistributed in premise, distributed in conclusion",
434 Self::IllicitMinor => "Minor term undistributed in premise, distributed in conclusion",
435 Self::FourTerms => "A-B, C-D |- invalid (4 terms, not 3)",
436 Self::ExistentialFallacy => "All P are Q |- Some P are Q (INVALID if no P exist)",
437 Self::AffirmingDisjunct => "P v Q, P |- ~Q (INVALID for inclusive or)",
438 Self::CircularReasoning => "P |- P (trivially valid but uninformative)",
439 Self::NonSequitur => "Premises do not entail conclusion",
440 Self::Composition => "Part(x) is P |- Whole(x) is P (INVALID)",
441 Self::Division => "Whole(x) is P |- Part(x) is P (INVALID)",
442 Self::PostHoc => "A then B |- A caused B (INVALID)",
443 Self::SlipperySlope => "A -> B -> C -> ... -> Z (chain not established)",
444 Self::FalseCause => "Correlation or sequence |- Causation (INVALID)",
445 Self::StrawMan => "Argument(A') attacked instead of Argument(A)",
446 Self::FalseDichotomy => "P v Q presented where P v Q v R v ... exists",
447 Self::Equivocation => "Term T used as T1 and T2 (different meanings)",
448 }
449 }
450
451 pub fn severity(&self) -> u8 {
453 match self {
454 Self::AffirmingConsequent | Self::DenyingAntecedent => 5,
455 Self::UndistributedMiddle | Self::IllicitMajor | Self::IllicitMinor => 5,
456 Self::FourTerms | Self::ExistentialFallacy => 4,
457 Self::CircularReasoning | Self::NonSequitur => 5,
458 Self::Composition | Self::Division => 3,
459 Self::PostHoc | Self::FalseCause | Self::SlipperySlope => 4,
460 Self::StrawMan | Self::FalseDichotomy | Self::Equivocation => 4,
461 Self::AffirmingDisjunct => 4,
462 }
463 }
464
465 pub fn is_formal(&self) -> bool {
467 matches!(
468 self,
469 Self::AffirmingConsequent
470 | Self::DenyingAntecedent
471 | Self::UndistributedMiddle
472 | Self::IllicitMajor
473 | Self::IllicitMinor
474 | Self::FourTerms
475 | Self::ExistentialFallacy
476 | Self::AffirmingDisjunct
477 )
478 }
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct DetectedFallacy {
484 pub fallacy: Fallacy,
486 pub confidence: f64,
488 pub evidence: String,
490 pub involved_premises: Vec<usize>,
492 pub suggestion: String,
494}
495
496impl DetectedFallacy {
497 pub fn new(fallacy: Fallacy, confidence: f64, evidence: &str) -> Self {
499 Self {
500 fallacy,
501 confidence,
502 evidence: evidence.to_string(),
503 involved_premises: Vec::new(),
504 suggestion: String::new(),
505 }
506 }
507
508 pub fn with_premises(mut self, premises: Vec<usize>) -> Self {
510 self.involved_premises = premises;
511 self
512 }
513
514 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
516 self.suggestion = suggestion.to_string();
517 self
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
527pub enum ValidityStatus {
528 Valid,
530 Invalid,
532 Undetermined,
534}
535
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
538pub enum SoundnessStatus {
539 Sound,
541 Unsound,
543 Undetermined,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct LaserLogicResult {
550 pub argument: Argument,
552 pub validity: ValidityStatus,
554 pub validity_explanation: String,
556 pub soundness: SoundnessStatus,
558 pub soundness_explanation: String,
560 pub fallacies: Vec<DetectedFallacy>,
562 pub argument_form: Option<ArgumentForm>,
564 pub confidence: f64,
566 pub reasoning_steps: Vec<String>,
568 pub suggestions: Vec<String>,
570 pub contradictions: Vec<Contradiction>,
572}
573
574impl LaserLogicResult {
575 fn new(argument: Argument) -> Self {
577 Self {
578 argument,
579 validity: ValidityStatus::Undetermined,
580 validity_explanation: String::new(),
581 soundness: SoundnessStatus::Undetermined,
582 soundness_explanation: String::new(),
583 fallacies: Vec::new(),
584 argument_form: None,
585 confidence: 0.0,
586 reasoning_steps: Vec::new(),
587 suggestions: Vec::new(),
588 contradictions: Vec::new(),
589 }
590 }
591
592 pub fn verdict(&self) -> &'static str {
594 match (self.validity, self.soundness) {
595 (ValidityStatus::Valid, SoundnessStatus::Sound) => {
596 "SOUND: Argument is valid with true premises"
597 }
598 (ValidityStatus::Valid, SoundnessStatus::Unsound) => {
599 "VALID BUT UNSOUND: Logic correct, but premises questionable"
600 }
601 (ValidityStatus::Valid, SoundnessStatus::Undetermined) => {
602 "VALID: Logic correct, premises unverified"
603 }
604 (ValidityStatus::Invalid, _) => "INVALID: Conclusion does not follow from premises",
605 (ValidityStatus::Undetermined, _) => "UNDETERMINED: Could not fully analyze",
606 }
607 }
608
609 pub fn has_fallacies(&self) -> bool {
611 !self.fallacies.is_empty()
612 }
613
614 pub fn most_severe_fallacy(&self) -> Option<&DetectedFallacy> {
616 self.fallacies
617 .iter()
618 .max_by_key(|f| (f.fallacy.severity(), (f.confidence * 100.0) as u32))
619 }
620
621 pub fn is_valid(&self) -> bool {
623 self.validity == ValidityStatus::Valid
624 }
625
626 pub fn is_sound(&self) -> bool {
628 self.soundness == SoundnessStatus::Sound
629 }
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct Contradiction {
635 pub statement_a: String,
637 pub statement_b: String,
639 pub contradiction_type: ContradictionType,
641 pub explanation: String,
643}
644
645#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
647pub enum ContradictionType {
648 DirectNegation,
650 MutualExclusion,
652 Implicit,
654}
655
656pub struct LaserLogic {
668 config: ThinkToolModuleConfig,
670 analysis_config: LaserLogicConfig,
672}
673
674impl Default for LaserLogic {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680impl LaserLogic {
681 pub fn new() -> Self {
683 Self {
684 config: ThinkToolModuleConfig {
685 name: "LaserLogic".to_string(),
686 version: "3.0.0".to_string(),
687 description: "Precision deductive reasoning with fallacy detection".to_string(),
688 confidence_weight: 0.25,
689 },
690 analysis_config: LaserLogicConfig::default(),
691 }
692 }
693
694 pub fn with_config(analysis_config: LaserLogicConfig) -> Self {
696 Self {
697 config: ThinkToolModuleConfig {
698 name: "LaserLogic".to_string(),
699 version: "3.0.0".to_string(),
700 description: "Precision deductive reasoning with fallacy detection".to_string(),
701 confidence_weight: 0.25,
702 },
703 analysis_config,
704 }
705 }
706
707 pub fn analysis_config(&self) -> &LaserLogicConfig {
709 &self.analysis_config
710 }
711
712 pub fn analyze_argument(
714 &self,
715 premises: &[&str],
716 conclusion: &str,
717 ) -> Result<LaserLogicResult> {
718 let argument = Argument::new(premises.to_vec(), conclusion);
719 self.analyze(argument)
720 }
721
722 pub fn analyze(&self, argument: Argument) -> Result<LaserLogicResult> {
724 if argument.premises.is_empty() {
726 return Err(Error::validation("Argument must have at least one premise"));
727 }
728
729 if argument.premises.len() > self.analysis_config.max_premise_depth {
730 return Err(Error::validation(format!(
731 "Too many premises ({} > {})",
732 argument.premises.len(),
733 self.analysis_config.max_premise_depth
734 )));
735 }
736
737 let mut result = LaserLogicResult::new(argument.clone());
738
739 result
741 .reasoning_steps
742 .push("Step 1: Identifying argument form...".to_string());
743 result.argument_form = self.detect_argument_form(&argument);
744 if let Some(form) = result.argument_form {
745 result
746 .reasoning_steps
747 .push(format!(" Detected: {}", form.description()));
748 }
749
750 if self.analysis_config.check_validity {
752 result
753 .reasoning_steps
754 .push("Step 2: Checking logical validity...".to_string());
755 let (validity, explanation) = self.check_validity(&argument);
756 result.validity = validity;
757 result.validity_explanation = explanation.clone();
758 result.reasoning_steps.push(format!(" {}", explanation));
759 }
760
761 if self.analysis_config.detect_fallacies {
763 result
764 .reasoning_steps
765 .push("Step 3: Scanning for fallacies...".to_string());
766 let fallacies = self.detect_fallacies(&argument);
767 for fallacy in &fallacies {
768 result.reasoning_steps.push(format!(
769 " Found: {} (confidence: {:.2})",
770 fallacy.fallacy.description(),
771 fallacy.confidence
772 ));
773 }
774 result.fallacies = fallacies;
775 }
776
777 if self.analysis_config.detect_contradictions {
779 result
780 .reasoning_steps
781 .push("Step 4: Checking for contradictions...".to_string());
782 result.contradictions = self.detect_contradictions(&argument);
783 if result.contradictions.is_empty() {
784 result
785 .reasoning_steps
786 .push(" No contradictions found.".to_string());
787 } else {
788 for contradiction in &result.contradictions {
789 result.reasoning_steps.push(format!(
790 " Contradiction: {} vs {} - {}",
791 contradiction.statement_a,
792 contradiction.statement_b,
793 contradiction.explanation
794 ));
795 }
796 }
797 }
798
799 if self.analysis_config.check_soundness {
801 result
802 .reasoning_steps
803 .push("Step 5: Evaluating soundness...".to_string());
804 let (soundness, explanation) = self.check_soundness(&argument, result.validity);
805 result.soundness = soundness;
806 result.soundness_explanation = explanation.clone();
807 result.reasoning_steps.push(format!(" {}", explanation));
808 }
809
810 result.confidence = self.calculate_confidence(&result);
812
813 result.suggestions = self.generate_suggestions(&result);
815
816 Ok(result)
817 }
818
819 fn clean_term(term: &str) -> String {
821 term.trim()
822 .trim_matches(|c: char| c.is_ascii_punctuation())
823 .trim()
824 .to_string()
825 }
826
827 fn detect_argument_form(&self, argument: &Argument) -> Option<ArgumentForm> {
829 let has_conditional = argument
831 .premises
832 .iter()
833 .any(|p| p.premise_type == PremiseType::Conditional);
834
835 let has_disjunctive = argument
836 .premises
837 .iter()
838 .any(|p| p.premise_type == PremiseType::Disjunctive);
839
840 let has_universal = argument
841 .premises
842 .iter()
843 .any(|p| p.premise_type == PremiseType::Universal);
844
845 if has_conditional && argument.premises.len() >= 2 {
847 let conditional = argument
849 .premises
850 .iter()
851 .find(|p| p.premise_type == PremiseType::Conditional);
852
853 if let Some(cond) = conditional {
854 let cond_lower = cond.statement.to_lowercase();
855 if cond_lower.starts_with("if ") {
856 if let Some(then_idx) = cond_lower.find(" then ") {
858 let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
860
861 let affirms_antecedent = argument.premises.iter().any(|p| {
863 let p_lower = p.statement.to_lowercase();
864 let p_clean = Self::clean_term(&p_lower);
865 p.premise_type != PremiseType::Conditional
866 && (p_clean == antecedent || p_clean.contains(&antecedent))
867 });
868
869 let denies_antecedent = argument.premises.iter().any(|p| {
871 let p_lower = p.statement.to_lowercase();
872 (p_lower.contains("not ") || p_lower.starts_with("no "))
873 && p_lower.contains(&antecedent)
874 });
875
876 if affirms_antecedent {
877 return Some(ArgumentForm::ModusPonens);
878 }
879 if denies_antecedent {
880 return Some(ArgumentForm::Unknown); }
883 }
884 }
885 }
886 }
887
888 if has_disjunctive && argument.premises.len() >= 2 {
890 let has_negation = argument.premises.iter().any(|p| {
891 p.premise_type == PremiseType::Negative
892 || p.statement.to_lowercase().contains("not ")
893 });
894
895 if has_negation {
896 return Some(ArgumentForm::DisjunctiveSyllogism);
897 }
898 }
899
900 if has_universal && argument.premises.len() >= 2 {
903 let universal_count = argument
904 .premises
905 .iter()
906 .filter(|p| p.premise_type == PremiseType::Universal)
907 .count();
908
909 if universal_count >= 2 {
911 return Some(ArgumentForm::CategoricalSyllogism);
912 }
913
914 let singular_count = argument
917 .premises
918 .iter()
919 .filter(|p| p.premise_type == PremiseType::Singular)
920 .count();
921
922 if universal_count >= 1 && singular_count >= 1 {
923 let universal_premise = argument
926 .premises
927 .iter()
928 .find(|p| p.premise_type == PremiseType::Universal)?;
929
930 let singular_premise = argument
931 .premises
932 .iter()
933 .find(|p| p.premise_type == PremiseType::Singular)?;
934
935 let univ_lower = universal_premise.statement.to_lowercase();
937 if univ_lower.starts_with("all ") {
938 if let Some(are_idx) = univ_lower.find(" are ") {
939 let subject_term = Self::clean_term(&univ_lower[4..are_idx]);
940
941 let sing_lower = singular_premise.statement.to_lowercase();
944 let subject_stem = subject_term.trim_end_matches('s');
945
946 if sing_lower.contains(&subject_term) || sing_lower.contains(subject_stem) {
947 return Some(ArgumentForm::CategoricalSyllogism);
948 }
949 }
950 }
951 }
952 }
953
954 None
955 }
956
957 fn check_validity(&self, argument: &Argument) -> (ValidityStatus, String) {
959 if let Some(form) = self.detect_argument_form(argument) {
961 if form.is_valid_form() {
962 return (
963 ValidityStatus::Valid,
964 format!("Argument follows valid {} pattern", form.description()),
965 );
966 }
967 }
968
969 let fallacies = self.detect_formal_fallacies(argument);
971 if !fallacies.is_empty() {
972 let fallacy_names: Vec<_> = fallacies.iter().map(|f| f.fallacy.pattern()).collect();
973 return (
974 ValidityStatus::Invalid,
975 format!(
976 "Argument contains formal fallacy: {}",
977 fallacy_names.join(", ")
978 ),
979 );
980 }
981
982 (
984 ValidityStatus::Undetermined,
985 "Argument structure too complex for automated validation. Manual review recommended."
986 .to_string(),
987 )
988 }
989
990 fn detect_formal_fallacies(&self, argument: &Argument) -> Vec<DetectedFallacy> {
992 let mut fallacies = Vec::new();
993
994 if let Some(fallacy) = self.check_affirming_consequent(argument) {
996 fallacies.push(fallacy);
997 }
998
999 if let Some(fallacy) = self.check_denying_antecedent(argument) {
1001 fallacies.push(fallacy);
1002 }
1003
1004 if let Some(fallacy) = self.check_undistributed_middle(argument) {
1006 fallacies.push(fallacy);
1007 }
1008
1009 fallacies
1010 }
1011
1012 fn detect_fallacies(&self, argument: &Argument) -> Vec<DetectedFallacy> {
1014 let mut fallacies = self.detect_formal_fallacies(argument);
1015
1016 if let Some(fallacy) = self.check_circular_reasoning(argument) {
1018 fallacies.push(fallacy);
1019 }
1020
1021 if let Some(fallacy) = self.check_false_dichotomy(argument) {
1023 fallacies.push(fallacy);
1024 }
1025
1026 if let Some(fallacy) = self.check_non_sequitur(argument) {
1028 fallacies.push(fallacy);
1029 }
1030
1031 fallacies
1032 }
1033
1034 fn check_affirming_consequent(&self, argument: &Argument) -> Option<DetectedFallacy> {
1036 let conditional = argument
1038 .premises
1039 .iter()
1040 .enumerate()
1041 .find(|(_, p)| p.premise_type == PremiseType::Conditional)?;
1042
1043 let cond_lower = conditional.1.statement.to_lowercase();
1044
1045 let then_idx = cond_lower.find(" then ")?;
1047 let consequent = Self::clean_term(&cond_lower[then_idx + 6..]);
1048
1049 let affirming_premise = argument.premises.iter().enumerate().find(|(i, p)| {
1051 *i != conditional.0 && p.premise_type != PremiseType::Conditional && {
1052 let p_clean = Self::clean_term(&p.statement.to_lowercase());
1053 p_clean.contains(&consequent) || consequent.contains(&p_clean)
1054 }
1055 });
1056
1057 if affirming_premise.is_some() {
1058 let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
1060 let conclusion_clean = Self::clean_term(&argument.conclusion.to_lowercase());
1061 if conclusion_clean.contains(&antecedent)
1062 || antecedent.contains(&conclusion_clean)
1063 || Self::terms_match(&conclusion_clean, &antecedent)
1064 {
1065 return Some(
1066 DetectedFallacy::new(
1067 Fallacy::AffirmingConsequent,
1068 0.85,
1069 "Argument affirms the consequent of a conditional to conclude the antecedent",
1070 )
1071 .with_premises(vec![conditional.0])
1072 .with_suggestion("Consider other possible causes for the consequent being true"),
1073 );
1074 }
1075 }
1076
1077 None
1078 }
1079
1080 fn terms_match(a: &str, b: &str) -> bool {
1082 if a == b {
1083 return true;
1084 }
1085
1086 let a_words: Vec<&str> = a.split_whitespace().collect();
1088 let b_words: Vec<&str> = b.split_whitespace().collect();
1089
1090 if a_words.len() == b_words.len() {
1091 let mut matches = 0;
1092 for (aw, bw) in a_words.iter().zip(b_words.iter()) {
1093 if aw == bw {
1094 matches += 1;
1095 } else {
1096 let aw_stem = aw.trim_end_matches("ed").trim_end_matches('s');
1098 let bw_stem = bw.trim_end_matches("ed").trim_end_matches('s');
1099 if aw_stem == bw_stem {
1100 matches += 1;
1101 }
1102 }
1103 }
1104 return matches == a_words.len();
1105 }
1106
1107 false
1108 }
1109
1110 fn check_denying_antecedent(&self, argument: &Argument) -> Option<DetectedFallacy> {
1112 let conditional = argument
1114 .premises
1115 .iter()
1116 .enumerate()
1117 .find(|(_, p)| p.premise_type == PremiseType::Conditional)?;
1118
1119 let cond_lower = conditional.1.statement.to_lowercase();
1120 let then_idx = cond_lower.find(" then ")?;
1121 let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
1122 let consequent = Self::clean_term(&cond_lower[then_idx + 6..]);
1123
1124 let denying_premise = argument.premises.iter().enumerate().find(|(i, p)| {
1126 *i != conditional.0
1127 && p.statement.to_lowercase().contains(&antecedent)
1128 && (p.statement.to_lowercase().contains("not ")
1129 || p.statement.to_lowercase().starts_with("no "))
1130 });
1131
1132 if denying_premise.is_some() {
1133 if argument.conclusion.to_lowercase().contains(&consequent)
1135 && (argument.conclusion.to_lowercase().contains("not ")
1136 || argument.conclusion.to_lowercase().starts_with("no "))
1137 {
1138 return Some(
1139 DetectedFallacy::new(
1140 Fallacy::DenyingAntecedent,
1141 0.85,
1142 "Argument denies the antecedent to conclude the negation of the consequent",
1143 )
1144 .with_premises(vec![conditional.0])
1145 .with_suggestion("The consequent might still be true for other reasons"),
1146 );
1147 }
1148 }
1149
1150 None
1151 }
1152
1153 fn check_undistributed_middle(&self, argument: &Argument) -> Option<DetectedFallacy> {
1155 let universals: Vec<_> = argument
1157 .premises
1158 .iter()
1159 .enumerate()
1160 .filter(|(_, p)| p.premise_type == PremiseType::Universal)
1161 .collect();
1162
1163 if universals.len() < 2 {
1164 return None;
1165 }
1166
1167 let mut terms: Vec<HashSet<String>> = Vec::new();
1169 for (_, premise) in &universals {
1170 let lower = premise.statement.to_lowercase();
1171 if lower.starts_with("all ") {
1172 if let Some(are_idx) = lower.find(" are ") {
1173 let subject = Self::clean_term(&lower[4..are_idx]);
1174 let predicate = Self::clean_term(&lower[are_idx + 5..]);
1175 let mut term_set = HashSet::new();
1176 term_set.insert(subject);
1177 term_set.insert(predicate);
1178 terms.push(term_set);
1179 }
1180 }
1181 }
1182
1183 if terms.len() >= 2 {
1184 let intersection: HashSet<_> = terms[0].intersection(&terms[1]).cloned().collect();
1186
1187 if intersection.is_empty() {
1188 return Some(
1190 DetectedFallacy::new(
1191 Fallacy::UndistributedMiddle,
1192 0.75,
1193 "The middle term may not be properly distributed across premises",
1194 )
1195 .with_premises(vec![universals[0].0, universals[1].0])
1196 .with_suggestion(
1197 "Ensure the middle term is distributed in at least one premise",
1198 ),
1199 );
1200 }
1201 }
1202
1203 None
1204 }
1205
1206 fn check_circular_reasoning(&self, argument: &Argument) -> Option<DetectedFallacy> {
1208 let conclusion_lower = argument.conclusion.to_lowercase();
1209 let conclusion_words: HashSet<_> = conclusion_lower
1210 .split_whitespace()
1211 .filter(|w| w.len() > 3)
1212 .collect();
1213
1214 if conclusion_words.len() < 2 {
1217 return None;
1218 }
1219
1220 for (idx, premise) in argument.premises.iter().enumerate() {
1221 let premise_lower = premise.statement.to_lowercase();
1222 let premise_words: HashSet<_> = premise_lower
1223 .split_whitespace()
1224 .filter(|w| w.len() > 3)
1225 .collect();
1226
1227 let overlap = conclusion_words.intersection(&premise_words).count();
1230 let conclusion_len = conclusion_words.len();
1231
1232 let conclusion_overlap_ratio = overlap as f64 / conclusion_len as f64;
1236
1237 if overlap >= 2 && conclusion_overlap_ratio >= 0.8 {
1239 return Some(
1240 DetectedFallacy::new(
1241 Fallacy::CircularReasoning,
1242 0.7,
1243 "Premise and conclusion appear to state essentially the same thing",
1244 )
1245 .with_premises(vec![idx])
1246 .with_suggestion(
1247 "Provide independent evidence that doesn't restate the conclusion",
1248 ),
1249 );
1250 }
1251 }
1252
1253 None
1254 }
1255
1256 fn check_false_dichotomy(&self, argument: &Argument) -> Option<DetectedFallacy> {
1258 for (idx, premise) in argument.premises.iter().enumerate() {
1260 let lower = premise.statement.to_lowercase();
1261
1262 if (lower.contains("either ") && lower.contains(" or ")) || lower.contains(" only ") {
1263 let or_count = lower.matches(" or ").count();
1265 if or_count == 1 {
1266 return Some(
1267 DetectedFallacy::new(
1268 Fallacy::FalseDichotomy,
1269 0.6,
1270 "Argument presents only two options when more may exist",
1271 )
1272 .with_premises(vec![idx])
1273 .with_suggestion("Consider whether additional alternatives exist"),
1274 );
1275 }
1276 }
1277 }
1278
1279 None
1280 }
1281
1282 fn check_non_sequitur(&self, argument: &Argument) -> Option<DetectedFallacy> {
1284 let conclusion_words: HashSet<_> = argument
1286 .conclusion
1287 .to_lowercase()
1288 .split_whitespace()
1289 .filter(|w| w.len() > 4)
1290 .map(|s| s.to_string())
1291 .collect();
1292
1293 let mut any_overlap = false;
1294 for premise in &argument.premises {
1295 let premise_words: HashSet<_> = premise
1296 .statement
1297 .to_lowercase()
1298 .split_whitespace()
1299 .filter(|w| w.len() > 4)
1300 .map(|s| s.to_string())
1301 .collect();
1302
1303 if !conclusion_words.is_disjoint(&premise_words) {
1304 any_overlap = true;
1305 break;
1306 }
1307 }
1308
1309 if !any_overlap && !conclusion_words.is_empty() {
1310 return Some(
1311 DetectedFallacy::new(
1312 Fallacy::NonSequitur,
1313 0.5,
1314 "Conclusion appears disconnected from the premises",
1315 )
1316 .with_suggestion(
1317 "Establish a clear logical connection between premises and conclusion",
1318 ),
1319 );
1320 }
1321
1322 None
1323 }
1324
1325 fn detect_contradictions(&self, argument: &Argument) -> Vec<Contradiction> {
1327 let mut contradictions = Vec::new();
1328
1329 for (i, p1) in argument.premises.iter().enumerate() {
1331 for (j, p2) in argument.premises.iter().enumerate().skip(i + 1) {
1332 if let Some(contradiction) = self.check_contradiction(&p1.statement, &p2.statement)
1333 {
1334 contradictions.push(Contradiction {
1335 statement_a: format!("Premise {}", i + 1),
1336 statement_b: format!("Premise {}", j + 1),
1337 contradiction_type: contradiction.0,
1338 explanation: contradiction.1,
1339 });
1340 }
1341 }
1342 }
1343
1344 for (i, premise) in argument.premises.iter().enumerate() {
1346 if let Some(contradiction) =
1347 self.check_contradiction(&premise.statement, &argument.conclusion)
1348 {
1349 contradictions.push(Contradiction {
1350 statement_a: format!("Premise {}", i + 1),
1351 statement_b: "Conclusion".to_string(),
1352 contradiction_type: contradiction.0,
1353 explanation: contradiction.1,
1354 });
1355 }
1356 }
1357
1358 contradictions
1359 }
1360
1361 fn check_contradiction(&self, a: &str, b: &str) -> Option<(ContradictionType, String)> {
1363 let a_lower = a.to_lowercase();
1364 let b_lower = b.to_lowercase();
1365
1366 let negation_patterns = [
1368 ("is true", "is false"),
1369 ("exists", "does not exist"),
1370 ("is valid", "is invalid"),
1371 ("should", "should not"),
1372 ("must", "must not"),
1373 ("always", "never"),
1374 ("all", "none"),
1375 ("is ", "is not "),
1376 ("can ", "cannot "),
1377 ("will ", "will not "),
1378 ];
1379
1380 for (pos, neg) in &negation_patterns {
1381 if (a_lower.contains(pos) && b_lower.contains(neg))
1382 || (a_lower.contains(neg) && b_lower.contains(pos))
1383 {
1384 if self.have_subject_overlap(&a_lower, &b_lower) {
1386 return Some((
1387 ContradictionType::DirectNegation,
1388 format!(
1389 "Direct contradiction detected: one states '{}' while other states '{}'",
1390 pos, neg
1391 ),
1392 ));
1393 }
1394 }
1395 }
1396
1397 None
1398 }
1399
1400 fn have_subject_overlap(&self, a: &str, b: &str) -> bool {
1402 let stopwords: HashSet<&str> = [
1403 "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has",
1404 "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "must",
1405 "shall", "can", "need", "dare", "ought", "used", "to", "of", "in", "for", "on", "with",
1406 "at", "by", "from", "as", "into", "through", "during", "before", "after", "above",
1407 "below", "between", "under", "again", "further", "then", "once", "that", "this",
1408 "these", "those", "not", "no", "nor", "and", "but", "or", "if", "while", "because",
1409 "until", "although", "since", "when", "where", "why", "how", "all", "each", "every",
1410 "both", "few", "more", "most", "other", "some", "such", "only", "own", "same", "so",
1411 "than", "too", "very", "just", "true", "false",
1412 ]
1413 .iter()
1414 .cloned()
1415 .collect();
1416
1417 let words_a: HashSet<&str> = a
1419 .split_whitespace()
1420 .filter(|w| !stopwords.contains(w) && !w.is_empty())
1421 .collect();
1422
1423 let words_b: HashSet<&str> = b
1424 .split_whitespace()
1425 .filter(|w| !stopwords.contains(w) && !w.is_empty())
1426 .collect();
1427
1428 if words_a.is_empty() || words_b.is_empty() {
1429 return true;
1433 }
1434
1435 let overlap = words_a.intersection(&words_b).count();
1436
1437 overlap > 0
1439 }
1440
1441 fn check_soundness(
1443 &self,
1444 argument: &Argument,
1445 validity: ValidityStatus,
1446 ) -> (SoundnessStatus, String) {
1447 if validity != ValidityStatus::Valid {
1448 return (
1449 SoundnessStatus::Unsound,
1450 "Argument is invalid, therefore unsound".to_string(),
1451 );
1452 }
1453
1454 let avg_confidence: f64 = argument.premises.iter().map(|p| p.confidence).sum::<f64>()
1456 / argument.premises.len() as f64;
1457
1458 if avg_confidence >= self.analysis_config.confidence_threshold {
1459 (
1460 SoundnessStatus::Sound,
1461 format!(
1462 "Argument is valid and premises have high confidence ({:.0}%)",
1463 avg_confidence * 100.0
1464 ),
1465 )
1466 } else if avg_confidence >= 0.5 {
1467 (
1468 SoundnessStatus::Undetermined,
1469 format!(
1470 "Argument is valid but premise truth is uncertain ({:.0}% confidence)",
1471 avg_confidence * 100.0
1472 ),
1473 )
1474 } else {
1475 (
1476 SoundnessStatus::Unsound,
1477 format!(
1478 "Argument is valid but premises have low confidence ({:.0}%)",
1479 avg_confidence * 100.0
1480 ),
1481 )
1482 }
1483 }
1484
1485 fn calculate_confidence(&self, result: &LaserLogicResult) -> f64 {
1487 let mut confidence = 0.7; if result.argument_form.is_some() {
1491 confidence += 0.1;
1492 }
1493
1494 if result.validity != ValidityStatus::Undetermined {
1496 confidence += 0.1;
1497 }
1498
1499 let fallacy_penalty = result.fallacies.len() as f64 * 0.05;
1501 confidence -= fallacy_penalty.min(0.2);
1502
1503 if !result.contradictions.is_empty() {
1505 confidence -= 0.15;
1506 }
1507
1508 confidence.clamp(0.0, 1.0)
1509 }
1510
1511 fn generate_suggestions(&self, result: &LaserLogicResult) -> Vec<String> {
1513 let mut suggestions = Vec::new();
1514
1515 if result.validity == ValidityStatus::Invalid {
1517 suggestions.push(
1518 "Restructure the argument to ensure the conclusion follows logically from the premises."
1519 .to_string(),
1520 );
1521 }
1522
1523 for fallacy in &result.fallacies {
1525 if !fallacy.suggestion.is_empty() {
1526 suggestions.push(fallacy.suggestion.clone());
1527 }
1528 }
1529
1530 if !result.contradictions.is_empty() {
1532 suggestions.push(
1533 "Resolve contradictions by revising conflicting premises or clarifying terms."
1534 .to_string(),
1535 );
1536 }
1537
1538 if result.soundness == SoundnessStatus::Undetermined {
1540 suggestions
1541 .push("Provide additional evidence to support premise truth claims.".to_string());
1542 }
1543
1544 if result.argument_form.is_none() {
1546 suggestions.push(
1547 "Consider restructuring into a standard argument form (modus ponens, syllogism, etc.) for clarity."
1548 .to_string(),
1549 );
1550 }
1551
1552 suggestions.sort();
1554 suggestions.dedup();
1555
1556 suggestions
1557 }
1558}
1559
1560impl ThinkToolModule for LaserLogic {
1561 fn config(&self) -> &ThinkToolModuleConfig {
1562 &self.config
1563 }
1564
1565 fn execute(&self, context: &ThinkToolContext) -> Result<ThinkToolOutput> {
1566 let query = &context.query;
1571
1572 let (premises, conclusion) = self.parse_argument_from_query(query)?;
1574
1575 let result = self.analyze_argument(&premises, conclusion)?;
1577
1578 Ok(ThinkToolOutput {
1580 module: self.config.name.clone(),
1581 confidence: result.confidence,
1582 output: serde_json::json!({
1583 "validity": format!("{:?}", result.validity),
1584 "validity_explanation": result.validity_explanation,
1585 "soundness": format!("{:?}", result.soundness),
1586 "soundness_explanation": result.soundness_explanation,
1587 "argument_form": result.argument_form.map(|f| f.description()),
1588 "fallacies": result.fallacies.iter().map(|f| {
1589 serde_json::json!({
1590 "type": format!("{:?}", f.fallacy),
1591 "description": f.fallacy.description(),
1592 "pattern": f.fallacy.pattern(),
1593 "confidence": f.confidence,
1594 "evidence": f.evidence,
1595 "suggestion": f.suggestion
1596 })
1597 }).collect::<Vec<_>>(),
1598 "contradictions": result.contradictions.iter().map(|c| {
1599 serde_json::json!({
1600 "between": [c.statement_a, c.statement_b],
1601 "type": format!("{:?}", c.contradiction_type),
1602 "explanation": c.explanation
1603 })
1604 }).collect::<Vec<_>>(),
1605 "verdict": result.verdict(),
1606 "suggestions": result.suggestions,
1607 "reasoning_steps": result.reasoning_steps
1608 }),
1609 })
1610 }
1611}
1612
1613impl LaserLogic {
1614 fn parse_argument_from_query<'a>(&self, query: &'a str) -> Result<(Vec<&'a str>, &'a str)> {
1616 let query = query.trim();
1617
1618 let conclusion_markers = [
1620 "therefore,",
1621 "therefore",
1622 "hence,",
1623 "hence",
1624 "thus,",
1625 "thus",
1626 "so,",
1627 "consequently,",
1628 "consequently",
1629 "it follows that",
1630 "we can conclude",
1631 "which means",
1632 ];
1633
1634 for marker in &conclusion_markers {
1635 if let Some(idx) = query.to_lowercase().find(marker) {
1636 let premises_part = &query[..idx];
1637 let conclusion_start = idx + marker.len();
1638 let conclusion = query[conclusion_start..]
1639 .trim()
1640 .trim_start_matches(',')
1641 .trim();
1642
1643 let premises: Vec<&str> = premises_part
1645 .split(['.', ';'])
1646 .map(|s| s.trim())
1647 .filter(|s| !s.is_empty())
1648 .collect();
1649
1650 if !premises.is_empty() && !conclusion.is_empty() {
1651 return Ok((premises, conclusion));
1652 }
1653 }
1654 }
1655
1656 let sentences: Vec<&str> = query
1658 .split('.')
1659 .map(|s| s.trim())
1660 .filter(|s| !s.is_empty())
1661 .collect();
1662
1663 if sentences.len() >= 2 {
1664 let premises = &sentences[..sentences.len() - 1];
1665 let conclusion = sentences.last().unwrap();
1666 return Ok((premises.to_vec(), conclusion));
1667 }
1668
1669 Err(Error::validation(
1670 "Could not parse argument structure. Expected format: 'Premise 1. Premise 2. Therefore, Conclusion.' or similar.",
1671 ))
1672 }
1673}
1674
1675#[cfg(test)]
1680mod tests {
1681 use super::*;
1682
1683 #[test]
1684 fn test_config_default() {
1685 let config = LaserLogicConfig::default();
1686 assert!(config.detect_fallacies);
1687 assert!(config.check_validity);
1688 assert!(config.check_soundness);
1689 }
1690
1691 #[test]
1692 fn test_config_presets() {
1693 let quick = LaserLogicConfig::quick();
1694 assert!(!quick.check_soundness);
1695 assert_eq!(quick.max_premise_depth, 5);
1696
1697 let deep = LaserLogicConfig::deep();
1698 assert!(deep.check_soundness);
1699 assert!(deep.verbose_output);
1700
1701 let paranoid = LaserLogicConfig::paranoid();
1702 assert_eq!(paranoid.confidence_threshold, 0.9);
1703 }
1704
1705 #[test]
1706 fn test_premise_type_inference() {
1707 assert_eq!(
1708 Premise::new("All humans are mortal").premise_type,
1709 PremiseType::Universal
1710 );
1711 assert_eq!(
1712 Premise::new("Some birds can fly").premise_type,
1713 PremiseType::Particular
1714 );
1715 assert_eq!(
1716 Premise::new("If it rains, the ground is wet").premise_type,
1717 PremiseType::Conditional
1718 );
1719 assert_eq!(
1720 Premise::new("Either we go or we stay").premise_type,
1721 PremiseType::Disjunctive
1722 );
1723 assert_eq!(
1724 Premise::new("Socrates is human").premise_type,
1725 PremiseType::Singular
1726 );
1727 }
1728
1729 #[test]
1730 fn test_fallacy_descriptions() {
1731 let fallacy = Fallacy::AffirmingConsequent;
1732 assert!(fallacy.description().contains("Inferring"));
1733 assert!(fallacy.pattern().contains("INVALID"));
1734 assert!(fallacy.is_formal());
1735 assert_eq!(fallacy.severity(), 5);
1736 }
1737
1738 #[test]
1739 fn test_valid_modus_ponens() {
1740 let laser = LaserLogic::new();
1741 let result = laser
1742 .analyze_argument(
1743 &["If it rains, then the ground is wet", "It rains"],
1744 "The ground is wet",
1745 )
1746 .unwrap();
1747
1748 assert!(result.argument_form.is_some());
1750 assert!(!result.has_fallacies());
1751 }
1752
1753 #[test]
1754 fn test_affirming_consequent_detection() {
1755 let laser = LaserLogic::new();
1756 let result = laser
1757 .analyze_argument(
1758 &["If it rains, then the ground is wet", "The ground is wet"],
1759 "It rained",
1760 )
1761 .unwrap();
1762
1763 assert!(result.has_fallacies());
1765 assert!(result
1766 .fallacies
1767 .iter()
1768 .any(|f| f.fallacy == Fallacy::AffirmingConsequent));
1769 }
1770
1771 #[test]
1772 fn test_categorical_syllogism() {
1773 let laser = LaserLogic::new();
1774 let result = laser
1775 .analyze_argument(
1776 &["All humans are mortal", "All Greeks are humans"],
1777 "All Greeks are mortal",
1778 )
1779 .unwrap();
1780
1781 assert_eq!(
1782 result.argument_form,
1783 Some(ArgumentForm::CategoricalSyllogism)
1784 );
1785 }
1786
1787 #[test]
1788 fn test_circular_reasoning_detection() {
1789 let laser = LaserLogic::new();
1790 let result = laser
1791 .analyze_argument(
1792 &["The Bible is true because it is the word of God"],
1793 "The Bible is the word of God",
1794 )
1795 .unwrap();
1796
1797 assert!(result
1798 .fallacies
1799 .iter()
1800 .any(|f| f.fallacy == Fallacy::CircularReasoning));
1801 }
1802
1803 #[test]
1804 fn test_contradiction_detection() {
1805 let laser = LaserLogic::with_config(LaserLogicConfig::deep());
1806 let result = laser
1807 .analyze_argument(&["X is true", "X is false"], "Something follows")
1808 .unwrap();
1809
1810 assert!(!result.contradictions.is_empty());
1811 }
1812
1813 #[test]
1814 fn test_thinkmodule_trait() {
1815 let laser = LaserLogic::new();
1816 let config = laser.config();
1817 assert_eq!(config.name, "LaserLogic");
1818 assert_eq!(config.version, "3.0.0");
1819 }
1820
1821 #[test]
1822 fn test_execute_with_context() {
1823 let laser = LaserLogic::new();
1824 let context = ThinkToolContext {
1825 query: "All humans are mortal. Socrates is human. Therefore, Socrates is mortal."
1826 .to_string(),
1827 previous_steps: vec![],
1828 };
1829
1830 let output = laser.execute(&context).unwrap();
1831 assert_eq!(output.module, "LaserLogic");
1832 assert!(output.confidence > 0.0);
1833 }
1834
1835 #[test]
1836 fn test_verdict_generation() {
1837 let mut result = LaserLogicResult::new(Argument::new(vec!["premise"], "conclusion"));
1838 result.validity = ValidityStatus::Valid;
1839 result.soundness = SoundnessStatus::Sound;
1840 assert!(result.verdict().contains("SOUND"));
1841
1842 result.validity = ValidityStatus::Invalid;
1843 assert!(result.verdict().contains("INVALID"));
1844 }
1845
1846 #[test]
1847 fn test_argument_form_validity() {
1848 assert!(ArgumentForm::ModusPonens.is_valid_form());
1849 assert!(ArgumentForm::ModusTollens.is_valid_form());
1850 assert!(!ArgumentForm::Unknown.is_valid_form());
1851 }
1852
1853 #[test]
1854 fn test_empty_premises_error() {
1855 let laser = LaserLogic::new();
1856 let result = laser.analyze_argument(&[], "Some conclusion");
1857 assert!(result.is_err());
1858 }
1859
1860 #[test]
1861 fn test_too_many_premises_error() {
1862 let laser = LaserLogic::with_config(LaserLogicConfig {
1863 max_premise_depth: 2,
1864 ..Default::default()
1865 });
1866 let result = laser.analyze_argument(&["P1", "P2", "P3"], "Conclusion");
1867 assert!(result.is_err());
1868 }
1869}