← 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,
  }
}