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,443 @@
|
|
|
1
|
+
"""Rule engine for evaluating validation rules against spec content."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
from ..models.validation_models import (
|
|
8
|
+
Severity,
|
|
9
|
+
ValidationConfig,
|
|
10
|
+
ValidationIssue,
|
|
11
|
+
ValidationRule,
|
|
12
|
+
)
|
|
13
|
+
from ..rules.builtin_rules import get_builtin_rules
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .crossref_service import CrossReferenceService
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RuleEngine:
|
|
20
|
+
"""Evaluates validation rules against spec content."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: Optional[ValidationConfig] = None) -> None:
|
|
23
|
+
"""Initialize rule engine.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Validation configuration. Uses defaults if None.
|
|
27
|
+
"""
|
|
28
|
+
self.config = config or ValidationConfig.default()
|
|
29
|
+
self._rules: list[ValidationRule] = []
|
|
30
|
+
self._load_rules()
|
|
31
|
+
|
|
32
|
+
def _load_rules(self) -> None:
|
|
33
|
+
"""Load and configure all rules."""
|
|
34
|
+
# Start with builtin rules
|
|
35
|
+
self._rules = get_builtin_rules()
|
|
36
|
+
|
|
37
|
+
# Apply disabled rules
|
|
38
|
+
for rule in self._rules:
|
|
39
|
+
if rule.id in self.config.disabled_rules:
|
|
40
|
+
rule.enabled = False
|
|
41
|
+
|
|
42
|
+
# Apply severity overrides
|
|
43
|
+
for override in self.config.overrides:
|
|
44
|
+
for rule in self._rules:
|
|
45
|
+
if rule.id == override.rule:
|
|
46
|
+
rule.severity = Severity(override.severity)
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
# Add custom rules
|
|
50
|
+
for custom in self.config.custom_rules:
|
|
51
|
+
custom_rule = ValidationRule(
|
|
52
|
+
id=custom.name,
|
|
53
|
+
name=custom.name.replace("-", " ").title(),
|
|
54
|
+
description=custom.description,
|
|
55
|
+
severity=Severity(custom.severity),
|
|
56
|
+
category=custom.category,
|
|
57
|
+
pattern=custom.pattern,
|
|
58
|
+
enabled=True,
|
|
59
|
+
builtin=False,
|
|
60
|
+
)
|
|
61
|
+
self._rules.append(custom_rule)
|
|
62
|
+
|
|
63
|
+
def get_rules(self) -> list[ValidationRule]:
|
|
64
|
+
"""Get all active rules (builtin + custom, minus disabled).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of ValidationRule to apply.
|
|
68
|
+
"""
|
|
69
|
+
return [rule for rule in self._rules if rule.enabled]
|
|
70
|
+
|
|
71
|
+
def evaluate(
|
|
72
|
+
self,
|
|
73
|
+
content: str,
|
|
74
|
+
spec_path: Path,
|
|
75
|
+
) -> list[ValidationIssue]:
|
|
76
|
+
"""Evaluate all rules against spec content.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
content: Full text content of spec file.
|
|
80
|
+
spec_path: Path to spec (for context in messages).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of ValidationIssue for all violations found.
|
|
84
|
+
"""
|
|
85
|
+
issues: list[ValidationIssue] = []
|
|
86
|
+
|
|
87
|
+
for rule in self.get_rules():
|
|
88
|
+
rule_issues = self.evaluate_rule(rule, content, spec_path)
|
|
89
|
+
issues.extend(rule_issues)
|
|
90
|
+
|
|
91
|
+
return issues
|
|
92
|
+
|
|
93
|
+
def evaluate_rule(
|
|
94
|
+
self,
|
|
95
|
+
rule: ValidationRule,
|
|
96
|
+
content: str,
|
|
97
|
+
spec_path: Path,
|
|
98
|
+
) -> list[ValidationIssue]:
|
|
99
|
+
"""Evaluate a single rule against content.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
rule: The rule to evaluate.
|
|
103
|
+
content: Spec file content.
|
|
104
|
+
spec_path: Path for context.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of issues found (empty if rule passes).
|
|
108
|
+
"""
|
|
109
|
+
issues: list[ValidationIssue] = []
|
|
110
|
+
|
|
111
|
+
# Traceability rules have no pattern and require cross-file analysis
|
|
112
|
+
if rule.category == "traceability":
|
|
113
|
+
issues = self._check_traceability(rule, spec_path)
|
|
114
|
+
return issues
|
|
115
|
+
|
|
116
|
+
if not rule.pattern:
|
|
117
|
+
return issues
|
|
118
|
+
|
|
119
|
+
# Determine rule type based on category and pattern
|
|
120
|
+
if rule.category == "structure":
|
|
121
|
+
# Structure rules check for presence of sections
|
|
122
|
+
issues = self._check_section_present(rule, content)
|
|
123
|
+
elif rule.category in ("requirements", "naming"):
|
|
124
|
+
# These rules check that patterns ARE followed where applicable
|
|
125
|
+
issues = self._check_pattern_compliance(rule, content)
|
|
126
|
+
elif rule.category == "acceptance":
|
|
127
|
+
# Check user stories have acceptance scenarios
|
|
128
|
+
issues = self._check_acceptance_scenarios(rule, content)
|
|
129
|
+
elif rule.category == "clarity":
|
|
130
|
+
# Clarity rules check for absence of problematic patterns
|
|
131
|
+
issues = self._check_pattern_absent(rule, content)
|
|
132
|
+
|
|
133
|
+
return issues
|
|
134
|
+
|
|
135
|
+
def _check_section_present(
|
|
136
|
+
self,
|
|
137
|
+
rule: ValidationRule,
|
|
138
|
+
content: str,
|
|
139
|
+
) -> list[ValidationIssue]:
|
|
140
|
+
"""Check if a required section is present.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
rule: The structure rule to check.
|
|
144
|
+
content: Spec content.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List with one issue if section missing, empty otherwise.
|
|
148
|
+
"""
|
|
149
|
+
if not rule.pattern:
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
if not re.search(rule.pattern, content, re.MULTILINE | re.IGNORECASE):
|
|
153
|
+
return [
|
|
154
|
+
ValidationIssue(
|
|
155
|
+
rule_id=rule.id,
|
|
156
|
+
severity=rule.severity,
|
|
157
|
+
line_number=0,
|
|
158
|
+
message=f"{rule.name}: {rule.description}",
|
|
159
|
+
suggestion=f"Add a '## {rule.name.replace('Missing ', '')}' section to your spec",
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
def _check_pattern_compliance(
|
|
165
|
+
self,
|
|
166
|
+
rule: ValidationRule,
|
|
167
|
+
content: str,
|
|
168
|
+
) -> list[ValidationIssue]:
|
|
169
|
+
"""Check that patterns are followed where applicable.
|
|
170
|
+
|
|
171
|
+
For FR/SC naming, we check if there ARE requirements/criteria
|
|
172
|
+
that don't follow the naming convention.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
rule: The pattern rule to check.
|
|
176
|
+
content: Spec content.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of issues for non-compliant patterns.
|
|
180
|
+
"""
|
|
181
|
+
issues: list[ValidationIssue] = []
|
|
182
|
+
|
|
183
|
+
if not rule.pattern:
|
|
184
|
+
return issues
|
|
185
|
+
|
|
186
|
+
# Find the relevant section
|
|
187
|
+
lines = content.split("\n")
|
|
188
|
+
|
|
189
|
+
# For FR naming, look in Requirements section
|
|
190
|
+
if rule.id == "fr-naming-convention":
|
|
191
|
+
in_section = False
|
|
192
|
+
for i, line in enumerate(lines):
|
|
193
|
+
if re.match(r"^##\s+Requirements", line, re.IGNORECASE):
|
|
194
|
+
in_section = True
|
|
195
|
+
continue
|
|
196
|
+
if in_section and line.startswith("##"):
|
|
197
|
+
in_section = False
|
|
198
|
+
if in_section and line.strip().startswith("- **FR-"):
|
|
199
|
+
# Check if it follows the pattern
|
|
200
|
+
if not re.match(rule.pattern, line):
|
|
201
|
+
issues.append(
|
|
202
|
+
ValidationIssue(
|
|
203
|
+
rule_id=rule.id,
|
|
204
|
+
severity=rule.severity,
|
|
205
|
+
line_number=i + 1,
|
|
206
|
+
message=f"Line {i + 1}: {rule.description}",
|
|
207
|
+
suggestion="Use format: - **FR-XXX**: Description",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# For SC naming, look in Success Criteria section
|
|
212
|
+
elif rule.id == "sc-naming-convention":
|
|
213
|
+
in_section = False
|
|
214
|
+
for i, line in enumerate(lines):
|
|
215
|
+
if re.match(r"^##\s+Success\s+Criteria", line, re.IGNORECASE):
|
|
216
|
+
in_section = True
|
|
217
|
+
continue
|
|
218
|
+
if in_section and line.startswith("##"):
|
|
219
|
+
in_section = False
|
|
220
|
+
if in_section and line.strip().startswith("- **SC-"):
|
|
221
|
+
if not re.match(rule.pattern, line):
|
|
222
|
+
issues.append(
|
|
223
|
+
ValidationIssue(
|
|
224
|
+
rule_id=rule.id,
|
|
225
|
+
severity=rule.severity,
|
|
226
|
+
line_number=i + 1,
|
|
227
|
+
message=f"Line {i + 1}: {rule.description}",
|
|
228
|
+
suggestion="Use format: - **SC-XXX**: Description",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# For feature branch format
|
|
233
|
+
elif rule.id == "feature-branch-format":
|
|
234
|
+
if not re.search(rule.pattern, content, re.MULTILINE):
|
|
235
|
+
# Check if there's a feature branch line at all
|
|
236
|
+
if re.search(r"\*\*Feature\s+Branch\*\*:", content):
|
|
237
|
+
issues.append(
|
|
238
|
+
ValidationIssue(
|
|
239
|
+
rule_id=rule.id,
|
|
240
|
+
severity=rule.severity,
|
|
241
|
+
line_number=0,
|
|
242
|
+
message=rule.description,
|
|
243
|
+
suggestion="Use format: `NNN-feature-name` (e.g., `029-spec-validation`)",
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return issues
|
|
248
|
+
|
|
249
|
+
def _check_acceptance_scenarios(
|
|
250
|
+
self,
|
|
251
|
+
rule: ValidationRule,
|
|
252
|
+
content: str,
|
|
253
|
+
) -> list[ValidationIssue]:
|
|
254
|
+
"""Check that user stories have acceptance scenarios.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
rule: The acceptance rule to check.
|
|
258
|
+
content: Spec content.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of issues for user stories without scenarios.
|
|
262
|
+
"""
|
|
263
|
+
issues: list[ValidationIssue] = []
|
|
264
|
+
|
|
265
|
+
if rule.id != "missing-acceptance-scenarios":
|
|
266
|
+
return issues
|
|
267
|
+
|
|
268
|
+
# Find user story sections
|
|
269
|
+
lines = content.split("\n")
|
|
270
|
+
current_story = None
|
|
271
|
+
story_line = 0
|
|
272
|
+
has_scenarios = False
|
|
273
|
+
|
|
274
|
+
for i, line in enumerate(lines):
|
|
275
|
+
# Detect user story headers
|
|
276
|
+
if re.match(r"^###\s+User\s+Story\s+\d+", line, re.IGNORECASE):
|
|
277
|
+
# Check previous story
|
|
278
|
+
if current_story and not has_scenarios:
|
|
279
|
+
issues.append(
|
|
280
|
+
ValidationIssue(
|
|
281
|
+
rule_id=rule.id,
|
|
282
|
+
severity=rule.severity,
|
|
283
|
+
line_number=story_line,
|
|
284
|
+
message=f"{current_story} has no acceptance scenarios",
|
|
285
|
+
suggestion="Add **Given/When/Then** scenarios under the user story",
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
current_story = line.strip().lstrip("#").strip()
|
|
289
|
+
story_line = i + 1
|
|
290
|
+
has_scenarios = False
|
|
291
|
+
|
|
292
|
+
# Detect acceptance scenarios
|
|
293
|
+
if current_story and re.search(
|
|
294
|
+
r"\*\*Given\*\*.*\*\*When\*\*.*\*\*Then\*\*", line
|
|
295
|
+
):
|
|
296
|
+
has_scenarios = True
|
|
297
|
+
|
|
298
|
+
# Detect next section (non-user story)
|
|
299
|
+
if current_story and re.match(r"^##[^#]", line):
|
|
300
|
+
if not has_scenarios:
|
|
301
|
+
issues.append(
|
|
302
|
+
ValidationIssue(
|
|
303
|
+
rule_id=rule.id,
|
|
304
|
+
severity=rule.severity,
|
|
305
|
+
line_number=story_line,
|
|
306
|
+
message=f"{current_story} has no acceptance scenarios",
|
|
307
|
+
suggestion="Add **Given/When/Then** scenarios under the user story",
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
current_story = None
|
|
311
|
+
has_scenarios = False
|
|
312
|
+
|
|
313
|
+
# Check last story
|
|
314
|
+
if current_story and not has_scenarios:
|
|
315
|
+
issues.append(
|
|
316
|
+
ValidationIssue(
|
|
317
|
+
rule_id=rule.id,
|
|
318
|
+
severity=rule.severity,
|
|
319
|
+
line_number=story_line,
|
|
320
|
+
message=f"{current_story} has no acceptance scenarios",
|
|
321
|
+
suggestion="Add **Given/When/Then** scenarios under the user story",
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return issues
|
|
326
|
+
|
|
327
|
+
def _check_pattern_absent(
|
|
328
|
+
self,
|
|
329
|
+
rule: ValidationRule,
|
|
330
|
+
content: str,
|
|
331
|
+
) -> list[ValidationIssue]:
|
|
332
|
+
"""Check that problematic patterns are absent.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
rule: The clarity rule to check.
|
|
336
|
+
content: Spec content.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
List of issues where pattern is found.
|
|
340
|
+
"""
|
|
341
|
+
issues: list[ValidationIssue] = []
|
|
342
|
+
|
|
343
|
+
if not rule.pattern:
|
|
344
|
+
return issues
|
|
345
|
+
|
|
346
|
+
# Special handling for TODO/FIXME in approved specs
|
|
347
|
+
if rule.id == "todo-in-approved-spec":
|
|
348
|
+
# Check if spec is in draft status
|
|
349
|
+
if re.search(r"\*\*Status\*\*:\s*Draft", content, re.IGNORECASE):
|
|
350
|
+
return [] # Don't flag TODOs in draft specs
|
|
351
|
+
|
|
352
|
+
lines = content.split("\n")
|
|
353
|
+
for i, line in enumerate(lines):
|
|
354
|
+
matches = re.findall(rule.pattern, line, re.IGNORECASE)
|
|
355
|
+
if matches:
|
|
356
|
+
issues.append(
|
|
357
|
+
ValidationIssue(
|
|
358
|
+
rule_id=rule.id,
|
|
359
|
+
severity=rule.severity,
|
|
360
|
+
line_number=i + 1,
|
|
361
|
+
message=f"Line {i + 1}: {rule.description}",
|
|
362
|
+
suggestion=self._get_clarity_suggestion(rule.id),
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return issues
|
|
367
|
+
|
|
368
|
+
def _get_clarity_suggestion(self, rule_id: str) -> str:
|
|
369
|
+
"""Get suggestion text for clarity rules.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
rule_id: The rule ID.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Suggestion text.
|
|
376
|
+
"""
|
|
377
|
+
suggestions = {
|
|
378
|
+
"unresolved-clarification": "Resolve the ambiguity and remove the [NEEDS CLARIFICATION] marker",
|
|
379
|
+
"todo-in-approved-spec": "Complete the TODO item or change spec status to Draft",
|
|
380
|
+
}
|
|
381
|
+
return suggestions.get(rule_id, "Review and address this issue")
|
|
382
|
+
|
|
383
|
+
def _check_traceability(
|
|
384
|
+
self,
|
|
385
|
+
rule: ValidationRule,
|
|
386
|
+
spec_path: Path,
|
|
387
|
+
) -> list[ValidationIssue]:
|
|
388
|
+
"""Check cross-reference traceability between spec.md and tasks.md.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
rule: The traceability rule to check.
|
|
392
|
+
spec_path: Path to the spec file.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of issues found.
|
|
396
|
+
"""
|
|
397
|
+
issues: list[ValidationIssue] = []
|
|
398
|
+
|
|
399
|
+
# Determine the feature directory from spec path
|
|
400
|
+
feature_dir = spec_path.parent
|
|
401
|
+
tasks_path = feature_dir / "tasks.md"
|
|
402
|
+
|
|
403
|
+
# Skip traceability checks if tasks.md doesn't exist
|
|
404
|
+
if not tasks_path.exists():
|
|
405
|
+
return issues
|
|
406
|
+
|
|
407
|
+
# Import here to avoid circular imports
|
|
408
|
+
from .crossref_service import CrossReferenceService
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
service = CrossReferenceService()
|
|
412
|
+
uncovered, orphaned = service.validate_references(spec_path=spec_path)
|
|
413
|
+
|
|
414
|
+
if rule.id == "orphaned-task-reference":
|
|
415
|
+
for task, ref_id in orphaned:
|
|
416
|
+
issues.append(
|
|
417
|
+
ValidationIssue(
|
|
418
|
+
rule_id=rule.id,
|
|
419
|
+
severity=rule.severity,
|
|
420
|
+
line_number=task.line_number,
|
|
421
|
+
message=f"Task references non-existent requirement {ref_id}",
|
|
422
|
+
suggestion=f"Verify {ref_id} exists in spec.md or remove the reference",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
elif rule.id == "uncovered-requirement":
|
|
427
|
+
for req_id in uncovered:
|
|
428
|
+
issues.append(
|
|
429
|
+
ValidationIssue(
|
|
430
|
+
rule_id=rule.id,
|
|
431
|
+
severity=rule.severity,
|
|
432
|
+
line_number=0,
|
|
433
|
+
message=f"Requirement {req_id} has no linked tasks",
|
|
434
|
+
suggestion=f"Add [task description] [{req_id}] to tasks.md",
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
except Exception:
|
|
439
|
+
# If cross-reference validation fails, skip silently
|
|
440
|
+
# (e.g., spec not found, parsing errors)
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
return issues
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Scaffolder service for creating doit project structure."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ..models.agent import Agent
|
|
7
|
+
from ..models.project import Project
|
|
8
|
+
from ..models.results import InitResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Scaffolder:
|
|
12
|
+
"""Service for creating doit project directory structure."""
|
|
13
|
+
|
|
14
|
+
# Subdirectories to create under .doit/
|
|
15
|
+
DOIT_SUBDIRS = ["memory", "templates", "scripts", "config", "logs"]
|
|
16
|
+
|
|
17
|
+
def __init__(self, project: Project):
|
|
18
|
+
self.project = project
|
|
19
|
+
self.created_directories: list[Path] = []
|
|
20
|
+
self.created_files: list[Path] = []
|
|
21
|
+
|
|
22
|
+
def create_doit_structure(self) -> InitResult:
|
|
23
|
+
"""Create the .doit/ directory structure.
|
|
24
|
+
|
|
25
|
+
Creates:
|
|
26
|
+
- .doit/
|
|
27
|
+
- .doit/memory/
|
|
28
|
+
- .doit/templates/
|
|
29
|
+
- .doit/scripts/
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# Create main .doit directory
|
|
33
|
+
if not self.project.doit_folder.exists():
|
|
34
|
+
self.project.doit_folder.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
self.created_directories.append(self.project.doit_folder)
|
|
36
|
+
|
|
37
|
+
# Create subdirectories
|
|
38
|
+
for subdir in self.DOIT_SUBDIRS:
|
|
39
|
+
subdir_path = self.project.doit_folder / subdir
|
|
40
|
+
if not subdir_path.exists():
|
|
41
|
+
subdir_path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
self.created_directories.append(subdir_path)
|
|
43
|
+
|
|
44
|
+
return InitResult(
|
|
45
|
+
success=True,
|
|
46
|
+
project=self.project,
|
|
47
|
+
created_directories=self.created_directories.copy(),
|
|
48
|
+
created_files=self.created_files.copy(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
except PermissionError as e:
|
|
52
|
+
return InitResult(
|
|
53
|
+
success=False,
|
|
54
|
+
project=self.project,
|
|
55
|
+
error_message=f"Permission denied: {e}",
|
|
56
|
+
)
|
|
57
|
+
except OSError as e:
|
|
58
|
+
return InitResult(
|
|
59
|
+
success=False,
|
|
60
|
+
project=self.project,
|
|
61
|
+
error_message=f"Failed to create directory: {e}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def create_agent_directory(self, agent: Agent) -> bool:
|
|
65
|
+
"""Create the command directory for a specific agent.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
agent: The agent to create directory for
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if directory was created, False if it already existed
|
|
72
|
+
"""
|
|
73
|
+
cmd_dir = self.project.command_directory(agent)
|
|
74
|
+
|
|
75
|
+
if agent == Agent.COPILOT:
|
|
76
|
+
# For Copilot, also ensure .github/ exists
|
|
77
|
+
github_dir = self.project.path / ".github"
|
|
78
|
+
if not github_dir.exists():
|
|
79
|
+
github_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
self.created_directories.append(github_dir)
|
|
81
|
+
|
|
82
|
+
if not cmd_dir.exists():
|
|
83
|
+
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
self.created_directories.append(cmd_dir)
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def create_backup_directory(self, timestamp: str) -> Path:
|
|
90
|
+
"""Create a timestamped backup directory.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
timestamp: Timestamp string for the backup folder name
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Path to the created backup directory
|
|
97
|
+
"""
|
|
98
|
+
backup_dir = self.project.backups_folder / timestamp
|
|
99
|
+
|
|
100
|
+
if not backup_dir.exists():
|
|
101
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
self.created_directories.append(backup_dir)
|
|
103
|
+
|
|
104
|
+
return backup_dir
|
|
105
|
+
|
|
106
|
+
def is_doit_file(self, path: Path, agent: Agent) -> bool:
|
|
107
|
+
"""Check if a file is a doit-managed file for the given agent.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
path: Path to check
|
|
111
|
+
agent: Agent to check against
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if file is doit-managed (matches doit.* or doit-* pattern)
|
|
115
|
+
"""
|
|
116
|
+
filename = path.name
|
|
117
|
+
if agent == Agent.CLAUDE:
|
|
118
|
+
return filename.startswith("doit.") and filename.endswith(".md")
|
|
119
|
+
else: # COPILOT
|
|
120
|
+
return filename.startswith("doit.") and filename.endswith(".prompt.md")
|
|
121
|
+
|
|
122
|
+
def get_doit_files(self, agent: Agent) -> list[Path]:
|
|
123
|
+
"""Get all doit-managed files for an agent.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
agent: Agent to get files for
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of paths to doit-managed files
|
|
130
|
+
"""
|
|
131
|
+
cmd_dir = self.project.command_directory(agent)
|
|
132
|
+
if not cmd_dir.exists():
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
return [f for f in cmd_dir.iterdir() if f.is_file() and self.is_doit_file(f, agent)]
|
|
136
|
+
|
|
137
|
+
def get_custom_files(self, agent: Agent) -> list[Path]:
|
|
138
|
+
"""Get all custom (non-doit) files for an agent.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
agent: Agent to get files for
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of paths to custom files
|
|
145
|
+
"""
|
|
146
|
+
cmd_dir = self.project.command_directory(agent)
|
|
147
|
+
if not cmd_dir.exists():
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
return [f for f in cmd_dir.iterdir() if f.is_file() and not self.is_doit_file(f, agent)]
|
|
151
|
+
|
|
152
|
+
def get_preserved_paths(self) -> list[Path]:
|
|
153
|
+
"""Get list of paths that should be preserved during updates.
|
|
154
|
+
|
|
155
|
+
These include:
|
|
156
|
+
- .doit/memory/ (user-managed content)
|
|
157
|
+
- Custom (non-doit-prefixed) command files
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of paths to preserve
|
|
161
|
+
"""
|
|
162
|
+
preserved = []
|
|
163
|
+
|
|
164
|
+
# Always preserve memory folder contents
|
|
165
|
+
memory_dir = self.project.memory_folder
|
|
166
|
+
if memory_dir.exists():
|
|
167
|
+
preserved.extend(f for f in memory_dir.rglob("*") if f.is_file())
|
|
168
|
+
|
|
169
|
+
# Preserve custom command files for each agent
|
|
170
|
+
for agent in [Agent.CLAUDE, Agent.COPILOT]:
|
|
171
|
+
preserved.extend(self.get_custom_files(agent))
|
|
172
|
+
|
|
173
|
+
return preserved
|
|
174
|
+
|
|
175
|
+
def get_files_to_update(self, agent: Agent) -> list[Path]:
|
|
176
|
+
"""Get list of doit-managed files that can be updated.
|
|
177
|
+
|
|
178
|
+
These are doit-prefixed files that will be overwritten during update.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
agent: Agent to get files for
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of paths that can be updated
|
|
185
|
+
"""
|
|
186
|
+
return self.get_doit_files(agent)
|
|
187
|
+
|
|
188
|
+
def should_preserve(self, path: Path) -> bool:
|
|
189
|
+
"""Check if a file should be preserved during update.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path: Path to check
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if file should be preserved
|
|
196
|
+
"""
|
|
197
|
+
# Preserve anything in memory folder
|
|
198
|
+
try:
|
|
199
|
+
path.relative_to(self.project.memory_folder)
|
|
200
|
+
return True
|
|
201
|
+
except ValueError:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# Preserve custom (non-doit) command files
|
|
205
|
+
for agent in [Agent.CLAUDE, Agent.COPILOT]:
|
|
206
|
+
cmd_dir = self.project.command_directory(agent)
|
|
207
|
+
try:
|
|
208
|
+
path.relative_to(cmd_dir)
|
|
209
|
+
# File is in command directory - check if it's doit-managed
|
|
210
|
+
if not self.is_doit_file(path, agent):
|
|
211
|
+
return True
|
|
212
|
+
except ValueError:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
return False
|