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
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:20 -0500
1"""Feature-related configuration dataclasses.
3Covers issue tracking, scanning, sprint management, loop management,
4and sync configuration.
5"""
7from __future__ import annotations
9from dataclasses import dataclass, field
10from typing import Any
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}
19# Default categories (same as required by default, could include optional defaults)
20DEFAULT_CATEGORIES: dict[str, dict[str, str]] = {
21 **REQUIRED_CATEGORIES,
22}
25@dataclass
26class CategoryConfig:
27 """Configuration for an issue category."""
29 prefix: str
30 dir: str
31 action: str = "fix"
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 )
43@dataclass
44class IssuesConfig:
45 """Issue management configuration."""
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"
55 @classmethod
56 def from_dict(cls, data: dict[str, Any]) -> IssuesConfig:
57 """Create IssuesConfig from dictionary.
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", {}))
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
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 )
83 def get_category_by_prefix(self, prefix: str) -> CategoryConfig | None:
84 """Get category config by prefix (e.g., 'BUG', 'FEAT').
86 Args:
87 prefix: Issue type prefix to look up
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
97 def get_category_by_dir(self, dir_name: str) -> CategoryConfig | None:
98 """Get category config by directory name.
100 Args:
101 dir_name: Directory name to look up
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
111 def get_all_prefixes(self) -> list[str]:
112 """Get all configured issue type prefixes.
114 Returns:
115 List of prefixes (e.g., ['BUG', 'FEAT', 'ENH'])
116 """
117 return [cat.prefix for cat in self.categories.values()]
119 def get_all_dirs(self) -> list[str]:
120 """Get all configured issue directory names.
122 Returns:
123 List of directory names (e.g., ['bugs', 'features', 'enhancements'])
124 """
125 return [cat.dir for cat in self.categories.values()]
128@dataclass
129class ScanConfig:
130 """Codebase scanning configuration."""
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)
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 )
151@dataclass
152class SprintsConfig:
153 """Sprint management configuration."""
155 sprints_dir: str = ".sprints"
156 default_timeout: int = 3600
157 default_max_workers: int = 2
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 )
169@dataclass
170class LoopsConfig:
171 """FSM loop configuration."""
173 loops_dir: str = ".loops"
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 )
183@dataclass
184class GitHubSyncConfig:
185 """GitHub-specific sync configuration."""
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"
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 )
211@dataclass
212class SyncConfig:
213 """Issue sync configuration."""
215 enabled: bool = False
216 provider: str = "github"
217 github: GitHubSyncConfig = field(default_factory=GitHubSyncConfig)
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 )