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,555 @@
|
|
|
1
|
+
"""Cross-reference commands for spec-task traceability."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..models.crossref_models import CoverageReport, CoverageStatus
|
|
12
|
+
from ..services.crossref_service import CrossReferenceService
|
|
13
|
+
from ..services.spec_scanner import NotADoitProjectError
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
# Create the xref subcommand group
|
|
18
|
+
xref_app = typer.Typer(
|
|
19
|
+
name="xref",
|
|
20
|
+
help="Cross-reference commands for spec-task traceability",
|
|
21
|
+
add_completion=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _detect_spec_from_branch() -> Optional[str]:
|
|
26
|
+
"""Try to detect spec name from current git branch.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Spec name if branch matches pattern XXX-feature-name, None otherwise.
|
|
30
|
+
"""
|
|
31
|
+
import subprocess
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["git", "branch", "--show-current"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
branch = result.stdout.strip()
|
|
41
|
+
# Check if branch matches spec pattern (e.g., 033-feature-name)
|
|
42
|
+
if branch and branch[0].isdigit():
|
|
43
|
+
return branch
|
|
44
|
+
except (FileNotFoundError, subprocess.SubprocessError):
|
|
45
|
+
pass
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_coverage_rich(report: CoverageReport, console: Console) -> None:
|
|
50
|
+
"""Format coverage report as Rich table."""
|
|
51
|
+
console.print()
|
|
52
|
+
console.print(
|
|
53
|
+
f"[bold]Requirement Coverage: {Path(report.spec_path).parent.name}[/bold]"
|
|
54
|
+
)
|
|
55
|
+
console.print("=" * 50)
|
|
56
|
+
console.print()
|
|
57
|
+
|
|
58
|
+
table = Table(show_header=True, header_style="bold")
|
|
59
|
+
table.add_column("Requirement", style="cyan")
|
|
60
|
+
table.add_column("Tasks", justify="right")
|
|
61
|
+
table.add_column("Status")
|
|
62
|
+
|
|
63
|
+
for rc in report.requirements:
|
|
64
|
+
status_icon = {
|
|
65
|
+
CoverageStatus.COVERED: "[green]\u2713 Covered[/green]",
|
|
66
|
+
CoverageStatus.PARTIAL: "[yellow]\u25d0 Partial[/yellow]",
|
|
67
|
+
CoverageStatus.UNCOVERED: "[red]\u26a0 Uncovered[/red]",
|
|
68
|
+
}.get(rc.status, "Unknown")
|
|
69
|
+
|
|
70
|
+
table.add_row(
|
|
71
|
+
rc.requirement.id,
|
|
72
|
+
str(rc.task_count),
|
|
73
|
+
status_icon,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
console.print()
|
|
78
|
+
|
|
79
|
+
# Summary
|
|
80
|
+
coverage_color = "green" if report.coverage_percent >= 100 else "yellow"
|
|
81
|
+
if report.coverage_percent < 50:
|
|
82
|
+
coverage_color = "red"
|
|
83
|
+
|
|
84
|
+
console.print(
|
|
85
|
+
f"[{coverage_color}]Coverage: {report.coverage_percent:.0f}% "
|
|
86
|
+
f"({report.covered_count}/{report.total_count})[/{coverage_color}]"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Show orphaned references if any
|
|
90
|
+
if report.orphaned_references:
|
|
91
|
+
console.print()
|
|
92
|
+
console.print("[red]Orphaned References:[/red]")
|
|
93
|
+
for task, ref_id in report.orphaned_references:
|
|
94
|
+
console.print(f" - Task at line {task.line_number} references {ref_id}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _format_coverage_json(report: CoverageReport) -> str:
|
|
98
|
+
"""Format coverage report as JSON."""
|
|
99
|
+
data = {
|
|
100
|
+
"spec": Path(report.spec_path).parent.name,
|
|
101
|
+
"requirements": [
|
|
102
|
+
{
|
|
103
|
+
"id": rc.requirement.id,
|
|
104
|
+
"task_count": rc.task_count,
|
|
105
|
+
"covered": rc.is_covered,
|
|
106
|
+
"status": rc.status.value,
|
|
107
|
+
}
|
|
108
|
+
for rc in report.requirements
|
|
109
|
+
],
|
|
110
|
+
"coverage_percent": round(report.coverage_percent, 1),
|
|
111
|
+
"covered_count": report.covered_count,
|
|
112
|
+
"total_count": report.total_count,
|
|
113
|
+
"orphaned_references": [
|
|
114
|
+
{"task_line": task.line_number, "reference": ref_id}
|
|
115
|
+
for task, ref_id in report.orphaned_references
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
return json.dumps(data, indent=2)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _format_coverage_markdown(report: CoverageReport) -> str:
|
|
122
|
+
"""Format coverage report as Markdown."""
|
|
123
|
+
lines = [
|
|
124
|
+
f"# Requirement Coverage: {Path(report.spec_path).parent.name}",
|
|
125
|
+
"",
|
|
126
|
+
"| Requirement | Tasks | Status |",
|
|
127
|
+
"|-------------|-------|--------|",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for rc in report.requirements:
|
|
131
|
+
status = {
|
|
132
|
+
CoverageStatus.COVERED: "\u2713 Covered",
|
|
133
|
+
CoverageStatus.PARTIAL: "\u25d0 Partial",
|
|
134
|
+
CoverageStatus.UNCOVERED: "\u26a0 Uncovered",
|
|
135
|
+
}.get(rc.status, "Unknown")
|
|
136
|
+
|
|
137
|
+
lines.append(f"| {rc.requirement.id} | {rc.task_count} | {status} |")
|
|
138
|
+
|
|
139
|
+
lines.extend(
|
|
140
|
+
[
|
|
141
|
+
"",
|
|
142
|
+
f"**Coverage**: {report.coverage_percent:.0f}% "
|
|
143
|
+
f"({report.covered_count}/{report.total_count})",
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if report.orphaned_references:
|
|
148
|
+
lines.extend(["", "## Orphaned References", ""])
|
|
149
|
+
for task, ref_id in report.orphaned_references:
|
|
150
|
+
lines.append(f"- Task at line {task.line_number} references `{ref_id}`")
|
|
151
|
+
|
|
152
|
+
return "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@xref_app.command(name="coverage")
|
|
156
|
+
def coverage_command(
|
|
157
|
+
spec_name: Optional[str] = typer.Argument(
|
|
158
|
+
None, help="Spec directory name (default: auto-detect from branch)"
|
|
159
|
+
),
|
|
160
|
+
output_format: str = typer.Option(
|
|
161
|
+
"rich", "--format", "-f", help="Output format: rich, json, markdown"
|
|
162
|
+
),
|
|
163
|
+
strict: bool = typer.Option(
|
|
164
|
+
False, "--strict", "-s", help="Treat uncovered requirements as errors"
|
|
165
|
+
),
|
|
166
|
+
output_file: Optional[Path] = typer.Option(
|
|
167
|
+
None, "--output", "-o", help="Write output to file"
|
|
168
|
+
),
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Generate a coverage report showing requirement-to-task mapping.
|
|
171
|
+
|
|
172
|
+
Shows which requirements have implementing tasks and identifies
|
|
173
|
+
any uncovered requirements or orphaned task references.
|
|
174
|
+
|
|
175
|
+
Exit codes:
|
|
176
|
+
0 - All requirements covered
|
|
177
|
+
1 - Uncovered requirements (with --strict) or errors
|
|
178
|
+
2 - Spec not found or invalid
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
# Auto-detect spec from branch if not provided
|
|
182
|
+
if not spec_name:
|
|
183
|
+
spec_name = _detect_spec_from_branch()
|
|
184
|
+
if not spec_name:
|
|
185
|
+
console.print(
|
|
186
|
+
"[red]Error:[/red] Could not detect spec. "
|
|
187
|
+
"Please provide spec name or run from a feature branch."
|
|
188
|
+
)
|
|
189
|
+
raise typer.Exit(code=2)
|
|
190
|
+
|
|
191
|
+
# Validate format
|
|
192
|
+
valid_formats = ["rich", "json", "markdown"]
|
|
193
|
+
if output_format not in valid_formats:
|
|
194
|
+
console.print(
|
|
195
|
+
f"[red]Error:[/red] Invalid format '{output_format}'. "
|
|
196
|
+
f"Valid: {', '.join(valid_formats)}"
|
|
197
|
+
)
|
|
198
|
+
raise typer.Exit(code=2)
|
|
199
|
+
|
|
200
|
+
# Get coverage report
|
|
201
|
+
service = CrossReferenceService()
|
|
202
|
+
try:
|
|
203
|
+
report = service.get_coverage(spec_name=spec_name)
|
|
204
|
+
except FileNotFoundError as e:
|
|
205
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
206
|
+
raise typer.Exit(code=2)
|
|
207
|
+
|
|
208
|
+
# Format output
|
|
209
|
+
if output_format == "json":
|
|
210
|
+
output_str = _format_coverage_json(report)
|
|
211
|
+
elif output_format == "markdown":
|
|
212
|
+
output_str = _format_coverage_markdown(report)
|
|
213
|
+
else:
|
|
214
|
+
output_str = None # Rich output handled separately
|
|
215
|
+
|
|
216
|
+
# Write to file or stdout
|
|
217
|
+
if output_file:
|
|
218
|
+
if output_format == "rich":
|
|
219
|
+
# For rich format to file, use markdown instead
|
|
220
|
+
output_str = _format_coverage_markdown(report)
|
|
221
|
+
output_file.write_text(output_str)
|
|
222
|
+
console.print(f"[green]Report written to {output_file}[/green]")
|
|
223
|
+
elif output_format == "rich":
|
|
224
|
+
_format_coverage_rich(report, console)
|
|
225
|
+
else:
|
|
226
|
+
print(output_str)
|
|
227
|
+
|
|
228
|
+
# Determine exit code
|
|
229
|
+
has_orphaned = len(report.orphaned_references) > 0
|
|
230
|
+
has_uncovered = report.uncovered_count > 0
|
|
231
|
+
|
|
232
|
+
if has_orphaned:
|
|
233
|
+
raise typer.Exit(code=1)
|
|
234
|
+
if strict and has_uncovered:
|
|
235
|
+
raise typer.Exit(code=1)
|
|
236
|
+
|
|
237
|
+
raise typer.Exit(code=0)
|
|
238
|
+
|
|
239
|
+
except NotADoitProjectError as e:
|
|
240
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
241
|
+
raise typer.Exit(code=2)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@xref_app.command(name="locate")
|
|
245
|
+
def locate_command(
|
|
246
|
+
requirement_id: str = typer.Argument(..., help="Requirement ID (e.g., FR-001)"),
|
|
247
|
+
spec: Optional[str] = typer.Option(
|
|
248
|
+
None, "--spec", "-s", help="Spec name or path (default: auto-detect)"
|
|
249
|
+
),
|
|
250
|
+
output_format: str = typer.Option(
|
|
251
|
+
"rich", "--format", "-f", help="Output format: rich, json, line"
|
|
252
|
+
),
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Find the definition of a requirement in spec.md.
|
|
255
|
+
|
|
256
|
+
Exit codes:
|
|
257
|
+
0 - Requirement found
|
|
258
|
+
1 - Requirement not found
|
|
259
|
+
2 - Spec file not found
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
# Determine spec name
|
|
263
|
+
spec_name = spec or _detect_spec_from_branch()
|
|
264
|
+
if not spec_name:
|
|
265
|
+
console.print(
|
|
266
|
+
"[red]Error:[/red] Could not detect spec. "
|
|
267
|
+
"Please provide --spec or run from a feature branch."
|
|
268
|
+
)
|
|
269
|
+
raise typer.Exit(code=2)
|
|
270
|
+
|
|
271
|
+
# Validate format
|
|
272
|
+
valid_formats = ["rich", "json", "line"]
|
|
273
|
+
if output_format not in valid_formats:
|
|
274
|
+
console.print(
|
|
275
|
+
f"[red]Error:[/red] Invalid format '{output_format}'. "
|
|
276
|
+
f"Valid: {', '.join(valid_formats)}"
|
|
277
|
+
)
|
|
278
|
+
raise typer.Exit(code=2)
|
|
279
|
+
|
|
280
|
+
# Find requirement
|
|
281
|
+
service = CrossReferenceService()
|
|
282
|
+
try:
|
|
283
|
+
req = service.locate_requirement(requirement_id, spec_name=spec_name)
|
|
284
|
+
except (FileNotFoundError, ValueError) as e:
|
|
285
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
286
|
+
raise typer.Exit(code=2)
|
|
287
|
+
|
|
288
|
+
if req is None:
|
|
289
|
+
console.print(
|
|
290
|
+
f"[yellow]Requirement {requirement_id} not found in spec.[/yellow]"
|
|
291
|
+
)
|
|
292
|
+
raise typer.Exit(code=1)
|
|
293
|
+
|
|
294
|
+
# Format output
|
|
295
|
+
if output_format == "json":
|
|
296
|
+
data = {
|
|
297
|
+
"id": req.id,
|
|
298
|
+
"description": req.description,
|
|
299
|
+
"file": req.spec_path,
|
|
300
|
+
"line": req.line_number,
|
|
301
|
+
}
|
|
302
|
+
print(json.dumps(data, indent=2))
|
|
303
|
+
elif output_format == "line":
|
|
304
|
+
# Just file:line for IDE integration
|
|
305
|
+
print(f"{req.spec_path}:{req.line_number}")
|
|
306
|
+
else:
|
|
307
|
+
# Rich format
|
|
308
|
+
console.print(f"[cyan]{req.id}[/cyan]: {req.description}")
|
|
309
|
+
console.print(f"Location: [dim]{req.spec_path}:{req.line_number}[/dim]")
|
|
310
|
+
|
|
311
|
+
raise typer.Exit(code=0)
|
|
312
|
+
|
|
313
|
+
except NotADoitProjectError as e:
|
|
314
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
315
|
+
raise typer.Exit(code=2)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@xref_app.command(name="tasks")
|
|
319
|
+
def tasks_command(
|
|
320
|
+
requirement_id: str = typer.Argument(..., help="Requirement ID (e.g., FR-001)"),
|
|
321
|
+
spec: Optional[str] = typer.Option(
|
|
322
|
+
None, "--spec", "-s", help="Spec name (default: auto-detect)"
|
|
323
|
+
),
|
|
324
|
+
output_format: str = typer.Option(
|
|
325
|
+
"rich", "--format", "-f", help="Output format: rich, json, markdown"
|
|
326
|
+
),
|
|
327
|
+
) -> None:
|
|
328
|
+
"""List all tasks that implement a specific requirement.
|
|
329
|
+
|
|
330
|
+
Exit codes:
|
|
331
|
+
0 - Tasks found
|
|
332
|
+
1 - No tasks found for requirement
|
|
333
|
+
2 - Requirement or spec not found
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
# Determine spec name
|
|
337
|
+
spec_name = spec or _detect_spec_from_branch()
|
|
338
|
+
if not spec_name:
|
|
339
|
+
console.print(
|
|
340
|
+
"[red]Error:[/red] Could not detect spec. "
|
|
341
|
+
"Please provide --spec or run from a feature branch."
|
|
342
|
+
)
|
|
343
|
+
raise typer.Exit(code=2)
|
|
344
|
+
|
|
345
|
+
# Validate format
|
|
346
|
+
valid_formats = ["rich", "json", "markdown"]
|
|
347
|
+
if output_format not in valid_formats:
|
|
348
|
+
console.print(
|
|
349
|
+
f"[red]Error:[/red] Invalid format '{output_format}'. "
|
|
350
|
+
f"Valid: {', '.join(valid_formats)}"
|
|
351
|
+
)
|
|
352
|
+
raise typer.Exit(code=2)
|
|
353
|
+
|
|
354
|
+
# Get tasks
|
|
355
|
+
service = CrossReferenceService()
|
|
356
|
+
try:
|
|
357
|
+
tasks = service.get_tasks_for_requirement(requirement_id, spec_name=spec_name)
|
|
358
|
+
except (FileNotFoundError, ValueError) as e:
|
|
359
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
360
|
+
raise typer.Exit(code=2)
|
|
361
|
+
|
|
362
|
+
if not tasks:
|
|
363
|
+
console.print(
|
|
364
|
+
f"[yellow]No tasks found implementing {requirement_id}[/yellow]"
|
|
365
|
+
)
|
|
366
|
+
raise typer.Exit(code=1)
|
|
367
|
+
|
|
368
|
+
# Format output
|
|
369
|
+
if output_format == "json":
|
|
370
|
+
data = {
|
|
371
|
+
"requirement_id": requirement_id,
|
|
372
|
+
"tasks": [
|
|
373
|
+
{
|
|
374
|
+
"line": t.line_number,
|
|
375
|
+
"completed": t.completed,
|
|
376
|
+
"description": t.description,
|
|
377
|
+
}
|
|
378
|
+
for t in tasks
|
|
379
|
+
],
|
|
380
|
+
"count": len(tasks),
|
|
381
|
+
"completed_count": sum(1 for t in tasks if t.completed),
|
|
382
|
+
}
|
|
383
|
+
print(json.dumps(data, indent=2))
|
|
384
|
+
elif output_format == "markdown":
|
|
385
|
+
lines = [
|
|
386
|
+
f"# Tasks implementing {requirement_id}",
|
|
387
|
+
"",
|
|
388
|
+
"| Line | Status | Description |",
|
|
389
|
+
"|------|--------|-------------|",
|
|
390
|
+
]
|
|
391
|
+
for t in tasks:
|
|
392
|
+
status = "[x]" if t.completed else "[ ]"
|
|
393
|
+
lines.append(f"| {t.line_number} | {status} | {t.description} |")
|
|
394
|
+
|
|
395
|
+
completed = sum(1 for t in tasks if t.completed)
|
|
396
|
+
lines.extend(
|
|
397
|
+
["", f"Found {len(tasks)} tasks ({completed} complete, {len(tasks) - completed} pending)"]
|
|
398
|
+
)
|
|
399
|
+
print("\n".join(lines))
|
|
400
|
+
else:
|
|
401
|
+
# Rich format
|
|
402
|
+
console.print(f"\n[bold]Tasks implementing {requirement_id}:[/bold]\n")
|
|
403
|
+
|
|
404
|
+
table = Table(show_header=True, header_style="bold")
|
|
405
|
+
table.add_column("Line", justify="right")
|
|
406
|
+
table.add_column("Status")
|
|
407
|
+
table.add_column("Description")
|
|
408
|
+
|
|
409
|
+
for t in tasks:
|
|
410
|
+
status = "[green][x][/green]" if t.completed else "[ ]"
|
|
411
|
+
table.add_row(str(t.line_number), status, t.description)
|
|
412
|
+
|
|
413
|
+
console.print(table)
|
|
414
|
+
|
|
415
|
+
completed = sum(1 for t in tasks if t.completed)
|
|
416
|
+
console.print(
|
|
417
|
+
f"\nFound {len(tasks)} tasks ({completed} complete, {len(tasks) - completed} pending)"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
raise typer.Exit(code=0)
|
|
421
|
+
|
|
422
|
+
except NotADoitProjectError as e:
|
|
423
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
424
|
+
raise typer.Exit(code=2)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@xref_app.command(name="validate")
|
|
428
|
+
def validate_command(
|
|
429
|
+
spec_name: Optional[str] = typer.Argument(
|
|
430
|
+
None, help="Spec directory name (default: auto-detect from branch)"
|
|
431
|
+
),
|
|
432
|
+
strict: bool = typer.Option(
|
|
433
|
+
False, "--strict", "-s", help="Treat warnings as errors"
|
|
434
|
+
),
|
|
435
|
+
output_format: str = typer.Option(
|
|
436
|
+
"rich", "--format", "-f", help="Output format: rich, json"
|
|
437
|
+
),
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Validate cross-reference integrity between spec and tasks.
|
|
440
|
+
|
|
441
|
+
Checks for:
|
|
442
|
+
- Orphaned task references (tasks referencing non-existent requirements)
|
|
443
|
+
- Uncovered requirements (requirements with no linked tasks)
|
|
444
|
+
|
|
445
|
+
Exit codes:
|
|
446
|
+
0 - All cross-references valid
|
|
447
|
+
1 - Validation errors found
|
|
448
|
+
2 - Files not found
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
# Auto-detect spec from branch if not provided
|
|
452
|
+
if not spec_name:
|
|
453
|
+
spec_name = _detect_spec_from_branch()
|
|
454
|
+
if not spec_name:
|
|
455
|
+
console.print(
|
|
456
|
+
"[red]Error:[/red] Could not detect spec. "
|
|
457
|
+
"Please provide spec name or run from a feature branch."
|
|
458
|
+
)
|
|
459
|
+
raise typer.Exit(code=2)
|
|
460
|
+
|
|
461
|
+
# Validate format
|
|
462
|
+
valid_formats = ["rich", "json"]
|
|
463
|
+
if output_format not in valid_formats:
|
|
464
|
+
console.print(
|
|
465
|
+
f"[red]Error:[/red] Invalid format '{output_format}'. "
|
|
466
|
+
f"Valid: {', '.join(valid_formats)}"
|
|
467
|
+
)
|
|
468
|
+
raise typer.Exit(code=2)
|
|
469
|
+
|
|
470
|
+
# Validate references
|
|
471
|
+
service = CrossReferenceService()
|
|
472
|
+
try:
|
|
473
|
+
uncovered, orphaned = service.validate_references(spec_name=spec_name)
|
|
474
|
+
report = service.get_coverage(spec_name=spec_name)
|
|
475
|
+
except FileNotFoundError as e:
|
|
476
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
477
|
+
raise typer.Exit(code=2)
|
|
478
|
+
|
|
479
|
+
# Count issues
|
|
480
|
+
error_count = len(orphaned) # Orphaned references are always errors
|
|
481
|
+
warning_count = len(uncovered) # Uncovered are warnings (errors in strict)
|
|
482
|
+
|
|
483
|
+
if strict:
|
|
484
|
+
error_count += warning_count
|
|
485
|
+
warning_count = 0
|
|
486
|
+
|
|
487
|
+
# Format output
|
|
488
|
+
if output_format == "json":
|
|
489
|
+
data = {
|
|
490
|
+
"spec": spec_name,
|
|
491
|
+
"valid": error_count == 0 and warning_count == 0,
|
|
492
|
+
"errors": error_count,
|
|
493
|
+
"warnings": warning_count,
|
|
494
|
+
"orphaned_references": [
|
|
495
|
+
{"task_line": t.line_number, "reference": ref}
|
|
496
|
+
for t, ref in orphaned
|
|
497
|
+
],
|
|
498
|
+
"uncovered_requirements": uncovered,
|
|
499
|
+
}
|
|
500
|
+
print(json.dumps(data, indent=2))
|
|
501
|
+
else:
|
|
502
|
+
# Rich format
|
|
503
|
+
console.print(f"\n[bold]Cross-Reference Validation: {spec_name}[/bold]")
|
|
504
|
+
console.print("=" * 50)
|
|
505
|
+
console.print()
|
|
506
|
+
|
|
507
|
+
# Show results
|
|
508
|
+
if not orphaned:
|
|
509
|
+
console.print(
|
|
510
|
+
f"[green]\u2713[/green] All {report.total_count} requirement "
|
|
511
|
+
"references are valid"
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
console.print("[red]\u2717 Orphaned References:[/red]")
|
|
515
|
+
for task, ref_id in orphaned:
|
|
516
|
+
console.print(
|
|
517
|
+
f" - Task at line {task.line_number} references "
|
|
518
|
+
f"non-existent {ref_id}"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if uncovered:
|
|
522
|
+
severity = "[red]\u2717" if strict else "[yellow]\u26a0"
|
|
523
|
+
console.print(
|
|
524
|
+
f"\n{severity} {len(uncovered)} requirements have no linked tasks: "
|
|
525
|
+
f"{', '.join(uncovered)}[/]"
|
|
526
|
+
)
|
|
527
|
+
elif report.total_count > 0:
|
|
528
|
+
console.print(
|
|
529
|
+
f"[green]\u2713[/green] All requirements have linked tasks"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Summary
|
|
533
|
+
console.print()
|
|
534
|
+
if error_count == 0 and warning_count == 0:
|
|
535
|
+
console.print("[green]Validation: PASS[/green]")
|
|
536
|
+
elif error_count > 0:
|
|
537
|
+
console.print(
|
|
538
|
+
f"[red]Validation: FAIL ({error_count} errors, "
|
|
539
|
+
f"{warning_count} warnings)[/red]"
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
console.print(
|
|
543
|
+
f"[yellow]Validation: WARN (0 errors, "
|
|
544
|
+
f"{warning_count} warnings)[/yellow]"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Determine exit code
|
|
548
|
+
if error_count > 0:
|
|
549
|
+
raise typer.Exit(code=1)
|
|
550
|
+
|
|
551
|
+
raise typer.Exit(code=0)
|
|
552
|
+
|
|
553
|
+
except NotADoitProjectError as e:
|
|
554
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
555
|
+
raise typer.Exit(code=2)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Formatters for status output."""
|
|
2
|
+
|
|
3
|
+
from .base import StatusFormatter
|
|
4
|
+
from .json_formatter import JsonFormatter
|
|
5
|
+
from .markdown_formatter import MarkdownFormatter
|
|
6
|
+
from .rich_formatter import RichFormatter
|
|
7
|
+
|
|
8
|
+
__all__ = ["StatusFormatter", "JsonFormatter", "MarkdownFormatter", "RichFormatter"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Base formatter for status output."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from ..models.status_models import StatusReport
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StatusFormatter(ABC):
|
|
9
|
+
"""Abstract base class for status output formatters.
|
|
10
|
+
|
|
11
|
+
Subclasses implement format() to produce output in different formats
|
|
12
|
+
(rich terminal, JSON, markdown, etc.).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def format(self, report: StatusReport, verbose: bool = False) -> str:
|
|
17
|
+
"""Format the status report for output.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
report: The StatusReport to format.
|
|
21
|
+
verbose: Include detailed validation errors.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Formatted string representation.
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def format_to_console(self, report: StatusReport, verbose: bool = False) -> None:
|
|
29
|
+
"""Format and print directly to console.
|
|
30
|
+
|
|
31
|
+
Default implementation calls format() and prints.
|
|
32
|
+
Subclasses may override for richer output (e.g., Rich tables).
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
report: The StatusReport to format.
|
|
36
|
+
verbose: Include detailed validation errors.
|
|
37
|
+
"""
|
|
38
|
+
print(self.format(report, verbose))
|