Coverage for little_loops / goals_parser.py: 0%
81 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""Parser for ll-goals.md product goals document.
3Provides structured access to product goals including persona and priorities.
4"""
6from __future__ import annotations
8from dataclasses import dataclass, field
9from pathlib import Path
10from typing import Any
12import yaml
15@dataclass
16class Persona:
17 """Primary user persona.
19 Attributes:
20 id: Unique identifier for the persona
21 name: Display name
22 role: Description of the persona's role
23 """
25 id: str
26 name: str
27 role: str
29 @classmethod
30 def from_dict(cls, data: dict[str, Any]) -> Persona:
31 """Create Persona from dictionary.
33 Args:
34 data: Dictionary with persona fields
36 Returns:
37 Persona instance
38 """
39 return cls(
40 id=data.get("id", "user"),
41 name=data.get("name", "User"),
42 role=data.get("role", ""),
43 )
46@dataclass
47class Priority:
48 """Strategic priority.
50 Attributes:
51 id: Unique identifier for the priority
52 name: Description of the priority
53 """
55 id: str
56 name: str
58 @classmethod
59 def from_dict(cls, data: dict[str, Any], index: int = 0) -> Priority:
60 """Create Priority from dictionary.
62 Args:
63 data: Dictionary with priority fields
64 index: Index for generating default ID
66 Returns:
67 Priority instance
68 """
69 return cls(
70 id=data.get("id", f"priority-{index}"),
71 name=data.get("name", ""),
72 )
75@dataclass
76class ProductGoals:
77 """Parsed product goals from ll-goals.md.
79 Attributes:
80 version: Schema version of the goals file
81 persona: Primary user persona (may be None)
82 priorities: List of strategic priorities
83 raw_content: Full markdown content for LLM context
84 """
86 version: str
87 persona: Persona | None
88 priorities: list[Priority] = field(default_factory=list)
89 raw_content: str = ""
91 @classmethod
92 def from_file(cls, path: Path) -> ProductGoals | None:
93 """Parse goals from ll-goals.md file.
95 Args:
96 path: Path to the goals file
98 Returns:
99 ProductGoals instance or None if file doesn't exist or is invalid
100 """
101 if not path.exists():
102 return None
104 try:
105 content = path.read_text(encoding="utf-8")
106 except (OSError, UnicodeDecodeError):
107 return None
109 return cls.from_content(content)
111 @classmethod
112 def from_content(cls, content: str) -> ProductGoals | None:
113 """Parse goals from string content.
115 Args:
116 content: Raw file content
118 Returns:
119 ProductGoals instance or None if content is invalid
120 """
121 if not content or not content.startswith("---"):
122 return None
124 # Find closing frontmatter delimiter
125 parts = content.split("---", 2)
126 if len(parts) < 3:
127 return None
129 frontmatter_text = parts[1].strip()
130 if not frontmatter_text:
131 return None
133 try:
134 frontmatter = yaml.safe_load(frontmatter_text)
135 except yaml.YAMLError:
136 return None
138 if not isinstance(frontmatter, dict):
139 return None
141 # Parse persona
142 persona = None
143 persona_data = frontmatter.get("persona")
144 if persona_data and isinstance(persona_data, dict):
145 persona = Persona.from_dict(persona_data)
147 # Parse priorities
148 priorities: list[Priority] = []
149 priorities_data = frontmatter.get("priorities", [])
150 if isinstance(priorities_data, list):
151 for i, p in enumerate(priorities_data, 1):
152 if isinstance(p, dict):
153 priorities.append(Priority.from_dict(p, i))
155 return cls(
156 version=str(frontmatter.get("version", "1.0")),
157 persona=persona,
158 priorities=priorities,
159 raw_content=content,
160 )
162 def is_valid(self) -> bool:
163 """Check if goals have minimum required content.
165 Returns:
166 True if persona and at least one priority are defined
167 """
168 return self.persona is not None and len(self.priorities) > 0
171def validate_goals(goals: ProductGoals) -> list[str]:
172 """Validate product goals and return warnings.
174 Args:
175 goals: ProductGoals instance to validate
177 Returns:
178 List of validation warning messages (empty if valid)
179 """
180 warnings: list[str] = []
182 if not goals.persona:
183 warnings.append("No persona defined - product analysis may be less effective")
185 if not goals.priorities:
186 warnings.append("No priorities defined - cannot assess goal alignment")
187 elif len(goals.priorities) > 5:
188 warnings.append("More than 5 priorities defined - consider focusing on top priorities")
190 if "[NEEDS REVIEW]" in goals.raw_content:
191 warnings.append("File contains [NEEDS REVIEW] placeholders - please update")
193 # Check for template placeholder priority names
194 template_placeholders = {"Primary goal description", "Secondary goal description"}
195 for i, priority in enumerate(goals.priorities, 1):
196 if not priority.name or priority.name in template_placeholders:
197 warnings.append(f"Priority {i} has placeholder or empty name")
199 # Check for template persona
200 if goals.persona and goals.persona.role == "Software developer using this project":
201 warnings.append("Persona has template description - please customize")
203 return warnings