doit-toolkit-cli 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of doit-toolkit-cli might be problematic. Click here for more details.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/roadmapit_command.py +10 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +51 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1123 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +389 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
- doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Context configuration models for AI context injection.
|
|
2
|
+
|
|
3
|
+
This module provides dataclasses for configuring and managing context loading
|
|
4
|
+
for doit commands. Context includes constitution, roadmap, current spec,
|
|
5
|
+
and related specifications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import date, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SummarizationConfig:
|
|
18
|
+
"""Configuration for context summarization behavior.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
enabled: Whether summarization features are enabled
|
|
22
|
+
threshold_percentage: Percentage of total_max_tokens that triggers condensation
|
|
23
|
+
source_priorities: Order of priority for preserving content during condensation
|
|
24
|
+
timeout_seconds: Timeout for AI summarization API calls (if used)
|
|
25
|
+
fallback_to_truncation: Whether to fall back to truncation on AI failure
|
|
26
|
+
completed_items_max_count: Maximum completed items to include
|
|
27
|
+
completed_items_min_relevance: Minimum relevance score for completed items
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
enabled: bool = True
|
|
31
|
+
threshold_percentage: float = 80.0
|
|
32
|
+
source_priorities: list[str] = field(
|
|
33
|
+
default_factory=lambda: ["constitution", "roadmap", "completed_roadmap", "current_spec"]
|
|
34
|
+
)
|
|
35
|
+
timeout_seconds: float = 10.0
|
|
36
|
+
fallback_to_truncation: bool = True
|
|
37
|
+
completed_items_max_count: int = 5
|
|
38
|
+
completed_items_min_relevance: float = 0.3
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""Validate configuration after initialization."""
|
|
42
|
+
if self.threshold_percentage < 50.0:
|
|
43
|
+
self.threshold_percentage = 50.0
|
|
44
|
+
if self.threshold_percentage > 100.0:
|
|
45
|
+
self.threshold_percentage = 100.0
|
|
46
|
+
if self.timeout_seconds <= 0:
|
|
47
|
+
self.timeout_seconds = 10.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class RoadmapItem:
|
|
52
|
+
"""Represents a single item from roadmap.md.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
text: The item text/description
|
|
56
|
+
priority: Priority level (P1, P2, P3, P4)
|
|
57
|
+
rationale: Optional rationale/reason for the item
|
|
58
|
+
feature_ref: Optional feature branch reference (e.g., '034-fixit-workflow')
|
|
59
|
+
completed: Whether the item is marked as completed
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
text: str
|
|
63
|
+
priority: str = "P4"
|
|
64
|
+
rationale: str = ""
|
|
65
|
+
feature_ref: str = ""
|
|
66
|
+
completed: bool = False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class CompletedItem:
|
|
71
|
+
"""Represents a matched item from completed_roadmap.md.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
text: The completed item description
|
|
75
|
+
priority: Original priority when active
|
|
76
|
+
completion_date: Date the item was completed
|
|
77
|
+
feature_branch: Feature branch that implemented it
|
|
78
|
+
relevance_score: Similarity score for matching (0.0 - 1.0)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
text: str
|
|
82
|
+
priority: str = ""
|
|
83
|
+
completion_date: Optional[date] = None
|
|
84
|
+
feature_branch: str = ""
|
|
85
|
+
relevance_score: float = 0.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class RoadmapSummary:
|
|
90
|
+
"""Condensed representation of the roadmap for context injection.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
condensed_text: Summarized markdown content
|
|
94
|
+
item_count: Number of items included in summary
|
|
95
|
+
priorities_included: Which priority levels were included
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
condensed_text: str
|
|
99
|
+
item_count: int = 0
|
|
100
|
+
priorities_included: list[str] = field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class SourceConfig:
|
|
105
|
+
"""Configuration for an individual context source type.
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
source_type: Type identifier (constitution, roadmap, current_spec, related_specs, custom)
|
|
109
|
+
enabled: Whether this source should be loaded
|
|
110
|
+
priority: Loading priority (lower = higher priority, loaded first)
|
|
111
|
+
max_count: Maximum items for multi-item sources (e.g., related_specs)
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
source_type: str = ""
|
|
115
|
+
enabled: bool = True
|
|
116
|
+
priority: int = 99
|
|
117
|
+
max_count: int = 1
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def default_sources(cls) -> dict[str, "SourceConfig"]:
|
|
121
|
+
"""Get default source configurations."""
|
|
122
|
+
return {
|
|
123
|
+
"constitution": cls(source_type="constitution", enabled=True, priority=1),
|
|
124
|
+
"roadmap": cls(source_type="roadmap", enabled=True, priority=2),
|
|
125
|
+
"completed_roadmap": cls(source_type="completed_roadmap", enabled=True, priority=3, max_count=5),
|
|
126
|
+
"current_spec": cls(source_type="current_spec", enabled=True, priority=4),
|
|
127
|
+
"related_specs": cls(source_type="related_specs", enabled=True, priority=5, max_count=3),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class CommandOverride:
|
|
133
|
+
"""Per-command configuration overrides.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
command_name: Command to override (specit, planit, etc.)
|
|
137
|
+
sources: Source configuration overrides for this command
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
command_name: str = ""
|
|
141
|
+
sources: dict[str, SourceConfig] = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ContextConfig:
|
|
146
|
+
"""Configuration for context loading behavior.
|
|
147
|
+
|
|
148
|
+
Loaded from `.doit/config/context.yaml`.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
version: Config schema version (must be 1)
|
|
152
|
+
enabled: Master toggle for context loading
|
|
153
|
+
max_tokens_per_source: Token limit per individual source
|
|
154
|
+
total_max_tokens: Token limit for all context combined
|
|
155
|
+
sources: Per-source configuration
|
|
156
|
+
commands: Per-command overrides
|
|
157
|
+
summarization: Configuration for summarization behavior
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
version: int = 1
|
|
161
|
+
enabled: bool = True
|
|
162
|
+
max_tokens_per_source: int = 4000
|
|
163
|
+
total_max_tokens: int = 16000
|
|
164
|
+
sources: dict[str, SourceConfig] = field(default_factory=SourceConfig.default_sources)
|
|
165
|
+
commands: dict[str, CommandOverride] = field(default_factory=dict)
|
|
166
|
+
summarization: SummarizationConfig = field(default_factory=SummarizationConfig)
|
|
167
|
+
|
|
168
|
+
def __post_init__(self) -> None:
|
|
169
|
+
"""Validate configuration after initialization."""
|
|
170
|
+
if self.max_tokens_per_source <= 0:
|
|
171
|
+
self.max_tokens_per_source = 4000
|
|
172
|
+
if self.total_max_tokens < self.max_tokens_per_source:
|
|
173
|
+
self.total_max_tokens = self.max_tokens_per_source
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def default(cls) -> "ContextConfig":
|
|
177
|
+
"""Create default configuration."""
|
|
178
|
+
return cls()
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_yaml(cls, path: Path) -> "ContextConfig":
|
|
182
|
+
"""Load configuration from YAML file.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path: Path to the context.yaml configuration file.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
ContextConfig instance with values from file or defaults.
|
|
189
|
+
"""
|
|
190
|
+
if not path.exists():
|
|
191
|
+
return cls.default()
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
with open(path, encoding="utf-8") as f:
|
|
195
|
+
data = yaml.safe_load(f) or {}
|
|
196
|
+
except (yaml.YAMLError, OSError):
|
|
197
|
+
# Return default config on parse error
|
|
198
|
+
return cls.default()
|
|
199
|
+
|
|
200
|
+
return cls._from_dict(data)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def _from_dict(cls, data: dict) -> "ContextConfig":
|
|
204
|
+
"""Create ContextConfig from dictionary."""
|
|
205
|
+
# Parse sources
|
|
206
|
+
sources_data = data.get("sources", {})
|
|
207
|
+
sources = SourceConfig.default_sources()
|
|
208
|
+
|
|
209
|
+
for source_type, source_config in sources_data.items():
|
|
210
|
+
if isinstance(source_config, dict):
|
|
211
|
+
if source_type in sources:
|
|
212
|
+
# Update existing source config
|
|
213
|
+
for key, value in source_config.items():
|
|
214
|
+
if hasattr(sources[source_type], key):
|
|
215
|
+
setattr(sources[source_type], key, value)
|
|
216
|
+
else:
|
|
217
|
+
# Create new custom source
|
|
218
|
+
sources[source_type] = SourceConfig(
|
|
219
|
+
source_type=source_type,
|
|
220
|
+
**{k: v for k, v in source_config.items()
|
|
221
|
+
if k in SourceConfig.__dataclass_fields__}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Parse command overrides
|
|
225
|
+
commands_data = data.get("commands", {})
|
|
226
|
+
commands: dict[str, CommandOverride] = {}
|
|
227
|
+
|
|
228
|
+
for cmd_name, cmd_config in commands_data.items():
|
|
229
|
+
if isinstance(cmd_config, dict):
|
|
230
|
+
cmd_sources: dict[str, SourceConfig] = {}
|
|
231
|
+
cmd_sources_data = cmd_config.get("sources", {})
|
|
232
|
+
|
|
233
|
+
for source_type, source_config in cmd_sources_data.items():
|
|
234
|
+
if isinstance(source_config, dict):
|
|
235
|
+
cmd_sources[source_type] = SourceConfig(
|
|
236
|
+
source_type=source_type,
|
|
237
|
+
**{k: v for k, v in source_config.items()
|
|
238
|
+
if k in SourceConfig.__dataclass_fields__}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
commands[cmd_name] = CommandOverride(
|
|
242
|
+
command_name=cmd_name,
|
|
243
|
+
sources=cmd_sources
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Parse summarization config
|
|
247
|
+
summarization_data = data.get("summarization", {})
|
|
248
|
+
summarization = SummarizationConfig()
|
|
249
|
+
if isinstance(summarization_data, dict):
|
|
250
|
+
# Handle nested completed_items config
|
|
251
|
+
completed_items = summarization_data.get("completed_items", {})
|
|
252
|
+
if isinstance(completed_items, dict):
|
|
253
|
+
if "max_count" in completed_items:
|
|
254
|
+
summarization_data["completed_items_max_count"] = completed_items["max_count"]
|
|
255
|
+
if "min_relevance" in completed_items:
|
|
256
|
+
summarization_data["completed_items_min_relevance"] = completed_items["min_relevance"]
|
|
257
|
+
|
|
258
|
+
# Build SummarizationConfig from data
|
|
259
|
+
summarization = SummarizationConfig(
|
|
260
|
+
enabled=summarization_data.get("enabled", True),
|
|
261
|
+
threshold_percentage=summarization_data.get("threshold_percentage", 80.0),
|
|
262
|
+
source_priorities=summarization_data.get(
|
|
263
|
+
"source_priorities",
|
|
264
|
+
["constitution", "roadmap", "completed_roadmap", "current_spec"]
|
|
265
|
+
),
|
|
266
|
+
timeout_seconds=summarization_data.get("timeout_seconds", 10.0),
|
|
267
|
+
fallback_to_truncation=summarization_data.get("fallback_to_truncation", True),
|
|
268
|
+
completed_items_max_count=summarization_data.get("completed_items_max_count", 5),
|
|
269
|
+
completed_items_min_relevance=summarization_data.get("completed_items_min_relevance", 0.3),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return cls(
|
|
273
|
+
version=data.get("version", 1),
|
|
274
|
+
enabled=data.get("enabled", True),
|
|
275
|
+
max_tokens_per_source=data.get("max_tokens_per_source", 4000),
|
|
276
|
+
total_max_tokens=data.get("total_max_tokens", 16000),
|
|
277
|
+
sources=sources,
|
|
278
|
+
commands=commands,
|
|
279
|
+
summarization=summarization,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def get_default_config_path(cls) -> Path:
|
|
284
|
+
"""Get the default configuration file path."""
|
|
285
|
+
return Path(".doit/config/context.yaml")
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def load_default(cls) -> "ContextConfig":
|
|
289
|
+
"""Load configuration from default location."""
|
|
290
|
+
return cls.from_yaml(cls.get_default_config_path())
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def load_from_project(cls, project_root: Path) -> "ContextConfig":
|
|
294
|
+
"""Load configuration from a project directory.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
project_root: Root directory of the project.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
ContextConfig loaded from the project or defaults.
|
|
301
|
+
"""
|
|
302
|
+
config_path = project_root / ".doit" / "config" / "context.yaml"
|
|
303
|
+
return cls.from_yaml(config_path)
|
|
304
|
+
|
|
305
|
+
def get_source_config(
|
|
306
|
+
self, source_type: str, command: Optional[str] = None
|
|
307
|
+
) -> SourceConfig:
|
|
308
|
+
"""Get effective source configuration, considering command overrides.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
source_type: Type of source (constitution, roadmap, etc.)
|
|
312
|
+
command: Current command name for per-command overrides
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Effective SourceConfig for the given source and command.
|
|
316
|
+
"""
|
|
317
|
+
# Start with base source config
|
|
318
|
+
base_config = self.sources.get(
|
|
319
|
+
source_type,
|
|
320
|
+
SourceConfig(source_type=source_type)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Check for command-specific override
|
|
324
|
+
if command and command in self.commands:
|
|
325
|
+
cmd_override = self.commands[command]
|
|
326
|
+
if source_type in cmd_override.sources:
|
|
327
|
+
override = cmd_override.sources[source_type]
|
|
328
|
+
# Merge override with base (override takes precedence)
|
|
329
|
+
return SourceConfig(
|
|
330
|
+
source_type=source_type,
|
|
331
|
+
enabled=override.enabled,
|
|
332
|
+
priority=override.priority if override.priority != 99 else base_config.priority,
|
|
333
|
+
max_count=override.max_count if override.max_count != 1 else base_config.max_count,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return base_config
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@dataclass
|
|
340
|
+
class ContextSource:
|
|
341
|
+
"""A loaded context source ready for injection.
|
|
342
|
+
|
|
343
|
+
Attributes:
|
|
344
|
+
source_type: Type of source (constitution, roadmap, etc.)
|
|
345
|
+
path: File path that was loaded
|
|
346
|
+
content: Loaded content (possibly truncated)
|
|
347
|
+
token_count: Estimated token count
|
|
348
|
+
truncated: Whether content was truncated
|
|
349
|
+
original_tokens: Original token count before truncation (if truncated)
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
source_type: str
|
|
353
|
+
path: Path
|
|
354
|
+
content: str
|
|
355
|
+
token_count: int
|
|
356
|
+
truncated: bool = False
|
|
357
|
+
original_tokens: Optional[int] = None
|
|
358
|
+
|
|
359
|
+
def __lt__(self, other: "ContextSource") -> bool:
|
|
360
|
+
"""Compare sources by their source type for sorting."""
|
|
361
|
+
return self.source_type < other.source_type
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@dataclass
|
|
365
|
+
class LoadedContext:
|
|
366
|
+
"""Aggregated context ready for injection into command prompt.
|
|
367
|
+
|
|
368
|
+
Attributes:
|
|
369
|
+
sources: All loaded sources, ordered by priority
|
|
370
|
+
total_tokens: Sum of all source token counts
|
|
371
|
+
any_truncated: True if any source was truncated
|
|
372
|
+
loaded_at: Timestamp when context was loaded
|
|
373
|
+
_guidance_prompt: Optional AI guidance prompt when context is condensed
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
sources: list[ContextSource] = field(default_factory=list)
|
|
377
|
+
total_tokens: int = 0
|
|
378
|
+
any_truncated: bool = False
|
|
379
|
+
loaded_at: datetime = field(default_factory=datetime.now)
|
|
380
|
+
_guidance_prompt: Optional[str] = None
|
|
381
|
+
|
|
382
|
+
def to_markdown(self) -> str:
|
|
383
|
+
"""Format all sources as markdown for injection.
|
|
384
|
+
|
|
385
|
+
If a guidance prompt is set (due to context condensation), it will be
|
|
386
|
+
included at the start of the output to help the AI prioritize context.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Markdown-formatted context suitable for AI consumption.
|
|
390
|
+
"""
|
|
391
|
+
if not self.sources:
|
|
392
|
+
return ""
|
|
393
|
+
|
|
394
|
+
sections = ["<!-- PROJECT CONTEXT - Auto-loaded by doit -->", ""]
|
|
395
|
+
|
|
396
|
+
# Add guidance prompt if context was condensed
|
|
397
|
+
if self._guidance_prompt:
|
|
398
|
+
sections.append(self._guidance_prompt)
|
|
399
|
+
sections.append("")
|
|
400
|
+
|
|
401
|
+
# Map source types to display names
|
|
402
|
+
display_names = {
|
|
403
|
+
"constitution": "Constitution",
|
|
404
|
+
"roadmap": "Roadmap",
|
|
405
|
+
"completed_roadmap": "Completed Roadmap",
|
|
406
|
+
"current_spec": "Current Spec",
|
|
407
|
+
"related_specs": "Related Specs",
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for source in self.sources:
|
|
411
|
+
display_name = display_names.get(source.source_type, source.source_type.title())
|
|
412
|
+
|
|
413
|
+
# Add section header
|
|
414
|
+
if source.source_type == "current_spec":
|
|
415
|
+
# Extract feature name from path if possible
|
|
416
|
+
feature_name = source.path.parent.name if source.path.parent.name != "." else ""
|
|
417
|
+
if feature_name:
|
|
418
|
+
sections.append(f"## {display_name}: {feature_name}")
|
|
419
|
+
else:
|
|
420
|
+
sections.append(f"## {display_name}")
|
|
421
|
+
elif source.source_type == "related_specs":
|
|
422
|
+
# For related specs, use individual headers
|
|
423
|
+
feature_name = source.path.parent.name if source.path.parent.name != "." else ""
|
|
424
|
+
sections.append(f"### {feature_name}")
|
|
425
|
+
else:
|
|
426
|
+
sections.append(f"## {display_name}")
|
|
427
|
+
|
|
428
|
+
sections.append("")
|
|
429
|
+
sections.append(source.content)
|
|
430
|
+
|
|
431
|
+
if source.truncated and source.original_tokens:
|
|
432
|
+
sections.append("")
|
|
433
|
+
sections.append(f"<!-- Content truncated from {source.original_tokens} to {source.token_count} tokens. Full file at: {source.path} -->")
|
|
434
|
+
|
|
435
|
+
sections.append("")
|
|
436
|
+
|
|
437
|
+
sections.append("<!-- End of project context -->")
|
|
438
|
+
|
|
439
|
+
return "\n".join(sections)
|
|
440
|
+
|
|
441
|
+
def get_source(self, source_type: str) -> Optional[ContextSource]:
|
|
442
|
+
"""Get specific source by type.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
source_type: Type of source to retrieve.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
ContextSource if found, None otherwise.
|
|
449
|
+
"""
|
|
450
|
+
for source in self.sources:
|
|
451
|
+
if source.source_type == source_type:
|
|
452
|
+
return source
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
def has_source(self, source_type: str) -> bool:
|
|
456
|
+
"""Check if source type is loaded.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
source_type: Type of source to check.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
True if source is loaded, False otherwise.
|
|
463
|
+
"""
|
|
464
|
+
return self.get_source(source_type) is not None
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Models for cross-reference support between specs and tasks."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CoverageStatus(str, Enum):
|
|
9
|
+
"""Coverage status for a requirement."""
|
|
10
|
+
|
|
11
|
+
UNCOVERED = "uncovered"
|
|
12
|
+
PARTIAL = "partial"
|
|
13
|
+
COVERED = "covered"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Requirement:
|
|
18
|
+
"""A functional requirement extracted from spec.md.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
id: Requirement ID in FR-XXX format (e.g., FR-001)
|
|
22
|
+
spec_path: Path to the spec.md file containing this requirement
|
|
23
|
+
description: Full requirement text
|
|
24
|
+
line_number: Line number in spec.md where requirement is defined
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
spec_path: str
|
|
29
|
+
description: str
|
|
30
|
+
line_number: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TaskReference:
|
|
35
|
+
"""A cross-reference from a task to a requirement.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
requirement_id: The FR-XXX ID being referenced
|
|
39
|
+
position: Order in the reference list (0-indexed)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
requirement_id: str
|
|
43
|
+
position: int = 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Task:
|
|
48
|
+
"""An implementation task extracted from tasks.md.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
id: Auto-generated hash of normalized description
|
|
52
|
+
tasks_file: Path to the tasks.md file containing this task
|
|
53
|
+
description: Task text (without references)
|
|
54
|
+
completed: Checkbox state (True if [x] or [X])
|
|
55
|
+
line_number: Line number in tasks.md
|
|
56
|
+
references: List of FR-XXX references found in this task
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
id: str
|
|
60
|
+
tasks_file: str
|
|
61
|
+
description: str
|
|
62
|
+
completed: bool
|
|
63
|
+
line_number: int
|
|
64
|
+
references: list[TaskReference] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def requirement_ids(self) -> list[str]:
|
|
68
|
+
"""Get list of requirement IDs this task references."""
|
|
69
|
+
return [ref.requirement_id for ref in self.references]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class CrossReference:
|
|
74
|
+
"""A link between a task and a requirement.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
requirement_id: FK to Requirement.id (FR-XXX format)
|
|
78
|
+
task_id: FK to Task.id
|
|
79
|
+
position: Order when multiple refs in same task (0-indexed)
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
requirement_id: str
|
|
83
|
+
task_id: str
|
|
84
|
+
position: int = 0
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def id(self) -> str:
|
|
88
|
+
"""Composite key for the cross-reference."""
|
|
89
|
+
return f"{self.requirement_id}:{self.task_id}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class RequirementCoverage:
|
|
94
|
+
"""Coverage information for a single requirement.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
requirement: The requirement being tracked
|
|
98
|
+
tasks: List of tasks that implement this requirement
|
|
99
|
+
status: Coverage status (uncovered, partial, covered)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
requirement: Requirement
|
|
103
|
+
tasks: list[Task] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def task_count(self) -> int:
|
|
107
|
+
"""Number of tasks implementing this requirement."""
|
|
108
|
+
return len(self.tasks)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def completed_count(self) -> int:
|
|
112
|
+
"""Number of completed tasks implementing this requirement."""
|
|
113
|
+
return sum(1 for t in self.tasks if t.completed)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def status(self) -> CoverageStatus:
|
|
117
|
+
"""Derive coverage status from task states."""
|
|
118
|
+
if not self.tasks:
|
|
119
|
+
return CoverageStatus.UNCOVERED
|
|
120
|
+
if all(t.completed for t in self.tasks):
|
|
121
|
+
return CoverageStatus.COVERED
|
|
122
|
+
return CoverageStatus.PARTIAL
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def is_covered(self) -> bool:
|
|
126
|
+
"""Whether this requirement has at least one linked task."""
|
|
127
|
+
return len(self.tasks) > 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class CoverageReport:
|
|
132
|
+
"""Aggregate coverage report for a specification.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
spec_path: Path to the spec.md file
|
|
136
|
+
requirements: List of RequirementCoverage objects
|
|
137
|
+
orphaned_references: Task references to non-existent requirements
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
spec_path: str
|
|
141
|
+
requirements: list[RequirementCoverage] = field(default_factory=list)
|
|
142
|
+
orphaned_references: list[tuple[Task, str]] = field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def total_count(self) -> int:
|
|
146
|
+
"""Total number of requirements."""
|
|
147
|
+
return len(self.requirements)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def covered_count(self) -> int:
|
|
151
|
+
"""Number of requirements with at least one linked task."""
|
|
152
|
+
return sum(1 for r in self.requirements if r.is_covered)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def uncovered_count(self) -> int:
|
|
156
|
+
"""Number of requirements with no linked tasks."""
|
|
157
|
+
return self.total_count - self.covered_count
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def coverage_percent(self) -> float:
|
|
161
|
+
"""Coverage percentage (0-100)."""
|
|
162
|
+
if self.total_count == 0:
|
|
163
|
+
return 100.0
|
|
164
|
+
return (self.covered_count / self.total_count) * 100
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def is_fully_covered(self) -> bool:
|
|
168
|
+
"""Whether all requirements have at least one linked task."""
|
|
169
|
+
return self.covered_count == self.total_count
|
|
170
|
+
|
|
171
|
+
def get_uncovered_requirements(self) -> list[Requirement]:
|
|
172
|
+
"""Get list of requirements with no linked tasks."""
|
|
173
|
+
return [rc.requirement for rc in self.requirements if not rc.is_covered]
|
|
174
|
+
|
|
175
|
+
def get_requirement_coverage(
|
|
176
|
+
self, requirement_id: str
|
|
177
|
+
) -> Optional[RequirementCoverage]:
|
|
178
|
+
"""Get coverage info for a specific requirement."""
|
|
179
|
+
for rc in self.requirements:
|
|
180
|
+
if rc.requirement.id == requirement_id:
|
|
181
|
+
return rc
|
|
182
|
+
return None
|