📄 struggle.py 6,283 bytes Apr 22, 2026 📋 Raw

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