doit-toolkit-cli 0.1.9__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.
- 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/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 +49 -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 +1121 -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 +368 -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.9.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
- doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Report generator for validation results."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.tree import Tree
|
|
11
|
+
|
|
12
|
+
from ..models.validation_models import Severity, ValidationResult, ValidationStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReportGenerator:
|
|
16
|
+
"""Generates validation reports in human-readable and JSON formats."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, console: Optional[Console] = None) -> None:
|
|
19
|
+
"""Initialize report generator.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
console: Rich console for output. Creates new one if None.
|
|
23
|
+
"""
|
|
24
|
+
self.console = console or Console()
|
|
25
|
+
|
|
26
|
+
def display_result(self, result: ValidationResult) -> None:
|
|
27
|
+
"""Display a single validation result with rich formatting.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
result: The validation result to display.
|
|
31
|
+
"""
|
|
32
|
+
spec_name = Path(result.spec_path).parent.name
|
|
33
|
+
self.console.print()
|
|
34
|
+
self.console.print(f"[bold]Validating:[/bold] {result.spec_path}")
|
|
35
|
+
self.console.print()
|
|
36
|
+
|
|
37
|
+
if not result.issues:
|
|
38
|
+
self.console.print("[green]No issues found[/green]")
|
|
39
|
+
else:
|
|
40
|
+
# Group issues by severity
|
|
41
|
+
errors = [i for i in result.issues if i.severity == Severity.ERROR]
|
|
42
|
+
warnings = [i for i in result.issues if i.severity == Severity.WARNING]
|
|
43
|
+
infos = [i for i in result.issues if i.severity == Severity.INFO]
|
|
44
|
+
|
|
45
|
+
# Display errors
|
|
46
|
+
if errors:
|
|
47
|
+
self._display_issue_group("ERRORS", errors, "red")
|
|
48
|
+
|
|
49
|
+
# Display warnings
|
|
50
|
+
if warnings:
|
|
51
|
+
self._display_issue_group("WARNINGS", warnings, "yellow")
|
|
52
|
+
|
|
53
|
+
# Display info
|
|
54
|
+
if infos:
|
|
55
|
+
self._display_issue_group("INFO", infos, "blue")
|
|
56
|
+
|
|
57
|
+
# Display summary line
|
|
58
|
+
self.console.print()
|
|
59
|
+
self.console.print("━" * 50)
|
|
60
|
+
self._display_summary_line(result)
|
|
61
|
+
|
|
62
|
+
def _display_issue_group(
|
|
63
|
+
self,
|
|
64
|
+
title: str,
|
|
65
|
+
issues: list,
|
|
66
|
+
color: str,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Display a group of issues with tree formatting.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
title: Group title (e.g., "ERRORS").
|
|
72
|
+
issues: List of issues to display.
|
|
73
|
+
color: Color for the group.
|
|
74
|
+
"""
|
|
75
|
+
tree = Tree(f"[{color}]{title} ({len(issues)})[/{color}]")
|
|
76
|
+
|
|
77
|
+
for i, issue in enumerate(issues):
|
|
78
|
+
is_last = i == len(issues) - 1
|
|
79
|
+
prefix = "└─" if is_last else "├─"
|
|
80
|
+
|
|
81
|
+
line_info = f"Line {issue.line_number}: " if issue.line_number > 0 else ""
|
|
82
|
+
tree.add(f"[{color}]{line_info}{issue.message}[/{color}]")
|
|
83
|
+
|
|
84
|
+
if issue.suggestion:
|
|
85
|
+
# Add suggestion as child
|
|
86
|
+
tree.add(f" [dim]Suggestion: {issue.suggestion}[/dim]")
|
|
87
|
+
|
|
88
|
+
self.console.print(tree)
|
|
89
|
+
|
|
90
|
+
def _display_summary_line(self, result: ValidationResult) -> None:
|
|
91
|
+
"""Display the summary line with score and status.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
result: The validation result.
|
|
95
|
+
"""
|
|
96
|
+
# Determine status color
|
|
97
|
+
status_colors = {
|
|
98
|
+
ValidationStatus.PASS: "green",
|
|
99
|
+
ValidationStatus.WARN: "yellow",
|
|
100
|
+
ValidationStatus.FAIL: "red",
|
|
101
|
+
}
|
|
102
|
+
color = status_colors.get(result.status, "white")
|
|
103
|
+
|
|
104
|
+
# Build status text
|
|
105
|
+
parts = []
|
|
106
|
+
if result.error_count > 0:
|
|
107
|
+
parts.append(f"{result.error_count} error{'s' if result.error_count != 1 else ''}")
|
|
108
|
+
if result.warning_count > 0:
|
|
109
|
+
parts.append(f"{result.warning_count} warning{'s' if result.warning_count != 1 else ''}")
|
|
110
|
+
if result.info_count > 0:
|
|
111
|
+
parts.append(f"{result.info_count} info")
|
|
112
|
+
|
|
113
|
+
status_detail = ", ".join(parts) if parts else "no issues"
|
|
114
|
+
|
|
115
|
+
self.console.print(
|
|
116
|
+
f"[bold]Quality Score:[/bold] {result.quality_score}/100"
|
|
117
|
+
)
|
|
118
|
+
self.console.print(
|
|
119
|
+
f"[bold]Status:[/bold] [{color}]{result.status.value.upper()}[/{color}] ({status_detail})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def display_summary(
|
|
123
|
+
self,
|
|
124
|
+
results: list[ValidationResult],
|
|
125
|
+
summary: dict,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Display summary table for multiple validation results.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
results: List of validation results.
|
|
131
|
+
summary: Summary statistics from ValidationService.get_summary().
|
|
132
|
+
"""
|
|
133
|
+
self.console.print()
|
|
134
|
+
self.console.print("[bold]Spec Validation Summary[/bold]")
|
|
135
|
+
self.console.print()
|
|
136
|
+
|
|
137
|
+
if not results:
|
|
138
|
+
self.console.print("[yellow]No spec files found to validate[/yellow]")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Create table
|
|
142
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
143
|
+
table.add_column("Spec", style="dim")
|
|
144
|
+
table.add_column("Score", justify="center")
|
|
145
|
+
table.add_column("Status", justify="center")
|
|
146
|
+
table.add_column("Errors", justify="center")
|
|
147
|
+
table.add_column("Warnings", justify="center")
|
|
148
|
+
|
|
149
|
+
for result in results:
|
|
150
|
+
# Get spec name from path
|
|
151
|
+
spec_name = Path(result.spec_path).parent.name
|
|
152
|
+
|
|
153
|
+
# Determine status color
|
|
154
|
+
status_colors = {
|
|
155
|
+
ValidationStatus.PASS: "green",
|
|
156
|
+
ValidationStatus.WARN: "yellow",
|
|
157
|
+
ValidationStatus.FAIL: "red",
|
|
158
|
+
}
|
|
159
|
+
color = status_colors.get(result.status, "white")
|
|
160
|
+
|
|
161
|
+
table.add_row(
|
|
162
|
+
spec_name,
|
|
163
|
+
str(result.quality_score),
|
|
164
|
+
f"[{color}]{result.status.value.upper()}[/{color}]",
|
|
165
|
+
str(result.error_count),
|
|
166
|
+
str(result.warning_count),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.console.print(table)
|
|
170
|
+
|
|
171
|
+
# Display summary line
|
|
172
|
+
self.console.print()
|
|
173
|
+
self.console.print("━" * 50)
|
|
174
|
+
self.console.print(
|
|
175
|
+
f"[bold]Total:[/bold] {summary['total_specs']} specs | "
|
|
176
|
+
f"[green]Passed: {summary['passed']}[/green] | "
|
|
177
|
+
f"[yellow]Warned: {summary['warned']}[/yellow] | "
|
|
178
|
+
f"[red]Failed: {summary['failed']}[/red] | "
|
|
179
|
+
f"Avg Score: {summary['average_score']}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def to_json(self, result: ValidationResult) -> str:
|
|
183
|
+
"""Convert single validation result to JSON string.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
result: The validation result.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
JSON string representation.
|
|
190
|
+
"""
|
|
191
|
+
output = {
|
|
192
|
+
"spec_path": result.spec_path,
|
|
193
|
+
"status": result.status.value,
|
|
194
|
+
"quality_score": result.quality_score,
|
|
195
|
+
"error_count": result.error_count,
|
|
196
|
+
"warning_count": result.warning_count,
|
|
197
|
+
"info_count": result.info_count,
|
|
198
|
+
"issues": [
|
|
199
|
+
{
|
|
200
|
+
"rule_id": issue.rule_id,
|
|
201
|
+
"severity": issue.severity.value,
|
|
202
|
+
"line_number": issue.line_number,
|
|
203
|
+
"message": issue.message,
|
|
204
|
+
"suggestion": issue.suggestion,
|
|
205
|
+
}
|
|
206
|
+
for issue in result.issues
|
|
207
|
+
],
|
|
208
|
+
"validated_at": result.validated_at.isoformat(),
|
|
209
|
+
}
|
|
210
|
+
return json.dumps(output, indent=2)
|
|
211
|
+
|
|
212
|
+
def to_json_summary(
|
|
213
|
+
self,
|
|
214
|
+
results: list[ValidationResult],
|
|
215
|
+
summary: dict,
|
|
216
|
+
) -> str:
|
|
217
|
+
"""Convert multiple results to JSON summary string.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
results: List of validation results.
|
|
221
|
+
summary: Summary statistics.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
JSON string representation.
|
|
225
|
+
"""
|
|
226
|
+
output = {
|
|
227
|
+
"summary": {
|
|
228
|
+
"total_specs": summary["total_specs"],
|
|
229
|
+
"passed": summary["passed"],
|
|
230
|
+
"warned": summary["warned"],
|
|
231
|
+
"failed": summary["failed"],
|
|
232
|
+
"average_score": summary["average_score"],
|
|
233
|
+
},
|
|
234
|
+
"results": [
|
|
235
|
+
{
|
|
236
|
+
"spec_path": result.spec_path,
|
|
237
|
+
"status": result.status.value,
|
|
238
|
+
"quality_score": result.quality_score,
|
|
239
|
+
"error_count": result.error_count,
|
|
240
|
+
"warning_count": result.warning_count,
|
|
241
|
+
"info_count": result.info_count,
|
|
242
|
+
"issues": [
|
|
243
|
+
{
|
|
244
|
+
"rule_id": issue.rule_id,
|
|
245
|
+
"severity": issue.severity.value,
|
|
246
|
+
"line_number": issue.line_number,
|
|
247
|
+
"message": issue.message,
|
|
248
|
+
"suggestion": issue.suggestion,
|
|
249
|
+
}
|
|
250
|
+
for issue in result.issues
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
for result in results
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
return json.dumps(output, indent=2)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Parser for extracting requirements from spec.md files."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..models.crossref_models import Requirement
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RequirementParser:
|
|
11
|
+
"""Parses spec.md files to extract functional requirements.
|
|
12
|
+
|
|
13
|
+
This parser extracts FR-XXX requirements from specification files
|
|
14
|
+
using the standard format: `- **FR-XXX**: Description text`
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Pattern to match: - **FR-XXX**: Description
|
|
18
|
+
# Captures: group(1) = FR-XXX, group(2) = description
|
|
19
|
+
REQUIREMENT_PATTERN = re.compile(
|
|
20
|
+
r"^\s*-\s*\*\*(?P<id>FR-\d{3})\*\*:\s*(?P<description>.+)$",
|
|
21
|
+
re.MULTILINE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __init__(self, spec_path: Optional[Path] = None) -> None:
|
|
25
|
+
"""Initialize parser with optional spec file path.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
spec_path: Path to spec.md file. Can be set later via parse().
|
|
29
|
+
"""
|
|
30
|
+
self.spec_path = spec_path
|
|
31
|
+
|
|
32
|
+
def parse(self, spec_path: Optional[Path] = None) -> list[Requirement]:
|
|
33
|
+
"""Parse spec.md and extract all functional requirements.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
spec_path: Path to spec.md file. Overrides constructor path.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of Requirement objects sorted by ID.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
FileNotFoundError: If spec file doesn't exist.
|
|
43
|
+
ValueError: If no spec path provided.
|
|
44
|
+
"""
|
|
45
|
+
path = spec_path or self.spec_path
|
|
46
|
+
if path is None:
|
|
47
|
+
raise ValueError("No spec path provided")
|
|
48
|
+
|
|
49
|
+
path = Path(path)
|
|
50
|
+
if not path.exists():
|
|
51
|
+
raise FileNotFoundError(f"Spec file not found: {path}")
|
|
52
|
+
|
|
53
|
+
content = path.read_text(encoding="utf-8")
|
|
54
|
+
return self.parse_content(content, str(path))
|
|
55
|
+
|
|
56
|
+
def parse_content(self, content: str, spec_path: str) -> list[Requirement]:
|
|
57
|
+
"""Parse content string and extract requirements.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
content: Full content of spec.md file.
|
|
61
|
+
spec_path: Path to associate with extracted requirements.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of Requirement objects sorted by ID.
|
|
65
|
+
"""
|
|
66
|
+
requirements: list[Requirement] = []
|
|
67
|
+
lines = content.split("\n")
|
|
68
|
+
|
|
69
|
+
for line_num, line in enumerate(lines, start=1):
|
|
70
|
+
match = self.REQUIREMENT_PATTERN.match(line)
|
|
71
|
+
if match:
|
|
72
|
+
req = Requirement(
|
|
73
|
+
id=match.group("id"),
|
|
74
|
+
spec_path=spec_path,
|
|
75
|
+
description=match.group("description").strip(),
|
|
76
|
+
line_number=line_num,
|
|
77
|
+
)
|
|
78
|
+
requirements.append(req)
|
|
79
|
+
|
|
80
|
+
# Sort by requirement ID (FR-001, FR-002, etc.)
|
|
81
|
+
requirements.sort(key=lambda r: r.id)
|
|
82
|
+
return requirements
|
|
83
|
+
|
|
84
|
+
def get_requirement(
|
|
85
|
+
self, requirement_id: str, spec_path: Optional[Path] = None
|
|
86
|
+
) -> Optional[Requirement]:
|
|
87
|
+
"""Get a specific requirement by ID.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
requirement_id: The FR-XXX ID to find.
|
|
91
|
+
spec_path: Path to spec.md file.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Requirement if found, None otherwise.
|
|
95
|
+
"""
|
|
96
|
+
requirements = self.parse(spec_path)
|
|
97
|
+
for req in requirements:
|
|
98
|
+
if req.id == requirement_id:
|
|
99
|
+
return req
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def get_requirement_ids(self, spec_path: Optional[Path] = None) -> list[str]:
|
|
103
|
+
"""Get list of all requirement IDs in a spec.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
spec_path: Path to spec.md file.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of FR-XXX IDs.
|
|
110
|
+
"""
|
|
111
|
+
requirements = self.parse(spec_path)
|
|
112
|
+
return [req.id for req in requirements]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Roadmap summarization service for AI context injection.
|
|
2
|
+
|
|
3
|
+
This module provides the RoadmapSummarizer service that parses roadmap.md
|
|
4
|
+
and generates condensed summaries with P1/P2 items prioritized.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..models.context_config import (
|
|
11
|
+
RoadmapItem,
|
|
12
|
+
RoadmapSummary,
|
|
13
|
+
SummarizationConfig,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RoadmapSummarizer:
|
|
18
|
+
"""Service for parsing and summarizing roadmap content.
|
|
19
|
+
|
|
20
|
+
Parses roadmap.md by priority sections (P1-P4) and generates condensed
|
|
21
|
+
summaries that include full P1/P2 items with rationale while reducing
|
|
22
|
+
P3/P4 to titles only.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: Optional[SummarizationConfig] = None) -> None:
|
|
26
|
+
"""Initialize with summarization configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Summarization configuration (uses defaults if None).
|
|
30
|
+
"""
|
|
31
|
+
self.config = config or SummarizationConfig()
|
|
32
|
+
|
|
33
|
+
def parse_roadmap(self, content: str) -> list[RoadmapItem]:
|
|
34
|
+
"""Parse roadmap.md content into structured items.
|
|
35
|
+
|
|
36
|
+
Extracts items from markdown, parsing P1-P4 sections, rationale,
|
|
37
|
+
and feature references.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
content: Raw markdown content of roadmap.md
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of RoadmapItem objects
|
|
44
|
+
"""
|
|
45
|
+
items: list[RoadmapItem] = []
|
|
46
|
+
lines = content.split("\n")
|
|
47
|
+
|
|
48
|
+
current_priority = "P4" # Default priority
|
|
49
|
+
i = 0
|
|
50
|
+
|
|
51
|
+
while i < len(lines):
|
|
52
|
+
line = lines[i]
|
|
53
|
+
|
|
54
|
+
# Check for priority section headers (### P1 - Critical, ## P2, etc.)
|
|
55
|
+
priority_match = re.match(r"^#{2,3}\s*(P[1-4])\s*[-:]?\s*", line, re.IGNORECASE)
|
|
56
|
+
if priority_match:
|
|
57
|
+
current_priority = priority_match.group(1).upper()
|
|
58
|
+
i += 1
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Check for checklist items (- [ ] or - [x]) or plain list items (- text)
|
|
62
|
+
item_match = re.match(r"^-\s*\[([ xX])\]\s*(.+)$", line)
|
|
63
|
+
plain_match = re.match(r"^-\s+([^[\s].+)$", line) if not item_match else None
|
|
64
|
+
|
|
65
|
+
if item_match:
|
|
66
|
+
completed = item_match.group(1).lower() == "x"
|
|
67
|
+
text = item_match.group(2).strip()
|
|
68
|
+
elif plain_match:
|
|
69
|
+
completed = False
|
|
70
|
+
text = plain_match.group(1).strip()
|
|
71
|
+
else:
|
|
72
|
+
i += 1
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Extract feature reference from text (e.g., `[034-fixit-workflow]` or [034-fixit])
|
|
76
|
+
feature_ref = ""
|
|
77
|
+
feature_match = re.search(r"`?\[?(\d{3}-[\w-]+)\]?`?", text)
|
|
78
|
+
if feature_match:
|
|
79
|
+
feature_ref = feature_match.group(1)
|
|
80
|
+
|
|
81
|
+
# Look for rationale in following lines
|
|
82
|
+
rationale = ""
|
|
83
|
+
j = i + 1
|
|
84
|
+
while j < len(lines):
|
|
85
|
+
next_line = lines[j].strip()
|
|
86
|
+
# Check for rationale line
|
|
87
|
+
rationale_match = re.match(r"^-?\s*\*?\*?Rationale\*?\*?:\s*(.+)$", next_line, re.IGNORECASE)
|
|
88
|
+
if rationale_match:
|
|
89
|
+
rationale = rationale_match.group(1).strip()
|
|
90
|
+
break
|
|
91
|
+
# Stop if we hit another item or section
|
|
92
|
+
if next_line.startswith("- [") or next_line.startswith("-") or next_line.startswith("#"):
|
|
93
|
+
break
|
|
94
|
+
j += 1
|
|
95
|
+
|
|
96
|
+
items.append(RoadmapItem(
|
|
97
|
+
text=text,
|
|
98
|
+
priority=current_priority,
|
|
99
|
+
rationale=rationale,
|
|
100
|
+
feature_ref=feature_ref,
|
|
101
|
+
completed=completed,
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
i += 1
|
|
105
|
+
|
|
106
|
+
return items
|
|
107
|
+
|
|
108
|
+
def summarize(
|
|
109
|
+
self,
|
|
110
|
+
items: list[RoadmapItem],
|
|
111
|
+
max_tokens: int = 2000,
|
|
112
|
+
current_feature: Optional[str] = None,
|
|
113
|
+
) -> RoadmapSummary:
|
|
114
|
+
"""Generate condensed roadmap summary.
|
|
115
|
+
|
|
116
|
+
Creates a summary with:
|
|
117
|
+
- Full P1/P2 items including rationale
|
|
118
|
+
- P3/P4 items as titles only
|
|
119
|
+
- Current feature highlighted if provided
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
items: Parsed roadmap items
|
|
123
|
+
max_tokens: Maximum tokens for output
|
|
124
|
+
current_feature: Current feature branch name for highlighting
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
RoadmapSummary with condensed content
|
|
128
|
+
"""
|
|
129
|
+
if not items:
|
|
130
|
+
return RoadmapSummary(
|
|
131
|
+
condensed_text="",
|
|
132
|
+
item_count=0,
|
|
133
|
+
priorities_included=[],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
sections: list[str] = ["## Roadmap Summary", ""]
|
|
137
|
+
priorities_included: set[str] = set()
|
|
138
|
+
item_count = 0
|
|
139
|
+
|
|
140
|
+
# Group items by priority
|
|
141
|
+
by_priority: dict[str, list[RoadmapItem]] = {"P1": [], "P2": [], "P3": [], "P4": []}
|
|
142
|
+
current_feature_items: list[RoadmapItem] = []
|
|
143
|
+
|
|
144
|
+
for item in items:
|
|
145
|
+
if item.completed:
|
|
146
|
+
continue # Skip completed items in roadmap summary
|
|
147
|
+
|
|
148
|
+
priority = item.priority.upper()
|
|
149
|
+
if priority in by_priority:
|
|
150
|
+
by_priority[priority].append(item)
|
|
151
|
+
|
|
152
|
+
# Check if this is the current feature
|
|
153
|
+
if current_feature and item.feature_ref:
|
|
154
|
+
# Match feature ref against current branch
|
|
155
|
+
if current_feature in item.feature_ref or item.feature_ref in current_feature:
|
|
156
|
+
current_feature_items.append(item)
|
|
157
|
+
|
|
158
|
+
# Add current feature section if we found matches
|
|
159
|
+
if current_feature_items:
|
|
160
|
+
sections.append("### Current Feature")
|
|
161
|
+
for item in current_feature_items:
|
|
162
|
+
sections.append(f"- **[CURRENT]** {item.text}")
|
|
163
|
+
if item.rationale:
|
|
164
|
+
sections.append(f" - *Rationale*: {item.rationale}")
|
|
165
|
+
item_count += 1
|
|
166
|
+
priorities_included.add(item.priority)
|
|
167
|
+
sections.append("")
|
|
168
|
+
|
|
169
|
+
# High priority section (P1/P2) - full details
|
|
170
|
+
high_priority = by_priority["P1"] + by_priority["P2"]
|
|
171
|
+
# Filter out items already shown in current feature
|
|
172
|
+
high_priority = [i for i in high_priority if i not in current_feature_items]
|
|
173
|
+
|
|
174
|
+
if high_priority:
|
|
175
|
+
sections.append("### High Priority (P1-P2)")
|
|
176
|
+
for item in high_priority:
|
|
177
|
+
marker = "[P1]" if item.priority == "P1" else "[P2]"
|
|
178
|
+
sections.append(f"- {marker} {item.text}")
|
|
179
|
+
if item.rationale:
|
|
180
|
+
sections.append(f" - *Rationale*: {item.rationale}")
|
|
181
|
+
item_count += 1
|
|
182
|
+
priorities_included.add(item.priority)
|
|
183
|
+
sections.append("")
|
|
184
|
+
|
|
185
|
+
# Lower priority section (P3/P4) - titles only
|
|
186
|
+
low_priority = by_priority["P3"] + by_priority["P4"]
|
|
187
|
+
# Filter out items already shown
|
|
188
|
+
low_priority = [i for i in low_priority if i not in current_feature_items]
|
|
189
|
+
|
|
190
|
+
if low_priority:
|
|
191
|
+
sections.append("### Other Priorities (P3-P4)")
|
|
192
|
+
for item in low_priority:
|
|
193
|
+
# Extract just the title (first sentence or up to first period)
|
|
194
|
+
title = item.text.split(".")[0].strip()
|
|
195
|
+
if len(title) > 80:
|
|
196
|
+
title = title[:77] + "..."
|
|
197
|
+
marker = "[P3]" if item.priority == "P3" else "[P4]"
|
|
198
|
+
sections.append(f"- {marker} {title}")
|
|
199
|
+
item_count += 1
|
|
200
|
+
priorities_included.add(item.priority)
|
|
201
|
+
sections.append("")
|
|
202
|
+
|
|
203
|
+
condensed_text = "\n".join(sections)
|
|
204
|
+
|
|
205
|
+
return RoadmapSummary(
|
|
206
|
+
condensed_text=condensed_text,
|
|
207
|
+
item_count=item_count,
|
|
208
|
+
priorities_included=sorted(priorities_included),
|
|
209
|
+
)
|