Coverage for little_loops / config / features.py: 74%

85 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:20 -0500

1"""Feature-related configuration dataclasses. 

2 

3Covers issue tracking, scanning, sprint management, loop management, 

4and sync configuration. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from typing import Any 

11 

12# Required categories that must always exist (cannot be removed by user config) 

13REQUIRED_CATEGORIES: dict[str, dict[str, str]] = { 

14 "bugs": {"prefix": "BUG", "dir": "bugs", "action": "fix"}, 

15 "features": {"prefix": "FEAT", "dir": "features", "action": "implement"}, 

16 "enhancements": {"prefix": "ENH", "dir": "enhancements", "action": "improve"}, 

17} 

18 

19# Default categories (same as required by default, could include optional defaults) 

20DEFAULT_CATEGORIES: dict[str, dict[str, str]] = { 

21 **REQUIRED_CATEGORIES, 

22} 

23 

24 

25@dataclass 

26class CategoryConfig: 

27 """Configuration for an issue category.""" 

28 

29 prefix: str 

30 dir: str 

31 action: str = "fix" 

32 

33 @classmethod 

34 def from_dict(cls, key: str, data: dict[str, Any]) -> CategoryConfig: 

35 """Create CategoryConfig from dictionary.""" 

36 return cls( 

37 prefix=data.get("prefix", key.upper()[:3]), 

38 dir=data.get("dir", key), 

39 action=data.get("action", "fix"), 

40 ) 

41 

42 

43@dataclass 

44class IssuesConfig: 

45 """Issue management configuration.""" 

46 

47 base_dir: str = ".issues" 

48 categories: dict[str, CategoryConfig] = field(default_factory=dict) 

49 completed_dir: str = "completed" 

50 deferred_dir: str = "deferred" 

51 priorities: list[str] = field(default_factory=lambda: ["P0", "P1", "P2", "P3", "P4", "P5"]) 

52 templates_dir: str | None = None 

53 capture_template: str = "full" 

54 

55 @classmethod 

56 def from_dict(cls, data: dict[str, Any]) -> IssuesConfig: 

57 """Create IssuesConfig from dictionary. 

58 

59 Required categories (bugs, features, enhancements) are automatically 

60 included if not specified in user config. 

61 """ 

62 # Start with user categories or empty dict 

63 categories_data = dict(data.get("categories", {})) 

64 

65 # Ensure required categories exist (merge with defaults) 

66 for key, defaults in REQUIRED_CATEGORIES.items(): 

67 if key not in categories_data: 

68 categories_data[key] = defaults 

69 

70 categories = { 

71 key: CategoryConfig.from_dict(key, value) for key, value in categories_data.items() 

72 } 

73 return cls( 

74 base_dir=data.get("base_dir", ".issues"), 

75 categories=categories, 

76 completed_dir=data.get("completed_dir", "completed"), 

77 deferred_dir=data.get("deferred_dir", "deferred"), 

78 priorities=data.get("priorities", ["P0", "P1", "P2", "P3", "P4", "P5"]), 

79 templates_dir=data.get("templates_dir"), 

80 capture_template=data.get("capture_template", "full"), 

81 ) 

82 

83 def get_category_by_prefix(self, prefix: str) -> CategoryConfig | None: 

84 """Get category config by prefix (e.g., 'BUG', 'FEAT'). 

85 

86 Args: 

87 prefix: Issue type prefix to look up 

88 

89 Returns: 

90 CategoryConfig if found, None otherwise 

91 """ 

92 for category in self.categories.values(): 

93 if category.prefix == prefix: 

94 return category 

95 return None 

96 

97 def get_category_by_dir(self, dir_name: str) -> CategoryConfig | None: 

98 """Get category config by directory name. 

99 

100 Args: 

101 dir_name: Directory name to look up 

102 

103 Returns: 

104 CategoryConfig if found, None otherwise 

105 """ 

106 for category in self.categories.values(): 

107 if category.dir == dir_name: 

108 return category 

109 return None 

110 

111 def get_all_prefixes(self) -> list[str]: 

112 """Get all configured issue type prefixes. 

113 

114 Returns: 

115 List of prefixes (e.g., ['BUG', 'FEAT', 'ENH']) 

116 """ 

117 return [cat.prefix for cat in self.categories.values()] 

118 

119 def get_all_dirs(self) -> list[str]: 

120 """Get all configured issue directory names. 

121 

122 Returns: 

123 List of directory names (e.g., ['bugs', 'features', 'enhancements']) 

124 """ 

125 return [cat.dir for cat in self.categories.values()] 

126 

127 

128@dataclass 

129class ScanConfig: 

130 """Codebase scanning configuration.""" 

131 

132 focus_dirs: list[str] = field(default_factory=lambda: ["src/", "tests/"]) 

133 exclude_patterns: list[str] = field( 

134 default_factory=lambda: ["**/node_modules/**", "**/__pycache__/**", "**/.git/**"] 

135 ) 

136 custom_agents: list[str] = field(default_factory=list) 

137 

138 @classmethod 

139 def from_dict(cls, data: dict[str, Any]) -> ScanConfig: 

140 """Create ScanConfig from dictionary.""" 

141 return cls( 

142 focus_dirs=data.get("focus_dirs", ["src/", "tests/"]), 

143 exclude_patterns=data.get( 

144 "exclude_patterns", 

145 ["**/node_modules/**", "**/__pycache__/**", "**/.git/**"], 

146 ), 

147 custom_agents=data.get("custom_agents", []), 

148 ) 

149 

150 

151@dataclass 

152class SprintsConfig: 

153 """Sprint management configuration.""" 

154 

155 sprints_dir: str = ".sprints" 

156 default_timeout: int = 3600 

157 default_max_workers: int = 2 

158 

159 @classmethod 

160 def from_dict(cls, data: dict[str, Any]) -> SprintsConfig: 

161 """Create SprintsConfig from dictionary.""" 

162 return cls( 

163 sprints_dir=data.get("sprints_dir", ".sprints"), 

164 default_timeout=data.get("default_timeout", 3600), 

165 default_max_workers=data.get("default_max_workers", 2), 

166 ) 

167 

168 

169@dataclass 

170class LoopsConfig: 

171 """FSM loop configuration.""" 

172 

173 loops_dir: str = ".loops" 

174 

175 @classmethod 

176 def from_dict(cls, data: dict[str, Any]) -> LoopsConfig: 

177 """Create LoopsConfig from dictionary.""" 

178 return cls( 

179 loops_dir=data.get("loops_dir", ".loops"), 

180 ) 

181 

182 

183@dataclass 

184class GitHubSyncConfig: 

185 """GitHub-specific sync configuration.""" 

186 

187 repo: str | None = None 

188 label_mapping: dict[str, str] = field( 

189 default_factory=lambda: {"BUG": "bug", "FEAT": "enhancement", "ENH": "enhancement"} 

190 ) 

191 priority_labels: bool = True 

192 sync_completed: bool = False 

193 state_file: str = ".claude/ll-sync-state.json" 

194 pull_template: str = "minimal" 

195 

196 @classmethod 

197 def from_dict(cls, data: dict[str, Any]) -> GitHubSyncConfig: 

198 """Create GitHubSyncConfig from dictionary.""" 

199 return cls( 

200 repo=data.get("repo"), 

201 label_mapping=data.get( 

202 "label_mapping", {"BUG": "bug", "FEAT": "enhancement", "ENH": "enhancement"} 

203 ), 

204 priority_labels=data.get("priority_labels", True), 

205 sync_completed=data.get("sync_completed", False), 

206 state_file=data.get("state_file", ".claude/ll-sync-state.json"), 

207 pull_template=data.get("pull_template", "minimal"), 

208 ) 

209 

210 

211@dataclass 

212class SyncConfig: 

213 """Issue sync configuration.""" 

214 

215 enabled: bool = False 

216 provider: str = "github" 

217 github: GitHubSyncConfig = field(default_factory=GitHubSyncConfig) 

218 

219 @classmethod 

220 def from_dict(cls, data: dict[str, Any]) -> SyncConfig: 

221 """Create SyncConfig from dictionary.""" 

222 return cls( 

223 enabled=data.get("enabled", False), 

224 provider=data.get("provider", "github"), 

225 github=GitHubSyncConfig.from_dict(data.get("github", {})), 

226 )