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