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,74 @@
1
+ """Agent enum and configuration for supported AI coding assistants."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Agent(str, Enum):
7
+ """Supported AI coding agents."""
8
+
9
+ CLAUDE = "claude"
10
+ COPILOT = "copilot"
11
+
12
+ @property
13
+ def display_name(self) -> str:
14
+ """Human-readable name for display."""
15
+ names = {
16
+ Agent.CLAUDE: "Claude Code",
17
+ Agent.COPILOT: "GitHub Copilot",
18
+ }
19
+ return names[self]
20
+
21
+ @property
22
+ def command_directory(self) -> str:
23
+ """Relative path to command/prompt directory."""
24
+ directories = {
25
+ Agent.CLAUDE: ".claude/commands",
26
+ Agent.COPILOT: ".github/prompts",
27
+ }
28
+ return directories[self]
29
+
30
+ @property
31
+ def template_directory(self) -> str:
32
+ """Relative path within bundled templates.
33
+
34
+ All agents now use commands/ as the single source of truth.
35
+ Copilot prompts are generated dynamically via transformation.
36
+ """
37
+ return "commands"
38
+
39
+ @property
40
+ def needs_transformation(self) -> bool:
41
+ """Whether templates need transformation for this agent.
42
+
43
+ Returns:
44
+ True for Copilot (requires transformation from command format),
45
+ False for Claude (direct copy).
46
+ """
47
+ return self == Agent.COPILOT
48
+
49
+ @property
50
+ def file_extension(self) -> str:
51
+ """File extension for command files."""
52
+ extensions = {
53
+ Agent.CLAUDE: ".md",
54
+ Agent.COPILOT: ".prompt.md",
55
+ }
56
+ return extensions[self]
57
+
58
+ @property
59
+ def file_pattern(self) -> str:
60
+ """Glob pattern for doit-managed files."""
61
+ patterns = {
62
+ Agent.CLAUDE: "doit.*.md",
63
+ Agent.COPILOT: "doit.*.prompt.md",
64
+ }
65
+ return patterns[self]
66
+
67
+ @property
68
+ def file_prefix(self) -> str:
69
+ """Filename prefix for doit commands."""
70
+ prefixes = {
71
+ Agent.CLAUDE: "doit.",
72
+ Agent.COPILOT: "doit.",
73
+ }
74
+ return prefixes[self]
@@ -0,0 +1,384 @@
1
+ """Models for spec analytics and metrics dashboard.
2
+
3
+ This module provides dataclasses for analytics data:
4
+ - SpecMetadata: Extended spec info with lifecycle dates
5
+ - CycleTimeRecord: Individual cycle time for completed specs
6
+ - CycleTimeStats: Statistical summary of cycle times
7
+ - VelocityDataPoint: Weekly velocity aggregation
8
+ - AnalyticsReport: Complete analytics report
9
+ """
10
+
11
+ from dataclasses import dataclass, field
12
+ from datetime import date, datetime, timedelta
13
+ from pathlib import Path
14
+ from statistics import mean, median, stdev
15
+ from typing import Optional
16
+
17
+ from .status_models import SpecState, SpecStatus
18
+
19
+
20
+ @dataclass
21
+ class SpecMetadata:
22
+ """Extended spec metadata with lifecycle dates.
23
+
24
+ Attributes:
25
+ name: Spec directory name (e.g., "036-spec-analytics-dashboard")
26
+ status: Current spec state (Draft, In Progress, Complete, Approved)
27
+ created_at: Date spec was created (from metadata or git)
28
+ completed_at: Date spec reached Complete status (None if not complete)
29
+ current_phase: Human-readable phase description
30
+ days_in_progress: Days since creation (for in-progress specs)
31
+ path: Path to the spec.md file
32
+ """
33
+
34
+ name: str
35
+ status: SpecState
36
+ created_at: Optional[date]
37
+ completed_at: Optional[date]
38
+ current_phase: str = ""
39
+ days_in_progress: int = 0
40
+ path: Optional[Path] = None
41
+
42
+ @classmethod
43
+ def from_spec_status(
44
+ cls,
45
+ spec_status: SpecStatus,
46
+ created_at: Optional[date],
47
+ completed_at: Optional[date],
48
+ ) -> "SpecMetadata":
49
+ """Create from existing SpecStatus with added dates.
50
+
51
+ Args:
52
+ spec_status: The base SpecStatus object
53
+ created_at: Inferred creation date
54
+ completed_at: Inferred completion date
55
+
56
+ Returns:
57
+ SpecMetadata with enriched date information
58
+ """
59
+ days = 0
60
+ if created_at and not completed_at:
61
+ days = (date.today() - created_at).days
62
+
63
+ phase = cls._determine_phase(spec_status.status)
64
+
65
+ return cls(
66
+ name=spec_status.name,
67
+ status=spec_status.status,
68
+ created_at=created_at,
69
+ completed_at=completed_at,
70
+ current_phase=phase,
71
+ days_in_progress=days,
72
+ path=spec_status.path,
73
+ )
74
+
75
+ @staticmethod
76
+ def _determine_phase(status: SpecState) -> str:
77
+ """Map status to human-readable phase.
78
+
79
+ Args:
80
+ status: The SpecState enum value
81
+
82
+ Returns:
83
+ Human-readable phase name
84
+ """
85
+ phases = {
86
+ SpecState.DRAFT: "Specification",
87
+ SpecState.IN_PROGRESS: "Implementation",
88
+ SpecState.COMPLETE: "Review",
89
+ SpecState.APPROVED: "Done",
90
+ SpecState.ERROR: "Unknown",
91
+ }
92
+ return phases.get(status, "Unknown")
93
+
94
+ @property
95
+ def is_completed(self) -> bool:
96
+ """Check if spec is in a completed state."""
97
+ return self.status in (SpecState.COMPLETE, SpecState.APPROVED)
98
+
99
+ @property
100
+ def cycle_time_days(self) -> Optional[int]:
101
+ """Calculate cycle time in days if completed."""
102
+ if self.created_at and self.completed_at:
103
+ return (self.completed_at - self.created_at).days
104
+ return None
105
+
106
+
107
+ @dataclass
108
+ class CycleTimeRecord:
109
+ """Cycle time for a single completed spec.
110
+
111
+ Attributes:
112
+ feature_name: Spec name (foreign key to SpecMetadata)
113
+ days_to_complete: Total days from creation to completion
114
+ start_date: Creation date
115
+ end_date: Completion date
116
+ """
117
+
118
+ feature_name: str
119
+ days_to_complete: int
120
+ start_date: date
121
+ end_date: date
122
+
123
+ @classmethod
124
+ def from_metadata(cls, metadata: SpecMetadata) -> Optional["CycleTimeRecord"]:
125
+ """Create from SpecMetadata if spec is complete.
126
+
127
+ Args:
128
+ metadata: SpecMetadata with date information
129
+
130
+ Returns:
131
+ CycleTimeRecord if spec has both dates, None otherwise
132
+ """
133
+ if not metadata.created_at or not metadata.completed_at:
134
+ return None
135
+
136
+ days = (metadata.completed_at - metadata.created_at).days
137
+ return cls(
138
+ feature_name=metadata.name,
139
+ days_to_complete=max(days, 0), # Handle negative edge case
140
+ start_date=metadata.created_at,
141
+ end_date=metadata.completed_at,
142
+ )
143
+
144
+
145
+ @dataclass
146
+ class CycleTimeStats:
147
+ """Statistical summary of cycle times.
148
+
149
+ Attributes:
150
+ average_days: Mean cycle time
151
+ median_days: Median cycle time
152
+ min_days: Shortest cycle time
153
+ max_days: Longest cycle time
154
+ std_dev_days: Standard deviation
155
+ sample_count: Number of completed specs in calculation
156
+ """
157
+
158
+ average_days: float
159
+ median_days: float
160
+ min_days: int
161
+ max_days: int
162
+ std_dev_days: float
163
+ sample_count: int
164
+
165
+ @classmethod
166
+ def calculate(cls, records: list[CycleTimeRecord]) -> Optional["CycleTimeStats"]:
167
+ """Calculate statistics from cycle time records.
168
+
169
+ Args:
170
+ records: List of CycleTimeRecord objects
171
+
172
+ Returns:
173
+ CycleTimeStats if records provided, None if empty
174
+ """
175
+ if not records:
176
+ return None
177
+
178
+ days = [r.days_to_complete for r in records]
179
+
180
+ return cls(
181
+ average_days=round(mean(days), 1),
182
+ median_days=round(median(days), 1),
183
+ min_days=min(days),
184
+ max_days=max(days),
185
+ std_dev_days=round(stdev(days), 1) if len(days) > 1 else 0.0,
186
+ sample_count=len(days),
187
+ )
188
+
189
+
190
+ @dataclass
191
+ class VelocityDataPoint:
192
+ """Velocity data for a single week.
193
+
194
+ Attributes:
195
+ week_key: ISO week identifier (e.g., "2026-W03")
196
+ week_start: Monday of the week
197
+ specs_completed: Number of specs completed this week
198
+ spec_names: Names of specs completed this week
199
+ """
200
+
201
+ week_key: str
202
+ week_start: date
203
+ specs_completed: int
204
+ spec_names: list[str] = field(default_factory=list)
205
+
206
+ @classmethod
207
+ def from_completion(cls, completion_date: date, spec_name: str) -> "VelocityDataPoint":
208
+ """Create a velocity point from a single completion.
209
+
210
+ Args:
211
+ completion_date: Date the spec was completed
212
+ spec_name: Name of the completed spec
213
+
214
+ Returns:
215
+ VelocityDataPoint for the week of the completion
216
+ """
217
+ year, week, _ = completion_date.isocalendar()
218
+ week_key = f"{year}-W{week:02d}"
219
+
220
+ # Calculate Monday of this ISO week
221
+ monday = completion_date - timedelta(days=completion_date.weekday())
222
+
223
+ return cls(
224
+ week_key=week_key,
225
+ week_start=monday,
226
+ specs_completed=1,
227
+ spec_names=[spec_name],
228
+ )
229
+
230
+ def merge(self, other: "VelocityDataPoint") -> "VelocityDataPoint":
231
+ """Merge another data point for the same week.
232
+
233
+ Args:
234
+ other: Another VelocityDataPoint for the same week
235
+
236
+ Returns:
237
+ Merged VelocityDataPoint with combined counts
238
+
239
+ Raises:
240
+ ValueError: If weeks don't match
241
+ """
242
+ if self.week_key != other.week_key:
243
+ raise ValueError("Cannot merge different weeks")
244
+
245
+ return VelocityDataPoint(
246
+ week_key=self.week_key,
247
+ week_start=self.week_start,
248
+ specs_completed=self.specs_completed + other.specs_completed,
249
+ spec_names=self.spec_names + other.spec_names,
250
+ )
251
+
252
+
253
+ @dataclass
254
+ class AnalyticsReport:
255
+ """Complete analytics report.
256
+
257
+ Attributes:
258
+ report_id: Unique identifier (timestamp-based)
259
+ generated_at: Report generation timestamp
260
+ project_root: Project directory path
261
+ specs: All spec metadata
262
+ total_specs: Total spec count
263
+ completion_pct: Percentage of completed/approved specs
264
+ by_status: Counts grouped by status
265
+ cycle_stats: Cycle time statistics (None if no completions)
266
+ velocity: Weekly velocity data points
267
+ """
268
+
269
+ report_id: str
270
+ generated_at: datetime
271
+ project_root: Path
272
+ specs: list[SpecMetadata]
273
+ total_specs: int
274
+ completion_pct: float
275
+ by_status: dict[SpecState, int]
276
+ cycle_stats: Optional[CycleTimeStats]
277
+ velocity: list[VelocityDataPoint]
278
+
279
+ @classmethod
280
+ def generate(
281
+ cls,
282
+ specs: list[SpecMetadata],
283
+ project_root: Path,
284
+ ) -> "AnalyticsReport":
285
+ """Generate a complete analytics report.
286
+
287
+ Args:
288
+ specs: List of SpecMetadata objects
289
+ project_root: Root path of the project
290
+
291
+ Returns:
292
+ Complete AnalyticsReport with all calculated metrics
293
+ """
294
+ now = datetime.now()
295
+ report_id = now.strftime("%Y%m%d-%H%M%S")
296
+
297
+ # Calculate completion percentage
298
+ completed = sum(
299
+ 1 for s in specs if s.status in (SpecState.COMPLETE, SpecState.APPROVED)
300
+ )
301
+ pct = (completed / len(specs) * 100) if specs else 0.0
302
+
303
+ # Group by status
304
+ by_status: dict[SpecState, int] = {}
305
+ for spec in specs:
306
+ by_status[spec.status] = by_status.get(spec.status, 0) + 1
307
+
308
+ # Calculate cycle times
309
+ records = [CycleTimeRecord.from_metadata(s) for s in specs]
310
+ records = [r for r in records if r is not None]
311
+ cycle_stats = CycleTimeStats.calculate(records)
312
+
313
+ # Calculate velocity
314
+ velocity = cls._calculate_velocity(specs)
315
+
316
+ return cls(
317
+ report_id=report_id,
318
+ generated_at=now,
319
+ project_root=project_root,
320
+ specs=specs,
321
+ total_specs=len(specs),
322
+ completion_pct=round(pct, 1),
323
+ by_status=by_status,
324
+ cycle_stats=cycle_stats,
325
+ velocity=velocity,
326
+ )
327
+
328
+ @staticmethod
329
+ def _calculate_velocity(specs: list[SpecMetadata]) -> list[VelocityDataPoint]:
330
+ """Aggregate completions by week.
331
+
332
+ Args:
333
+ specs: List of SpecMetadata objects
334
+
335
+ Returns:
336
+ List of VelocityDataPoint sorted by week
337
+ """
338
+ weekly: dict[str, VelocityDataPoint] = {}
339
+
340
+ for spec in specs:
341
+ if spec.completed_at:
342
+ point = VelocityDataPoint.from_completion(spec.completed_at, spec.name)
343
+ if point.week_key in weekly:
344
+ weekly[point.week_key] = weekly[point.week_key].merge(point)
345
+ else:
346
+ weekly[point.week_key] = point
347
+
348
+ # Sort by week key (descending - most recent first)
349
+ return sorted(weekly.values(), key=lambda v: v.week_key, reverse=True)
350
+
351
+ def to_dict(self) -> dict:
352
+ """Convert report to dictionary for JSON serialization.
353
+
354
+ Returns:
355
+ Dictionary representation of the report
356
+ """
357
+ return {
358
+ "success": True,
359
+ "report_id": self.report_id,
360
+ "generated_at": self.generated_at.isoformat(),
361
+ "data": {
362
+ "total_specs": self.total_specs,
363
+ "completion_pct": self.completion_pct,
364
+ "by_status": {
365
+ state.value: count for state, count in self.by_status.items()
366
+ },
367
+ "cycle_stats": (
368
+ {
369
+ "average_days": self.cycle_stats.average_days,
370
+ "median_days": self.cycle_stats.median_days,
371
+ "min_days": self.cycle_stats.min_days,
372
+ "max_days": self.cycle_stats.max_days,
373
+ "std_dev_days": self.cycle_stats.std_dev_days,
374
+ "sample_count": self.cycle_stats.sample_count,
375
+ }
376
+ if self.cycle_stats
377
+ else None
378
+ ),
379
+ "velocity": [
380
+ {"week": v.week_key, "completed": v.specs_completed}
381
+ for v in self.velocity
382
+ ],
383
+ },
384
+ }