"""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)