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,390 @@
|
|
|
1
|
+
"""Interactive prompts for guided workflows.
|
|
2
|
+
|
|
3
|
+
This module provides the InteractivePrompt and ProgressDisplay classes
|
|
4
|
+
for collecting user input and showing workflow progress.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.prompt import Prompt, Confirm
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from rich.theme import Theme
|
|
16
|
+
|
|
17
|
+
from ..models.workflow_models import (
|
|
18
|
+
WorkflowStep,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
NavigationCommand,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Theme Configuration
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
WORKFLOW_THEME = Theme({
|
|
30
|
+
"prompt": "bold cyan",
|
|
31
|
+
"prompt.default": "dim",
|
|
32
|
+
"error": "bold red",
|
|
33
|
+
"success": "bold green",
|
|
34
|
+
"warning": "bold yellow",
|
|
35
|
+
"info": "dim",
|
|
36
|
+
"step.current": "bold white",
|
|
37
|
+
"step.completed": "green",
|
|
38
|
+
"step.skipped": "dim",
|
|
39
|
+
"step.pending": "dim white",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# InteractivePrompt Class
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InteractivePrompt:
|
|
49
|
+
"""Collects user input for workflow steps.
|
|
50
|
+
|
|
51
|
+
This class handles interactive prompting for workflow steps,
|
|
52
|
+
including text input, choice selection, and confirmation dialogs.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Environment variable to force non-interactive mode
|
|
56
|
+
NON_INTERACTIVE_ENV_VAR = "DOIT_NON_INTERACTIVE"
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
console: Console | None = None,
|
|
61
|
+
force_non_interactive: bool = False,
|
|
62
|
+
):
|
|
63
|
+
"""Initialize the prompt handler.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
console: Rich console for output. Creates a new one if not provided.
|
|
67
|
+
force_non_interactive: If True, always use non-interactive mode.
|
|
68
|
+
"""
|
|
69
|
+
self.console = console or Console(theme=WORKFLOW_THEME)
|
|
70
|
+
self._force_non_interactive = force_non_interactive
|
|
71
|
+
|
|
72
|
+
def prompt(
|
|
73
|
+
self,
|
|
74
|
+
step: WorkflowStep,
|
|
75
|
+
validator: "InputValidator | None" = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Prompt user for step input.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
step: Step to prompt for
|
|
81
|
+
validator: Optional validator for real-time feedback
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
User-provided value
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
NavigationCommand: If user types 'back' or 'skip'
|
|
88
|
+
KeyboardInterrupt: If user presses Ctrl+C
|
|
89
|
+
"""
|
|
90
|
+
# Non-TTY mode - return default or raise error
|
|
91
|
+
if not self._is_interactive():
|
|
92
|
+
if step.default_value is not None:
|
|
93
|
+
return step.default_value
|
|
94
|
+
if not step.required:
|
|
95
|
+
return ""
|
|
96
|
+
raise ValueError(f"Required step '{step.id}' has no default for non-interactive mode")
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
# Display the prompt
|
|
100
|
+
self._display_prompt(step)
|
|
101
|
+
|
|
102
|
+
# Get input
|
|
103
|
+
value = self._get_input(step)
|
|
104
|
+
|
|
105
|
+
# Handle navigation commands
|
|
106
|
+
nav_result = self._check_navigation(value, step)
|
|
107
|
+
if nav_result is not None:
|
|
108
|
+
raise nav_result
|
|
109
|
+
|
|
110
|
+
# Handle empty input for optional steps
|
|
111
|
+
if not value and not step.required:
|
|
112
|
+
return step.default_value or ""
|
|
113
|
+
|
|
114
|
+
# Validate if validator provided
|
|
115
|
+
if validator:
|
|
116
|
+
from ..services.input_validator import validate_step
|
|
117
|
+
|
|
118
|
+
result = validator.validate(value, step, {})
|
|
119
|
+
if not result.passed:
|
|
120
|
+
self._show_validation_error(result)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
return value
|
|
124
|
+
|
|
125
|
+
def prompt_choice(
|
|
126
|
+
self,
|
|
127
|
+
step: WorkflowStep,
|
|
128
|
+
options: dict[str, str],
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Prompt user to select from options.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
step: Step with choice options
|
|
134
|
+
options: {value: description} mapping
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Selected option value
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
NavigationCommand: If user types 'back'
|
|
141
|
+
KeyboardInterrupt: If user presses Ctrl+C
|
|
142
|
+
"""
|
|
143
|
+
# Non-TTY mode - return first option or default
|
|
144
|
+
if not self._is_interactive():
|
|
145
|
+
if step.default_value and step.default_value in options:
|
|
146
|
+
return step.default_value
|
|
147
|
+
return list(options.keys())[0]
|
|
148
|
+
|
|
149
|
+
while True:
|
|
150
|
+
# Display prompt and options
|
|
151
|
+
self._display_prompt(step)
|
|
152
|
+
self._display_options(options)
|
|
153
|
+
|
|
154
|
+
# Get choice
|
|
155
|
+
value = self._get_choice_input(step, options)
|
|
156
|
+
|
|
157
|
+
# Handle navigation
|
|
158
|
+
nav_result = self._check_navigation(value, step)
|
|
159
|
+
if nav_result is not None:
|
|
160
|
+
raise nav_result
|
|
161
|
+
|
|
162
|
+
# Validate choice
|
|
163
|
+
normalized = value.strip().lower()
|
|
164
|
+
for key in options:
|
|
165
|
+
if key.lower() == normalized:
|
|
166
|
+
return key
|
|
167
|
+
|
|
168
|
+
self._show_invalid_choice(value, options)
|
|
169
|
+
|
|
170
|
+
def prompt_confirm(
|
|
171
|
+
self,
|
|
172
|
+
message: str,
|
|
173
|
+
default: bool = True,
|
|
174
|
+
) -> bool:
|
|
175
|
+
"""Prompt for yes/no confirmation.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
message: Confirmation prompt
|
|
179
|
+
default: Default if Enter pressed
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True for yes, False for no
|
|
183
|
+
"""
|
|
184
|
+
# Non-TTY mode - return default
|
|
185
|
+
if not self._is_interactive():
|
|
186
|
+
return default
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
default_str = "Y/n" if default else "y/N"
|
|
190
|
+
self.console.print(f"[prompt]{message}[/prompt] [{default_str}] ", end="")
|
|
191
|
+
|
|
192
|
+
value = self._get_input(None)
|
|
193
|
+
|
|
194
|
+
if not value:
|
|
195
|
+
return default
|
|
196
|
+
|
|
197
|
+
normalized = value.strip().lower()
|
|
198
|
+
if normalized in ("y", "yes"):
|
|
199
|
+
return True
|
|
200
|
+
if normalized in ("n", "no"):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
self._show_invalid_confirm()
|
|
204
|
+
|
|
205
|
+
# =========================================================================
|
|
206
|
+
# Internal Methods
|
|
207
|
+
# =========================================================================
|
|
208
|
+
|
|
209
|
+
def _is_interactive(self) -> bool:
|
|
210
|
+
"""Check if running in an interactive terminal.
|
|
211
|
+
|
|
212
|
+
Returns False if:
|
|
213
|
+
- force_non_interactive is True
|
|
214
|
+
- DOIT_NON_INTERACTIVE env var is set to "true", "1", or "yes"
|
|
215
|
+
- stdin is not a TTY
|
|
216
|
+
"""
|
|
217
|
+
if self._force_non_interactive:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# Check environment variable
|
|
221
|
+
env_value = os.environ.get(self.NON_INTERACTIVE_ENV_VAR, "").lower()
|
|
222
|
+
if env_value in ("true", "1", "yes"):
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
return sys.stdin.isatty()
|
|
226
|
+
|
|
227
|
+
def _display_prompt(self, step: WorkflowStep) -> None:
|
|
228
|
+
"""Display the prompt text for a step."""
|
|
229
|
+
self.console.print()
|
|
230
|
+
self.console.print(f"[prompt]{step.prompt_text}[/prompt]")
|
|
231
|
+
|
|
232
|
+
if step.default_value and not step.options:
|
|
233
|
+
self.console.print(f"[prompt.default](default: {step.default_value})[/prompt.default]")
|
|
234
|
+
|
|
235
|
+
if not step.required:
|
|
236
|
+
self.console.print("[info]Type 'skip' to skip this step[/info]")
|
|
237
|
+
|
|
238
|
+
self.console.print("[info]Type 'back' to go to previous step[/info]")
|
|
239
|
+
|
|
240
|
+
def _display_options(self, options: dict[str, str]) -> None:
|
|
241
|
+
"""Display available options for a choice step."""
|
|
242
|
+
self.console.print()
|
|
243
|
+
for key, description in options.items():
|
|
244
|
+
self.console.print(f" [bold]{key}[/bold]: {description}")
|
|
245
|
+
self.console.print()
|
|
246
|
+
|
|
247
|
+
def _get_input(self, step: WorkflowStep | None) -> str:
|
|
248
|
+
"""Get raw input from user."""
|
|
249
|
+
try:
|
|
250
|
+
return input("> ").strip()
|
|
251
|
+
except EOFError:
|
|
252
|
+
# Handle piped input ending
|
|
253
|
+
if step and step.default_value:
|
|
254
|
+
return step.default_value
|
|
255
|
+
return ""
|
|
256
|
+
|
|
257
|
+
def _get_choice_input(self, step: WorkflowStep, options: dict[str, str]) -> str:
|
|
258
|
+
"""Get choice input from user."""
|
|
259
|
+
return self._get_input(step)
|
|
260
|
+
|
|
261
|
+
def _check_navigation(
|
|
262
|
+
self,
|
|
263
|
+
value: str,
|
|
264
|
+
step: WorkflowStep,
|
|
265
|
+
) -> NavigationCommand | None:
|
|
266
|
+
"""Check if input is a navigation command."""
|
|
267
|
+
normalized = value.strip().lower()
|
|
268
|
+
|
|
269
|
+
if normalized == "back":
|
|
270
|
+
return NavigationCommand("back")
|
|
271
|
+
|
|
272
|
+
if normalized == "skip":
|
|
273
|
+
if not step.required:
|
|
274
|
+
return NavigationCommand("skip")
|
|
275
|
+
# For required steps, 'skip' is treated as regular input
|
|
276
|
+
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
def _show_validation_error(self, result: ValidationResult) -> None:
|
|
280
|
+
"""Display a validation error message."""
|
|
281
|
+
self.console.print()
|
|
282
|
+
self.console.print(f"[error]Error: {result.error_message}[/error]")
|
|
283
|
+
if result.suggestion:
|
|
284
|
+
self.console.print(f"[info]Hint: {result.suggestion}[/info]")
|
|
285
|
+
self.console.print()
|
|
286
|
+
|
|
287
|
+
def _show_invalid_choice(self, value: str, options: dict[str, str]) -> None:
|
|
288
|
+
"""Display an invalid choice error."""
|
|
289
|
+
valid_options = ", ".join(options.keys())
|
|
290
|
+
self.console.print()
|
|
291
|
+
self.console.print(f"[error]Invalid choice: '{value}'[/error]")
|
|
292
|
+
self.console.print(f"[info]Valid options: {valid_options}[/info]")
|
|
293
|
+
self.console.print()
|
|
294
|
+
|
|
295
|
+
def _show_invalid_confirm(self) -> None:
|
|
296
|
+
"""Display an invalid confirmation error."""
|
|
297
|
+
self.console.print("[error]Please enter 'y' or 'n'[/error]")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# =============================================================================
|
|
301
|
+
# ProgressDisplay Class
|
|
302
|
+
# =============================================================================
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class ProgressDisplay:
|
|
306
|
+
"""Displays workflow progress to the user.
|
|
307
|
+
|
|
308
|
+
Shows the current step, completed steps, and overall progress
|
|
309
|
+
through a multi-step workflow.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def __init__(self, console: Console | None = None):
|
|
313
|
+
"""Initialize the progress display.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
console: Rich console for output. Creates a new one if not provided.
|
|
317
|
+
"""
|
|
318
|
+
self.console = console or Console(theme=WORKFLOW_THEME)
|
|
319
|
+
self._completed_steps: list[str] = []
|
|
320
|
+
self._skipped_steps: list[str] = []
|
|
321
|
+
|
|
322
|
+
def show_step(
|
|
323
|
+
self,
|
|
324
|
+
step: WorkflowStep,
|
|
325
|
+
current: int,
|
|
326
|
+
total: int,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Show current step indicator.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
step: Current step
|
|
332
|
+
current: Current step number (1-indexed)
|
|
333
|
+
total: Total step count
|
|
334
|
+
"""
|
|
335
|
+
percentage = int((current - 1) / total * 100) if total > 0 else 0
|
|
336
|
+
|
|
337
|
+
header = f"Step {current} of {total}: {step.name}"
|
|
338
|
+
if not step.required:
|
|
339
|
+
header += " (optional)"
|
|
340
|
+
|
|
341
|
+
self.console.print()
|
|
342
|
+
self.console.rule(f"[step.current]{header}[/step.current]")
|
|
343
|
+
|
|
344
|
+
# Show progress percentage
|
|
345
|
+
self.console.print(f"[info]Progress: {percentage}% complete[/info]")
|
|
346
|
+
|
|
347
|
+
def mark_complete(self, step: WorkflowStep) -> None:
|
|
348
|
+
"""Mark step as completed in display."""
|
|
349
|
+
self._completed_steps.append(step.id)
|
|
350
|
+
self.console.print(f"[success]\u2713 {step.name} completed[/success]")
|
|
351
|
+
|
|
352
|
+
def mark_skipped(self, step: WorkflowStep) -> None:
|
|
353
|
+
"""Mark step as skipped in display."""
|
|
354
|
+
self._skipped_steps.append(step.id)
|
|
355
|
+
self.console.print(f"[step.skipped]- {step.name} skipped[/step.skipped]")
|
|
356
|
+
|
|
357
|
+
def show_error(
|
|
358
|
+
self,
|
|
359
|
+
step: WorkflowStep,
|
|
360
|
+
error: ValidationResult,
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Show validation error with suggestion."""
|
|
363
|
+
self.console.print()
|
|
364
|
+
self.console.print(Panel(
|
|
365
|
+
f"[error]{error.error_message}[/error]\n\n"
|
|
366
|
+
f"[info]{error.suggestion or 'Please try again.'}[/info]",
|
|
367
|
+
title=f"Validation Error: {step.name}",
|
|
368
|
+
border_style="red",
|
|
369
|
+
))
|
|
370
|
+
|
|
371
|
+
def show_summary(self, total_steps: int) -> None:
|
|
372
|
+
"""Show final summary of workflow progress."""
|
|
373
|
+
completed = len(self._completed_steps)
|
|
374
|
+
skipped = len(self._skipped_steps)
|
|
375
|
+
|
|
376
|
+
self.console.print()
|
|
377
|
+
self.console.rule("[success]Workflow Complete[/success]")
|
|
378
|
+
self.console.print(f"[success]Completed: {completed} steps[/success]")
|
|
379
|
+
if skipped > 0:
|
|
380
|
+
self.console.print(f"[step.skipped]Skipped: {skipped} steps[/step.skipped]")
|
|
381
|
+
self.console.print()
|
|
382
|
+
|
|
383
|
+
def show_interrupted(self, current_step: int, total_steps: int) -> None:
|
|
384
|
+
"""Show message when workflow is interrupted."""
|
|
385
|
+
self.console.print()
|
|
386
|
+
self.console.print(
|
|
387
|
+
f"[warning]Workflow interrupted at step {current_step} of {total_steps}[/warning]"
|
|
388
|
+
)
|
|
389
|
+
self.console.print("[info]Progress has been saved. Run the command again to resume.[/info]")
|
|
390
|
+
self.console.print()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Built-in validation rules for spec files."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
|
|
5
|
+
from ..models.validation_models import Severity, ValidationRule
|
|
6
|
+
|
|
7
|
+
# Default validation rules
|
|
8
|
+
BUILTIN_RULES: list[ValidationRule] = [
|
|
9
|
+
# Structure rules (error severity)
|
|
10
|
+
ValidationRule(
|
|
11
|
+
id="missing-user-scenarios",
|
|
12
|
+
name="Missing User Scenarios",
|
|
13
|
+
description="Spec must include a User Scenarios section",
|
|
14
|
+
severity=Severity.ERROR,
|
|
15
|
+
category="structure",
|
|
16
|
+
pattern=r"^##\s+User\s+Scenarios",
|
|
17
|
+
),
|
|
18
|
+
ValidationRule(
|
|
19
|
+
id="missing-requirements",
|
|
20
|
+
name="Missing Requirements",
|
|
21
|
+
description="Spec must include a Requirements section",
|
|
22
|
+
severity=Severity.ERROR,
|
|
23
|
+
category="structure",
|
|
24
|
+
pattern=r"^##\s+Requirements",
|
|
25
|
+
),
|
|
26
|
+
ValidationRule(
|
|
27
|
+
id="missing-success-criteria",
|
|
28
|
+
name="Missing Success Criteria",
|
|
29
|
+
description="Spec must include a Success Criteria section",
|
|
30
|
+
severity=Severity.ERROR,
|
|
31
|
+
category="structure",
|
|
32
|
+
pattern=r"^##\s+Success\s+Criteria",
|
|
33
|
+
),
|
|
34
|
+
# Requirements rules (warning severity)
|
|
35
|
+
ValidationRule(
|
|
36
|
+
id="fr-naming-convention",
|
|
37
|
+
name="FR Naming Convention",
|
|
38
|
+
description="Functional requirements should follow FR-XXX pattern",
|
|
39
|
+
severity=Severity.WARNING,
|
|
40
|
+
category="requirements",
|
|
41
|
+
pattern=r"^\s*-\s+\*\*FR-\d{3}\*\*:",
|
|
42
|
+
),
|
|
43
|
+
ValidationRule(
|
|
44
|
+
id="sc-naming-convention",
|
|
45
|
+
name="SC Naming Convention",
|
|
46
|
+
description="Success criteria should follow SC-XXX pattern",
|
|
47
|
+
severity=Severity.WARNING,
|
|
48
|
+
category="requirements",
|
|
49
|
+
pattern=r"^\s*-\s+\*\*SC-\d{3}\*\*:",
|
|
50
|
+
),
|
|
51
|
+
# Acceptance rules (warning severity)
|
|
52
|
+
ValidationRule(
|
|
53
|
+
id="missing-acceptance-scenarios",
|
|
54
|
+
name="Missing Acceptance Scenarios",
|
|
55
|
+
description="User stories should have acceptance scenarios with Given/When/Then",
|
|
56
|
+
severity=Severity.WARNING,
|
|
57
|
+
category="acceptance",
|
|
58
|
+
pattern=r"\*\*Given\*\*.*\*\*When\*\*.*\*\*Then\*\*",
|
|
59
|
+
),
|
|
60
|
+
ValidationRule(
|
|
61
|
+
id="incomplete-given-when-then",
|
|
62
|
+
name="Incomplete Given/When/Then",
|
|
63
|
+
description="Acceptance scenarios should have complete Given/When/Then format",
|
|
64
|
+
severity=Severity.INFO,
|
|
65
|
+
category="acceptance",
|
|
66
|
+
pattern=r"Given.*When.*Then",
|
|
67
|
+
),
|
|
68
|
+
# Clarity rules (warning severity)
|
|
69
|
+
ValidationRule(
|
|
70
|
+
id="unresolved-clarification",
|
|
71
|
+
name="Unresolved Clarification",
|
|
72
|
+
description="Spec should not have [NEEDS CLARIFICATION] markers",
|
|
73
|
+
severity=Severity.WARNING,
|
|
74
|
+
category="clarity",
|
|
75
|
+
pattern=r"\[NEEDS\s+CLARIFICATION",
|
|
76
|
+
),
|
|
77
|
+
ValidationRule(
|
|
78
|
+
id="todo-in-approved-spec",
|
|
79
|
+
name="TODO in Approved Spec",
|
|
80
|
+
description="Approved specs should not have TODO or FIXME markers",
|
|
81
|
+
severity=Severity.WARNING,
|
|
82
|
+
category="clarity",
|
|
83
|
+
pattern=r"\b(TODO|FIXME)\b",
|
|
84
|
+
),
|
|
85
|
+
# Naming rules (info severity)
|
|
86
|
+
ValidationRule(
|
|
87
|
+
id="feature-branch-format",
|
|
88
|
+
name="Feature Branch Format",
|
|
89
|
+
description="Feature branch should follow NNN-feature-name format",
|
|
90
|
+
severity=Severity.INFO,
|
|
91
|
+
category="naming",
|
|
92
|
+
pattern=r"\*\*Feature\s+Branch\*\*:\s+`\d{3}-[\w-]+`",
|
|
93
|
+
),
|
|
94
|
+
# Traceability rules (cross-reference validation)
|
|
95
|
+
ValidationRule(
|
|
96
|
+
id="orphaned-task-reference",
|
|
97
|
+
name="Orphaned Task Reference",
|
|
98
|
+
description="Task references a requirement that does not exist in spec.md",
|
|
99
|
+
severity=Severity.ERROR,
|
|
100
|
+
category="traceability",
|
|
101
|
+
pattern=None, # Requires cross-file analysis
|
|
102
|
+
),
|
|
103
|
+
ValidationRule(
|
|
104
|
+
id="uncovered-requirement",
|
|
105
|
+
name="Uncovered Requirement",
|
|
106
|
+
description="Requirement has no linked tasks in tasks.md",
|
|
107
|
+
severity=Severity.WARNING,
|
|
108
|
+
category="traceability",
|
|
109
|
+
pattern=None, # Requires cross-file analysis
|
|
110
|
+
),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_builtin_rules() -> list[ValidationRule]:
|
|
115
|
+
"""Get all built-in validation rules.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of ValidationRule objects with default configuration.
|
|
119
|
+
Each call returns fresh copies to prevent mutation issues.
|
|
120
|
+
"""
|
|
121
|
+
return [replace(rule) for rule in BUILTIN_RULES]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_rule_by_id(rule_id: str) -> ValidationRule | None:
|
|
125
|
+
"""Get a specific built-in rule by ID.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
rule_id: The unique identifier of the rule.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The ValidationRule if found, None otherwise.
|
|
132
|
+
"""
|
|
133
|
+
for rule in BUILTIN_RULES:
|
|
134
|
+
if rule.id == rule_id:
|
|
135
|
+
return rule
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_rules_by_category(category: str) -> list[ValidationRule]:
|
|
140
|
+
"""Get all built-in rules in a category.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
category: Category name (structure, requirements, acceptance, clarity, naming).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of ValidationRule objects in the category.
|
|
147
|
+
"""
|
|
148
|
+
return [rule for rule in BUILTIN_RULES if rule.category == category]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_rules_by_severity(severity: Severity) -> list[ValidationRule]:
|
|
152
|
+
"""Get all built-in rules with a specific severity.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
severity: Severity level to filter by.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of ValidationRule objects with that severity.
|
|
159
|
+
"""
|
|
160
|
+
return [rule for rule in BUILTIN_RULES if rule.severity == severity]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Services for doit-cli project initialization and management."""
|
|
2
|
+
|
|
3
|
+
from .agent_detector import AgentDetector
|
|
4
|
+
from .backup_service import BackupService
|
|
5
|
+
from .context_loader import ContextLoader, estimate_tokens, truncate_content
|
|
6
|
+
from .scaffolder import Scaffolder
|
|
7
|
+
from .template_manager import TemplateManager
|
|
8
|
+
from .validator import Validator
|
|
9
|
+
from .input_validator import (
|
|
10
|
+
InputValidator,
|
|
11
|
+
RequiredValidator,
|
|
12
|
+
PathExistsValidator,
|
|
13
|
+
ChoiceValidator,
|
|
14
|
+
PatternValidator,
|
|
15
|
+
get_validator,
|
|
16
|
+
register_validator,
|
|
17
|
+
chain_validators,
|
|
18
|
+
validate_step,
|
|
19
|
+
)
|
|
20
|
+
from .workflow_engine import WorkflowEngine
|
|
21
|
+
from .state_manager import StateManager
|
|
22
|
+
from .spec_scanner import SpecScanner, NotADoitProjectError, SpecNotFoundError
|
|
23
|
+
from .status_reporter import StatusReporter
|
|
24
|
+
from .requirement_parser import RequirementParser
|
|
25
|
+
from .task_parser import TaskParser
|
|
26
|
+
from .coverage_calculator import CoverageCalculator
|
|
27
|
+
from .crossref_service import CrossReferenceService
|
|
28
|
+
from .diagram_service import DiagramService
|
|
29
|
+
from .section_parser import SectionParser
|
|
30
|
+
from .user_story_parser import UserStoryParser
|
|
31
|
+
from .entity_parser import EntityParser
|
|
32
|
+
from .user_journey_generator import UserJourneyGenerator
|
|
33
|
+
from .er_diagram_generator import ERDiagramGenerator
|
|
34
|
+
from .mermaid_validator import MermaidValidator
|
|
35
|
+
from .architecture_generator import ArchitectureGenerator
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AgentDetector",
|
|
39
|
+
"BackupService",
|
|
40
|
+
"ContextLoader",
|
|
41
|
+
"Scaffolder",
|
|
42
|
+
"TemplateManager",
|
|
43
|
+
"Validator",
|
|
44
|
+
"estimate_tokens",
|
|
45
|
+
"truncate_content",
|
|
46
|
+
# Input validation
|
|
47
|
+
"InputValidator",
|
|
48
|
+
"RequiredValidator",
|
|
49
|
+
"PathExistsValidator",
|
|
50
|
+
"ChoiceValidator",
|
|
51
|
+
"PatternValidator",
|
|
52
|
+
"get_validator",
|
|
53
|
+
"register_validator",
|
|
54
|
+
"chain_validators",
|
|
55
|
+
"validate_step",
|
|
56
|
+
# Workflow engine
|
|
57
|
+
"WorkflowEngine",
|
|
58
|
+
# State management
|
|
59
|
+
"StateManager",
|
|
60
|
+
# Status dashboard
|
|
61
|
+
"SpecScanner",
|
|
62
|
+
"NotADoitProjectError",
|
|
63
|
+
"SpecNotFoundError",
|
|
64
|
+
"StatusReporter",
|
|
65
|
+
# Cross-reference services
|
|
66
|
+
"RequirementParser",
|
|
67
|
+
"TaskParser",
|
|
68
|
+
"CoverageCalculator",
|
|
69
|
+
"CrossReferenceService",
|
|
70
|
+
# Diagram services
|
|
71
|
+
"DiagramService",
|
|
72
|
+
"SectionParser",
|
|
73
|
+
"UserStoryParser",
|
|
74
|
+
"EntityParser",
|
|
75
|
+
"UserJourneyGenerator",
|
|
76
|
+
"ERDiagramGenerator",
|
|
77
|
+
"MermaidValidator",
|
|
78
|
+
"ArchitectureGenerator",
|
|
79
|
+
]
|