import { LogicManager } from './logicManager.js';
import { AssumptionExtractor, AssumptionReport } from './assumptionExtractor.js';
import { LogicalSystem } from './types.js';
import { Loggers } from './utils/logger.js';
const logger = Loggers.server;
/**
* Individual scoring component
*/
export interface ScoreComponent {
score: number; // 0-100
weight: number; // Weight in overall score
explanation: string;
details?: string[];
}
/**
* Complete argument score with breakdown
*/
export interface ArgumentScore {
overallScore: number; // 0-100
grade: 'A' | 'B' | 'C' | 'D' | 'F';
components: {
validity: ScoreComponent;
premisePlausibility: ScoreComponent;
inferenceStrength: ScoreComponent;
structureQuality: ScoreComponent;
fallacyPenalty: ScoreComponent;
assumptionComplexity: ScoreComponent;
};
strengths: string[];
weaknesses: string[];
recommendations: string[];
comparisonToAverage: string;
}
/**
* Scores logical arguments on multiple dimensions
*/
export class ArgumentScorer {
private logicManager: LogicManager;
private assumptionExtractor: AssumptionExtractor;
// Scoring weights (sum to 1.0)
private weights = {
validity: 0.30,
premisePlausibility: 0.20,
inferenceStrength: 0.20,
structureQuality: 0.10,
fallacyPenalty: 0.10,
assumptionComplexity: 0.10
};
// Historical average for comparison (starts at 65, updates with usage)
private historicalAverage = 65;
constructor(logicManager: LogicManager, assumptionExtractor: AssumptionExtractor) {
this.logicManager = logicManager;
this.assumptionExtractor = assumptionExtractor;
}
/**
* Score an argument on multiple dimensions
*/
async scoreArgument(
premises: string[],
conclusion: string,
system: LogicalSystem = 'propositional'
): Promise<ArgumentScore> {
logger.info(`Scoring argument with ${premises.length} premises in ${system} logic`);
// Calculate each component
const validity = await this.scoreValidity(premises, conclusion, system);
const premisePlausibility = this.scorePremisePlausibility(premises);
const inferenceStrength = this.scoreInferenceStrength(premises, conclusion, system);
const structureQuality = this.scoreStructureQuality(premises, conclusion);
const fallacyPenalty = await this.scoreFallacyPenalty(premises, conclusion, system);
const assumptionComplexity = await this.scoreAssumptionComplexity(premises, conclusion, system);
// Calculate weighted overall score
const overallScore = Math.round(
validity.score * this.weights.validity +
premisePlausibility.score * this.weights.premisePlausibility +
inferenceStrength.score * this.weights.inferenceStrength +
structureQuality.score * this.weights.structureQuality -
fallacyPenalty.score * this.weights.fallacyPenalty -
assumptionComplexity.score * this.weights.assumptionComplexity
);
// Ensure score is in valid range
const finalScore = Math.max(0, Math.min(100, overallScore));
// Convert to letter grade
const grade = this.convertToGrade(finalScore);
// Identify strengths and weaknesses
const strengths = this.identifyStrengths({
validity,
premisePlausibility,
inferenceStrength,
structureQuality,
fallacyPenalty,
assumptionComplexity
});
const weaknesses = this.identifyWeaknesses({
validity,
premisePlausibility,
inferenceStrength,
structureQuality,
fallacyPenalty,
assumptionComplexity
});
// Generate recommendations
const recommendations = this.generateRecommendations({
validity,
premisePlausibility,
inferenceStrength,
structureQuality,
fallacyPenalty,
assumptionComplexity
}, premises, conclusion);
// Compare to average
const comparisonToAverage = this.compareToAverage(finalScore);
return {
overallScore: finalScore,
grade,
components: {
validity,
premisePlausibility,
inferenceStrength,
structureQuality,
fallacyPenalty,
assumptionComplexity
},
strengths,
weaknesses,
recommendations,
comparisonToAverage
};
}
/**
* Score logical validity (0-100)
*/
private async scoreValidity(
premises: string[],
conclusion: string,
system: LogicalSystem
): Promise<ScoreComponent> {
const details: string[] = [];
let score = 0;
try {
const argumentString = premises.join('. ') + '. Therefore, ' + conclusion;
const result = await this.logicManager.process(system, 'validate', argumentString, 'natural');
if (result.status === 'success' && result.details?.analysis) {
const analysis = result.details.analysis;
if (analysis.isValid) {
score = 100;
details.push('Argument is logically valid');
details.push('Conclusion necessarily follows from premises');
} else {
score = 0;
details.push('Argument is logically invalid');
if (analysis.counterexample) {
details.push(`Counterexample found: ${analysis.counterexample}`);
}
if (analysis.explanation) {
details.push(analysis.explanation);
}
}
} else {
// Could not validate - give partial credit
score = 50;
details.push('Could not determine validity conclusively');
}
} catch (error) {
score = 50;
details.push('Validity check encountered an error');
logger.debug(`Validity scoring error: ${error instanceof Error ? error.message : String(error)}`);
}
return {
score,
weight: this.weights.validity,
explanation: score === 100
? 'Logically valid - conclusion necessarily follows from premises'
: score === 0
? 'Logically invalid - conclusion does not follow from premises'
: 'Validity uncertain - may require formalization',
details
};
}
/**
* Score premise plausibility (0-100)
*/
private scorePremisePlausibility(premises: string[]): ScoreComponent {
const details: string[] = [];
let totalScore = 0;
if (premises.length === 0) {
return {
score: 0,
weight: this.weights.premisePlausibility,
explanation: 'No premises provided',
details: ['An argument requires at least one premise']
};
}
// Heuristic scoring based on premise characteristics
for (const premise of premises) {
let premiseScore = 70; // Start with neutral score
// Penalize vague or unclear premises
if (premise.length < 10) {
premiseScore -= 10;
details.push(`"${premise}" - Very brief, may lack clarity`);
}
// Penalize unsupported universal claims
if (/\b(all|every|always|never|none)\b/i.test(premise) && premise.length < 30) {
premiseScore -= 15;
details.push(`"${premise}" - Universal claim without justification`);
}
// Reward specific, detailed premises
if (premise.length > 50 && premise.length < 200) {
premiseScore += 10;
details.push(`"${premise}" - Specific and detailed`);
}
// Penalize overly complex premises
if (premise.length > 200) {
premiseScore -= 5;
details.push(`"${premise.substring(0, 50)}..." - Very complex, may need breaking down`);
}
// Penalize emotional or loaded language
if (/\b(obviously|clearly|everyone knows|it's obvious)\b/i.test(premise)) {
premiseScore -= 10;
details.push(`Contains presumptive language that may hide weak support`);
}
totalScore += premiseScore;
}
const avgScore = Math.round(totalScore / premises.length);
return {
score: Math.max(0, Math.min(100, avgScore)),
weight: this.weights.premisePlausibility,
explanation: avgScore >= 80
? 'Premises are well-supported and plausible'
: avgScore >= 60
? 'Premises are moderately plausible but could be stronger'
: 'Premises need better support or clarification',
details
};
}
/**
* Score inference strength (0-100)
*/
private scoreInferenceStrength(
premises: string[],
conclusion: string,
system: LogicalSystem
): ScoreComponent {
const details: string[] = [];
let score = 70; // Start with moderate score
// Check how many premises actually contribute to the conclusion
const conclusionTerms = this.extractKeyTerms(conclusion);
let relevantPremises = 0;
for (const premise of premises) {
const premiseTerms = this.extractKeyTerms(premise);
const overlap = premiseTerms.filter(t => conclusionTerms.includes(t));
if (overlap.length > 0) {
relevantPremises++;
details.push(`Premise "${premise.substring(0, 50)}..." connects to conclusion via: ${overlap.join(', ')}`);
}
}
// Score based on premise relevance
const relevanceRatio = premises.length > 0 ? relevantPremises / premises.length : 0;
if (relevanceRatio >= 0.8) {
score = 90;
details.push('Strong connection between premises and conclusion');
} else if (relevanceRatio >= 0.5) {
score = 70;
details.push('Moderate connection between premises and conclusion');
} else {
score = 40;
details.push('Weak connection between premises and conclusion');
}
// Bonus for having intermediate steps
if (premises.length >= 3) {
score += 5;
details.push('Multiple premises provide stronger support');
}
return {
score: Math.max(0, Math.min(100, score)),
weight: this.weights.inferenceStrength,
explanation: score >= 80
? 'Strong inferential connection between premises and conclusion'
: score >= 60
? 'Moderate inferential connection'
: 'Weak inferential connection - premises may not adequately support conclusion',
details
};
}
/**
* Score argument structure quality (0-100)
*/
private scoreStructureQuality(premises: string[], conclusion: string): ScoreComponent {
const details: string[] = [];
let score = 70;
// Check for proper number of premises
if (premises.length === 0) {
score = 0;
details.push('No premises provided');
} else if (premises.length === 1) {
score = 60;
details.push('Single premise - may be insufficient');
} else if (premises.length === 2 || premises.length === 3) {
score = 85;
details.push('Optimal number of premises for clarity');
} else if (premises.length <= 5) {
score = 75;
details.push('Multiple premises provide thorough support');
} else {
score = 65;
details.push('Many premises - ensure all are necessary');
}
// Check for clear conclusion
if (conclusion.length < 10) {
score -= 10;
details.push('Conclusion is very brief');
} else if (conclusion.length > 30 && conclusion.length < 150) {
score += 5;
details.push('Conclusion is clear and specific');
}
// Check for logical indicators
const premisesText = premises.join(' ');
const hasLogicalIndicators = /\b(because|since|given that|assuming|if|when)\b/i.test(premisesText);
if (hasLogicalIndicators) {
score += 5;
details.push('Uses logical indicator words');
}
return {
score: Math.max(0, Math.min(100, score)),
weight: this.weights.structureQuality,
explanation: score >= 80
? 'Well-structured argument with clear organization'
: score >= 60
? 'Adequate structure with room for improvement'
: 'Poor structure - needs better organization',
details
};
}
/**
* Score fallacy penalty (0-100, lower is better)
*/
private async scoreFallacyPenalty(
premises: string[],
conclusion: string,
system: LogicalSystem
): Promise<ScoreComponent> {
const details: string[] = [];
let penalty = 0;
try {
const argumentString = premises.join('. ') + '. Therefore, ' + conclusion;
const result = await this.logicManager.process(system, 'validate', argumentString, 'natural');
if (result.status === 'success' && result.details?.analysis?.fallacies) {
const fallacies = result.details.analysis.fallacies;
if (fallacies.length > 0) {
penalty = Math.min(100, fallacies.length * 25); // 25 points per fallacy, max 100
details.push(`Detected ${fallacies.length} logical fallacy/fallacies`);
for (const fallacy of fallacies.slice(0, 3)) { // Show up to 3
details.push(`${fallacy.name}: ${fallacy.description}`);
}
} else {
details.push('No logical fallacies detected');
}
}
} catch (error) {
logger.debug(`Fallacy detection error: ${error instanceof Error ? error.message : String(error)}`);
}
// Additional heuristic fallacy checks
const allText = [...premises, conclusion].join(' ').toLowerCase();
// Ad hominem indicators
if (/\b(stupid|idiot|fool|ignorant)\b/.test(allText)) {
penalty += 15;
details.push('Possible ad hominem - attacks on person rather than argument');
}
// Appeal to emotion
if (/\b(think of the children|won't somebody|heart-wrenching|tragic)\b/.test(allText)) {
penalty += 10;
details.push('Possible appeal to emotion');
}
// False dichotomy
if (/\b(either .* or|only two (options|choices))\b/.test(allText)) {
penalty += 10;
details.push('Possible false dichotomy');
}
return {
score: Math.min(100, penalty),
weight: this.weights.fallacyPenalty,
explanation: penalty === 0
? 'No logical fallacies detected'
: penalty < 30
? 'Minor fallacious reasoning detected'
: 'Significant fallacious reasoning detected',
details
};
}
/**
* Score assumption complexity (0-100, lower is better)
*/
private async scoreAssumptionComplexity(
premises: string[],
conclusion: string,
system: LogicalSystem
): Promise<ScoreComponent> {
const details: string[] = [];
let complexity = 0;
try {
const report = await this.assumptionExtractor.extractAssumptions(premises, conclusion, system);
const hiddenAssumptions = report.hiddenAssumptions;
const criticalAssumptions = hiddenAssumptions.filter(a => a.necessity === 'required');
const gaps = report.validityGaps;
// Score based on number and severity of assumptions
complexity = Math.min(100, (criticalAssumptions.length * 20) + (hiddenAssumptions.length * 5) + (gaps.length * 15));
details.push(`${hiddenAssumptions.length} hidden assumption(s) detected`);
if (criticalAssumptions.length > 0) {
details.push(`${criticalAssumptions.length} critical assumption(s) required for validity`);
}
if (gaps.length > 0) {
details.push(`${gaps.length} validity gap(s) identified`);
}
// List most critical assumptions
for (const assumption of criticalAssumptions.slice(0, 2)) {
details.push(`Critical: ${assumption.statement}`);
}
} catch (error) {
logger.debug(`Assumption extraction error: ${error instanceof Error ? error.message : String(error)}`);
complexity = 30; // Moderate penalty if we can't extract
details.push('Could not fully analyze hidden assumptions');
}
return {
score: Math.min(100, complexity),
weight: this.weights.assumptionComplexity,
explanation: complexity === 0
? 'Minimal hidden assumptions - argument is explicit'
: complexity < 30
? 'Few hidden assumptions - mostly explicit'
: complexity < 60
? 'Moderate hidden assumptions - could be more explicit'
: 'Many hidden assumptions - significant gaps in reasoning',
details
};
}
/**
* Convert numeric score to letter grade
*/
private convertToGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F' {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
/**
* Identify argument strengths
*/
private identifyStrengths(components: ArgumentScore['components']): string[] {
const strengths: string[] = [];
if (components.validity.score === 100) {
strengths.push('Logically valid argument structure');
}
if (components.premisePlausibility.score >= 80) {
strengths.push('Strong, well-supported premises');
}
if (components.inferenceStrength.score >= 80) {
strengths.push('Clear inferential connection to conclusion');
}
if (components.structureQuality.score >= 80) {
strengths.push('Well-organized and clearly structured');
}
if (components.fallacyPenalty.score === 0) {
strengths.push('Free from logical fallacies');
}
if (components.assumptionComplexity.score < 20) {
strengths.push('Minimal hidden assumptions');
}
if (strengths.length === 0) {
strengths.push('Argument shows basic reasoning structure');
}
return strengths;
}
/**
* Identify argument weaknesses
*/
private identifyWeaknesses(components: ArgumentScore['components']): string[] {
const weaknesses: string[] = [];
if (components.validity.score === 0) {
weaknesses.push('Logically invalid - conclusion does not follow');
}
if (components.premisePlausibility.score < 60) {
weaknesses.push('Weak or unsupported premises');
}
if (components.inferenceStrength.score < 60) {
weaknesses.push('Weak connection between premises and conclusion');
}
if (components.structureQuality.score < 60) {
weaknesses.push('Poor argument structure or organization');
}
if (components.fallacyPenalty.score >= 30) {
weaknesses.push('Contains significant logical fallacies');
}
if (components.assumptionComplexity.score >= 60) {
weaknesses.push('Many hidden assumptions and logical gaps');
}
return weaknesses;
}
/**
* Generate recommendations for improvement
*/
private generateRecommendations(
components: ArgumentScore['components'],
premises: string[],
conclusion: string
): string[] {
const recommendations: string[] = [];
if (components.validity.score === 0) {
recommendations.push('Add missing premises to make the argument logically valid');
}
if (components.premisePlausibility.score < 70) {
recommendations.push('Provide evidence or justification for premises');
}
if (components.inferenceStrength.score < 70) {
recommendations.push('Clarify how premises connect to the conclusion');
}
if (components.structureQuality.score < 70) {
recommendations.push('Reorganize argument with clearer premise-conclusion structure');
}
if (components.fallacyPenalty.score >= 20) {
recommendations.push('Address and eliminate logical fallacies');
}
if (components.assumptionComplexity.score >= 40) {
recommendations.push('Make hidden assumptions explicit');
}
if (premises.length < 2) {
recommendations.push('Consider adding additional supporting premises');
}
if (recommendations.length === 0) {
recommendations.push('Strong argument overall - minor refinements may further improve clarity');
}
return recommendations;
}
/**
* Compare score to historical average
*/
private compareToAverage(score: number): string {
const diff = score - this.historicalAverage;
if (Math.abs(diff) < 5) {
return `About average (baseline: ${this.historicalAverage})`;
} else if (diff > 0) {
return `Above average by ${Math.round(diff)} points (baseline: ${this.historicalAverage})`;
} else {
return `Below average by ${Math.round(Math.abs(diff))} points (baseline: ${this.historicalAverage})`;
}
}
/**
* Extract key terms from text
*/
private extractKeyTerms(text: string): string[] {
// Remove common words and extract significant terms
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'must', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'as', 'by', 'at', 'from', 'that', 'this', 'it', 'not']);
const words = text
.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 3 && !stopWords.has(w));
return Array.from(new Set(words));
}
}