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

1"""Parser for ll-goals.md product goals document. 

2 

3Provides structured access to product goals including persona and priorities. 

4""" 

5 

6from __future__ import annotations 

7 

8from dataclasses import dataclass, field 

9from pathlib import Path 

10from typing import Any 

11 

12import yaml 

13 

14 

15@dataclass 

16class Persona: 

17 """Primary user persona. 

18 

19 Attributes: 

20 id: Unique identifier for the persona 

21 name: Display name 

22 role: Description of the persona's role 

23 """ 

24 

25 id: str 

26 name: str 

27 role: str 

28 

29 @classmethod 

30 def from_dict(cls, data: dict[str, Any]) -> Persona: 

31 """Create Persona from dictionary. 

32 

33 Args: 

34 data: Dictionary with persona fields 

35 

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 ) 

44 

45 

46@dataclass 

47class Priority: 

48 """Strategic priority. 

49 

50 Attributes: 

51 id: Unique identifier for the priority 

52 name: Description of the priority 

53 """ 

54 

55 id: str 

56 name: str 

57 

58 @classmethod 

59 def from_dict(cls, data: dict[str, Any], index: int = 0) -> Priority: 

60 """Create Priority from dictionary. 

61 

62 Args: 

63 data: Dictionary with priority fields 

64 index: Index for generating default ID 

65 

66 Returns: 

67 Priority instance 

68 """ 

69 return cls( 

70 id=data.get("id", f"priority-{index}"), 

71 name=data.get("name", ""), 

72 ) 

73 

74 

75@dataclass 

76class ProductGoals: 

77 """Parsed product goals from ll-goals.md. 

78 

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

85 

86 version: str 

87 persona: Persona | None 

88 priorities: list[Priority] = field(default_factory=list) 

89 raw_content: str = "" 

90 

91 @classmethod 

92 def from_file(cls, path: Path) -> ProductGoals | None: 

93 """Parse goals from ll-goals.md file. 

94 

95 Args: 

96 path: Path to the goals file 

97 

98 Returns: 

99 ProductGoals instance or None if file doesn't exist or is invalid 

100 """ 

101 if not path.exists(): 

102 return None 

103 

104 try: 

105 content = path.read_text(encoding="utf-8") 

106 except (OSError, UnicodeDecodeError): 

107 return None 

108 

109 return cls.from_content(content) 

110 

111 @classmethod 

112 def from_content(cls, content: str) -> ProductGoals | None: 

113 """Parse goals from string content. 

114 

115 Args: 

116 content: Raw file content 

117 

118 Returns: 

119 ProductGoals instance or None if content is invalid 

120 """ 

121 if not content or not content.startswith("---"): 

122 return None 

123 

124 # Find closing frontmatter delimiter 

125 parts = content.split("---", 2) 

126 if len(parts) < 3: 

127 return None 

128 

129 frontmatter_text = parts[1].strip() 

130 if not frontmatter_text: 

131 return None 

132 

133 try: 

134 frontmatter = yaml.safe_load(frontmatter_text) 

135 except yaml.YAMLError: 

136 return None 

137 

138 if not isinstance(frontmatter, dict): 

139 return None 

140 

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) 

146 

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

154 

155 return cls( 

156 version=str(frontmatter.get("version", "1.0")), 

157 persona=persona, 

158 priorities=priorities, 

159 raw_content=content, 

160 ) 

161 

162 def is_valid(self) -> bool: 

163 """Check if goals have minimum required content. 

164 

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 

169 

170 

171def validate_goals(goals: ProductGoals) -> list[str]: 

172 """Validate product goals and return warnings. 

173 

174 Args: 

175 goals: ProductGoals instance to validate 

176 

177 Returns: 

178 List of validation warning messages (empty if valid) 

179 """ 

180 warnings: list[str] = [] 

181 

182 if not goals.persona: 

183 warnings.append("No persona defined - product analysis may be less effective") 

184 

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

189 

190 if "[NEEDS REVIEW]" in goals.raw_content: 

191 warnings.append("File contains [NEEDS REVIEW] placeholders - please update") 

192 

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

198 

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

202 

203 return warnings