doit-toolkit-cli 0.1.9__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.
Files changed (134) 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/status_command.py +117 -0
  11. doit_cli/cli/sync_prompts_command.py +248 -0
  12. doit_cli/cli/validate_command.py +196 -0
  13. doit_cli/cli/verify_command.py +204 -0
  14. doit_cli/cli/workflow_mixin.py +224 -0
  15. doit_cli/cli/xref_command.py +555 -0
  16. doit_cli/formatters/__init__.py +8 -0
  17. doit_cli/formatters/base.py +38 -0
  18. doit_cli/formatters/json_formatter.py +126 -0
  19. doit_cli/formatters/markdown_formatter.py +97 -0
  20. doit_cli/formatters/rich_formatter.py +257 -0
  21. doit_cli/main.py +49 -0
  22. doit_cli/models/__init__.py +139 -0
  23. doit_cli/models/agent.py +74 -0
  24. doit_cli/models/analytics_models.py +384 -0
  25. doit_cli/models/context_config.py +464 -0
  26. doit_cli/models/crossref_models.py +182 -0
  27. doit_cli/models/diagram_models.py +363 -0
  28. doit_cli/models/fixit_models.py +355 -0
  29. doit_cli/models/hook_config.py +125 -0
  30. doit_cli/models/project.py +91 -0
  31. doit_cli/models/results.py +121 -0
  32. doit_cli/models/search_models.py +228 -0
  33. doit_cli/models/status_models.py +195 -0
  34. doit_cli/models/sync_models.py +146 -0
  35. doit_cli/models/template.py +77 -0
  36. doit_cli/models/validation_models.py +175 -0
  37. doit_cli/models/workflow_models.py +319 -0
  38. doit_cli/prompts/__init__.py +5 -0
  39. doit_cli/prompts/fixit_prompts.py +344 -0
  40. doit_cli/prompts/interactive.py +390 -0
  41. doit_cli/rules/__init__.py +5 -0
  42. doit_cli/rules/builtin_rules.py +160 -0
  43. doit_cli/services/__init__.py +79 -0
  44. doit_cli/services/agent_detector.py +168 -0
  45. doit_cli/services/analytics_service.py +218 -0
  46. doit_cli/services/architecture_generator.py +290 -0
  47. doit_cli/services/backup_service.py +204 -0
  48. doit_cli/services/config_loader.py +113 -0
  49. doit_cli/services/context_loader.py +1121 -0
  50. doit_cli/services/coverage_calculator.py +142 -0
  51. doit_cli/services/crossref_service.py +237 -0
  52. doit_cli/services/cycle_time_calculator.py +134 -0
  53. doit_cli/services/date_inferrer.py +349 -0
  54. doit_cli/services/diagram_service.py +337 -0
  55. doit_cli/services/drift_detector.py +109 -0
  56. doit_cli/services/entity_parser.py +301 -0
  57. doit_cli/services/er_diagram_generator.py +197 -0
  58. doit_cli/services/fixit_service.py +699 -0
  59. doit_cli/services/github_service.py +192 -0
  60. doit_cli/services/hook_manager.py +258 -0
  61. doit_cli/services/hook_validator.py +528 -0
  62. doit_cli/services/input_validator.py +322 -0
  63. doit_cli/services/memory_search.py +527 -0
  64. doit_cli/services/mermaid_validator.py +334 -0
  65. doit_cli/services/prompt_transformer.py +91 -0
  66. doit_cli/services/prompt_writer.py +133 -0
  67. doit_cli/services/query_interpreter.py +428 -0
  68. doit_cli/services/report_exporter.py +219 -0
  69. doit_cli/services/report_generator.py +256 -0
  70. doit_cli/services/requirement_parser.py +112 -0
  71. doit_cli/services/roadmap_summarizer.py +209 -0
  72. doit_cli/services/rule_engine.py +443 -0
  73. doit_cli/services/scaffolder.py +215 -0
  74. doit_cli/services/score_calculator.py +172 -0
  75. doit_cli/services/section_parser.py +204 -0
  76. doit_cli/services/spec_scanner.py +327 -0
  77. doit_cli/services/state_manager.py +355 -0
  78. doit_cli/services/status_reporter.py +143 -0
  79. doit_cli/services/task_parser.py +347 -0
  80. doit_cli/services/template_manager.py +710 -0
  81. doit_cli/services/template_reader.py +158 -0
  82. doit_cli/services/user_journey_generator.py +214 -0
  83. doit_cli/services/user_story_parser.py +232 -0
  84. doit_cli/services/validation_service.py +188 -0
  85. doit_cli/services/validator.py +232 -0
  86. doit_cli/services/velocity_tracker.py +173 -0
  87. doit_cli/services/workflow_engine.py +405 -0
  88. doit_cli/templates/agent-file-template.md +28 -0
  89. doit_cli/templates/checklist-template.md +39 -0
  90. doit_cli/templates/commands/doit.checkin.md +363 -0
  91. doit_cli/templates/commands/doit.constitution.md +187 -0
  92. doit_cli/templates/commands/doit.documentit.md +485 -0
  93. doit_cli/templates/commands/doit.fixit.md +181 -0
  94. doit_cli/templates/commands/doit.implementit.md +265 -0
  95. doit_cli/templates/commands/doit.planit.md +262 -0
  96. doit_cli/templates/commands/doit.reviewit.md +355 -0
  97. doit_cli/templates/commands/doit.roadmapit.md +368 -0
  98. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  99. doit_cli/templates/commands/doit.specit.md +521 -0
  100. doit_cli/templates/commands/doit.taskit.md +304 -0
  101. doit_cli/templates/commands/doit.testit.md +277 -0
  102. doit_cli/templates/config/context.yaml +134 -0
  103. doit_cli/templates/config/hooks.yaml +93 -0
  104. doit_cli/templates/config/validation-rules.yaml +64 -0
  105. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  106. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  107. doit_cli/templates/github-issue-templates/task.yml +129 -0
  108. doit_cli/templates/hooks/.gitkeep +0 -0
  109. doit_cli/templates/hooks/post-commit.sh +25 -0
  110. doit_cli/templates/hooks/post-merge.sh +75 -0
  111. doit_cli/templates/hooks/pre-commit.sh +17 -0
  112. doit_cli/templates/hooks/pre-push.sh +18 -0
  113. doit_cli/templates/memory/completed_roadmap.md +50 -0
  114. doit_cli/templates/memory/constitution.md +125 -0
  115. doit_cli/templates/memory/roadmap.md +61 -0
  116. doit_cli/templates/plan-template.md +146 -0
  117. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  118. doit_cli/templates/scripts/bash/common.sh +156 -0
  119. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  120. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  121. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  122. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  123. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  124. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  125. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  126. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  127. doit_cli/templates/spec-template.md +159 -0
  128. doit_cli/templates/tasks-template.md +313 -0
  129. doit_cli/templates/vscode-settings.json +14 -0
  130. doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
  131. doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
  132. doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
  133. doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
  134. doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,213 @@
1
+ """CLI commands for AI context management."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from ..models.context_config import ContextConfig
10
+ from ..services.context_loader import ContextLoader
11
+
12
+ console = Console()
13
+
14
+ # Create the context command group
15
+ context_app = typer.Typer(
16
+ name="context",
17
+ help="Manage AI context injection for doit commands.",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+
22
+ @context_app.command("show")
23
+ def show_context(
24
+ command: str = typer.Option(
25
+ None, "--command", "-c", help="Show context for specific command (applies overrides)"
26
+ ),
27
+ verbose: bool = typer.Option(
28
+ False, "--verbose", "-v", help="Show full content of each source"
29
+ ),
30
+ ) -> None:
31
+ """Show what context would be loaded for commands."""
32
+ project_root = Path.cwd()
33
+
34
+ # Load configuration
35
+ config = ContextConfig.load_from_project(project_root)
36
+
37
+ console.print("[bold]AI Context Status[/bold]")
38
+ console.print("=" * 50)
39
+
40
+ # Show global status
41
+ if not config.enabled:
42
+ console.print("\n[yellow]Context loading is disabled globally.[/yellow]")
43
+ console.print("Enable in .doit/config/context.yaml by setting 'enabled: true'")
44
+ return
45
+
46
+ console.print(f"\n[bold]Global Settings:[/bold]")
47
+ console.print(f" Enabled: [green]yes[/green]")
48
+ console.print(f" Max tokens per source: {config.max_tokens_per_source:,}")
49
+ console.print(f" Total max tokens: {config.total_max_tokens:,}")
50
+
51
+ # Show summarization settings
52
+ if config.summarization.enabled:
53
+ soft_threshold = int(config.total_max_tokens * (config.summarization.threshold_percentage / 100.0))
54
+ console.print(f"\n[bold]Summarization:[/bold]")
55
+ console.print(f" Enabled: [green]yes[/green]")
56
+ console.print(f" Soft threshold: {soft_threshold:,} tokens ({config.summarization.threshold_percentage}%)")
57
+ console.print(f" Fallback to truncation: {'yes' if config.summarization.fallback_to_truncation else 'no'}")
58
+
59
+ # Load context
60
+ loader = ContextLoader(
61
+ project_root=project_root,
62
+ config=config,
63
+ command=command,
64
+ )
65
+
66
+ try:
67
+ context = loader.load()
68
+ except Exception as e:
69
+ console.print(f"\n[red]Error loading context: {e}[/red]")
70
+ raise typer.Exit(1)
71
+
72
+ # Show command-specific info
73
+ if command:
74
+ console.print(f"\n[bold]Context for command:[/bold] {command}")
75
+ if command in config.commands:
76
+ console.print(" [dim]Command-specific overrides applied[/dim]")
77
+
78
+ # Create source table
79
+ console.print(f"\n[bold]Loaded Sources:[/bold]")
80
+
81
+ if not context.sources:
82
+ console.print(" [dim]No sources loaded[/dim]")
83
+ console.print("\n[dim]Check that .doit/memory/ contains constitution.md or roadmap.md[/dim]")
84
+ return
85
+
86
+ table = Table(show_header=True, header_style="bold")
87
+ table.add_column("Source", style="cyan")
88
+ table.add_column("Path", style="dim")
89
+ table.add_column("Tokens", justify="right")
90
+ table.add_column("Status")
91
+
92
+ for source in context.sources:
93
+ # Truncate path for display
94
+ path_str = str(source.path)
95
+ if len(path_str) > 40:
96
+ path_str = "..." + path_str[-37:]
97
+
98
+ # Status indicator
99
+ if source.truncated:
100
+ status = f"[yellow]truncated ({source.original_tokens:,} -> {source.token_count:,})[/yellow]"
101
+ elif source.source_type == "roadmap" and config.summarization.enabled:
102
+ status = "[cyan]summarized[/cyan]"
103
+ elif source.source_type == "completed_roadmap":
104
+ status = "[cyan]formatted[/cyan]"
105
+ else:
106
+ status = "[green]complete[/green]"
107
+
108
+ table.add_row(
109
+ source.source_type,
110
+ path_str,
111
+ f"{source.token_count:,}",
112
+ status,
113
+ )
114
+
115
+ console.print(table)
116
+
117
+ # Summary
118
+ console.print(f"\n[bold]Summary:[/bold]")
119
+ console.print(f" Total sources: {len(context.sources)}")
120
+ console.print(f" Total tokens: {context.total_tokens:,}")
121
+
122
+ if context.any_truncated:
123
+ console.print(" [yellow]Some sources were truncated to fit token limits[/yellow]")
124
+
125
+ # Check for guidance prompt (indicates context was condensed)
126
+ if hasattr(context, '_guidance_prompt') and context._guidance_prompt:
127
+ console.print(" [cyan]AI guidance prompt included (context condensed)[/cyan]")
128
+
129
+ # Show verbose content
130
+ if verbose:
131
+ console.print(f"\n[bold]Source Content Preview:[/bold]")
132
+ for source in context.sources:
133
+ console.print(f"\n[cyan]--- {source.source_type} ---[/cyan]")
134
+ # Show first 500 chars
135
+ preview = source.content[:500]
136
+ if len(source.content) > 500:
137
+ preview += "\n... [truncated for display]"
138
+ console.print(preview)
139
+
140
+
141
+ @context_app.command("status")
142
+ def context_status() -> None:
143
+ """Show context configuration and file availability."""
144
+ project_root = Path.cwd()
145
+
146
+ console.print("[bold]Context Configuration Status[/bold]")
147
+ console.print("=" * 50)
148
+
149
+ # Check config file
150
+ config_path = project_root / ".doit" / "config" / "context.yaml"
151
+ console.print(f"\n[bold]Configuration:[/bold]")
152
+
153
+ if config_path.exists():
154
+ console.print(f" [green]\u2713[/green] Config file: {config_path}")
155
+ config = ContextConfig.load_from_project(project_root)
156
+ else:
157
+ console.print(f" [dim]\u2717[/dim] Config file: {config_path} [dim](using defaults)[/dim]")
158
+ config = ContextConfig.load_default()
159
+
160
+ # Show source availability
161
+ console.print(f"\n[bold]Source Files:[/bold]")
162
+
163
+ memory_dir = project_root / ".doit" / "memory"
164
+ specs_dir = project_root / "specs"
165
+
166
+ sources = [
167
+ ("constitution", memory_dir / "constitution.md"),
168
+ ("roadmap", memory_dir / "roadmap.md"),
169
+ ("completed_roadmap", memory_dir / "completed_roadmap.md"),
170
+ ]
171
+
172
+ for name, path in sources:
173
+ source_config = config.get_source_config(name)
174
+ enabled = source_config.enabled if source_config else True
175
+
176
+ if path.exists():
177
+ if enabled:
178
+ console.print(f" [green]\u2713[/green] {name}: {path}")
179
+ else:
180
+ console.print(f" [yellow]\u2713[/yellow] {name}: {path} [dim](disabled)[/dim]")
181
+ else:
182
+ console.print(f" [dim]\u2717[/dim] {name}: {path} [dim](not found)[/dim]")
183
+
184
+ # Check specs directory
185
+ if specs_dir.exists():
186
+ spec_count = len(list(specs_dir.glob("*/")))
187
+ console.print(f" [green]\u2713[/green] specs directory: {spec_count} feature(s)")
188
+ else:
189
+ console.print(f" [dim]\u2717[/dim] specs directory: [dim](not found)[/dim]")
190
+
191
+ # Show current branch info
192
+ console.print(f"\n[bold]Git Branch:[/bold]")
193
+ loader = ContextLoader(project_root=project_root, config=config)
194
+ branch = loader.get_current_branch()
195
+
196
+ if branch:
197
+ console.print(f" Current: {branch}")
198
+ feature = loader.extract_feature_name(branch)
199
+ if feature:
200
+ console.print(f" Feature: {feature}")
201
+ spec_path = specs_dir / feature
202
+ if spec_path.exists():
203
+ console.print(f" [green]\u2713[/green] Current spec: {spec_path}")
204
+ else:
205
+ console.print(f" [dim]\u2717[/dim] Current spec: [dim](no matching spec)[/dim]")
206
+ else:
207
+ console.print(f" [dim]Not a feature branch (pattern: NNN-feature-name)[/dim]")
208
+ else:
209
+ console.print(f" [dim]Not in a git repository or git not available[/dim]")
210
+
211
+ # Usage hints
212
+ console.print(f"\n[dim]Use 'doit context show' to see loaded context[/dim]")
213
+ console.print(f"[dim]Use 'doit context show --command specit' to see command-specific context[/dim]")
@@ -0,0 +1,304 @@
1
+ """CLI commands for diagram generation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from ..models.diagram_models import DiagramType
11
+ from ..services.diagram_service import DiagramService
12
+
13
+ # Create Typer app for diagram subcommands
14
+ app = typer.Typer(
15
+ name="diagram",
16
+ help="Generate and validate Mermaid diagrams from specifications",
17
+ add_completion=False,
18
+ )
19
+
20
+ console = Console()
21
+
22
+
23
+ def _resolve_file_path(file: Optional[Path]) -> Optional[Path]:
24
+ """Resolve file path, auto-detecting if not provided.
25
+
26
+ Args:
27
+ file: Optional path to spec/plan file
28
+
29
+ Returns:
30
+ Resolved path or None if not found
31
+ """
32
+ if file:
33
+ return file.resolve()
34
+
35
+ # Try to auto-detect spec.md in current directory or specs/
36
+ cwd = Path.cwd()
37
+
38
+ # Check current directory
39
+ if (cwd / "spec.md").exists():
40
+ return cwd / "spec.md"
41
+
42
+ # Check for specs/ directory with current feature
43
+ specs_dir = cwd / "specs"
44
+ if specs_dir.exists():
45
+ # Look for most recently modified spec.md
46
+ spec_files = list(specs_dir.glob("*/spec.md"))
47
+ if spec_files:
48
+ return max(spec_files, key=lambda p: p.stat().st_mtime)
49
+
50
+ return None
51
+
52
+
53
+ def _parse_diagram_types(type_str: str) -> list[DiagramType]:
54
+ """Parse diagram type string to list of DiagramType.
55
+
56
+ Args:
57
+ type_str: Type string (user-journey, er-diagram, architecture, all)
58
+
59
+ Returns:
60
+ List of DiagramType values
61
+ """
62
+ if type_str == "all":
63
+ return [DiagramType.USER_JOURNEY, DiagramType.ER_DIAGRAM]
64
+
65
+ try:
66
+ return [DiagramType.from_string(type_str)]
67
+ except ValueError:
68
+ return []
69
+
70
+
71
+ @app.command(name="generate")
72
+ def generate_command(
73
+ file: Optional[Path] = typer.Argument(
74
+ None,
75
+ help="Path to spec.md or plan.md file",
76
+ exists=False,
77
+ ),
78
+ strict: bool = typer.Option(
79
+ False,
80
+ "--strict",
81
+ "-s",
82
+ help="Fail on validation errors",
83
+ ),
84
+ no_insert: bool = typer.Option(
85
+ False,
86
+ "--no-insert",
87
+ help="Output diagrams without inserting into file",
88
+ ),
89
+ diagram_type: str = typer.Option(
90
+ "all",
91
+ "--type",
92
+ "-t",
93
+ help="Diagram type: user-journey, er-diagram, architecture, all",
94
+ ),
95
+ output: Optional[Path] = typer.Option(
96
+ None,
97
+ "--output",
98
+ "-o",
99
+ help="Write diagrams to separate file",
100
+ ),
101
+ ) -> None:
102
+ """Generate Mermaid diagrams for a specification file.
103
+
104
+ If FILE is not provided, auto-detects spec.md in current directory
105
+ or finds the most recently modified spec in specs/.
106
+
107
+ Examples:
108
+ doit diagram generate
109
+ doit diagram generate specs/035-feature/spec.md
110
+ doit diagram generate --type er-diagram --strict
111
+ doit diagram generate --no-insert --output diagrams.md
112
+ """
113
+ # Resolve file path
114
+ resolved_path = _resolve_file_path(file)
115
+ if not resolved_path:
116
+ console.print("[red]Error:[/red] No spec file found. Provide a path or run from a spec directory.")
117
+ raise typer.Exit(code=1)
118
+
119
+ if not resolved_path.exists():
120
+ console.print(f"[red]Error:[/red] File not found: {resolved_path}")
121
+ raise typer.Exit(code=1)
122
+
123
+ # Parse diagram types
124
+ types = _parse_diagram_types(diagram_type)
125
+ if not types and diagram_type != "all":
126
+ console.print(f"[red]Error:[/red] Unknown diagram type: {diagram_type}")
127
+ console.print("Valid types: user-journey, er-diagram, architecture, all")
128
+ raise typer.Exit(code=1)
129
+
130
+ console.print(f"Generating diagrams for: [cyan]{resolved_path}[/cyan]")
131
+ console.print()
132
+
133
+ # Create service and generate
134
+ service = DiagramService(strict=strict, backup=True)
135
+ result = service.generate(
136
+ file_path=resolved_path,
137
+ diagram_types=types if types else None,
138
+ insert=not no_insert,
139
+ )
140
+
141
+ # Handle errors
142
+ if not result.success:
143
+ console.print(f"[red]Error:[/red] {result.error}")
144
+ raise typer.Exit(code=2 if strict else 1)
145
+
146
+ # Display results table
147
+ if result.diagrams:
148
+ table = Table(show_header=True, header_style="bold")
149
+ table.add_column("Diagram Type")
150
+ table.add_column("Status")
151
+ table.add_column("Nodes")
152
+
153
+ for diagram in result.diagrams:
154
+ status = "[green]✅ Generated[/green]" if diagram.is_valid else "[yellow]⚠️ Generated (warnings)[/yellow]"
155
+ table.add_row(
156
+ diagram.diagram_type.value,
157
+ status,
158
+ str(diagram.node_count),
159
+ )
160
+
161
+ console.print(table)
162
+ console.print()
163
+
164
+ # Show validation warnings
165
+ for diagram in result.diagrams:
166
+ if diagram.validation and diagram.validation.warnings:
167
+ console.print(f"[yellow]Warnings for {diagram.diagram_type.value}:[/yellow]")
168
+ for warning in diagram.validation.warnings:
169
+ console.print(f" • {warning}")
170
+ console.print()
171
+
172
+ # Output to file if requested
173
+ if output:
174
+ output_content = []
175
+ for diagram in result.diagrams:
176
+ output_content.append(f"## {diagram.diagram_type.value}\n")
177
+ output_content.append(diagram.wrapped_content)
178
+ output_content.append("\n")
179
+
180
+ output.write_text("\n".join(output_content), encoding="utf-8")
181
+ console.print(f"[green]✅[/green] Diagrams written to: {output}")
182
+ elif not no_insert and result.sections_updated:
183
+ console.print(f"[green]✅[/green] Diagrams inserted into {resolved_path.name}")
184
+ console.print(f" Sections updated: {', '.join(result.sections_updated)}")
185
+ elif no_insert:
186
+ # Print diagram content to stdout
187
+ for diagram in result.diagrams:
188
+ console.print(f"\n[bold]## {diagram.diagram_type.value}[/bold]\n")
189
+ console.print(diagram.wrapped_content)
190
+ else:
191
+ console.print("[yellow]No diagrams generated.[/yellow] Check that the spec has User Stories or Key Entities.")
192
+ raise typer.Exit(code=3)
193
+
194
+
195
+ @app.command(name="validate")
196
+ def validate_command(
197
+ file: Optional[Path] = typer.Argument(
198
+ None,
199
+ help="Path to file containing Mermaid diagrams",
200
+ exists=False,
201
+ ),
202
+ strict: bool = typer.Option(
203
+ False,
204
+ "--strict",
205
+ "-s",
206
+ help="Use stricter validation rules",
207
+ ),
208
+ ) -> None:
209
+ """Validate Mermaid diagrams in a file.
210
+
211
+ Checks all Mermaid code blocks in the file for syntax errors.
212
+
213
+ Examples:
214
+ doit diagram validate spec.md
215
+ doit diagram validate --strict
216
+ """
217
+ import re
218
+
219
+ # Resolve file path
220
+ resolved_path = _resolve_file_path(file)
221
+ if not resolved_path:
222
+ console.print("[red]Error:[/red] No file found to validate.")
223
+ raise typer.Exit(code=2)
224
+
225
+ if not resolved_path.exists():
226
+ console.print(f"[red]Error:[/red] File not found: {resolved_path}")
227
+ raise typer.Exit(code=2)
228
+
229
+ console.print(f"Validating diagrams in: [cyan]{resolved_path}[/cyan]")
230
+ console.print()
231
+
232
+ content = resolved_path.read_text(encoding="utf-8")
233
+
234
+ # Find all mermaid code blocks
235
+ mermaid_pattern = re.compile(r"```mermaid\s*\n(.*?)\n```", re.DOTALL)
236
+ matches = list(mermaid_pattern.finditer(content))
237
+
238
+ if not matches:
239
+ console.print("[yellow]No Mermaid diagrams found in file.[/yellow]")
240
+ raise typer.Exit(code=0)
241
+
242
+ from ..services.mermaid_validator import MermaidValidator
243
+
244
+ validator = MermaidValidator()
245
+ has_errors = False
246
+
247
+ table = Table(show_header=True, header_style="bold")
248
+ table.add_column("Diagram #")
249
+ table.add_column("Type")
250
+ table.add_column("Status")
251
+ table.add_column("Issues")
252
+
253
+ for i, match in enumerate(matches, start=1):
254
+ diagram_content = match.group(1)
255
+ result = validator.validate(diagram_content)
256
+
257
+ # Determine type
258
+ if "erDiagram" in diagram_content.lower():
259
+ diagram_type = "ER Diagram"
260
+ elif "flowchart" in diagram_content.lower():
261
+ diagram_type = "Flowchart"
262
+ else:
263
+ diagram_type = "Unknown"
264
+
265
+ if result.passed and not result.warnings:
266
+ status = "[green]✅ Valid[/green]"
267
+ issues = "None"
268
+ elif result.passed:
269
+ status = "[yellow]⚠️ Warning[/yellow]"
270
+ issues = f"{result.warning_count} warnings"
271
+ else:
272
+ status = "[red]❌ Invalid[/red]"
273
+ issues = f"{result.error_count} errors"
274
+ has_errors = True
275
+
276
+ table.add_row(str(i), diagram_type, status, issues)
277
+
278
+ console.print(table)
279
+
280
+ # Show detailed errors/warnings
281
+ for i, match in enumerate(matches, start=1):
282
+ diagram_content = match.group(1)
283
+ result = validator.validate(diagram_content)
284
+
285
+ if result.errors:
286
+ console.print(f"\n[red]Errors in diagram {i}:[/red]")
287
+ for error in result.errors:
288
+ console.print(f" • {error}")
289
+
290
+ if result.warnings:
291
+ console.print(f"\n[yellow]Warnings in diagram {i}:[/yellow]")
292
+ for warning in result.warnings:
293
+ console.print(f" • {warning}")
294
+
295
+ if has_errors:
296
+ console.print("\n[red]Validation failed.[/red]")
297
+ raise typer.Exit(code=1)
298
+ else:
299
+ console.print("\n[green]All diagrams valid.[/green]")
300
+ raise typer.Exit(code=0)
301
+
302
+
303
+ # Export the app for registration
304
+ diagram_app = app