📄 brief.py 9,056 bytes Apr 22, 2026 📋 Raw

"""Brief Validation — Real-time field checks.

Validates struggle-first content briefs before storage.
"""

import re
from typing import List, Dict, Any, Optional
from dataclasses import dataclass

@dataclass
class ValidationError:
field: str
message: str
severity: str = "error" # "error" or "warning"

class BriefValidator:
"""Validates content briefs against struggle-first requirements."""

# Minimum lengths for required fields
MIN_STRUGGLE_ANGLE = 50
MIN_ORIGIN_STORY = 100
MIN_THE_MOMENT = 50
MIN_THE_FIX = 50
MIN_REFLECTION = 50

def __init__(self):
    self.errors: List[ValidationError] = []
    self.warnings: List[ValidationError] = []

def validate(self, brief: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate a brief and return result.

    Returns:
        {
            "valid": bool,
            "errors": [{"field": str, "message": str}],
            "warnings": [{"field": str, "message": str}],
            "voice_score": int  # 0-6 based on checklist
        }
    """
    self.errors = []
    self.warnings = []

    # Required fields
    required_fields = [
        "title", "struggle_angle", "origin_story", "attempts",
        "the_moment", "the_fix", "reflection", "voice_checklist"
    ]

    for field in required_fields:
        if field not in brief or not brief[field]:
            self.errors.append(ValidationError(field, f"{field} is required"))

    # Skip detailed validation if required fields missing
    if self.errors:
        return self._result()

    # Field-specific validations
    self._validate_struggle_angle(brief.get("struggle_angle", ""))
    self._validate_origin_story(brief.get("origin_story", ""))
    self._validate_attempts(brief.get("attempts", []))
    self._validate_the_moment(brief.get("the_moment", ""))
    self._validate_the_fix(brief.get("the_fix", ""))
    self._validate_reflection(brief.get("reflection", ""))
    self._validate_voice_checklist(brief.get("voice_checklist", {}))
    self._validate_title(brief.get("title", ""))

    return self._result()

def _validate_struggle_angle(self, value: str):
    """Validate struggle angle has personal stakes."""
    if len(value) < self.MIN_STRUGGLE_ANGLE:
        self.errors.append(ValidationError(
            "struggle_angle",
            f"Must be at least {self.MIN_STRUGGLE_ANGLE} characters"
        ))

    # Check for "I" statements
    if not re.search(r"\bI\s+\w+", value):
        self.errors.append(ValidationError(
            "struggle_angle",
            "Must include first-person perspective ('I...')"
        ))

    # Check for stakes (who was affected)
    if not re.search(r"\b(wife|Aundrea|kid|family|my)\b", value, re.IGNORECASE):
        self.warnings.append(ValidationError(
            "struggle_angle",
            "Consider adding who was affected (family, wife, etc.)",
            "warning"
        ))

def _validate_origin_story(self, value: str):
    """Validate origin story has timestamp/location."""
    if len(value) < self.MIN_ORIGIN_STORY:
        self.errors.append(ValidationError(
            "origin_story",
            f"Must be at least {self.MIN_ORIGIN_STORY} characters"
        ))

    # Check for temporal markers
    if not re.search(r"\b\d{1,2}:\d{2}|yesterday|last night|2 AM|morning|evening\b", value, re.IGNORECASE):
        self.warnings.append(ValidationError(
            "origin_story",
            "Consider adding when it happened (timestamp)",
            "warning"
        ))

def _validate_attempts(self, attempts: List):
    """Validate at least 2 attempts with failures."""
    if not isinstance(attempts, list):
        self.errors.append(ValidationError("attempts", "Must be an array"))
        return

    if len(attempts) < 2:
        self.errors.append(ValidationError(
            "attempts",
            "Must include at least 2 failed attempts"
        ))

    for i, attempt in enumerate(attempts):
        if not isinstance(attempt, dict):
            continue

        if not attempt.get("attempt"):
            self.errors.append(ValidationError(
                "attempts",
                f"Attempt {i+1}: description required"
            ))

        if not attempt.get("why_failed"):
            self.errors.append(ValidationError(
                "attempts",
                f"Attempt {i+1}: why it failed is required"
            ))

def _validate_the_moment(self, value: str):
    """Validate 'the moment' has realization or break."""
    if len(value) < self.MIN_THE_MOMENT:
        self.errors.append(ValidationError(
            "the_moment",
            f"Must be at least {self.MIN_THE_MOMENT} characters"
        ))

    # Check for realization pattern
    if not re.search(r"\b(realized|suddenly|then it hit|that.s when)\b", value, re.IGNORECASE):
        self.warnings.append(ValidationError(
            "the_moment",
            "Consider describing the realization moment",
            "warning"
        ))

def _validate_the_fix(self, value: str):
    """Validate fix includes caveats."""
    if len(value) < self.MIN_THE_FIX:
        self.errors.append(ValidationError(
            "the_fix",
            f"Must be at least {self.MIN_THE_FIX} characters"
        ))

    # Check for caveats
    if not re.search(r"\b(but|caveat|tradeoff|downside|issue|problem with)\b", value, re.IGNORECASE):
        self.warnings.append(ValidationError(
            "the_fix",
            "Consider adding caveats or tradeoffs",
            "warning"
        ))

def _validate_reflection(self, value: str):
    """Validate reflection is honest, not preachy."""
    if len(value) < self.MIN_REFLECTION:
        self.errors.append(ValidationError(
            "reflection",
            f"Must be at least {self.MIN_REFLECTION} characters"
        ))

    # Check for admission pattern
    if not re.search(r"\b(I learned|I should have|I would|I wish|I didn't expect)\b", value, re.IGNORECASE):
        self.warnings.append(ValidationError(
            "reflection",
            "Consider what you'd do differently",
            "warning"
        ))

def _validate_voice_checklist(self, checklist: Dict[str, bool]):
    """Validate voice checklist items."""
    required_items = [
        "first_person",
        "specific_moments",
        "struggle_pattern",
        "cost_mentioned",
        "quote_included",
        "admission_made"
    ]

    for item in required_items:
        if not checklist.get(item):
            self.warnings.append(ValidationError(
                "voice_checklist",
                f"Consider adding: {item.replace('_', ' ').title()}",
                "warning"
            ))

def _validate_title(self, value: str):
    """Validate title is specific, not generic."""
    if len(value) < 10:
        self.errors.append(ValidationError("title", "Title too short"))

    # Check for generic tutorial titles
    generic_patterns = [r"^how to", r"^guide to", r"^introduction", r"^getting started"]
    for pattern in generic_patterns:
        if re.search(pattern, value, re.IGNORECASE):
            self.warnings.append(ValidationError(
                "title",
                "Consider a more specific/story-driven title",
                "warning"
            ))
            break

def _result(self) -> Dict[str, Any]:
    """Build validation result."""
    # Calculate voice score (0-6)
    voice_score = 0
    for item in ["first_person", "specific_moments", "struggle_pattern", 
                 "cost_mentioned", "quote_included", "admission_made"]:
        # This is calculated from warnings, not ideal but works
        pass

    return {
        "valid": len(self.errors) == 0,
        "errors": [{"field": e.field, "message": e.message} for e in self.errors],
        "warnings": [{"field": w.field, "message": w.message} for w in self.warnings],
        "voice_score": 6 - len(self.warnings) if self.warnings else 6
    }

Global validator instance

validator = BriefValidator()

def validate_brief(brief: Dict[str, Any]) -> Dict[str, Any]:
"""Convenience function for brief validation."""
return validator.validate(brief)