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