use super::{ThinkToolContext, ThinkToolModule, ThinkToolModuleConfig, ThinkToolOutput};
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaserLogicConfig {
pub detect_fallacies: bool,
pub check_validity: bool,
pub check_soundness: bool,
pub max_premise_depth: usize,
pub analyze_syllogisms: bool,
pub detect_contradictions: bool,
pub confidence_threshold: f64,
pub verbose_output: bool,
}
impl Default for LaserLogicConfig {
fn default() -> Self {
Self {
detect_fallacies: true,
check_validity: true,
check_soundness: true,
max_premise_depth: 10,
analyze_syllogisms: true,
detect_contradictions: true,
confidence_threshold: 0.7,
verbose_output: false,
}
}
}
impl LaserLogicConfig {
pub fn quick() -> Self {
Self {
detect_fallacies: true,
check_validity: true,
check_soundness: false,
max_premise_depth: 5,
analyze_syllogisms: false,
detect_contradictions: false,
confidence_threshold: 0.6,
verbose_output: false,
}
}
pub fn deep() -> Self {
Self {
detect_fallacies: true,
check_validity: true,
check_soundness: true,
max_premise_depth: 20,
analyze_syllogisms: true,
detect_contradictions: true,
confidence_threshold: 0.8,
verbose_output: true,
}
}
pub fn paranoid() -> Self {
Self {
detect_fallacies: true,
check_validity: true,
check_soundness: true,
max_premise_depth: 50,
analyze_syllogisms: true,
detect_contradictions: true,
confidence_threshold: 0.9,
verbose_output: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VotingStrategy {
#[default]
Majority,
WeightedByConfidence,
Unanimous,
Plurality,
Threshold,
}
impl VotingStrategy {
pub fn description(&self) -> &'static str {
match self {
Self::Majority => "Most common answer wins (>50% required)",
Self::WeightedByConfidence => "Votes weighted by analysis confidence",
Self::Unanimous => "All samples must agree",
Self::Plurality => "Highest count wins (no minimum threshold)",
Self::Threshold => "Minimum agreement percentage required",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfConsistencyConfig {
pub num_samples: usize,
pub strategy: VotingStrategy,
pub diversity_factor: f64,
pub agreement_threshold: f64,
pub include_minority_opinions: bool,
pub confidence_weight: f64,
pub track_reasoning_chains: bool,
}
impl Default for SelfConsistencyConfig {
fn default() -> Self {
Self {
num_samples: 5, strategy: VotingStrategy::default(),
diversity_factor: 0.7,
agreement_threshold: 0.5,
include_minority_opinions: true,
confidence_weight: 1.0,
track_reasoning_chains: true,
}
}
}
impl SelfConsistencyConfig {
pub fn low_diversity() -> Self {
Self {
num_samples: 3,
diversity_factor: 0.3,
..Self::default()
}
}
pub fn high_diversity() -> Self {
Self {
num_samples: 10,
diversity_factor: 0.9,
include_minority_opinions: true,
..Self::default()
}
}
pub fn strict_consensus() -> Self {
Self {
num_samples: 5,
strategy: VotingStrategy::Unanimous,
agreement_threshold: 1.0,
..Self::default()
}
}
pub fn with_samples(mut self, k: usize) -> Self {
self.num_samples = k;
self
}
pub fn with_strategy(mut self, strategy: VotingStrategy) -> Self {
self.strategy = strategy;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
pub verdict: ValidityStatus,
pub confidence: f64,
pub reasoning_chain: Option<Vec<String>>,
pub sample_index: usize,
pub fallacies_detected: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelfConsistencyResult {
pub consensus_verdict: ValidityStatus,
pub agreement_ratio: f64,
pub total_samples: usize,
pub vote_counts: std::collections::HashMap<String, usize>,
pub aggregated_confidence: f64,
pub votes: Vec<Vote>,
pub consensus_reached: bool,
pub minority_opinions: Vec<(ValidityStatus, usize)>,
pub strategy_used: VotingStrategy,
}
impl SelfConsistencyResult {
pub fn new(strategy: VotingStrategy) -> Self {
Self {
consensus_verdict: ValidityStatus::Undetermined,
agreement_ratio: 0.0,
total_samples: 0,
vote_counts: std::collections::HashMap::new(),
aggregated_confidence: 0.0,
votes: Vec::new(),
consensus_reached: false,
minority_opinions: Vec::new(),
strategy_used: strategy,
}
}
pub fn verdict_string(&self) -> &'static str {
match self.consensus_verdict {
ValidityStatus::Valid => "VALID",
ValidityStatus::Invalid => "INVALID",
ValidityStatus::Undetermined => "UNDETERMINED",
}
}
pub fn is_strong_consensus(&self) -> bool {
self.agreement_ratio >= 0.8
}
pub fn summary(&self) -> String {
format!(
"Consensus: {} ({:.1}% agreement, {} samples, {})",
self.verdict_string(),
self.agreement_ratio * 100.0,
self.total_samples,
if self.consensus_reached {
"consensus reached"
} else {
"no consensus"
}
)
}
}
pub struct VotingAggregator {
config: SelfConsistencyConfig,
votes: Vec<Vote>,
}
impl VotingAggregator {
pub fn new(config: SelfConsistencyConfig) -> Self {
Self {
config,
votes: Vec::new(),
}
}
pub fn add_vote(
&mut self,
verdict: ValidityStatus,
confidence: f64,
reasoning: Option<Vec<String>>,
fallacy_count: usize,
) {
let sample_index = self.votes.len();
self.votes.push(Vote {
verdict,
confidence,
reasoning_chain: reasoning,
sample_index,
fallacies_detected: fallacy_count,
});
}
pub fn aggregate(self) -> SelfConsistencyResult {
let mut result = SelfConsistencyResult::new(self.config.strategy);
result.total_samples = self.votes.len();
if self.votes.is_empty() {
return result;
}
let mut valid_count = 0usize;
let mut invalid_count = 0usize;
let mut undetermined_count = 0usize;
let mut _valid_confidence_sum = 0.0;
let mut _invalid_confidence_sum = 0.0;
for vote in &self.votes {
match vote.verdict {
ValidityStatus::Valid => {
valid_count += 1;
_valid_confidence_sum += vote.confidence;
}
ValidityStatus::Invalid => {
invalid_count += 1;
_invalid_confidence_sum += vote.confidence;
}
ValidityStatus::Undetermined => {
undetermined_count += 1;
}
}
}
result.vote_counts.insert("valid".to_string(), valid_count);
result
.vote_counts
.insert("invalid".to_string(), invalid_count);
result
.vote_counts
.insert("undetermined".to_string(), undetermined_count);
let (winner, winner_count, consensus) = match self.config.strategy {
VotingStrategy::Majority => {
let total = self.votes.len();
let majority_threshold = total / 2 + 1;
if valid_count >= majority_threshold {
(ValidityStatus::Valid, valid_count, true)
} else if invalid_count >= majority_threshold {
(ValidityStatus::Invalid, invalid_count, true)
} else {
if valid_count > invalid_count {
(ValidityStatus::Valid, valid_count, false)
} else if invalid_count > valid_count {
(ValidityStatus::Invalid, invalid_count, false)
} else {
(ValidityStatus::Undetermined, undetermined_count, false)
}
}
}
VotingStrategy::WeightedByConfidence => {
let weight = self.config.confidence_weight;
let valid_weighted: f64 = self
.votes
.iter()
.filter(|v| v.verdict == ValidityStatus::Valid)
.map(|v| v.confidence.powf(weight))
.sum();
let invalid_weighted: f64 = self
.votes
.iter()
.filter(|v| v.verdict == ValidityStatus::Invalid)
.map(|v| v.confidence.powf(weight))
.sum();
if valid_weighted > invalid_weighted {
(
ValidityStatus::Valid,
valid_count,
valid_weighted > invalid_weighted * 1.5,
)
} else if invalid_weighted > valid_weighted {
(
ValidityStatus::Invalid,
invalid_count,
invalid_weighted > valid_weighted * 1.5,
)
} else {
(ValidityStatus::Undetermined, undetermined_count, false)
}
}
VotingStrategy::Unanimous => {
if valid_count == self.votes.len() {
(ValidityStatus::Valid, valid_count, true)
} else if invalid_count == self.votes.len() {
(ValidityStatus::Invalid, invalid_count, true)
} else {
(ValidityStatus::Undetermined, 0, false)
}
}
VotingStrategy::Plurality => {
if valid_count > invalid_count && valid_count > undetermined_count {
(ValidityStatus::Valid, valid_count, true)
} else if invalid_count > valid_count && invalid_count > undetermined_count {
(ValidityStatus::Invalid, invalid_count, true)
} else {
(
ValidityStatus::Undetermined,
undetermined_count,
undetermined_count > 0,
)
}
}
VotingStrategy::Threshold => {
let total = self.votes.len() as f64;
let threshold = self.config.agreement_threshold;
if (valid_count as f64 / total) >= threshold {
(ValidityStatus::Valid, valid_count, true)
} else if (invalid_count as f64 / total) >= threshold {
(ValidityStatus::Invalid, invalid_count, true)
} else {
if valid_count > invalid_count {
(ValidityStatus::Valid, valid_count, false)
} else if invalid_count > valid_count {
(ValidityStatus::Invalid, invalid_count, false)
} else {
(ValidityStatus::Undetermined, undetermined_count, false)
}
}
}
};
result.consensus_verdict = winner;
result.consensus_reached = consensus;
result.agreement_ratio = winner_count as f64 / self.votes.len() as f64;
let confidence_sum: f64 = self.votes.iter().map(|v| v.confidence).sum();
result.aggregated_confidence = if !self.votes.is_empty() {
confidence_sum / self.votes.len() as f64 * result.agreement_ratio
} else {
0.0
};
if self.config.include_minority_opinions {
let mut minorities = Vec::new();
if winner != ValidityStatus::Valid && valid_count > 0 {
minorities.push((ValidityStatus::Valid, valid_count));
}
if winner != ValidityStatus::Invalid && invalid_count > 0 {
minorities.push((ValidityStatus::Invalid, invalid_count));
}
if winner != ValidityStatus::Undetermined && undetermined_count > 0 {
minorities.push((ValidityStatus::Undetermined, undetermined_count));
}
result.minority_opinions = minorities;
}
if self.config.track_reasoning_chains {
result.votes = self.votes;
}
result
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Argument {
pub premises: Vec<Premise>,
pub conclusion: String,
pub form: Option<ArgumentForm>,
}
impl Argument {
pub fn new(premises: Vec<&str>, conclusion: &str) -> Self {
Self {
premises: premises.iter().map(|p| Premise::new(p)).collect(),
conclusion: conclusion.to_string(),
form: None,
}
}
pub fn with_premises(premises: Vec<Premise>, conclusion: &str) -> Self {
Self {
premises,
conclusion: conclusion.to_string(),
form: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Premise {
pub statement: String,
pub premise_type: PremiseType,
pub confidence: f64,
pub evidence: Vec<String>,
}
impl Premise {
pub fn new(statement: &str) -> Self {
let premise_type = Self::infer_type(statement);
Self {
statement: statement.to_string(),
premise_type,
confidence: 1.0,
evidence: Vec::new(),
}
}
pub fn with_type(statement: &str, premise_type: PremiseType, confidence: f64) -> Self {
Self {
statement: statement.to_string(),
premise_type,
confidence,
evidence: Vec::new(),
}
}
fn infer_type(statement: &str) -> PremiseType {
let lower = statement.to_lowercase();
if lower.starts_with("all ")
|| lower.starts_with("every ")
|| lower.starts_with("each ")
|| lower.contains(" always ")
|| lower.starts_with("no ")
{
return PremiseType::Universal;
}
if lower.starts_with("some ")
|| lower.starts_with("most ")
|| lower.starts_with("many ")
|| lower.starts_with("few ")
|| lower.contains(" sometimes ")
{
return PremiseType::Particular;
}
if lower.starts_with("if ")
|| lower.contains(" then ")
|| lower.contains(" implies ")
|| lower.contains(" only if ")
{
return PremiseType::Conditional;
}
if lower.contains(" or ") || lower.starts_with("either ") {
return PremiseType::Disjunctive;
}
if lower.starts_with("not ") || lower.contains(" not ") || lower.starts_with("no ") {
return PremiseType::Negative;
}
PremiseType::Singular
}
pub fn with_evidence(mut self, evidence: &str) -> Self {
self.evidence.push(evidence.to_string());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PremiseType {
Universal,
Particular,
Singular,
Conditional,
Disjunctive,
Negative,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ArgumentForm {
ModusPonens,
ModusTollens,
HypotheticalSyllogism,
DisjunctiveSyllogism,
CategoricalSyllogism,
ConstructiveDilemma,
DestructiveDilemma,
ReductioAdAbsurdum,
Unknown,
}
impl ArgumentForm {
pub fn description(&self) -> &'static str {
match self {
Self::ModusPonens => "Modus Ponens (affirming the antecedent)",
Self::ModusTollens => "Modus Tollens (denying the consequent)",
Self::HypotheticalSyllogism => "Hypothetical Syllogism (chain reasoning)",
Self::DisjunctiveSyllogism => "Disjunctive Syllogism (process of elimination)",
Self::CategoricalSyllogism => "Categorical Syllogism (term-based reasoning)",
Self::ConstructiveDilemma => "Constructive Dilemma (complex conditional)",
Self::DestructiveDilemma => "Destructive Dilemma (complex conditional)",
Self::ReductioAdAbsurdum => "Reductio Ad Absurdum (proof by contradiction)",
Self::Unknown => "Unknown or complex argument form",
}
}
pub fn is_valid_form(&self) -> bool {
!matches!(self, Self::Unknown)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Fallacy {
AffirmingConsequent,
DenyingAntecedent,
UndistributedMiddle,
IllicitMajor,
IllicitMinor,
FourTerms,
ExistentialFallacy,
AffirmingDisjunct,
CircularReasoning,
NonSequitur,
Composition,
Division,
PostHoc,
SlipperySlope,
FalseCause,
StrawMan,
FalseDichotomy,
Equivocation,
}
impl Fallacy {
pub fn description(&self) -> &'static str {
match self {
Self::AffirmingConsequent => {
"Inferring P from P->Q and Q. Just because the outcome occurred doesn't mean the specific cause happened."
}
Self::DenyingAntecedent => {
"Inferring ~Q from P->Q and ~P. The consequent might be true for other reasons."
}
Self::UndistributedMiddle => {
"The middle term is not distributed in at least one premise of the syllogism."
}
Self::IllicitMajor => {
"The major term is distributed in the conclusion but not in the major premise."
}
Self::IllicitMinor => {
"The minor term is distributed in the conclusion but not in the minor premise."
}
Self::FourTerms => "Four distinct terms used where a syllogism requires exactly three.",
Self::ExistentialFallacy => {
"Drawing an existential conclusion from purely universal premises."
}
Self::AffirmingDisjunct => {
"Concluding ~Q from (P v Q) and P. With inclusive OR, both can be true."
}
Self::CircularReasoning => "The conclusion is essentially restated in the premises.",
Self::NonSequitur => "The conclusion does not logically follow from the premises.",
Self::Composition => "Assuming what's true of parts must be true of the whole.",
Self::Division => "Assuming what's true of the whole must be true of the parts.",
Self::PostHoc => "Assuming A caused B simply because A preceded B.",
Self::SlipperySlope => {
"Claiming that one event will inevitably lead to a chain of negative events."
}
Self::FalseCause => "Incorrectly identifying something as the cause.",
Self::StrawMan => "Misrepresenting someone's argument to make it easier to attack.",
Self::FalseDichotomy => "Presenting only two options when more exist.",
Self::Equivocation => {
"Using the same term with different meanings in different parts of the argument."
}
}
}
pub fn pattern(&self) -> &'static str {
match self {
Self::AffirmingConsequent => "P->Q, Q |- P (INVALID)",
Self::DenyingAntecedent => "P->Q, ~P |- ~Q (INVALID)",
Self::UndistributedMiddle => "All A are B, All C are B |- All A are C (INVALID)",
Self::IllicitMajor => "Major term undistributed in premise, distributed in conclusion",
Self::IllicitMinor => "Minor term undistributed in premise, distributed in conclusion",
Self::FourTerms => "A-B, C-D |- invalid (4 terms, not 3)",
Self::ExistentialFallacy => "All P are Q |- Some P are Q (INVALID if no P exist)",
Self::AffirmingDisjunct => "P v Q, P |- ~Q (INVALID for inclusive or)",
Self::CircularReasoning => "P |- P (trivially valid but uninformative)",
Self::NonSequitur => "Premises do not entail conclusion",
Self::Composition => "Part(x) is P |- Whole(x) is P (INVALID)",
Self::Division => "Whole(x) is P |- Part(x) is P (INVALID)",
Self::PostHoc => "A then B |- A caused B (INVALID)",
Self::SlipperySlope => "A -> B -> C -> ... -> Z (chain not established)",
Self::FalseCause => "Correlation or sequence |- Causation (INVALID)",
Self::StrawMan => "Argument(A') attacked instead of Argument(A)",
Self::FalseDichotomy => "P v Q presented where P v Q v R v ... exists",
Self::Equivocation => "Term T used as T1 and T2 (different meanings)",
}
}
pub fn severity(&self) -> u8 {
match self {
Self::AffirmingConsequent | Self::DenyingAntecedent => 5,
Self::UndistributedMiddle | Self::IllicitMajor | Self::IllicitMinor => 5,
Self::FourTerms | Self::ExistentialFallacy => 4,
Self::CircularReasoning | Self::NonSequitur => 5,
Self::Composition | Self::Division => 3,
Self::PostHoc | Self::FalseCause | Self::SlipperySlope => 4,
Self::StrawMan | Self::FalseDichotomy | Self::Equivocation => 4,
Self::AffirmingDisjunct => 4,
}
}
pub fn is_formal(&self) -> bool {
matches!(
self,
Self::AffirmingConsequent
| Self::DenyingAntecedent
| Self::UndistributedMiddle
| Self::IllicitMajor
| Self::IllicitMinor
| Self::FourTerms
| Self::ExistentialFallacy
| Self::AffirmingDisjunct
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectedFallacy {
pub fallacy: Fallacy,
pub confidence: f64,
pub evidence: String,
pub involved_premises: Vec<usize>,
pub suggestion: String,
}
impl DetectedFallacy {
pub fn new(fallacy: Fallacy, confidence: f64, evidence: &str) -> Self {
Self {
fallacy,
confidence,
evidence: evidence.to_string(),
involved_premises: Vec::new(),
suggestion: String::new(),
}
}
pub fn with_premises(mut self, premises: Vec<usize>) -> Self {
self.involved_premises = premises;
self
}
pub fn with_suggestion(mut self, suggestion: &str) -> Self {
self.suggestion = suggestion.to_string();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidityStatus {
Valid,
Invalid,
Undetermined,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SoundnessStatus {
Sound,
Unsound,
Undetermined,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaserLogicResult {
pub argument: Argument,
pub validity: ValidityStatus,
pub validity_explanation: String,
pub soundness: SoundnessStatus,
pub soundness_explanation: String,
pub fallacies: Vec<DetectedFallacy>,
pub argument_form: Option<ArgumentForm>,
pub confidence: f64,
pub reasoning_steps: Vec<String>,
pub suggestions: Vec<String>,
pub contradictions: Vec<Contradiction>,
}
impl LaserLogicResult {
fn new(argument: Argument) -> Self {
Self {
argument,
validity: ValidityStatus::Undetermined,
validity_explanation: String::new(),
soundness: SoundnessStatus::Undetermined,
soundness_explanation: String::new(),
fallacies: Vec::new(),
argument_form: None,
confidence: 0.0,
reasoning_steps: Vec::new(),
suggestions: Vec::new(),
contradictions: Vec::new(),
}
}
pub fn verdict(&self) -> &'static str {
match (self.validity, self.soundness) {
(ValidityStatus::Valid, SoundnessStatus::Sound) => {
"SOUND: Argument is valid with true premises"
}
(ValidityStatus::Valid, SoundnessStatus::Unsound) => {
"VALID BUT UNSOUND: Logic correct, but premises questionable"
}
(ValidityStatus::Valid, SoundnessStatus::Undetermined) => {
"VALID: Logic correct, premises unverified"
}
(ValidityStatus::Invalid, _) => "INVALID: Conclusion does not follow from premises",
(ValidityStatus::Undetermined, _) => "UNDETERMINED: Could not fully analyze",
}
}
pub fn has_fallacies(&self) -> bool {
!self.fallacies.is_empty()
}
pub fn most_severe_fallacy(&self) -> Option<&DetectedFallacy> {
self.fallacies
.iter()
.max_by_key(|f| (f.fallacy.severity(), (f.confidence * 100.0) as u32))
}
pub fn is_valid(&self) -> bool {
self.validity == ValidityStatus::Valid
}
pub fn is_sound(&self) -> bool {
self.soundness == SoundnessStatus::Sound
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contradiction {
pub statement_a: String,
pub statement_b: String,
pub contradiction_type: ContradictionType,
pub explanation: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContradictionType {
DirectNegation,
MutualExclusion,
Implicit,
}
pub struct LaserLogic {
config: ThinkToolModuleConfig,
analysis_config: LaserLogicConfig,
}
impl Default for LaserLogic {
fn default() -> Self {
Self::new()
}
}
impl LaserLogic {
pub fn new() -> Self {
Self {
config: ThinkToolModuleConfig {
name: "LaserLogic".to_string(),
version: "3.0.0".to_string(),
description: "Precision deductive reasoning with fallacy detection".to_string(),
confidence_weight: 0.25,
},
analysis_config: LaserLogicConfig::default(),
}
}
pub fn with_config(analysis_config: LaserLogicConfig) -> Self {
Self {
config: ThinkToolModuleConfig {
name: "LaserLogic".to_string(),
version: "3.0.0".to_string(),
description: "Precision deductive reasoning with fallacy detection".to_string(),
confidence_weight: 0.25,
},
analysis_config,
}
}
pub fn analysis_config(&self) -> &LaserLogicConfig {
&self.analysis_config
}
pub fn analyze_argument(
&self,
premises: &[&str],
conclusion: &str,
) -> Result<LaserLogicResult> {
let argument = Argument::new(premises.to_vec(), conclusion);
self.analyze(argument)
}
pub fn analyze_with_self_consistency(
&self,
premises: &[&str],
conclusion: &str,
sc_config: &SelfConsistencyConfig,
) -> Result<SelfConsistencyResult> {
let argument = Argument::new(premises.to_vec(), conclusion);
self.analyze_self_consistent(argument, sc_config)
}
pub fn analyze_self_consistent(
&self,
argument: Argument,
sc_config: &SelfConsistencyConfig,
) -> Result<SelfConsistencyResult> {
if argument.premises.is_empty() {
return Err(Error::validation("Argument must have at least one premise"));
}
let mut aggregator = VotingAggregator::new(sc_config.clone());
for sample_idx in 0..sc_config.num_samples {
let mut varied_config = self.analysis_config.clone();
if sc_config.diversity_factor > 0.0 {
let variation = (sample_idx as f64 / sc_config.num_samples as f64)
* sc_config.diversity_factor
* 0.2;
varied_config.confidence_threshold =
(varied_config.confidence_threshold - 0.1 + variation).clamp(0.1, 0.95);
}
let varied_analyzer = LaserLogic::with_config(varied_config);
match varied_analyzer.analyze(argument.clone()) {
Ok(result) => {
let reasoning = if sc_config.track_reasoning_chains {
Some(result.reasoning_steps.clone())
} else {
None
};
aggregator.add_vote(
result.validity,
result.confidence,
reasoning,
result.fallacies.len(),
);
}
Err(_) => {
aggregator.add_vote(ValidityStatus::Undetermined, 0.0, None, 0);
}
}
}
Ok(aggregator.aggregate())
}
pub fn analyze(&self, argument: Argument) -> Result<LaserLogicResult> {
if argument.premises.is_empty() {
return Err(Error::validation("Argument must have at least one premise"));
}
if argument.premises.len() > self.analysis_config.max_premise_depth {
return Err(Error::validation(format!(
"Too many premises ({} > {})",
argument.premises.len(),
self.analysis_config.max_premise_depth
)));
}
let mut result = LaserLogicResult::new(argument.clone());
result
.reasoning_steps
.push("Step 1: Identifying argument form...".to_string());
result.argument_form = self.detect_argument_form(&argument);
if let Some(form) = result.argument_form {
result
.reasoning_steps
.push(format!(" Detected: {}", form.description()));
}
if self.analysis_config.check_validity {
result
.reasoning_steps
.push("Step 2: Checking logical validity...".to_string());
let (validity, explanation) = self.check_validity(&argument);
result.validity = validity;
result.validity_explanation = explanation.clone();
result.reasoning_steps.push(format!(" {}", explanation));
}
if self.analysis_config.detect_fallacies {
result
.reasoning_steps
.push("Step 3: Scanning for fallacies...".to_string());
let fallacies = self.detect_fallacies(&argument);
for fallacy in &fallacies {
result.reasoning_steps.push(format!(
" Found: {} (confidence: {:.2})",
fallacy.fallacy.description(),
fallacy.confidence
));
}
result.fallacies = fallacies;
}
if self.analysis_config.detect_contradictions {
result
.reasoning_steps
.push("Step 4: Checking for contradictions...".to_string());
result.contradictions = self.detect_contradictions(&argument);
if result.contradictions.is_empty() {
result
.reasoning_steps
.push(" No contradictions found.".to_string());
} else {
for contradiction in &result.contradictions {
result.reasoning_steps.push(format!(
" Contradiction: {} vs {} - {}",
contradiction.statement_a,
contradiction.statement_b,
contradiction.explanation
));
}
}
}
if self.analysis_config.check_soundness {
result
.reasoning_steps
.push("Step 5: Evaluating soundness...".to_string());
let (soundness, explanation) = self.check_soundness(&argument, result.validity);
result.soundness = soundness;
result.soundness_explanation = explanation.clone();
result.reasoning_steps.push(format!(" {}", explanation));
}
result.confidence = self.calculate_confidence(&result);
result.suggestions = self.generate_suggestions(&result);
Ok(result)
}
fn clean_term(term: &str) -> String {
term.trim()
.trim_matches(|c: char| c.is_ascii_punctuation())
.trim()
.to_string()
}
fn detect_argument_form(&self, argument: &Argument) -> Option<ArgumentForm> {
let has_conditional = argument
.premises
.iter()
.any(|p| p.premise_type == PremiseType::Conditional);
let has_disjunctive = argument
.premises
.iter()
.any(|p| p.premise_type == PremiseType::Disjunctive);
let has_universal = argument
.premises
.iter()
.any(|p| p.premise_type == PremiseType::Universal);
if has_conditional && argument.premises.len() >= 2 {
let conditional = argument
.premises
.iter()
.find(|p| p.premise_type == PremiseType::Conditional);
if let Some(cond) = conditional {
let cond_lower = cond.statement.to_lowercase();
if cond_lower.starts_with("if ") {
if let Some(then_idx) = cond_lower.find(" then ") {
let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
let affirms_antecedent = argument.premises.iter().any(|p| {
let p_lower = p.statement.to_lowercase();
let p_clean = Self::clean_term(&p_lower);
p.premise_type != PremiseType::Conditional
&& (p_clean == antecedent || p_clean.contains(&antecedent))
});
let denies_antecedent = argument.premises.iter().any(|p| {
let p_lower = p.statement.to_lowercase();
(p_lower.contains("not ") || p_lower.starts_with("no "))
&& p_lower.contains(&antecedent)
});
if affirms_antecedent {
return Some(ArgumentForm::ModusPonens);
}
if denies_antecedent {
return Some(ArgumentForm::Unknown); }
}
}
}
}
if has_disjunctive && argument.premises.len() >= 2 {
let has_negation = argument.premises.iter().any(|p| {
p.premise_type == PremiseType::Negative
|| p.statement.to_lowercase().contains("not ")
});
if has_negation {
return Some(ArgumentForm::DisjunctiveSyllogism);
}
}
if has_universal && argument.premises.len() >= 2 {
let universal_count = argument
.premises
.iter()
.filter(|p| p.premise_type == PremiseType::Universal)
.count();
if universal_count >= 2 {
return Some(ArgumentForm::CategoricalSyllogism);
}
let singular_count = argument
.premises
.iter()
.filter(|p| p.premise_type == PremiseType::Singular)
.count();
if universal_count >= 1 && singular_count >= 1 {
let universal_premise = argument
.premises
.iter()
.find(|p| p.premise_type == PremiseType::Universal)?;
let singular_premise = argument
.premises
.iter()
.find(|p| p.premise_type == PremiseType::Singular)?;
let univ_lower = universal_premise.statement.to_lowercase();
if univ_lower.starts_with("all ") {
if let Some(are_idx) = univ_lower.find(" are ") {
let subject_term = Self::clean_term(&univ_lower[4..are_idx]);
let sing_lower = singular_premise.statement.to_lowercase();
let subject_stem = subject_term.trim_end_matches('s');
if sing_lower.contains(&subject_term) || sing_lower.contains(subject_stem) {
return Some(ArgumentForm::CategoricalSyllogism);
}
}
}
}
}
None
}
fn check_validity(&self, argument: &Argument) -> (ValidityStatus, String) {
if let Some(form) = self.detect_argument_form(argument) {
if form.is_valid_form() {
return (
ValidityStatus::Valid,
format!("Argument follows valid {} pattern", form.description()),
);
}
}
let fallacies = self.detect_formal_fallacies(argument);
if !fallacies.is_empty() {
let fallacy_names: Vec<_> = fallacies.iter().map(|f| f.fallacy.pattern()).collect();
return (
ValidityStatus::Invalid,
format!(
"Argument contains formal fallacy: {}",
fallacy_names.join(", ")
),
);
}
(
ValidityStatus::Undetermined,
"Argument structure too complex for automated validation. Manual review recommended."
.to_string(),
)
}
fn detect_formal_fallacies(&self, argument: &Argument) -> Vec<DetectedFallacy> {
let mut fallacies = Vec::new();
if let Some(fallacy) = self.check_affirming_consequent(argument) {
fallacies.push(fallacy);
}
if let Some(fallacy) = self.check_denying_antecedent(argument) {
fallacies.push(fallacy);
}
if let Some(fallacy) = self.check_undistributed_middle(argument) {
fallacies.push(fallacy);
}
fallacies
}
fn detect_fallacies(&self, argument: &Argument) -> Vec<DetectedFallacy> {
let mut fallacies = self.detect_formal_fallacies(argument);
if let Some(fallacy) = self.check_circular_reasoning(argument) {
fallacies.push(fallacy);
}
if let Some(fallacy) = self.check_false_dichotomy(argument) {
fallacies.push(fallacy);
}
if let Some(fallacy) = self.check_non_sequitur(argument) {
fallacies.push(fallacy);
}
fallacies
}
fn check_affirming_consequent(&self, argument: &Argument) -> Option<DetectedFallacy> {
let conditional = argument
.premises
.iter()
.enumerate()
.find(|(_, p)| p.premise_type == PremiseType::Conditional)?;
let cond_lower = conditional.1.statement.to_lowercase();
let then_idx = cond_lower.find(" then ")?;
let consequent = Self::clean_term(&cond_lower[then_idx + 6..]);
let affirming_premise = argument.premises.iter().enumerate().find(|(i, p)| {
*i != conditional.0 && p.premise_type != PremiseType::Conditional && {
let p_clean = Self::clean_term(&p.statement.to_lowercase());
p_clean.contains(&consequent) || consequent.contains(&p_clean)
}
});
if affirming_premise.is_some() {
let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
let conclusion_clean = Self::clean_term(&argument.conclusion.to_lowercase());
if conclusion_clean.contains(&antecedent)
|| antecedent.contains(&conclusion_clean)
|| Self::terms_match(&conclusion_clean, &antecedent)
{
return Some(
DetectedFallacy::new(
Fallacy::AffirmingConsequent,
0.85,
"Argument affirms the consequent of a conditional to conclude the antecedent",
)
.with_premises(vec![conditional.0])
.with_suggestion("Consider other possible causes for the consequent being true"),
);
}
}
None
}
fn terms_match(a: &str, b: &str) -> bool {
if a == b {
return true;
}
let a_words: Vec<&str> = a.split_whitespace().collect();
let b_words: Vec<&str> = b.split_whitespace().collect();
if a_words.len() == b_words.len() {
let mut matches = 0;
for (aw, bw) in a_words.iter().zip(b_words.iter()) {
if aw == bw {
matches += 1;
} else {
let aw_stem = aw.trim_end_matches("ed").trim_end_matches('s');
let bw_stem = bw.trim_end_matches("ed").trim_end_matches('s');
if aw_stem == bw_stem {
matches += 1;
}
}
}
return matches == a_words.len();
}
false
}
fn check_denying_antecedent(&self, argument: &Argument) -> Option<DetectedFallacy> {
let conditional = argument
.premises
.iter()
.enumerate()
.find(|(_, p)| p.premise_type == PremiseType::Conditional)?;
let cond_lower = conditional.1.statement.to_lowercase();
let then_idx = cond_lower.find(" then ")?;
let antecedent = Self::clean_term(&cond_lower[3..then_idx]);
let consequent = Self::clean_term(&cond_lower[then_idx + 6..]);
let denying_premise = argument.premises.iter().enumerate().find(|(i, p)| {
*i != conditional.0
&& p.statement.to_lowercase().contains(&antecedent)
&& (p.statement.to_lowercase().contains("not ")
|| p.statement.to_lowercase().starts_with("no "))
});
if denying_premise.is_some() {
if argument.conclusion.to_lowercase().contains(&consequent)
&& (argument.conclusion.to_lowercase().contains("not ")
|| argument.conclusion.to_lowercase().starts_with("no "))
{
return Some(
DetectedFallacy::new(
Fallacy::DenyingAntecedent,
0.85,
"Argument denies the antecedent to conclude the negation of the consequent",
)
.with_premises(vec![conditional.0])
.with_suggestion("The consequent might still be true for other reasons"),
);
}
}
None
}
fn check_undistributed_middle(&self, argument: &Argument) -> Option<DetectedFallacy> {
let universals: Vec<_> = argument
.premises
.iter()
.enumerate()
.filter(|(_, p)| p.premise_type == PremiseType::Universal)
.collect();
if universals.len() < 2 {
return None;
}
let mut terms: Vec<HashSet<String>> = Vec::new();
for (_, premise) in &universals {
let lower = premise.statement.to_lowercase();
if lower.starts_with("all ") {
if let Some(are_idx) = lower.find(" are ") {
let subject = Self::clean_term(&lower[4..are_idx]);
let predicate = Self::clean_term(&lower[are_idx + 5..]);
let mut term_set = HashSet::new();
term_set.insert(subject);
term_set.insert(predicate);
terms.push(term_set);
}
}
}
if terms.len() >= 2 {
let intersection: HashSet<_> = terms[0].intersection(&terms[1]).cloned().collect();
if intersection.is_empty() {
return Some(
DetectedFallacy::new(
Fallacy::UndistributedMiddle,
0.75,
"The middle term may not be properly distributed across premises",
)
.with_premises(vec![universals[0].0, universals[1].0])
.with_suggestion(
"Ensure the middle term is distributed in at least one premise",
),
);
}
}
None
}
fn check_circular_reasoning(&self, argument: &Argument) -> Option<DetectedFallacy> {
let conclusion_lower = argument.conclusion.to_lowercase();
let conclusion_words: HashSet<_> = conclusion_lower
.split_whitespace()
.filter(|w| w.len() > 3)
.collect();
if conclusion_words.len() < 2 {
return None;
}
for (idx, premise) in argument.premises.iter().enumerate() {
let premise_lower = premise.statement.to_lowercase();
let premise_words: HashSet<_> = premise_lower
.split_whitespace()
.filter(|w| w.len() > 3)
.collect();
let overlap = conclusion_words.intersection(&premise_words).count();
let conclusion_len = conclusion_words.len();
let conclusion_overlap_ratio = overlap as f64 / conclusion_len as f64;
if overlap >= 2 && conclusion_overlap_ratio >= 0.8 {
return Some(
DetectedFallacy::new(
Fallacy::CircularReasoning,
0.7,
"Premise and conclusion appear to state essentially the same thing",
)
.with_premises(vec![idx])
.with_suggestion(
"Provide independent evidence that doesn't restate the conclusion",
),
);
}
}
None
}
fn check_false_dichotomy(&self, argument: &Argument) -> Option<DetectedFallacy> {
for (idx, premise) in argument.premises.iter().enumerate() {
let lower = premise.statement.to_lowercase();
if (lower.contains("either ") && lower.contains(" or ")) || lower.contains(" only ") {
let or_count = lower.matches(" or ").count();
if or_count == 1 {
return Some(
DetectedFallacy::new(
Fallacy::FalseDichotomy,
0.6,
"Argument presents only two options when more may exist",
)
.with_premises(vec![idx])
.with_suggestion("Consider whether additional alternatives exist"),
);
}
}
}
None
}
fn check_non_sequitur(&self, argument: &Argument) -> Option<DetectedFallacy> {
let conclusion_words: HashSet<_> = argument
.conclusion
.to_lowercase()
.split_whitespace()
.filter(|w| w.len() > 4)
.map(|s| s.to_string())
.collect();
let mut any_overlap = false;
for premise in &argument.premises {
let premise_words: HashSet<_> = premise
.statement
.to_lowercase()
.split_whitespace()
.filter(|w| w.len() > 4)
.map(|s| s.to_string())
.collect();
if !conclusion_words.is_disjoint(&premise_words) {
any_overlap = true;
break;
}
}
if !any_overlap && !conclusion_words.is_empty() {
return Some(
DetectedFallacy::new(
Fallacy::NonSequitur,
0.5,
"Conclusion appears disconnected from the premises",
)
.with_suggestion(
"Establish a clear logical connection between premises and conclusion",
),
);
}
None
}
fn detect_contradictions(&self, argument: &Argument) -> Vec<Contradiction> {
let mut contradictions = Vec::new();
for (i, p1) in argument.premises.iter().enumerate() {
for (j, p2) in argument.premises.iter().enumerate().skip(i + 1) {
if let Some(contradiction) = self.check_contradiction(&p1.statement, &p2.statement)
{
contradictions.push(Contradiction {
statement_a: format!("Premise {}", i + 1),
statement_b: format!("Premise {}", j + 1),
contradiction_type: contradiction.0,
explanation: contradiction.1,
});
}
}
}
for (i, premise) in argument.premises.iter().enumerate() {
if let Some(contradiction) =
self.check_contradiction(&premise.statement, &argument.conclusion)
{
contradictions.push(Contradiction {
statement_a: format!("Premise {}", i + 1),
statement_b: "Conclusion".to_string(),
contradiction_type: contradiction.0,
explanation: contradiction.1,
});
}
}
contradictions
}
fn check_contradiction(&self, a: &str, b: &str) -> Option<(ContradictionType, String)> {
let a_lower = a.to_lowercase();
let b_lower = b.to_lowercase();
let negation_patterns = [
("is true", "is false"),
("exists", "does not exist"),
("is valid", "is invalid"),
("should", "should not"),
("must", "must not"),
("always", "never"),
("all", "none"),
("is ", "is not "),
("can ", "cannot "),
("will ", "will not "),
];
for (pos, neg) in &negation_patterns {
if (a_lower.contains(pos) && b_lower.contains(neg))
|| (a_lower.contains(neg) && b_lower.contains(pos))
{
if self.have_subject_overlap(&a_lower, &b_lower) {
return Some((
ContradictionType::DirectNegation,
format!(
"Direct contradiction detected: one states '{}' while other states '{}'",
pos, neg
),
));
}
}
}
None
}
fn have_subject_overlap(&self, a: &str, b: &str) -> bool {
let stopwords: HashSet<&str> = [
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has",
"had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "must",
"shall", "can", "need", "dare", "ought", "used", "to", "of", "in", "for", "on", "with",
"at", "by", "from", "as", "into", "through", "during", "before", "after", "above",
"below", "between", "under", "again", "further", "then", "once", "that", "this",
"these", "those", "not", "no", "nor", "and", "but", "or", "if", "while", "because",
"until", "although", "since", "when", "where", "why", "how", "all", "each", "every",
"both", "few", "more", "most", "other", "some", "such", "only", "own", "same", "so",
"than", "too", "very", "just", "true", "false",
]
.iter()
.cloned()
.collect();
let words_a: HashSet<&str> = a
.split_whitespace()
.filter(|w| !stopwords.contains(w) && !w.is_empty())
.collect();
let words_b: HashSet<&str> = b
.split_whitespace()
.filter(|w| !stopwords.contains(w) && !w.is_empty())
.collect();
if words_a.is_empty() || words_b.is_empty() {
return true;
}
let overlap = words_a.intersection(&words_b).count();
overlap > 0
}
fn check_soundness(
&self,
argument: &Argument,
validity: ValidityStatus,
) -> (SoundnessStatus, String) {
if validity != ValidityStatus::Valid {
return (
SoundnessStatus::Unsound,
"Argument is invalid, therefore unsound".to_string(),
);
}
let avg_confidence: f64 = argument.premises.iter().map(|p| p.confidence).sum::<f64>()
/ argument.premises.len() as f64;
if avg_confidence >= self.analysis_config.confidence_threshold {
(
SoundnessStatus::Sound,
format!(
"Argument is valid and premises have high confidence ({:.0}%)",
avg_confidence * 100.0
),
)
} else if avg_confidence >= 0.5 {
(
SoundnessStatus::Undetermined,
format!(
"Argument is valid but premise truth is uncertain ({:.0}% confidence)",
avg_confidence * 100.0
),
)
} else {
(
SoundnessStatus::Unsound,
format!(
"Argument is valid but premises have low confidence ({:.0}%)",
avg_confidence * 100.0
),
)
}
}
fn calculate_confidence(&self, result: &LaserLogicResult) -> f64 {
let mut confidence = 0.7;
if result.argument_form.is_some() {
confidence += 0.1;
}
if result.validity != ValidityStatus::Undetermined {
confidence += 0.1;
}
let fallacy_penalty = result.fallacies.len() as f64 * 0.05;
confidence -= fallacy_penalty.min(0.2);
if !result.contradictions.is_empty() {
confidence -= 0.15;
}
confidence.clamp(0.0, 1.0)
}
fn generate_suggestions(&self, result: &LaserLogicResult) -> Vec<String> {
let mut suggestions = Vec::new();
if result.validity == ValidityStatus::Invalid {
suggestions.push(
"Restructure the argument to ensure the conclusion follows logically from the premises."
.to_string(),
);
}
for fallacy in &result.fallacies {
if !fallacy.suggestion.is_empty() {
suggestions.push(fallacy.suggestion.clone());
}
}
if !result.contradictions.is_empty() {
suggestions.push(
"Resolve contradictions by revising conflicting premises or clarifying terms."
.to_string(),
);
}
if result.soundness == SoundnessStatus::Undetermined {
suggestions
.push("Provide additional evidence to support premise truth claims.".to_string());
}
if result.argument_form.is_none() {
suggestions.push(
"Consider restructuring into a standard argument form (modus ponens, syllogism, etc.) for clarity."
.to_string(),
);
}
suggestions.sort();
suggestions.dedup();
suggestions
}
}
impl ThinkToolModule for LaserLogic {
fn config(&self) -> &ThinkToolModuleConfig {
&self.config
}
fn execute(&self, context: &ThinkToolContext) -> Result<ThinkToolOutput> {
let query = &context.query;
let (premises, conclusion) = self.parse_argument_from_query(query)?;
let result = self.analyze_argument(&premises, conclusion)?;
Ok(ThinkToolOutput {
module: self.config.name.clone(),
confidence: result.confidence,
output: serde_json::json!({
"validity": format!("{:?}", result.validity),
"validity_explanation": result.validity_explanation,
"soundness": format!("{:?}", result.soundness),
"soundness_explanation": result.soundness_explanation,
"argument_form": result.argument_form.map(|f| f.description()),
"fallacies": result.fallacies.iter().map(|f| {
serde_json::json!({
"type": format!("{:?}", f.fallacy),
"description": f.fallacy.description(),
"pattern": f.fallacy.pattern(),
"confidence": f.confidence,
"evidence": f.evidence,
"suggestion": f.suggestion
})
}).collect::<Vec<_>>(),
"contradictions": result.contradictions.iter().map(|c| {
serde_json::json!({
"between": [c.statement_a, c.statement_b],
"type": format!("{:?}", c.contradiction_type),
"explanation": c.explanation
})
}).collect::<Vec<_>>(),
"verdict": result.verdict(),
"suggestions": result.suggestions,
"reasoning_steps": result.reasoning_steps
}),
})
}
}
impl LaserLogic {
fn parse_argument_from_query<'a>(&self, query: &'a str) -> Result<(Vec<&'a str>, &'a str)> {
let query = query.trim();
let conclusion_markers = [
"therefore,",
"therefore",
"hence,",
"hence",
"thus,",
"thus",
"so,",
"consequently,",
"consequently",
"it follows that",
"we can conclude",
"which means",
];
for marker in &conclusion_markers {
if let Some(idx) = query.to_lowercase().find(marker) {
let premises_part = &query[..idx];
let conclusion_start = idx + marker.len();
let conclusion = query[conclusion_start..]
.trim()
.trim_start_matches(',')
.trim();
let premises: Vec<&str> = premises_part
.split(['.', ';'])
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if !premises.is_empty() && !conclusion.is_empty() {
return Ok((premises, conclusion));
}
}
}
let sentences: Vec<&str> = query
.split('.')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if sentences.len() >= 2 {
let premises = &sentences[..sentences.len() - 1];
let conclusion = sentences.last().unwrap();
return Ok((premises.to_vec(), conclusion));
}
Err(Error::validation(
"Could not parse argument structure. Expected format: 'Premise 1. Premise 2. Therefore, Conclusion.' or similar.",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = LaserLogicConfig::default();
assert!(config.detect_fallacies);
assert!(config.check_validity);
assert!(config.check_soundness);
}
#[test]
fn test_config_presets() {
let quick = LaserLogicConfig::quick();
assert!(!quick.check_soundness);
assert_eq!(quick.max_premise_depth, 5);
let deep = LaserLogicConfig::deep();
assert!(deep.check_soundness);
assert!(deep.verbose_output);
let paranoid = LaserLogicConfig::paranoid();
assert_eq!(paranoid.confidence_threshold, 0.9);
}
#[test]
fn test_premise_type_inference() {
assert_eq!(
Premise::new("All humans are mortal").premise_type,
PremiseType::Universal
);
assert_eq!(
Premise::new("Some birds can fly").premise_type,
PremiseType::Particular
);
assert_eq!(
Premise::new("If it rains, the ground is wet").premise_type,
PremiseType::Conditional
);
assert_eq!(
Premise::new("Either we go or we stay").premise_type,
PremiseType::Disjunctive
);
assert_eq!(
Premise::new("Socrates is human").premise_type,
PremiseType::Singular
);
}
#[test]
fn test_fallacy_descriptions() {
let fallacy = Fallacy::AffirmingConsequent;
assert!(fallacy.description().contains("Inferring"));
assert!(fallacy.pattern().contains("INVALID"));
assert!(fallacy.is_formal());
assert_eq!(fallacy.severity(), 5);
}
#[test]
fn test_valid_modus_ponens() {
let laser = LaserLogic::new();
let result = laser
.analyze_argument(
&["If it rains, then the ground is wet", "It rains"],
"The ground is wet",
)
.unwrap();
assert!(result.argument_form.is_some());
assert!(!result.has_fallacies());
}
#[test]
fn test_affirming_consequent_detection() {
let laser = LaserLogic::new();
let result = laser
.analyze_argument(
&["If it rains, then the ground is wet", "The ground is wet"],
"It rained",
)
.unwrap();
assert!(result.has_fallacies());
assert!(result
.fallacies
.iter()
.any(|f| f.fallacy == Fallacy::AffirmingConsequent));
}
#[test]
fn test_categorical_syllogism() {
let laser = LaserLogic::new();
let result = laser
.analyze_argument(
&["All humans are mortal", "All Greeks are humans"],
"All Greeks are mortal",
)
.unwrap();
assert_eq!(
result.argument_form,
Some(ArgumentForm::CategoricalSyllogism)
);
}
#[test]
fn test_circular_reasoning_detection() {
let laser = LaserLogic::new();
let result = laser
.analyze_argument(
&["The Bible is true because it is the word of God"],
"The Bible is the word of God",
)
.unwrap();
assert!(result
.fallacies
.iter()
.any(|f| f.fallacy == Fallacy::CircularReasoning));
}
#[test]
fn test_contradiction_detection() {
let laser = LaserLogic::with_config(LaserLogicConfig::deep());
let result = laser
.analyze_argument(&["X is true", "X is false"], "Something follows")
.unwrap();
assert!(!result.contradictions.is_empty());
}
#[test]
fn test_thinkmodule_trait() {
let laser = LaserLogic::new();
let config = laser.config();
assert_eq!(config.name, "LaserLogic");
assert_eq!(config.version, "3.0.0");
}
#[test]
fn test_execute_with_context() {
let laser = LaserLogic::new();
let context = ThinkToolContext {
query: "All humans are mortal. Socrates is human. Therefore, Socrates is mortal."
.to_string(),
previous_steps: vec![],
};
let output = laser.execute(&context).unwrap();
assert_eq!(output.module, "LaserLogic");
assert!(output.confidence > 0.0);
}
#[test]
fn test_verdict_generation() {
let mut result = LaserLogicResult::new(Argument::new(vec!["premise"], "conclusion"));
result.validity = ValidityStatus::Valid;
result.soundness = SoundnessStatus::Sound;
assert!(result.verdict().contains("SOUND"));
result.validity = ValidityStatus::Invalid;
assert!(result.verdict().contains("INVALID"));
}
#[test]
fn test_argument_form_validity() {
assert!(ArgumentForm::ModusPonens.is_valid_form());
assert!(ArgumentForm::ModusTollens.is_valid_form());
assert!(!ArgumentForm::Unknown.is_valid_form());
}
#[test]
fn test_empty_premises_error() {
let laser = LaserLogic::new();
let result = laser.analyze_argument(&[], "Some conclusion");
assert!(result.is_err());
}
#[test]
fn test_too_many_premises_error() {
let laser = LaserLogic::with_config(LaserLogicConfig {
max_premise_depth: 2,
..Default::default()
});
let result = laser.analyze_argument(&["P1", "P2", "P3"], "Conclusion");
assert!(result.is_err());
}
#[test]
fn test_voting_strategy_default() {
let strategy = VotingStrategy::default();
assert_eq!(strategy, VotingStrategy::Majority);
}
#[test]
fn test_self_consistency_config_default() {
let config = SelfConsistencyConfig::default();
assert_eq!(config.num_samples, 5); assert_eq!(config.strategy, VotingStrategy::Majority);
assert!((config.diversity_factor - 0.7).abs() < 0.001);
assert!((config.agreement_threshold - 0.5).abs() < 0.001);
assert!(config.include_minority_opinions);
assert!((config.confidence_weight - 1.0).abs() < 0.001);
assert!(config.track_reasoning_chains);
}
#[test]
fn test_vote_struct_creation() {
let vote = Vote {
verdict: ValidityStatus::Valid,
confidence: 0.9,
reasoning_chain: Some(vec!["Step 1".to_string(), "Step 2".to_string()]),
sample_index: 0,
fallacies_detected: 0,
};
assert_eq!(vote.verdict, ValidityStatus::Valid);
assert!((vote.confidence - 0.9).abs() < 0.001);
assert!(vote.reasoning_chain.is_some());
assert_eq!(vote.sample_index, 0);
}
#[test]
fn test_voting_aggregator_majority_clear_winner() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.88, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.91, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.7, None, 1);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(result.consensus_reached);
assert!((result.agreement_ratio - 0.8).abs() < 0.001);
assert_eq!(*result.vote_counts.get("valid").unwrap(), 4);
assert_eq!(*result.vote_counts.get("invalid").unwrap(), 1);
}
#[test]
fn test_voting_aggregator_majority_no_majority() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.8, None, 1);
aggregator.add_vote(ValidityStatus::Invalid, 0.75, None, 1);
aggregator.add_vote(ValidityStatus::Undetermined, 0.5, None, 0);
let result = aggregator.aggregate();
assert!(!result.consensus_reached);
}
#[test]
fn test_voting_aggregator_weighted_confidence() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::WeightedByConfidence,
confidence_weight: 2.0, ..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.95, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.92, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.4, None, 1);
aggregator.add_vote(ValidityStatus::Invalid, 0.35, None, 1);
aggregator.add_vote(ValidityStatus::Invalid, 0.3, None, 2);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
}
#[test]
fn test_voting_aggregator_unanimous_success() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Unanimous,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.88, None, 0);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(result.consensus_reached);
assert!((result.agreement_ratio - 1.0).abs() < 0.001);
}
#[test]
fn test_voting_aggregator_unanimous_failure() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Unanimous,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.7, None, 1);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Undetermined);
assert!(!result.consensus_reached);
}
#[test]
fn test_voting_aggregator_plurality() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Plurality,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.7, None, 1);
aggregator.add_vote(ValidityStatus::Undetermined, 0.5, None, 0);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(result.consensus_reached);
}
#[test]
fn test_voting_aggregator_threshold_success() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Threshold,
agreement_threshold: 0.7,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.88, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.87, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.6, None, 1);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(result.consensus_reached);
}
#[test]
fn test_voting_aggregator_threshold_failure() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Threshold,
agreement_threshold: 0.9, ..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.88, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.87, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.6, None, 1);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(!result.consensus_reached);
}
#[test]
fn test_voting_aggregator_empty_votes() {
let config = SelfConsistencyConfig::default();
let aggregator = VotingAggregator::new(config);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Undetermined);
assert!(!result.consensus_reached);
assert_eq!(result.total_samples, 0);
assert!((result.aggregated_confidence - 0.0).abs() < 0.001);
}
#[test]
fn test_voting_aggregator_single_vote() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert!(result.consensus_reached); assert_eq!(result.total_samples, 1);
}
#[test]
fn test_voting_aggregator_minority_opinions() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
include_minority_opinions: true,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.88, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.7, None, 1);
aggregator.add_vote(ValidityStatus::Undetermined, 0.5, None, 0);
let result = aggregator.aggregate();
assert_eq!(result.consensus_verdict, ValidityStatus::Valid);
assert_eq!(result.minority_opinions.len(), 2);
assert!(result
.minority_opinions
.iter()
.any(|(v, c)| *v == ValidityStatus::Invalid && *c == 1));
assert!(result
.minority_opinions
.iter()
.any(|(v, c)| *v == ValidityStatus::Undetermined && *c == 1));
}
#[test]
fn test_voting_aggregator_no_minority_tracking() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
include_minority_opinions: false,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 0.9, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.85, None, 0);
aggregator.add_vote(ValidityStatus::Invalid, 0.7, None, 1);
let result = aggregator.aggregate();
assert!(result.minority_opinions.is_empty());
}
#[test]
fn test_voting_aggregator_reasoning_chain_tracking() {
let config = SelfConsistencyConfig {
track_reasoning_chains: true,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(
ValidityStatus::Valid,
0.9,
Some(vec!["Chain 1".to_string()]),
0,
);
aggregator.add_vote(
ValidityStatus::Valid,
0.85,
Some(vec!["Chain 2".to_string()]),
0,
);
let result = aggregator.aggregate();
assert_eq!(result.votes.len(), 2);
assert!(result.votes[0].reasoning_chain.is_some());
assert!(result.votes[1].reasoning_chain.is_some());
}
#[test]
fn test_voting_aggregator_no_reasoning_chain_tracking() {
let config = SelfConsistencyConfig {
track_reasoning_chains: false,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(
ValidityStatus::Valid,
0.9,
Some(vec!["Chain 1".to_string()]),
0,
);
aggregator.add_vote(
ValidityStatus::Valid,
0.85,
Some(vec!["Chain 2".to_string()]),
0,
);
let result = aggregator.aggregate();
assert!(result.votes.is_empty());
}
#[test]
fn test_voting_aggregator_aggregated_confidence_calculation() {
let config = SelfConsistencyConfig {
strategy: VotingStrategy::Majority,
..Default::default()
};
let mut aggregator = VotingAggregator::new(config);
aggregator.add_vote(ValidityStatus::Valid, 1.0, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.8, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.6, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.4, None, 0);
aggregator.add_vote(ValidityStatus::Valid, 0.2, None, 0);
let result = aggregator.aggregate();
assert!((result.aggregated_confidence - 0.6).abs() < 0.001);
}
#[test]
fn test_self_consistency_result_new() {
let result = SelfConsistencyResult::new(VotingStrategy::Majority);
assert_eq!(result.consensus_verdict, ValidityStatus::Undetermined);
assert!((result.agreement_ratio - 0.0).abs() < 0.001);
assert_eq!(result.total_samples, 0);
assert!(result.vote_counts.is_empty());
assert!(!result.consensus_reached);
assert_eq!(result.strategy_used, VotingStrategy::Majority);
}
#[test]
fn test_vote_serialization() {
let vote = Vote {
verdict: ValidityStatus::Valid,
confidence: 0.85,
reasoning_chain: Some(vec!["Test chain".to_string()]),
sample_index: 0,
fallacies_detected: 0,
};
let serialized = serde_json::to_string(&vote).unwrap();
let deserialized: Vote = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.verdict, vote.verdict);
assert!((deserialized.confidence - vote.confidence).abs() < 0.001);
assert_eq!(deserialized.reasoning_chain, vote.reasoning_chain);
}
#[test]
fn test_self_consistency_config_serialization() {
let config = SelfConsistencyConfig {
num_samples: 7,
strategy: VotingStrategy::WeightedByConfidence,
diversity_factor: 0.5,
agreement_threshold: 0.8,
include_minority_opinions: false,
confidence_weight: 2.0,
track_reasoning_chains: false,
};
let serialized = serde_json::to_string(&config).unwrap();
let deserialized: SelfConsistencyConfig = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.num_samples, 7);
assert_eq!(deserialized.strategy, VotingStrategy::WeightedByConfidence);
assert!((deserialized.diversity_factor - 0.5).abs() < 0.001);
}
#[test]
fn test_voting_strategy_serialization() {
let strategies = vec![
VotingStrategy::Majority,
VotingStrategy::WeightedByConfidence,
VotingStrategy::Unanimous,
VotingStrategy::Plurality,
VotingStrategy::Threshold,
];
for strategy in strategies {
let serialized = serde_json::to_string(&strategy).unwrap();
let deserialized: VotingStrategy = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, strategy);
}
}
#[test]
fn test_self_consistency_result_verdict_string() {
let mut result = SelfConsistencyResult::new(VotingStrategy::Majority);
result.consensus_verdict = ValidityStatus::Valid;
assert_eq!(result.verdict_string(), "VALID");
result.consensus_verdict = ValidityStatus::Invalid;
assert_eq!(result.verdict_string(), "INVALID");
result.consensus_verdict = ValidityStatus::Undetermined;
assert_eq!(result.verdict_string(), "UNDETERMINED");
}
#[test]
fn test_self_consistency_result_is_strong_consensus() {
let mut result = SelfConsistencyResult::new(VotingStrategy::Majority);
result.agreement_ratio = 0.9;
assert!(result.is_strong_consensus());
result.agreement_ratio = 0.8;
assert!(result.is_strong_consensus());
result.agreement_ratio = 0.79;
assert!(!result.is_strong_consensus());
}
#[test]
fn test_self_consistency_result_summary() {
let mut result = SelfConsistencyResult::new(VotingStrategy::Majority);
result.consensus_verdict = ValidityStatus::Valid;
result.agreement_ratio = 0.85;
result.total_samples = 5;
result.consensus_reached = true;
let summary = result.summary();
assert!(summary.contains("VALID"));
assert!(summary.contains("85.0%"));
assert!(summary.contains("5 samples"));
assert!(summary.contains("consensus reached"));
}
#[test]
fn test_self_consistency_config_builder() {
let config = SelfConsistencyConfig::default()
.with_samples(10)
.with_strategy(VotingStrategy::WeightedByConfidence);
assert_eq!(config.num_samples, 10);
assert_eq!(config.strategy, VotingStrategy::WeightedByConfidence);
}
}