← back to ClawCoder
src/lib/analyzer/scorer.ts
69 lines
import type { AnalysisResult, CriterionResult } from './types'
import { ALL_RULES } from './rules'
/**
* Calculate a letter grade from a percentage (0-100).
*/
function letterGrade(pct: number): string {
if (pct >= 97) return 'A+'
if (pct >= 93) return 'A'
if (pct >= 90) return 'A-'
if (pct >= 87) return 'B+'
if (pct >= 83) return 'B'
if (pct >= 80) return 'B-'
if (pct >= 77) return 'C+'
if (pct >= 73) return 'C'
if (pct >= 70) return 'C-'
if (pct >= 67) return 'D+'
if (pct >= 63) return 'D'
if (pct >= 60) return 'D-'
return 'F'
}
/**
* Analyze a CLAUDE.md content string and return a full AnalysisResult.
*/
export function analyzeClaudeMd(content: string): AnalysisResult {
// Run all 12 rules
const criteria: CriterionResult[] = ALL_RULES.map(rule => rule(content))
// Calculate totals
const overallScore = criteria.reduce((sum, c) => sum + c.score, 0)
const maxScore = criteria.reduce((sum, c) => sum + c.maxScore, 0)
const percentage = maxScore > 0 ? Math.round((overallScore / maxScore) * 100) : 0
// Generate pros: criteria that earned full score
const pros: string[] = criteria
.filter(c => c.score === c.maxScore)
.map(c => `${c.name}: full marks (${c.score}/${c.maxScore})`)
// Generate cons: criteria with zero score or critical findings
const cons: string[] = []
for (const c of criteria) {
if (c.score === 0) {
const criticalFindings = c.findings.filter(f => f.severity === 'critical')
if (criticalFindings.length > 0) {
cons.push(`${c.name}: ${criticalFindings[0].message}`)
} else if (c.findings.length > 0) {
cons.push(`${c.name}: ${c.findings[0].message}`)
} else {
cons.push(`${c.name}: scored 0/${c.maxScore}`)
}
}
}
const lineCount = content.split('\n').length
return {
overallScore,
maxScore,
percentage,
letterGrade: letterGrade(percentage),
criteria,
pros,
cons,
suggestions: [], // Suggestions are generated separately by the suggester
lineCount,
}
}