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.

Files changed (135) hide show
  1. doit_cli/__init__.py +1356 -0
  2. doit_cli/cli/__init__.py +26 -0
  3. doit_cli/cli/analytics_command.py +616 -0
  4. doit_cli/cli/context_command.py +213 -0
  5. doit_cli/cli/diagram_command.py +304 -0
  6. doit_cli/cli/fixit_command.py +641 -0
  7. doit_cli/cli/hooks_command.py +211 -0
  8. doit_cli/cli/init_command.py +613 -0
  9. doit_cli/cli/memory_command.py +293 -0
  10. doit_cli/cli/roadmapit_command.py +10 -0
  11. doit_cli/cli/status_command.py +117 -0
  12. doit_cli/cli/sync_prompts_command.py +248 -0
  13. doit_cli/cli/validate_command.py +196 -0
  14. doit_cli/cli/verify_command.py +204 -0
  15. doit_cli/cli/workflow_mixin.py +224 -0
  16. doit_cli/cli/xref_command.py +555 -0
  17. doit_cli/formatters/__init__.py +8 -0
  18. doit_cli/formatters/base.py +38 -0
  19. doit_cli/formatters/json_formatter.py +126 -0
  20. doit_cli/formatters/markdown_formatter.py +97 -0
  21. doit_cli/formatters/rich_formatter.py +257 -0
  22. doit_cli/main.py +51 -0
  23. doit_cli/models/__init__.py +139 -0
  24. doit_cli/models/agent.py +74 -0
  25. doit_cli/models/analytics_models.py +384 -0
  26. doit_cli/models/context_config.py +464 -0
  27. doit_cli/models/crossref_models.py +182 -0
  28. doit_cli/models/diagram_models.py +363 -0
  29. doit_cli/models/fixit_models.py +355 -0
  30. doit_cli/models/hook_config.py +125 -0
  31. doit_cli/models/project.py +91 -0
  32. doit_cli/models/results.py +121 -0
  33. doit_cli/models/search_models.py +228 -0
  34. doit_cli/models/status_models.py +195 -0
  35. doit_cli/models/sync_models.py +146 -0
  36. doit_cli/models/template.py +77 -0
  37. doit_cli/models/validation_models.py +175 -0
  38. doit_cli/models/workflow_models.py +319 -0
  39. doit_cli/prompts/__init__.py +5 -0
  40. doit_cli/prompts/fixit_prompts.py +344 -0
  41. doit_cli/prompts/interactive.py +390 -0
  42. doit_cli/rules/__init__.py +5 -0
  43. doit_cli/rules/builtin_rules.py +160 -0
  44. doit_cli/services/__init__.py +79 -0
  45. doit_cli/services/agent_detector.py +168 -0
  46. doit_cli/services/analytics_service.py +218 -0
  47. doit_cli/services/architecture_generator.py +290 -0
  48. doit_cli/services/backup_service.py +204 -0
  49. doit_cli/services/config_loader.py +113 -0
  50. doit_cli/services/context_loader.py +1123 -0
  51. doit_cli/services/coverage_calculator.py +142 -0
  52. doit_cli/services/crossref_service.py +237 -0
  53. doit_cli/services/cycle_time_calculator.py +134 -0
  54. doit_cli/services/date_inferrer.py +349 -0
  55. doit_cli/services/diagram_service.py +337 -0
  56. doit_cli/services/drift_detector.py +109 -0
  57. doit_cli/services/entity_parser.py +301 -0
  58. doit_cli/services/er_diagram_generator.py +197 -0
  59. doit_cli/services/fixit_service.py +699 -0
  60. doit_cli/services/github_service.py +192 -0
  61. doit_cli/services/hook_manager.py +258 -0
  62. doit_cli/services/hook_validator.py +528 -0
  63. doit_cli/services/input_validator.py +322 -0
  64. doit_cli/services/memory_search.py +527 -0
  65. doit_cli/services/mermaid_validator.py +334 -0
  66. doit_cli/services/prompt_transformer.py +91 -0
  67. doit_cli/services/prompt_writer.py +133 -0
  68. doit_cli/services/query_interpreter.py +428 -0
  69. doit_cli/services/report_exporter.py +219 -0
  70. doit_cli/services/report_generator.py +256 -0
  71. doit_cli/services/requirement_parser.py +112 -0
  72. doit_cli/services/roadmap_summarizer.py +209 -0
  73. doit_cli/services/rule_engine.py +443 -0
  74. doit_cli/services/scaffolder.py +215 -0
  75. doit_cli/services/score_calculator.py +172 -0
  76. doit_cli/services/section_parser.py +204 -0
  77. doit_cli/services/spec_scanner.py +327 -0
  78. doit_cli/services/state_manager.py +355 -0
  79. doit_cli/services/status_reporter.py +143 -0
  80. doit_cli/services/task_parser.py +347 -0
  81. doit_cli/services/template_manager.py +710 -0
  82. doit_cli/services/template_reader.py +158 -0
  83. doit_cli/services/user_journey_generator.py +214 -0
  84. doit_cli/services/user_story_parser.py +232 -0
  85. doit_cli/services/validation_service.py +188 -0
  86. doit_cli/services/validator.py +232 -0
  87. doit_cli/services/velocity_tracker.py +173 -0
  88. doit_cli/services/workflow_engine.py +405 -0
  89. doit_cli/templates/agent-file-template.md +28 -0
  90. doit_cli/templates/checklist-template.md +39 -0
  91. doit_cli/templates/commands/doit.checkin.md +363 -0
  92. doit_cli/templates/commands/doit.constitution.md +187 -0
  93. doit_cli/templates/commands/doit.documentit.md +485 -0
  94. doit_cli/templates/commands/doit.fixit.md +181 -0
  95. doit_cli/templates/commands/doit.implementit.md +265 -0
  96. doit_cli/templates/commands/doit.planit.md +262 -0
  97. doit_cli/templates/commands/doit.reviewit.md +355 -0
  98. doit_cli/templates/commands/doit.roadmapit.md +389 -0
  99. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  100. doit_cli/templates/commands/doit.specit.md +521 -0
  101. doit_cli/templates/commands/doit.taskit.md +304 -0
  102. doit_cli/templates/commands/doit.testit.md +277 -0
  103. doit_cli/templates/config/context.yaml +134 -0
  104. doit_cli/templates/config/hooks.yaml +93 -0
  105. doit_cli/templates/config/validation-rules.yaml +64 -0
  106. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  107. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  108. doit_cli/templates/github-issue-templates/task.yml +129 -0
  109. doit_cli/templates/hooks/.gitkeep +0 -0
  110. doit_cli/templates/hooks/post-commit.sh +25 -0
  111. doit_cli/templates/hooks/post-merge.sh +75 -0
  112. doit_cli/templates/hooks/pre-commit.sh +17 -0
  113. doit_cli/templates/hooks/pre-push.sh +18 -0
  114. doit_cli/templates/memory/completed_roadmap.md +50 -0
  115. doit_cli/templates/memory/constitution.md +125 -0
  116. doit_cli/templates/memory/roadmap.md +61 -0
  117. doit_cli/templates/plan-template.md +146 -0
  118. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  119. doit_cli/templates/scripts/bash/common.sh +156 -0
  120. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  121. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  122. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  123. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  124. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  125. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  126. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  127. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  128. doit_cli/templates/spec-template.md +159 -0
  129. doit_cli/templates/tasks-template.md +313 -0
  130. doit_cli/templates/vscode-settings.json +14 -0
  131. doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
  132. doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
  133. doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
  134. doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
  135. doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,347 @@
1
+ """Parser for extracting tasks and cross-references from tasks.md files."""
2
+
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from ..models.crossref_models import Task, TaskReference
9
+
10
+
11
+ class TaskParser:
12
+ """Parses tasks.md files to extract tasks and their requirement references.
13
+
14
+ This parser extracts task items with checkbox format and identifies
15
+ [FR-XXX] cross-references within task descriptions.
16
+ """
17
+
18
+ # Pattern to match task lines: - [ ] or - [x] or - [X] followed by description
19
+ # Captures: group(1) = checkbox state (space, x, or X), group(2) = description
20
+ TASK_PATTERN = re.compile(
21
+ r"^\s*-\s*\[(?P<state>[ xX])\]\s*(?P<description>.+)$",
22
+ re.MULTILINE,
23
+ )
24
+
25
+ # Pattern to extract [FR-XXX] or [FR-001, FR-002] references
26
+ # Matches single or comma-separated FR references
27
+ REFERENCE_PATTERN = re.compile(
28
+ r"\[(?P<refs>FR-\d{3}(?:,\s*FR-\d{3})*)\]"
29
+ )
30
+
31
+ # Pattern to extract individual FR-XXX from comma-separated list
32
+ FR_PATTERN = re.compile(r"FR-\d{3}")
33
+
34
+ def __init__(self, tasks_path: Optional[Path] = None) -> None:
35
+ """Initialize parser with optional tasks file path.
36
+
37
+ Args:
38
+ tasks_path: Path to tasks.md file. Can be set later via parse().
39
+ """
40
+ self.tasks_path = tasks_path
41
+
42
+ def parse(self, tasks_path: Optional[Path] = None) -> list[Task]:
43
+ """Parse tasks.md and extract all tasks with their references.
44
+
45
+ Args:
46
+ tasks_path: Path to tasks.md file. Overrides constructor path.
47
+
48
+ Returns:
49
+ List of Task objects in file order.
50
+
51
+ Raises:
52
+ FileNotFoundError: If tasks file doesn't exist.
53
+ ValueError: If no tasks path provided.
54
+ """
55
+ path = tasks_path or self.tasks_path
56
+ if path is None:
57
+ raise ValueError("No tasks path provided")
58
+
59
+ path = Path(path)
60
+ if not path.exists():
61
+ raise FileNotFoundError(f"Tasks file not found: {path}")
62
+
63
+ content = path.read_text(encoding="utf-8")
64
+ return self.parse_content(content, str(path))
65
+
66
+ def parse_content(self, content: str, tasks_file: str) -> list[Task]:
67
+ """Parse content string and extract tasks.
68
+
69
+ Args:
70
+ content: Full content of tasks.md file.
71
+ tasks_file: Path to associate with extracted tasks.
72
+
73
+ Returns:
74
+ List of Task objects in file order.
75
+ """
76
+ tasks: list[Task] = []
77
+ lines = content.split("\n")
78
+
79
+ for line_num, line in enumerate(lines, start=1):
80
+ match = self.TASK_PATTERN.match(line)
81
+ if match:
82
+ state = match.group("state")
83
+ description = match.group("description").strip()
84
+ completed = state.lower() == "x"
85
+
86
+ # Extract FR references from description
87
+ references = self._extract_references(description)
88
+
89
+ # Generate unique ID from normalized description
90
+ task_id = self._generate_task_id(description)
91
+
92
+ task = Task(
93
+ id=task_id,
94
+ tasks_file=tasks_file,
95
+ description=self._clean_description(description),
96
+ completed=completed,
97
+ line_number=line_num,
98
+ references=references,
99
+ )
100
+ tasks.append(task)
101
+
102
+ return tasks
103
+
104
+ def _extract_references(self, description: str) -> list[TaskReference]:
105
+ """Extract FR-XXX references from task description.
106
+
107
+ Handles both single references [FR-001] and multiple [FR-001, FR-003].
108
+
109
+ Args:
110
+ description: Task description text.
111
+
112
+ Returns:
113
+ List of TaskReference objects in order found.
114
+ """
115
+ references: list[TaskReference] = []
116
+ position = 0
117
+
118
+ # Find all reference patterns in the description
119
+ for match in self.REFERENCE_PATTERN.finditer(description):
120
+ refs_str = match.group("refs")
121
+ # Extract individual FR-XXX IDs
122
+ for fr_match in self.FR_PATTERN.finditer(refs_str):
123
+ ref = TaskReference(
124
+ requirement_id=fr_match.group(),
125
+ position=position,
126
+ )
127
+ references.append(ref)
128
+ position += 1
129
+
130
+ return references
131
+
132
+ def _clean_description(self, description: str) -> str:
133
+ """Remove FR reference brackets from description for clean display.
134
+
135
+ Args:
136
+ description: Raw task description with [FR-XXX] references.
137
+
138
+ Returns:
139
+ Description with [FR-XXX] patterns removed.
140
+ """
141
+ # Remove [FR-XXX] or [FR-001, FR-002] patterns
142
+ cleaned = self.REFERENCE_PATTERN.sub("", description)
143
+ # Clean up extra whitespace
144
+ return " ".join(cleaned.split()).strip()
145
+
146
+ def _generate_task_id(self, description: str) -> str:
147
+ """Generate unique ID from task description.
148
+
149
+ Uses first 8 characters of MD5 hash of normalized description.
150
+
151
+ Args:
152
+ description: Task description text.
153
+
154
+ Returns:
155
+ 8-character hex ID.
156
+ """
157
+ # Normalize: lowercase, remove extra whitespace, remove FR refs
158
+ normalized = self._clean_description(description).lower()
159
+ normalized = " ".join(normalized.split())
160
+ return hashlib.md5(normalized.encode()).hexdigest()[:8]
161
+
162
+ def get_tasks_for_requirement(
163
+ self, requirement_id: str, tasks_path: Optional[Path] = None
164
+ ) -> list[Task]:
165
+ """Get all tasks that reference a specific requirement.
166
+
167
+ Args:
168
+ requirement_id: The FR-XXX ID to search for.
169
+ tasks_path: Path to tasks.md file.
170
+
171
+ Returns:
172
+ List of tasks that reference the requirement.
173
+ """
174
+ tasks = self.parse(tasks_path)
175
+ return [t for t in tasks if requirement_id in t.requirement_ids]
176
+
177
+ def get_all_referenced_ids(self, tasks_path: Optional[Path] = None) -> set[str]:
178
+ """Get set of all requirement IDs referenced in tasks.
179
+
180
+ Args:
181
+ tasks_path: Path to tasks.md file.
182
+
183
+ Returns:
184
+ Set of FR-XXX IDs referenced by at least one task.
185
+ """
186
+ tasks = self.parse(tasks_path)
187
+ all_ids: set[str] = set()
188
+ for task in tasks:
189
+ all_ids.update(task.requirement_ids)
190
+ return all_ids
191
+
192
+ def preserve_references(
193
+ self,
194
+ old_tasks: list[Task],
195
+ new_tasks: list[Task],
196
+ similarity_threshold: float = 0.7,
197
+ ) -> list[Task]:
198
+ """Preserve cross-references from old tasks when regenerating tasks.md.
199
+
200
+ Matches new tasks to old tasks by description similarity and transfers
201
+ [FR-XXX] references to maintain traceability through regeneration.
202
+
203
+ Args:
204
+ old_tasks: Tasks from previous version of tasks.md with references.
205
+ new_tasks: Newly generated tasks that may lack references.
206
+ similarity_threshold: Minimum similarity score (0-1) for matching.
207
+
208
+ Returns:
209
+ New tasks with references preserved from matching old tasks.
210
+ """
211
+ from dataclasses import replace as dataclass_replace
212
+
213
+ result: list[Task] = []
214
+ used_old_tasks: set[str] = set()
215
+
216
+ for new_task in new_tasks:
217
+ best_match: Optional[Task] = None
218
+ best_score = 0.0
219
+
220
+ for old_task in old_tasks:
221
+ # Skip already matched old tasks
222
+ if old_task.id in used_old_tasks:
223
+ continue
224
+
225
+ score = self._calculate_similarity(
226
+ new_task.description, old_task.description
227
+ )
228
+ if score > best_score and score >= similarity_threshold:
229
+ best_score = score
230
+ best_match = old_task
231
+
232
+ if best_match and best_match.references:
233
+ # Preserve references from the matched old task
234
+ used_old_tasks.add(best_match.id)
235
+ preserved_task = dataclass_replace(
236
+ new_task, references=list(best_match.references)
237
+ )
238
+ result.append(preserved_task)
239
+ else:
240
+ result.append(new_task)
241
+
242
+ return result
243
+
244
+ def _calculate_similarity(self, text1: str, text2: str) -> float:
245
+ """Calculate text similarity between two task descriptions.
246
+
247
+ Uses Jaccard similarity on word sets after normalization.
248
+
249
+ Args:
250
+ text1: First description text.
251
+ text2: Second description text.
252
+
253
+ Returns:
254
+ Similarity score between 0.0 (no match) and 1.0 (identical).
255
+ """
256
+ words1 = set(self._normalize_for_matching(text1).split())
257
+ words2 = set(self._normalize_for_matching(text2).split())
258
+
259
+ if not words1 or not words2:
260
+ return 0.0
261
+
262
+ intersection = words1 & words2
263
+ union = words1 | words2
264
+
265
+ return len(intersection) / len(union) if union else 0.0
266
+
267
+ def _normalize_for_matching(self, text: str) -> str:
268
+ """Normalize description for similarity comparison.
269
+
270
+ Removes task IDs, reference markers, and normalizes whitespace/case.
271
+
272
+ Args:
273
+ text: Task description text.
274
+
275
+ Returns:
276
+ Normalized text suitable for comparison.
277
+ """
278
+ # Remove task IDs (T001, T002, etc.)
279
+ normalized = re.sub(r"\bT\d{3}\b", "", text)
280
+ # Remove parallel markers [P]
281
+ normalized = re.sub(r"\[P\]", "", normalized)
282
+ # Remove user story markers [US1], [US2], etc.
283
+ normalized = re.sub(r"\[US\d+\]", "", normalized)
284
+ # Remove FR references
285
+ normalized = self.REFERENCE_PATTERN.sub("", normalized)
286
+ # Lowercase and normalize whitespace
287
+ normalized = normalized.lower()
288
+ normalized = " ".join(normalized.split())
289
+ return normalized
290
+
291
+ def apply_references_to_content(
292
+ self,
293
+ content: str,
294
+ reference_map: dict[str, list[TaskReference]],
295
+ ) -> str:
296
+ """Apply preserved references to regenerated tasks.md content.
297
+
298
+ Args:
299
+ content: New tasks.md content without references.
300
+ reference_map: Map of task description -> references to apply.
301
+
302
+ Returns:
303
+ Content with [FR-XXX] references inserted where applicable.
304
+ """
305
+ lines = content.split("\n")
306
+ result_lines: list[str] = []
307
+
308
+ for line in lines:
309
+ match = self.TASK_PATTERN.match(line)
310
+ if match:
311
+ description = match.group("description").strip()
312
+ cleaned = self._clean_description(description)
313
+ normalized = self._normalize_for_matching(cleaned)
314
+
315
+ # Look for matching references
316
+ refs_to_add: list[TaskReference] = []
317
+ for key, refs in reference_map.items():
318
+ if self._calculate_similarity(normalized, key) >= 0.7:
319
+ refs_to_add = refs
320
+ break
321
+
322
+ if refs_to_add and not self.REFERENCE_PATTERN.search(line):
323
+ # Format references to add
324
+ ref_ids = [r.requirement_id for r in refs_to_add]
325
+ ref_str = f"[{', '.join(ref_ids)}]"
326
+ # Insert before end of line
327
+ line = f"{line.rstrip()} {ref_str}"
328
+
329
+ result_lines.append(line)
330
+
331
+ return "\n".join(result_lines)
332
+
333
+ def build_reference_map(self, tasks: list[Task]) -> dict[str, list[TaskReference]]:
334
+ """Build a map of normalized descriptions to their references.
335
+
336
+ Args:
337
+ tasks: List of tasks with references.
338
+
339
+ Returns:
340
+ Dict mapping normalized description to TaskReference list.
341
+ """
342
+ ref_map: dict[str, list[TaskReference]] = {}
343
+ for task in tasks:
344
+ if task.references:
345
+ key = self._normalize_for_matching(task.description)
346
+ ref_map[key] = list(task.references)
347
+ return ref_map