"""Struggle Score Calculator — Heuristic v1. Calculates content quality score based on struggle-first criteria. """ import re from typing import Dict, Any from dataclasses import dataclass @dataclass class StruggleMetrics: """Raw metrics for struggle calculation.""" i_statement_ratio: float temporal_markers: int struggle_patterns: int cost_mentions: int admission_count: int class StruggleScorer: """Heuristic struggle score calculator.""" # Weights for final score WEIGHTS = { "i_statement_ratio": 0.30, "temporal_markers": 0.15, "struggle_patterns": 0.25, "cost_mentions": 0.20, "admission_count": 0.10 } # Thresholds for normalization MAX_TEMPORAL_MARKERS = 10 MAX_STRUGGLE_PATTERNS = 5 MAX_COST_MENTIONS = 5 MAX_ADMISSIONS = 5 def calculate(self, content: str) -> Dict[str, Any]: """ Calculate struggle score from content. Returns: { "score": float, # 0-100 "metrics": StruggleMetrics, "breakdown": Dict[str, float], "suggestions": List[str] } """ # Split into paragraphs paragraphs = [p.strip() for p in content.split('\n\n') if p.strip()] # Calculate raw metrics metrics = self._extract_metrics(content, paragraphs) # Normalize to 0-1 scale normalized = self._normalize_metrics(metrics, len(paragraphs)) # Calculate weighted score score = self._calculate_weighted_score(normalized) # Generate suggestions suggestions = self._generate_suggestions(metrics, normalized) return { "score": round(score * 100, 1), "metrics": metrics, "breakdown": { "i_statement_ratio": round(normalized["i_statement_ratio"] * self.WEIGHTS["i_statement_ratio"], 3), "temporal_markers": round(normalized["temporal_markers"] * self.WEIGHTS["temporal_markers"], 3), "struggle_patterns": round(normalized["struggle_patterns"] * self.WEIGHTS["struggle_patterns"], 3), "cost_mentions": round(normalized["cost_mentions"] * self.WEIGHTS["cost_mentions"], 3), "admission_count": round(normalized["admission_count"] * self.WEIGHTS["admission_count"], 3), }, "suggestions": suggestions } def _extract_metrics(self, content: str, paragraphs: list) -> StruggleMetrics: """Extract raw metrics from content.""" # I-statement ratio (first 3 paragraphs) first_three = paragraphs[:3] if len(paragraphs) >= 3 else paragraphs i_statements = sum( 1 for p in first_three if re.search(r"^I\s+", p, re.IGNORECASE) ) i_ratio = i_statements / len(first_three) if first_three else 0 # Temporal markers temporal_count = len(re.findall( r"\b\d{1,2}:\d{2}|yesterday|last night|2 AM|3 AM|morning|evening|tonight|today|afternoon\b", content, re.IGNORECASE )) # Struggle patterns struggle_count = len(re.findall( r"\bI thought.*but|I tried.*failed|didn't work|broke|failed|couldn't|wouldn't|error|bug|issue|problem\b", content, re.IGNORECASE )) # Cost mentions cost_count = len(re.findall( r"\b(hour|minute|sleep|wife|kid|Aundrea|family|frustrated|angry|mad|annoyed)\b", content, re.IGNORECASE )) # Admissions admission_count = len(re.findall( r"\b(I was wrong|I didn't expect|should have|wish I|learned|if only|realized|turns out)\b", content, re.IGNORECASE )) return StruggleMetrics( i_statement_ratio=i_ratio, temporal_markers=temporal_count, struggle_patterns=struggle_count, cost_mentions=cost_count, admission_count=admission_count ) def _normalize_metrics(self, metrics: StruggleMetrics, paragraph_count: int) -> Dict[str, float]: """Normalize raw metrics to 0-1 scale.""" return { "i_statement_ratio": min(metrics.i_statement_ratio, 1.0), "temporal_markers": min(metrics.temporal_markers / self.MAX_TEMPORAL_MARKERS, 1.0), "struggle_patterns": min(metrics.struggle_patterns / self.MAX_STRUGGLE_PATTERNS, 1.0), "cost_mentions": min(metrics.cost_mentions / self.MAX_COST_MENTIONS, 1.0), "admission_count": min(metrics.admission_count / self.MAX_ADMISSIONS, 1.0), } def _calculate_weighted_score(self, normalized: Dict[str, float]) -> float: """Calculate final weighted score.""" score = sum( normalized[key] * self.WEIGHTS[key] for key in self.WEIGHTS.keys() ) return min(score, 1.0) def _generate_suggestions(self, metrics: StruggleMetrics, normalized: Dict[str, float]) -> list: """Generate improvement suggestions based on low scores.""" suggestions = [] if normalized["i_statement_ratio"] < 0.3: suggestions.append("Add more first-person perspective ('I...') in opening paragraphs") if normalized["temporal_markers"] < 0.2: suggestions.append("Include specific timestamps or temporal markers") if normalized["struggle_patterns"] < 0.3: suggestions.append("Add more struggle narrative ('I tried...but')") if normalized["cost_mentions"] < 0.2: suggestions.append("Mention who was affected or what it cost (time, sleep, relationships)") if normalized["admission_count"] < 0.2: suggestions.append("Include honest reflection or 'I was wrong' admission") return suggestions # Global scorer instance scorer = StruggleScorer() def calculate_struggle_score(content: str) -> Dict[str, Any]: """Convenience function for struggle score calculation.""" return scorer.calculate(content)