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,175 @@
|
|
|
1
|
+
"""Models for spec validation and linting."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Severity(str, Enum):
|
|
10
|
+
"""Severity level for validation issues."""
|
|
11
|
+
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
WARNING = "warning"
|
|
14
|
+
INFO = "info"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ValidationStatus(str, Enum):
|
|
18
|
+
"""Status of a validation result."""
|
|
19
|
+
|
|
20
|
+
PASS = "pass"
|
|
21
|
+
WARN = "warn"
|
|
22
|
+
FAIL = "fail"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ValidationRule:
|
|
27
|
+
"""Represents a single validation check for spec files.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
id: Unique identifier (e.g., "missing-requirements")
|
|
31
|
+
name: Human-readable name
|
|
32
|
+
description: What this rule checks
|
|
33
|
+
severity: error, warning, or info
|
|
34
|
+
category: Group (structure, requirements, acceptance, clarity, naming)
|
|
35
|
+
pattern: Regex pattern for matching (if applicable)
|
|
36
|
+
enabled: Whether rule is active
|
|
37
|
+
builtin: True for default rules, false for custom
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
id: str
|
|
41
|
+
name: str
|
|
42
|
+
description: str
|
|
43
|
+
severity: Severity
|
|
44
|
+
category: str
|
|
45
|
+
pattern: Optional[str] = None
|
|
46
|
+
enabled: bool = True
|
|
47
|
+
builtin: bool = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ValidationIssue:
|
|
52
|
+
"""Individual problem found during validation.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
rule_id: FK to ValidationRule that triggered
|
|
56
|
+
severity: Severity level (error, warning, info)
|
|
57
|
+
line_number: Line in spec where issue found (0 if N/A)
|
|
58
|
+
message: Human-readable description of problem
|
|
59
|
+
suggestion: How to fix the issue (optional)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
rule_id: str
|
|
63
|
+
severity: Severity
|
|
64
|
+
line_number: int
|
|
65
|
+
message: str
|
|
66
|
+
suggestion: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ValidationResult:
|
|
71
|
+
"""Aggregate result of validating a single spec file.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
spec_path: Path to validated spec file
|
|
75
|
+
issues: List of validation issues found
|
|
76
|
+
quality_score: 0-100 score based on weighted issues
|
|
77
|
+
validated_at: Timestamp of validation
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
spec_path: str
|
|
81
|
+
issues: list[ValidationIssue] = field(default_factory=list)
|
|
82
|
+
quality_score: int = 100
|
|
83
|
+
validated_at: datetime = field(default_factory=datetime.now)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def status(self) -> ValidationStatus:
|
|
87
|
+
"""Derive status from issue counts."""
|
|
88
|
+
if self.error_count > 0:
|
|
89
|
+
return ValidationStatus.FAIL
|
|
90
|
+
if self.warning_count > 0:
|
|
91
|
+
return ValidationStatus.WARN
|
|
92
|
+
return ValidationStatus.PASS
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def error_count(self) -> int:
|
|
96
|
+
"""Count error-severity issues."""
|
|
97
|
+
return sum(1 for i in self.issues if i.severity == Severity.ERROR)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def warning_count(self) -> int:
|
|
101
|
+
"""Count warning-severity issues."""
|
|
102
|
+
return sum(1 for i in self.issues if i.severity == Severity.WARNING)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def info_count(self) -> int:
|
|
106
|
+
"""Count info-severity issues."""
|
|
107
|
+
return sum(1 for i in self.issues if i.severity == Severity.INFO)
|
|
108
|
+
|
|
109
|
+
def add_issue(self, issue: ValidationIssue) -> None:
|
|
110
|
+
"""Add an issue to the result."""
|
|
111
|
+
self.issues.append(issue)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class RuleOverride:
|
|
116
|
+
"""Override for built-in rule behavior.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
rule: Rule ID to override
|
|
120
|
+
severity: New severity level
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
rule: str
|
|
124
|
+
severity: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class CustomRule:
|
|
129
|
+
"""User-defined validation rule.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
name: Unique name (becomes ID)
|
|
133
|
+
description: What this checks
|
|
134
|
+
pattern: Regex to match (required or absent sections)
|
|
135
|
+
severity: error, warning, or info
|
|
136
|
+
category: Logical grouping
|
|
137
|
+
check: "present" (must match), "absent" (must not match), or "count"
|
|
138
|
+
max: For check="count", maximum occurrences
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
name: str
|
|
142
|
+
description: str
|
|
143
|
+
pattern: str
|
|
144
|
+
severity: str
|
|
145
|
+
category: str
|
|
146
|
+
check: str = "present"
|
|
147
|
+
max: Optional[int] = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class ValidationConfig:
|
|
152
|
+
"""Project-level validation configuration.
|
|
153
|
+
|
|
154
|
+
Loaded from .doit/validation-rules.yaml.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
path: Path to configuration file
|
|
158
|
+
version: Schema version (e.g., "1.0")
|
|
159
|
+
enabled: Whether validation is enabled
|
|
160
|
+
disabled_rules: Rule IDs to skip
|
|
161
|
+
overrides: Severity overrides for built-in rules
|
|
162
|
+
custom_rules: User-defined validation rules
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
path: str = ""
|
|
166
|
+
version: str = "1.0"
|
|
167
|
+
enabled: bool = True
|
|
168
|
+
disabled_rules: list[str] = field(default_factory=list)
|
|
169
|
+
overrides: list[RuleOverride] = field(default_factory=list)
|
|
170
|
+
custom_rules: list[CustomRule] = field(default_factory=list)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def default(cls) -> "ValidationConfig":
|
|
174
|
+
"""Return default configuration with no customizations."""
|
|
175
|
+
return cls()
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Data models and exceptions for the guided workflow system.
|
|
2
|
+
|
|
3
|
+
This module contains dataclasses for workflow definitions, state management,
|
|
4
|
+
and validation results, along with the exception hierarchy for workflow errors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# Enums
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkflowStatus(str, Enum):
|
|
20
|
+
"""Status of a workflow execution."""
|
|
21
|
+
|
|
22
|
+
PENDING = "pending"
|
|
23
|
+
RUNNING = "running"
|
|
24
|
+
COMPLETED = "completed"
|
|
25
|
+
INTERRUPTED = "interrupted"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Core Dataclasses (T003)
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class WorkflowStep:
|
|
35
|
+
"""Defines a single step in a guided workflow.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
id: Step identifier (e.g., "select-ai-tool")
|
|
39
|
+
name: Display name shown in progress
|
|
40
|
+
prompt_text: Question or instruction for user
|
|
41
|
+
required: Whether step must be completed
|
|
42
|
+
order: Sequence position (0-indexed)
|
|
43
|
+
validation_type: Validator class name (e.g., "PathExistsValidator")
|
|
44
|
+
default_value: Value used when skipped or non-interactive
|
|
45
|
+
options: For choice steps: {key: description} mapping
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
id: str
|
|
49
|
+
name: str
|
|
50
|
+
prompt_text: str
|
|
51
|
+
required: bool
|
|
52
|
+
order: int
|
|
53
|
+
validation_type: str | None = None
|
|
54
|
+
default_value: str | None = None
|
|
55
|
+
options: dict[str, str] | None = None
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
"""Validate step configuration."""
|
|
59
|
+
if not self.required and self.default_value is None:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Optional step '{self.id}' must have a default_value"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Workflow:
|
|
67
|
+
"""Defines a guided workflow for a specific command.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
id: Unique identifier (e.g., "init-workflow")
|
|
71
|
+
command_name: CLI command this workflow belongs to
|
|
72
|
+
description: Human-readable workflow description
|
|
73
|
+
interactive: Whether this workflow prompts for input
|
|
74
|
+
steps: Ordered list of workflow steps
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
id: str
|
|
78
|
+
command_name: str
|
|
79
|
+
description: str
|
|
80
|
+
interactive: bool
|
|
81
|
+
steps: list[WorkflowStep] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
"""Validate workflow configuration."""
|
|
85
|
+
if not self.steps:
|
|
86
|
+
raise ValueError(f"Workflow '{self.id}' must have at least one step")
|
|
87
|
+
|
|
88
|
+
# Validate step order uniqueness
|
|
89
|
+
orders = [s.order for s in self.steps]
|
|
90
|
+
if len(orders) != len(set(orders)):
|
|
91
|
+
raise ValueError(f"Workflow '{self.id}' has duplicate step orders")
|
|
92
|
+
|
|
93
|
+
# Validate step ID uniqueness
|
|
94
|
+
ids = [s.id for s in self.steps]
|
|
95
|
+
if len(ids) != len(set(ids)):
|
|
96
|
+
raise ValueError(f"Workflow '{self.id}' has duplicate step IDs")
|
|
97
|
+
|
|
98
|
+
def get_step_by_id(self, step_id: str) -> WorkflowStep | None:
|
|
99
|
+
"""Get a step by its ID."""
|
|
100
|
+
for step in self.steps:
|
|
101
|
+
if step.id == step_id:
|
|
102
|
+
return step
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def get_step_by_order(self, order: int) -> WorkflowStep | None:
|
|
106
|
+
"""Get a step by its order index."""
|
|
107
|
+
for step in self.steps:
|
|
108
|
+
if step.order == order:
|
|
109
|
+
return step
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# State Dataclasses (T004)
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class StepResponse:
|
|
120
|
+
"""Records a user's response to a workflow step.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
step_id: Which step this responds to
|
|
124
|
+
value: User-provided or default value
|
|
125
|
+
skipped: Whether step was skipped
|
|
126
|
+
responded_at: When response was captured
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
step_id: str
|
|
130
|
+
value: str
|
|
131
|
+
skipped: bool = False
|
|
132
|
+
responded_at: datetime = field(default_factory=datetime.now)
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict:
|
|
135
|
+
"""Convert to dictionary for JSON serialization."""
|
|
136
|
+
return {
|
|
137
|
+
"step_id": self.step_id,
|
|
138
|
+
"value": self.value,
|
|
139
|
+
"skipped": self.skipped,
|
|
140
|
+
"responded_at": self.responded_at.isoformat(),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_dict(cls, data: dict) -> "StepResponse":
|
|
145
|
+
"""Create from dictionary (JSON deserialization)."""
|
|
146
|
+
return cls(
|
|
147
|
+
step_id=data["step_id"],
|
|
148
|
+
value=data["value"],
|
|
149
|
+
skipped=data.get("skipped", False),
|
|
150
|
+
responded_at=datetime.fromisoformat(data["responded_at"]),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class WorkflowState:
|
|
156
|
+
"""Persists workflow progress for recovery.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
id: Unique state identifier
|
|
160
|
+
workflow_id: Which workflow is running
|
|
161
|
+
command_name: Command being executed
|
|
162
|
+
current_step: Index of current step
|
|
163
|
+
total_steps: Total number of steps in workflow
|
|
164
|
+
status: Current workflow status
|
|
165
|
+
created_at: When workflow started
|
|
166
|
+
updated_at: Last state update
|
|
167
|
+
responses: step_id -> StepResponse mapping
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
id: str
|
|
171
|
+
workflow_id: str
|
|
172
|
+
command_name: str
|
|
173
|
+
current_step: int = 0
|
|
174
|
+
total_steps: int = 0
|
|
175
|
+
status: WorkflowStatus = WorkflowStatus.PENDING
|
|
176
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
177
|
+
updated_at: datetime = field(default_factory=datetime.now)
|
|
178
|
+
responses: dict[str, StepResponse] = field(default_factory=dict)
|
|
179
|
+
|
|
180
|
+
def get_response(self, step_id: str) -> StepResponse | None:
|
|
181
|
+
"""Get response for a specific step."""
|
|
182
|
+
return self.responses.get(step_id)
|
|
183
|
+
|
|
184
|
+
def set_response(self, response: StepResponse) -> None:
|
|
185
|
+
"""Set or update a step response."""
|
|
186
|
+
self.responses[response.step_id] = response
|
|
187
|
+
self.updated_at = datetime.now()
|
|
188
|
+
|
|
189
|
+
def advance_step(self) -> None:
|
|
190
|
+
"""Move to the next step."""
|
|
191
|
+
self.current_step += 1
|
|
192
|
+
self.updated_at = datetime.now()
|
|
193
|
+
|
|
194
|
+
def go_back(self) -> bool:
|
|
195
|
+
"""Go back to previous step. Returns False if already at first step."""
|
|
196
|
+
if self.current_step > 0:
|
|
197
|
+
self.current_step -= 1
|
|
198
|
+
self.updated_at = datetime.now()
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> dict:
|
|
203
|
+
"""Convert to dictionary for JSON serialization."""
|
|
204
|
+
return {
|
|
205
|
+
"id": self.id,
|
|
206
|
+
"workflow_id": self.workflow_id,
|
|
207
|
+
"command_name": self.command_name,
|
|
208
|
+
"current_step": self.current_step,
|
|
209
|
+
"total_steps": self.total_steps,
|
|
210
|
+
"status": self.status.value,
|
|
211
|
+
"created_at": self.created_at.isoformat(),
|
|
212
|
+
"updated_at": self.updated_at.isoformat(),
|
|
213
|
+
"responses": {
|
|
214
|
+
step_id: resp.to_dict()
|
|
215
|
+
for step_id, resp in self.responses.items()
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def from_dict(cls, data: dict) -> "WorkflowState":
|
|
221
|
+
"""Create from dictionary (JSON deserialization)."""
|
|
222
|
+
responses = {
|
|
223
|
+
step_id: StepResponse.from_dict(resp_data)
|
|
224
|
+
for step_id, resp_data in data.get("responses", {}).items()
|
|
225
|
+
}
|
|
226
|
+
return cls(
|
|
227
|
+
id=data["id"],
|
|
228
|
+
workflow_id=data["workflow_id"],
|
|
229
|
+
command_name=data["command_name"],
|
|
230
|
+
current_step=data.get("current_step", 0),
|
|
231
|
+
total_steps=data.get("total_steps", 0),
|
|
232
|
+
status=WorkflowStatus(data.get("status", "pending")),
|
|
233
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
234
|
+
updated_at=datetime.fromisoformat(data["updated_at"]),
|
|
235
|
+
responses=responses,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# =============================================================================
|
|
240
|
+
# Validation Result
|
|
241
|
+
# =============================================================================
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class ValidationResult:
|
|
246
|
+
"""Result of validating a step input.
|
|
247
|
+
|
|
248
|
+
Attributes:
|
|
249
|
+
passed: Whether validation passed
|
|
250
|
+
error_message: Error description if failed
|
|
251
|
+
suggestion: Guidance for fixing the error
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
passed: bool
|
|
255
|
+
error_message: str | None = None
|
|
256
|
+
suggestion: str | None = None
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def success(cls) -> "ValidationResult":
|
|
260
|
+
"""Create a successful validation result."""
|
|
261
|
+
return cls(passed=True)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def failure(
|
|
265
|
+
cls, error_message: str, suggestion: str | None = None
|
|
266
|
+
) -> "ValidationResult":
|
|
267
|
+
"""Create a failed validation result."""
|
|
268
|
+
return cls(passed=False, error_message=error_message, suggestion=suggestion)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# =============================================================================
|
|
272
|
+
# Exception Hierarchy (T005)
|
|
273
|
+
# =============================================================================
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class WorkflowError(Exception):
|
|
277
|
+
"""Base exception for workflow errors."""
|
|
278
|
+
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ValidationError(WorkflowError):
|
|
283
|
+
"""Input validation failed.
|
|
284
|
+
|
|
285
|
+
Attributes:
|
|
286
|
+
result: The validation result containing error details
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(self, result: ValidationResult, message: str | None = None) -> None:
|
|
290
|
+
self.result = result
|
|
291
|
+
msg = message or result.error_message or "Validation failed"
|
|
292
|
+
super().__init__(msg)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class NavigationCommand(WorkflowError):
|
|
296
|
+
"""User requested navigation (not an error).
|
|
297
|
+
|
|
298
|
+
This exception is used for flow control when user types 'back' or 'skip'.
|
|
299
|
+
|
|
300
|
+
Attributes:
|
|
301
|
+
command: The navigation command ("back" or "skip")
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(self, command: Literal["back", "skip"]) -> None:
|
|
305
|
+
self.command = command
|
|
306
|
+
super().__init__(f"Navigation: {command}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class StateCorruptionError(WorkflowError):
|
|
310
|
+
"""State file is corrupted or invalid.
|
|
311
|
+
|
|
312
|
+
Attributes:
|
|
313
|
+
state_path: Path to the corrupted state file
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
def __init__(self, state_path: Path, message: str | None = None) -> None:
|
|
317
|
+
self.state_path = state_path
|
|
318
|
+
msg = message or f"State file corrupted: {state_path}"
|
|
319
|
+
super().__init__(msg)
|