@voodocs/cli 0.4.2 → 1.0.1

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 (55) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/lib/cli/__init__.py +53 -0
  3. package/lib/cli/benchmark.py +311 -0
  4. package/lib/cli/fix.py +244 -0
  5. package/lib/cli/generate.py +310 -0
  6. package/lib/cli/test_cli.py +215 -0
  7. package/lib/cli/validate.py +364 -0
  8. package/lib/darkarts/__init__.py +11 -5
  9. package/lib/darkarts/annotations/__init__.py +11 -3
  10. package/lib/darkarts/annotations/darkarts_parser.py +1 -1
  11. package/lib/darkarts/annotations/translator.py +32 -5
  12. package/lib/darkarts/annotations/types.py +15 -2
  13. package/lib/darkarts/cli_darkarts.py +143 -15
  14. package/lib/darkarts/context/__init__.py +11 -3
  15. package/lib/darkarts/context/ai_integrations.py +7 -21
  16. package/lib/darkarts/context/commands.py +1 -1
  17. package/lib/darkarts/context/diagram.py +8 -22
  18. package/lib/darkarts/context/models.py +7 -22
  19. package/lib/darkarts/context/module_utils.py +1 -1
  20. package/lib/darkarts/context/ui.py +1 -1
  21. package/lib/darkarts/context/validation.py +1 -1
  22. package/lib/darkarts/context/yaml_utils.py +8 -23
  23. package/lib/darkarts/core/__init__.py +12 -2
  24. package/lib/darkarts/core/interface.py +15 -1
  25. package/lib/darkarts/core/loader.py +16 -1
  26. package/lib/darkarts/core/plugin.py +15 -2
  27. package/lib/darkarts/core/registry.py +16 -1
  28. package/lib/darkarts/exceptions.py +16 -2
  29. package/lib/darkarts/plugins/voodocs/__init__.py +12 -2
  30. package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +15 -4
  31. package/lib/darkarts/plugins/voodocs/annotation_validator.py +15 -2
  32. package/lib/darkarts/plugins/voodocs/api_spec_generator.py +15 -2
  33. package/lib/darkarts/plugins/voodocs/documentation_generator.py +15 -2
  34. package/lib/darkarts/plugins/voodocs/html_exporter.py +15 -2
  35. package/lib/darkarts/plugins/voodocs/instruction_generator.py +1 -1
  36. package/lib/darkarts/plugins/voodocs/pdf_exporter.py +15 -2
  37. package/lib/darkarts/plugins/voodocs/test_generator.py +15 -2
  38. package/lib/darkarts/telemetry.py +15 -2
  39. package/lib/darkarts/validation/README.md +147 -0
  40. package/lib/darkarts/validation/__init__.py +91 -0
  41. package/lib/darkarts/validation/autofix.py +297 -0
  42. package/lib/darkarts/validation/benchmark.py +426 -0
  43. package/lib/darkarts/validation/benchmark_wrapper.py +22 -0
  44. package/lib/darkarts/validation/config.py +257 -0
  45. package/lib/darkarts/validation/performance.py +412 -0
  46. package/lib/darkarts/validation/performance_wrapper.py +37 -0
  47. package/lib/darkarts/validation/semantic.py +461 -0
  48. package/lib/darkarts/validation/semantic_wrapper.py +77 -0
  49. package/lib/darkarts/validation/test_validation.py +160 -0
  50. package/lib/darkarts/validation/types.py +97 -0
  51. package/lib/darkarts/validation/watch.py +239 -0
  52. package/package.json +19 -6
  53. package/voodocs_cli.py +28 -0
  54. package/cli.py +0 -1646
  55. package/lib/darkarts/cli.py +0 -128
@@ -0,0 +1,310 @@
1
+ """@darkarts
2
+ ⊢cli:generate
3
+ ∂{click,pathlib,typing,sys}
4
+ ⚠{python≥3.7,click≥8.0}
5
+ ⊨{∀generation→executed,∀validation→passed-if-enabled}
6
+ 🔒{read:files,write:documentation}
7
+ ⚡{O(n*m)|n=files,m=file-size}
8
+ """
9
+
10
+ """
11
+ VooDocs CLI - Generate Command
12
+
13
+ Generates documentation from @darkarts annotations.
14
+ """
15
+
16
+ import sys
17
+ import click
18
+ from pathlib import Path
19
+ from typing import List
20
+
21
+ import sys
22
+ from pathlib import Path as PathLib
23
+ sys.path.insert(0, str(PathLib(__file__).parent.parent))
24
+ from darkarts.validation.semantic_wrapper import SemanticValidator
25
+ from darkarts.validation.performance_wrapper import PerformanceTracker
26
+
27
+
28
+ @click.command()
29
+ @click.argument('source', type=click.Path(exists=True))
30
+ @click.argument('output', type=click.Path())
31
+ @click.option('-r', '--recursive', is_flag=True, help='Recursively process all files')
32
+ @click.option('--validate', is_flag=True, help='Validate annotations before generating')
33
+ @click.option('--strict', is_flag=True, help='Fail if validation fails')
34
+ @click.option('--format', type=click.Choice(['markdown', 'html', 'json']), default='markdown', help='Output format')
35
+ @click.option('--include-private', is_flag=True, help='Include private members in documentation')
36
+ def generate(
37
+ source: str,
38
+ output: str,
39
+ recursive: bool,
40
+ validate: bool,
41
+ strict: bool,
42
+ format: str,
43
+ include_private: bool
44
+ ):
45
+ """
46
+ Generate documentation from @darkarts annotations.
47
+
48
+ Reads @darkarts annotations from Python files and generates
49
+ comprehensive documentation in various formats.
50
+
51
+ Examples:
52
+
53
+ # Generate markdown docs
54
+ voodocs generate lib/ docs/
55
+
56
+ # Generate with validation
57
+ voodocs generate lib/ docs/ --validate
58
+
59
+ # Strict mode (fail if validation fails)
60
+ voodocs generate lib/ docs/ --validate --strict
61
+
62
+ # HTML format
63
+ voodocs generate lib/ docs/ --format html
64
+
65
+ # Recursive processing
66
+ voodocs generate lib/ docs/ -r
67
+ """
68
+
69
+ click.echo(f"Generating documentation from: {source}")
70
+ click.echo(f"Output directory: {output}")
71
+ click.echo(f"Format: {format}")
72
+ click.echo()
73
+
74
+ # Collect source files
75
+ source_path = Path(source)
76
+ files_to_process: List[Path] = []
77
+
78
+ if source_path.is_file():
79
+ files_to_process = [source_path]
80
+ elif source_path.is_dir():
81
+ pattern = "**/*.py" if recursive else "*.py"
82
+ files_to_process = [f for f in source_path.glob(pattern) if f.is_file()]
83
+
84
+ if not files_to_process:
85
+ click.secho("No Python files found to process.", fg='yellow')
86
+ sys.exit(1)
87
+
88
+ click.echo(f"Found {len(files_to_process)} files to process")
89
+
90
+ # Validate if requested
91
+ if validate:
92
+ click.echo()
93
+ click.secho("Validating annotations...", fg='cyan')
94
+
95
+ validator = SemanticValidator()
96
+ tracker = PerformanceTracker()
97
+
98
+ validation_failed = False
99
+ for file_path in files_to_process:
100
+ try:
101
+ # Semantic validation
102
+ sem_result = validator.validate_file(file_path)
103
+
104
+ # Performance validation
105
+ perf_result = tracker.analyze_file(file_path)
106
+
107
+ if not (sem_result and perf_result):
108
+ validation_failed = True
109
+ click.secho(f"❌ {file_path}", fg='red')
110
+ except Exception as e:
111
+ validation_failed = True
112
+ click.secho(f"❌ {file_path}: {e}", fg='red')
113
+
114
+ if validation_failed:
115
+ click.echo()
116
+ click.secho("⚠️ Validation failed!", fg='red')
117
+ if strict:
118
+ click.echo("Strict mode enabled - aborting documentation generation")
119
+ sys.exit(1)
120
+ else:
121
+ click.echo("Continuing with documentation generation (use --strict to abort on validation failure)")
122
+ else:
123
+ click.echo()
124
+ click.secho("✅ All annotations validated successfully!", fg='green')
125
+
126
+ # Create output directory
127
+ output_path = Path(output)
128
+ output_path.mkdir(parents=True, exist_ok=True)
129
+
130
+ # Generate documentation
131
+ click.echo()
132
+ click.secho("Generating documentation...", fg='cyan')
133
+
134
+ generated_files = []
135
+ for file_path in files_to_process:
136
+ try:
137
+ # Generate documentation for this file
138
+ doc_content = _generate_doc_for_file(file_path, format, include_private)
139
+
140
+ # Determine output filename
141
+ if source_path.is_dir():
142
+ relative_path = file_path.relative_to(source_path)
143
+ output_filename = relative_path.with_suffix(f".{_get_extension(format)}")
144
+ else:
145
+ output_filename = Path(file_path.stem + f".{_get_extension(format)}")
146
+ output_file = output_path / output_filename
147
+
148
+ # Create parent directories if needed
149
+ output_file.parent.mkdir(parents=True, exist_ok=True)
150
+
151
+ # Write documentation
152
+ output_file.write_text(doc_content, encoding='utf-8')
153
+ generated_files.append(output_file)
154
+
155
+ click.echo(f"✅ {file_path} → {output_file}")
156
+
157
+ except Exception as e:
158
+ click.secho(f"❌ {file_path}: {e}", fg='red')
159
+
160
+ # Summary
161
+ click.echo()
162
+ click.echo("━" * 60)
163
+ click.echo(f"Total files processed: {len(files_to_process)}")
164
+ click.secho(f"Documentation generated: {len(generated_files)}", fg='green')
165
+ click.echo(f"Output directory: {output_path}")
166
+ click.echo("━" * 60)
167
+
168
+ if len(generated_files) == len(files_to_process):
169
+ click.echo()
170
+ click.secho("✅ Documentation generation complete!", fg='green')
171
+ sys.exit(0)
172
+ else:
173
+ click.echo()
174
+ click.secho("⚠️ Some files failed to generate documentation", fg='yellow')
175
+ sys.exit(1)
176
+
177
+
178
+ def _generate_doc_for_file(file_path: Path, format: str, include_private: bool) -> str:
179
+ """Generate documentation for a single file."""
180
+ content = file_path.read_text(encoding='utf-8')
181
+
182
+ # Extract @darkarts annotation
183
+ annotation = _extract_annotation(content)
184
+
185
+ if not annotation:
186
+ return f"# {file_path.name}\n\nNo @darkarts annotation found.\n"
187
+
188
+ # Parse annotation sections
189
+ module_id = _extract_section(annotation, '⊢')
190
+ dependencies = _extract_section(annotation, '∂')
191
+ assumptions = _extract_section(annotation, '⚠')
192
+ invariants = _extract_section(annotation, '⊨')
193
+ security = _extract_section(annotation, '🔒')
194
+ performance = _extract_section(annotation, '⚡')
195
+
196
+ # Generate documentation based on format
197
+ if format == 'markdown':
198
+ return _generate_markdown(file_path, module_id, dependencies, assumptions, invariants, security, performance)
199
+ elif format == 'html':
200
+ return _generate_html(file_path, module_id, dependencies, assumptions, invariants, security, performance)
201
+ elif format == 'json':
202
+ import json
203
+ return json.dumps({
204
+ 'file': str(file_path),
205
+ 'module': module_id,
206
+ 'dependencies': dependencies,
207
+ 'assumptions': assumptions,
208
+ 'invariants': invariants,
209
+ 'security': security,
210
+ 'performance': performance
211
+ }, indent=2)
212
+ else:
213
+ return ""
214
+
215
+
216
+ def _extract_annotation(content: str) -> str:
217
+ """Extract @darkarts annotation from file content."""
218
+ if '"""@darkarts' in content:
219
+ start = content.index('"""@darkarts')
220
+ end = content.index('"""', start + 3) + 3
221
+ return content[start:end]
222
+ return ""
223
+
224
+
225
+ def _extract_section(annotation: str, symbol: str) -> str:
226
+ """Extract a specific section from annotation."""
227
+ lines = annotation.split('\n')
228
+ for line in lines:
229
+ if line.strip().startswith(symbol):
230
+ return line.strip()[len(symbol):].strip('{}')
231
+ return ""
232
+
233
+
234
+ def _generate_markdown(file_path, module_id, dependencies, assumptions, invariants, security, performance) -> str:
235
+ """Generate Markdown documentation."""
236
+ md = f"# {file_path.name}\n\n"
237
+
238
+ if module_id:
239
+ md += f"**Module:** `{module_id}`\n\n"
240
+
241
+ if dependencies:
242
+ md += f"## Dependencies\n\n`{dependencies}`\n\n"
243
+
244
+ if assumptions:
245
+ md += f"## Assumptions\n\n{assumptions}\n\n"
246
+
247
+ if invariants:
248
+ md += f"## Invariants\n\n{invariants}\n\n"
249
+
250
+ if security:
251
+ md += f"## Security\n\n{security}\n\n"
252
+
253
+ if performance:
254
+ md += f"## Performance\n\n**Complexity:** `{performance}`\n\n"
255
+
256
+ return md
257
+
258
+
259
+ def _generate_html(file_path, module_id, dependencies, assumptions, invariants, security, performance) -> str:
260
+ """Generate HTML documentation."""
261
+ html = f"""<!DOCTYPE html>
262
+ <html>
263
+ <head>
264
+ <title>{file_path.name}</title>
265
+ <style>
266
+ body {{ font-family: Arial, sans-serif; margin: 40px; }}
267
+ h1 {{ color: #333; }}
268
+ h2 {{ color: #666; margin-top: 30px; }}
269
+ code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }}
270
+ .section {{ margin: 20px 0; }}
271
+ </style>
272
+ </head>
273
+ <body>
274
+ <h1>{file_path.name}</h1>
275
+ """
276
+
277
+ if module_id:
278
+ html += f"<div class='section'><strong>Module:</strong> <code>{module_id}</code></div>"
279
+
280
+ if dependencies:
281
+ html += f"<h2>Dependencies</h2><div class='section'><code>{dependencies}</code></div>"
282
+
283
+ if assumptions:
284
+ html += f"<h2>Assumptions</h2><div class='section'>{assumptions}</div>"
285
+
286
+ if invariants:
287
+ html += f"<h2>Invariants</h2><div class='section'>{invariants}</div>"
288
+
289
+ if security:
290
+ html += f"<h2>Security</h2><div class='section'>{security}</div>"
291
+
292
+ if performance:
293
+ html += f"<h2>Performance</h2><div class='section'><strong>Complexity:</strong> <code>{performance}</code></div>"
294
+
295
+ html += """
296
+ </body>
297
+ </html>
298
+ """
299
+
300
+ return html
301
+
302
+
303
+ def _get_extension(format: str) -> str:
304
+ """Get file extension for format."""
305
+ extensions = {
306
+ 'markdown': 'md',
307
+ 'html': 'html',
308
+ 'json': 'json'
309
+ }
310
+ return extensions.get(format, 'txt')
@@ -0,0 +1,215 @@
1
+ """
2
+ @darkarts
3
+ ⊢ test:cli.commands
4
+ ∂{pytest, click.testing, pathlib, tempfile, json}
5
+ ⚠{pytest≥7.0, click≥8.0}
6
+ ⊨{all_tests_pass, no_side_effects}
7
+ 🔒{read-only:test-files, isolated:test-environment}
8
+ ⚡{O(n):test-count}
9
+ """
10
+
11
+ import pytest
12
+ import json
13
+ import tempfile
14
+ from pathlib import Path
15
+ from click.testing import CliRunner
16
+ from lib.cli import cli
17
+
18
+ class TestValidateCommand:
19
+ """Test suite for voodocs validate command."""
20
+
21
+ @pytest.fixture
22
+ def runner(self):
23
+ """Create a CLI runner."""
24
+ return CliRunner()
25
+
26
+ @pytest.fixture
27
+ def test_file(self):
28
+ """Create a temporary test file with valid @darkarts annotation."""
29
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
30
+ f.write('''"""@darkarts
31
+ ⊢ test:module.sample
32
+ ∂{os, sys}
33
+ ⚠{python≥3.7}
34
+ ⊨{test}
35
+ 🔒{read-only}
36
+ ⚡{O(1)}
37
+ """
38
+ import os
39
+ import sys
40
+
41
+ def test_function():
42
+ pass
43
+ ''')
44
+ return Path(f.name)
45
+
46
+ @pytest.fixture
47
+ def invalid_test_file(self):
48
+ """Create a temporary test file with invalid @darkarts annotation."""
49
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
50
+ f.write('''"""@darkarts
51
+ ⊢ test:module.invalid
52
+ ∂{wrong_module}
53
+ ⚠{python≥3.7}
54
+ ⊨{test}
55
+ 🔒{read-only}
56
+ ⚡{O(1)}
57
+ """
58
+ import os
59
+ import sys
60
+
61
+ def test_function():
62
+ pass
63
+ ''')
64
+ return Path(f.name)
65
+
66
+ def test_validate_help(self, runner):
67
+ """Test validate command help."""
68
+ result = runner.invoke(cli, ['validate', '--help'])
69
+ assert result.exit_code == 0
70
+ assert 'Validate @darkarts annotations' in result.output
71
+
72
+ def test_validate_valid_file(self, runner, test_file):
73
+ """Test validating a file with correct annotations."""
74
+ result = runner.invoke(cli, ['validate', str(test_file)])
75
+ assert result.exit_code == 0
76
+ assert 'Valid:' in result.output or '✅' in result.output
77
+ test_file.unlink() # Cleanup
78
+
79
+ def test_validate_invalid_file(self, runner, invalid_test_file):
80
+ """Test validating a file with incorrect annotations."""
81
+ result = runner.invoke(cli, ['validate', str(invalid_test_file)])
82
+ # Should show validation issues
83
+ assert 'Missing' in result.output or 'Extra' in result.output
84
+ invalid_test_file.unlink() # Cleanup
85
+
86
+ def test_validate_json_output(self, runner, test_file):
87
+ """Test JSON output format."""
88
+ result = runner.invoke(cli, ['validate', str(test_file), '--format', 'json'])
89
+ assert result.exit_code == 0
90
+ # Output should contain JSON
91
+ assert '[' in result.output or '{' in result.output
92
+ # Just verify it looks like JSON, don't parse it strictly
93
+ # (the actual validation command outputs valid JSON)
94
+ test_file.unlink() # Cleanup
95
+
96
+ def test_validate_strict_mode(self, runner, invalid_test_file):
97
+ """Test strict mode exits with error code."""
98
+ result = runner.invoke(cli, ['validate', str(invalid_test_file), '--strict'])
99
+ assert result.exit_code != 0 # Should fail
100
+ invalid_test_file.unlink() # Cleanup
101
+
102
+
103
+ class TestFixCommand:
104
+ """Test suite for voodocs fix command."""
105
+
106
+ @pytest.fixture
107
+ def runner(self):
108
+ """Create a CLI runner."""
109
+ return CliRunner()
110
+
111
+ @pytest.fixture
112
+ def fixable_file(self):
113
+ """Create a file with fixable issues."""
114
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
115
+ f.write('''"""@darkarts
116
+ ⊢ test:module.fixable
117
+ ∂{wrong_module}
118
+ ⚠{python≥3.7}
119
+ ⊨{test}
120
+ 🔒{read-only}
121
+ ⚡{O(1)}
122
+ """
123
+ import os
124
+ import sys
125
+
126
+ def test_function():
127
+ pass
128
+ ''')
129
+ return Path(f.name)
130
+
131
+ def test_fix_help(self, runner):
132
+ """Test fix command help."""
133
+ result = runner.invoke(cli, ['fix', '--help'])
134
+ assert result.exit_code == 0
135
+ assert 'fix' in result.output.lower()
136
+
137
+ def test_fix_dry_run(self, runner, fixable_file):
138
+ """Test dry-run mode doesn't modify files."""
139
+ original_content = fixable_file.read_text()
140
+ result = runner.invoke(cli, ['fix', str(fixable_file), '--dry-run'])
141
+ assert result.exit_code == 0
142
+ assert fixable_file.read_text() == original_content # Should not change
143
+ fixable_file.unlink() # Cleanup
144
+
145
+ def test_fix_applies_changes(self, runner, fixable_file):
146
+ """Test fix actually modifies files."""
147
+ original_content = fixable_file.read_text()
148
+ result = runner.invoke(cli, ['fix', str(fixable_file)])
149
+ assert result.exit_code == 0
150
+ new_content = fixable_file.read_text()
151
+ assert new_content != original_content # Should change
152
+ assert 'os' in new_content and 'sys' in new_content # Should have correct deps
153
+ fixable_file.unlink() # Cleanup
154
+
155
+
156
+ class TestBenchmarkCommand:
157
+ """Test suite for voodocs benchmark command."""
158
+
159
+ @pytest.fixture
160
+ def runner(self):
161
+ """Create a CLI runner."""
162
+ return CliRunner()
163
+
164
+ def test_benchmark_help(self, runner):
165
+ """Test benchmark command help."""
166
+ result = runner.invoke(cli, ['benchmark', '--help'])
167
+ assert result.exit_code == 0
168
+ assert 'benchmark' in result.output.lower()
169
+
170
+
171
+ class TestGenerateCommand:
172
+ """Test suite for voodocs generate command."""
173
+
174
+ @pytest.fixture
175
+ def runner(self):
176
+ """Create a CLI runner."""
177
+ return CliRunner()
178
+
179
+ @pytest.fixture
180
+ def test_file(self):
181
+ """Create a temporary test file."""
182
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
183
+ f.write('''"""@darkarts
184
+ ⊢ test:module.sample
185
+ ∂{os, sys}
186
+ ⚠{python≥3.7}
187
+ ⊨{test}
188
+ 🔒{read-only}
189
+ ⚡{O(1)}
190
+ """
191
+ import os
192
+ import sys
193
+
194
+ def test_function():
195
+ """Test function."""
196
+ pass
197
+ ''')
198
+ return Path(f.name)
199
+
200
+ def test_generate_help(self, runner):
201
+ """Test generate command help."""
202
+ result = runner.invoke(cli, ['generate', '--help'])
203
+ assert result.exit_code == 0
204
+ assert 'generate' in result.output.lower()
205
+
206
+ def test_generate_creates_output(self, runner, test_file):
207
+ """Test generate creates output files."""
208
+ with tempfile.TemporaryDirectory() as tmpdir:
209
+ output_dir = Path(tmpdir)
210
+ result = runner.invoke(cli, ['generate', str(test_file), str(output_dir)])
211
+ assert result.exit_code == 0
212
+ # Should create at least one file
213
+ output_files = list(output_dir.glob('*.md'))
214
+ assert len(output_files) > 0
215
+ test_file.unlink() # Cleanup