@voodocs/cli 0.4.2 → 1.0.0

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 (54) hide show
  1. package/CHANGELOG.md +312 -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/types.py +16 -3
  12. package/lib/darkarts/cli_darkarts.py +1 -1
  13. package/lib/darkarts/context/__init__.py +11 -3
  14. package/lib/darkarts/context/ai_integrations.py +7 -21
  15. package/lib/darkarts/context/commands.py +1 -1
  16. package/lib/darkarts/context/diagram.py +8 -22
  17. package/lib/darkarts/context/models.py +7 -22
  18. package/lib/darkarts/context/module_utils.py +1 -1
  19. package/lib/darkarts/context/ui.py +1 -1
  20. package/lib/darkarts/context/validation.py +1 -1
  21. package/lib/darkarts/context/yaml_utils.py +8 -23
  22. package/lib/darkarts/core/__init__.py +12 -2
  23. package/lib/darkarts/core/interface.py +16 -2
  24. package/lib/darkarts/core/loader.py +17 -2
  25. package/lib/darkarts/core/plugin.py +16 -3
  26. package/lib/darkarts/core/registry.py +17 -2
  27. package/lib/darkarts/exceptions.py +17 -3
  28. package/lib/darkarts/plugins/voodocs/__init__.py +12 -2
  29. package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +16 -5
  30. package/lib/darkarts/plugins/voodocs/annotation_validator.py +16 -3
  31. package/lib/darkarts/plugins/voodocs/api_spec_generator.py +16 -3
  32. package/lib/darkarts/plugins/voodocs/documentation_generator.py +16 -3
  33. package/lib/darkarts/plugins/voodocs/html_exporter.py +16 -3
  34. package/lib/darkarts/plugins/voodocs/instruction_generator.py +1 -1
  35. package/lib/darkarts/plugins/voodocs/pdf_exporter.py +16 -3
  36. package/lib/darkarts/plugins/voodocs/test_generator.py +16 -3
  37. package/lib/darkarts/telemetry.py +16 -3
  38. package/lib/darkarts/validation/README.md +147 -0
  39. package/lib/darkarts/validation/__init__.py +91 -0
  40. package/lib/darkarts/validation/autofix.py +297 -0
  41. package/lib/darkarts/validation/benchmark.py +426 -0
  42. package/lib/darkarts/validation/benchmark_wrapper.py +22 -0
  43. package/lib/darkarts/validation/config.py +257 -0
  44. package/lib/darkarts/validation/performance.py +412 -0
  45. package/lib/darkarts/validation/performance_wrapper.py +37 -0
  46. package/lib/darkarts/validation/semantic.py +461 -0
  47. package/lib/darkarts/validation/semantic_wrapper.py +77 -0
  48. package/lib/darkarts/validation/test_validation.py +160 -0
  49. package/lib/darkarts/validation/types.py +97 -0
  50. package/lib/darkarts/validation/watch.py +239 -0
  51. package/package.json +19 -6
  52. package/voodocs_cli.py +28 -0
  53. package/cli.py +0 -1646
  54. package/lib/darkarts/cli.py +0 -128
package/lib/cli/fix.py ADDED
@@ -0,0 +1,244 @@
1
+ """@darkarts
2
+ ⊢cli:fix
3
+ ∂{click,pathlib,typing,sys}
4
+ ⚠{python≥3.7,click≥8.0}
5
+ ⊨{∀fix→executed,∀backup→created}
6
+ 🔒{write:files,read:files}
7
+ ⚡{O(n*m)|n=files,m=file-size}
8
+ """
9
+
10
+ """
11
+ VooDocs CLI - Fix Command
12
+
13
+ Automatically fixes validation issues in @darkarts annotations.
14
+ """
15
+
16
+ import sys
17
+ import click
18
+ from pathlib import Path
19
+ from typing import List, Dict, Any
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.autofix import AutoFixer
25
+
26
+
27
+ @click.command()
28
+ @click.argument('path', type=click.Path(exists=True))
29
+ @click.option('-r', '--recursive', is_flag=True, help='Recursively fix all files')
30
+ @click.option('--dry-run', is_flag=True, help='Preview changes without applying')
31
+ @click.option('--no-backup', is_flag=True, help='Skip creating backup files')
32
+ @click.option('--deps-only', is_flag=True, help='Only fix dependency issues')
33
+ @click.option('--perf-only', is_flag=True, help='Only fix performance issues')
34
+ @click.option('--exclude', multiple=True, help='Exclude patterns (can be used multiple times)')
35
+ @click.option('--format', type=click.Choice(['text', 'json']), default='text', help='Output format')
36
+ @click.option('--strict', is_flag=True, help='Exit with error code if fixes needed')
37
+ def fix(
38
+ path: str,
39
+ recursive: bool,
40
+ dry_run: bool,
41
+ no_backup: bool,
42
+ deps_only: bool,
43
+ perf_only: bool,
44
+ exclude: tuple,
45
+ format: str,
46
+ strict: bool
47
+ ):
48
+ """
49
+ Automatically fix validation issues in @darkarts annotations.
50
+
51
+ Fixes include:
52
+ - Update ∂{} to match actual imports
53
+ - Update ⚡{} to match detected complexity
54
+ - Add missing annotation sections
55
+
56
+ Examples:
57
+
58
+ # Preview fixes for a file
59
+ voodocs fix myfile.py --dry-run
60
+
61
+ # Apply fixes to a file
62
+ voodocs fix myfile.py
63
+
64
+ # Fix all files in directory (with backup)
65
+ voodocs fix lib/ -r
66
+
67
+ # Fix without backup
68
+ voodocs fix lib/ -r --no-backup
69
+
70
+ # Only fix dependencies
71
+ voodocs fix lib/ -r --deps-only
72
+
73
+ # Only fix performance claims
74
+ voodocs fix lib/ -r --perf-only
75
+
76
+ # JSON output
77
+ voodocs fix lib/ -r --format json
78
+ """
79
+
80
+ if format == 'text':
81
+ click.echo(f"Fixing: {path}")
82
+ if dry_run:
83
+ click.secho("(Dry run - no changes will be made)", fg='yellow')
84
+ click.echo()
85
+
86
+ # Collect files to fix
87
+ path_obj = Path(path)
88
+ files_to_fix: List[Path] = []
89
+
90
+ if path_obj.is_file():
91
+ files_to_fix = [path_obj]
92
+ elif path_obj.is_dir():
93
+ pattern = "**/*.py" if recursive else "*.py"
94
+ files_to_fix = [
95
+ f for f in path_obj.glob(pattern)
96
+ if f.is_file() and not _should_exclude(f, exclude)
97
+ ]
98
+
99
+ if not files_to_fix:
100
+ if format == 'text':
101
+ click.secho("No Python files found to fix.", fg='yellow')
102
+ else:
103
+ click.echo('{"files": [], "fixed": 0, "errors": 0}')
104
+ sys.exit(0)
105
+
106
+ # Initialize auto-fix
107
+ autofix = AutoFixer(dry_run=dry_run)
108
+
109
+ # Track results
110
+ results = {
111
+ 'files': [],
112
+ 'fixed': 0,
113
+ 'errors': 0,
114
+ 'skipped': 0
115
+ }
116
+
117
+ # Process each file
118
+ for file_path in files_to_fix:
119
+ try:
120
+ # Create backup if needed
121
+ if not dry_run and not no_backup:
122
+ try:
123
+ autofix.create_backup(file_path)
124
+ except Exception as e:
125
+ if format == 'text':
126
+ click.secho(f"⚠️ Warning: Could not create backup for {file_path}: {e}", fg='yellow')
127
+
128
+ # Run auto-fix
129
+ fix_result = autofix.fix_file(file_path)
130
+
131
+ # Track result
132
+ changes = []
133
+ if fix_result.success:
134
+ if fix_result.original_deps != fix_result.fixed_deps:
135
+ changes.append(f"Updated ∂{{}} from {fix_result.original_deps} to {fix_result.fixed_deps}")
136
+
137
+ file_result = {
138
+ 'file': str(file_path),
139
+ 'fixed': fix_result.success and len(changes) > 0,
140
+ 'changes': changes,
141
+ 'error': fix_result.error if not fix_result.success else None
142
+ }
143
+ results['files'].append(file_result)
144
+
145
+ if file_result['fixed']:
146
+ results['fixed'] += 1
147
+ elif file_result['error']:
148
+ results['errors'] += 1
149
+ else:
150
+ results['skipped'] += 1
151
+
152
+ # Display result (text mode)
153
+ if format == 'text':
154
+ _display_file_result(file_path, file_result, dry_run)
155
+
156
+ except Exception as e:
157
+ results['errors'] += 1
158
+ results['files'].append({
159
+ 'file': str(file_path),
160
+ 'fixed': False,
161
+ 'error': str(e)
162
+ })
163
+
164
+ if format == 'text':
165
+ click.secho(f"❌ {file_path}", fg='red')
166
+ click.secho(f" Error: {e}", fg='red')
167
+
168
+ # Display summary
169
+ if format == 'text':
170
+ _display_summary(results, dry_run, strict)
171
+ else:
172
+ import json
173
+ click.echo(json.dumps(results, indent=2))
174
+
175
+ # Exit code
176
+ if strict and results['fixed'] > 0:
177
+ sys.exit(1)
178
+ elif results['errors'] > 0:
179
+ sys.exit(1)
180
+ else:
181
+ sys.exit(0)
182
+
183
+
184
+ def _should_exclude(file_path: Path, exclude_patterns: tuple) -> bool:
185
+ """Check if file should be excluded based on patterns."""
186
+ file_str = str(file_path)
187
+ for pattern in exclude_patterns:
188
+ if pattern in file_str:
189
+ return True
190
+ return False
191
+
192
+
193
+ def _display_file_result(file_path: Path, result: Dict[str, Any], dry_run: bool):
194
+ """Display result for a single file."""
195
+ if result.get('error'):
196
+ click.secho(f"❌ {file_path}", fg='red')
197
+ click.secho(f" Error: {result['error']}", fg='red')
198
+ elif result.get('fixed'):
199
+ action = "Would fix" if dry_run else "Fixed"
200
+ click.secho(f"✅ {file_path}", fg='green')
201
+ changes = result.get('changes', [])
202
+ for change in changes:
203
+ click.echo(f" • {change}")
204
+ else:
205
+ click.secho(f"⚪ {file_path}", fg='white')
206
+ click.echo(f" No fixes needed")
207
+
208
+
209
+ def _display_summary(results: Dict[str, Any], dry_run: bool, strict: bool):
210
+ """Display summary of fix results."""
211
+ total = len(results['files'])
212
+ fixed = results['fixed']
213
+ errors = results['errors']
214
+ skipped = results['skipped']
215
+
216
+ click.echo()
217
+ click.echo("━" * 60)
218
+ click.echo(f"Total files: {total}")
219
+
220
+ if dry_run:
221
+ click.secho(f"Would fix: {fixed}", fg='yellow')
222
+ else:
223
+ click.secho(f"Fixed: {fixed}", fg='green' if fixed > 0 else 'white')
224
+
225
+ click.echo(f"No changes needed: {skipped}")
226
+
227
+ if errors > 0:
228
+ click.secho(f"Errors: {errors}", fg='red')
229
+
230
+ click.echo("━" * 60)
231
+
232
+ if dry_run and fixed > 0:
233
+ click.echo()
234
+ click.secho("💡 Run without --dry-run to apply fixes", fg='cyan')
235
+ elif not dry_run and fixed > 0:
236
+ click.echo()
237
+ click.secho("✅ All fixes applied!", fg='green')
238
+ elif fixed == 0 and errors == 0:
239
+ click.echo()
240
+ click.secho("✅ All files are already valid!", fg='green')
241
+
242
+ if strict and fixed > 0:
243
+ click.echo()
244
+ click.secho("⚠️ Strict mode: Exiting with error code (fixes were needed)", fg='yellow')
@@ -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')