@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/cli.py DELETED
@@ -1,1646 +0,0 @@
1
- #!/usr/bin/env python3
2
- """@voodocs
3
- module_purpose: "Main CLI entry point for VooDocs - command routing and argument parsing"
4
- dependencies: [
5
- "argparse: Command-line argument parsing",
6
- "pathlib: Path handling for symlink resolution",
7
- "darkarts.context: Context system commands",
8
- "darkarts.plugins.voodocs: Documentation generation"
9
- ]
10
- assumptions: [
11
- "Python 3.7+ is installed",
12
- "Script is executable (chmod +x)",
13
- "npm/pnpm may create symlinks for global installs",
14
- "lib/ directory is in package root"
15
- ]
16
- invariants: [
17
- "All commands must return 0 (success) or 1 (error) exit code",
18
- "Symlinks must be resolved to find actual package location",
19
- "lib/ must be added to sys.path before importing darkarts",
20
- "Error messages must be user-friendly",
21
- "Help text must be comprehensive"
22
- ]
23
- security_model: "Execute user commands in current directory, no privilege escalation"
24
- performance_model: "O(1) for command routing, actual performance depends on subcommand"
25
-
26
- VooDocs CLI - AI-native documentation for modern development
27
-
28
- Commands:
29
- init - Initialize VooDocs in a project
30
- instruct - Generate AI assistant instructions
31
- generate - Generate docs, tests, and API specs from annotations
32
- status - Show project status and statistics
33
- version - Show VooDocs version
34
- """
35
-
36
- import sys
37
- import argparse
38
- import traceback
39
- import time
40
- from pathlib import Path
41
- from functools import wraps
42
-
43
- # Add lib to path (resolve symlinks for npm/pnpm bin)
44
- import os
45
-
46
- # Get the directory containing this script
47
- # Handle both direct execution and symlinked execution (npm/pnpm global install)
48
- script_path = Path(__file__)
49
- if script_path.is_symlink():
50
- # Follow the symlink to get the actual package location
51
- script_path = Path(os.readlink(script_path))
52
- if not script_path.is_absolute():
53
- # Relative symlink - resolve relative to the symlink's directory
54
- script_path = (Path(__file__).parent / script_path).resolve()
55
- else:
56
- # Not a symlink - use realpath to resolve any relative paths
57
- script_path = Path(os.path.realpath(__file__))
58
-
59
- # Get the package root directory (where cli.py and lib/ are located)
60
- package_root = script_path.parent
61
-
62
- # Add lib directory to Python path
63
- sys.path.insert(0, str(package_root / "lib"))
64
-
65
- from darkarts.plugins.voodocs.instruction_generator import InstructionGenerator
66
- from darkarts.plugins.voodocs.documentation_generator import DocumentationGenerator
67
- from darkarts.plugins.voodocs.test_generator import TestGenerator
68
- from darkarts.plugins.voodocs.api_spec_generator import APISpecGenerator
69
- from darkarts.plugins.voodocs.annotation_validator import AnnotationValidator
70
- from darkarts.plugins.voodocs.html_exporter import HTMLExporter
71
- from darkarts.plugins.voodocs.pdf_exporter import PDFExporter
72
- from darkarts.annotations.parser import AnnotationParser
73
- from darkarts.telemetry import get_telemetry_client, track_command
74
- from darkarts.cli_darkarts import add_darkarts_subcommands, dispatch_darkarts_command
75
- from darkarts.exceptions import (
76
- VooDocsError,
77
- ParserError,
78
- ParserNotBuiltError,
79
- AnnotationError,
80
- GeneratorError,
81
- ConfigurationError
82
- )
83
-
84
- # Telemetry environment variables
85
- os.environ.setdefault("VOODOCS_SUPABASE_URL", "https://sjatkayudkbkmipubhfy.supabase.co")
86
- os.environ.setdefault("VOODOCS_SUPABASE_ANON_KEY", "sb_publishable_rHfzdx4jc7QH3JFSRC-sDA_6HHhzdU5")
87
-
88
- def track_command_execution(command_name):
89
- """Decorator to track command execution with telemetry."""
90
- def decorator(func):
91
- @wraps(func)
92
- def wrapper(args):
93
- start_time = time.time()
94
- success = False
95
- error_type = None
96
-
97
- try:
98
- result = func(args)
99
- success = True
100
- return result
101
- except Exception as e:
102
- error_type = type(e).__name__
103
- raise
104
- finally:
105
- duration_ms = int((time.time() - start_time) * 1000)
106
-
107
- # Track telemetry
108
- try:
109
- track_command(
110
- command=command_name,
111
- success=success,
112
- duration_ms=duration_ms,
113
- error_type=error_type
114
- )
115
- except:
116
- pass # Never let telemetry break the CLI
117
-
118
- return wrapper
119
- return decorator
120
-
121
-
122
- def main():
123
- """Main CLI entry point."""
124
- parser = argparse.ArgumentParser(
125
- prog="voodocs",
126
- description="""VooDocs - AI-native documentation for modern development
127
-
128
- VooDocs teaches AI coding assistants to document code in the DarkArts language,
129
- then automatically generates human-readable docs, tests, and API specs.
130
- """,
131
- epilog="""Examples:
132
- # Initialize a new project
133
- voodocs init
134
-
135
- # Generate AI assistant instructions
136
- voodocs instruct --assistant cursor
137
-
138
- # Generate docs, tests, and API specs
139
- voodocs generate src/
140
- voodocs generate --docs-only src/
141
- voodocs generate --tests-only src/
142
-
143
- # Validate annotation quality
144
- voodocs validate src/ --strict
145
-
146
- # Check coverage for CI/CD
147
- voodocs check . --min-coverage 80 --min-quality 70
148
-
149
- # Export documentation
150
- voodocs export docs/api.md --format html
151
-
152
- # Watch for changes
153
- voodocs watch src/
154
-
155
- For more information, visit: https://voodocs.com
156
- Documentation: https://github.com/3vilEnterprises/vooodooo-magic/tree/main/packages/voodocs
157
- """,
158
- formatter_class=argparse.RawDescriptionHelpFormatter
159
- )
160
-
161
- parser.add_argument(
162
- "--version",
163
- action="version",
164
- version="VooDocs 0.3.2"
165
- )
166
-
167
- subparsers = parser.add_subparsers(dest="command", help="Available commands")
168
-
169
- # init command
170
- init_parser = subparsers.add_parser(
171
- "init",
172
- help="Initialize VooDocs in a project"
173
- )
174
- init_parser.add_argument(
175
- "--project-name",
176
- type=str,
177
- help="Project name (default: current directory name)"
178
- )
179
- init_parser.add_argument(
180
- "--language",
181
- type=str,
182
- choices=["python", "typescript", "javascript"],
183
- default="python",
184
- help="Primary programming language (default: python)"
185
- )
186
-
187
- # instruct command
188
- instruct_parser = subparsers.add_parser(
189
- "instruct",
190
- help="Generate AI assistant instructions"
191
- )
192
- instruct_parser.add_argument(
193
- "--assistant",
194
- type=str,
195
- choices=["cursor", "claude", "copilot", "windsurf", "generic"],
196
- help="AI assistant type (auto-detected if not specified)"
197
- )
198
- instruct_parser.add_argument(
199
- "--language",
200
- type=str,
201
- choices=["python", "typescript", "javascript"],
202
- default="python",
203
- help="Primary programming language (default: python)"
204
- )
205
- instruct_parser.add_argument(
206
- "--output",
207
- type=str,
208
- help="Output file path (default: assistant's config file)"
209
- )
210
-
211
- # generate command
212
- generate_parser = subparsers.add_parser(
213
- "generate",
214
- help="Generate docs, tests, and API specs from annotations"
215
- )
216
- generate_parser.add_argument(
217
- "--debug",
218
- action="store_true",
219
- help="Show full stack traces on errors"
220
- )
221
- generate_parser.add_argument(
222
- "paths",
223
- nargs="+",
224
- help="Files or directories to process"
225
- )
226
- generate_parser.add_argument(
227
- "--output-dir",
228
- type=str,
229
- default="./voodocs-output",
230
- help="Output directory (default: ./voodocs-output)"
231
- )
232
- generate_parser.add_argument(
233
- "--docs-only",
234
- action="store_true",
235
- help="Generate only documentation"
236
- )
237
- generate_parser.add_argument(
238
- "--tests-only",
239
- action="store_true",
240
- help="Generate only tests"
241
- )
242
- generate_parser.add_argument(
243
- "--api-only",
244
- action="store_true",
245
- help="Generate only API specs"
246
- )
247
- generate_parser.add_argument(
248
- "--test-framework",
249
- type=str,
250
- choices=["pytest", "unittest", "jest"],
251
- default="pytest",
252
- help="Test framework (default: pytest)"
253
- )
254
- generate_parser.add_argument(
255
- "--api-format",
256
- type=str,
257
- choices=["openapi", "swagger", "graphql"],
258
- default="openapi",
259
- help="API spec format (default: openapi)"
260
- )
261
- generate_parser.add_argument(
262
- "--recursive",
263
- action="store_true",
264
- default=True,
265
- help="Process directories recursively (default: true)"
266
- )
267
- generate_parser.add_argument(
268
- "--verbose",
269
- action="store_true",
270
- help="Show detailed output"
271
- )
272
-
273
- # status command
274
- status_parser = subparsers.add_parser(
275
- "status",
276
- help="Show project status and statistics"
277
- )
278
- status_parser.add_argument(
279
- "paths",
280
- nargs="*",
281
- default=["."],
282
- help="Paths to analyze (default: current directory)"
283
- )
284
-
285
- # watch command
286
- watch_parser = subparsers.add_parser(
287
- "watch",
288
- help="Watch files and automatically regenerate documentation"
289
- )
290
- watch_parser.add_argument(
291
- "paths",
292
- nargs="+",
293
- help="Paths to watch for changes"
294
- )
295
- watch_parser.add_argument(
296
- "--output-dir",
297
- default="./voodocs-output",
298
- help="Output directory (default: ./voodocs-output)"
299
- )
300
- watch_parser.add_argument(
301
- "--docs-only",
302
- action="store_true",
303
- help="Generate only documentation"
304
- )
305
- watch_parser.add_argument(
306
- "--tests-only",
307
- action="store_true",
308
- help="Generate only tests"
309
- )
310
- watch_parser.add_argument(
311
- "--api-only",
312
- action="store_true",
313
- help="Generate only API specs"
314
- )
315
- watch_parser.add_argument(
316
- "--interval",
317
- type=int,
318
- default=2,
319
- help="Check interval in seconds (default: 2)"
320
- )
321
-
322
- # validate command
323
- validate_parser = subparsers.add_parser(
324
- "validate",
325
- help="Validate annotation quality and correctness"
326
- )
327
- validate_parser.add_argument(
328
- "paths",
329
- nargs="+",
330
- help="Paths to validate"
331
- )
332
- validate_parser.add_argument(
333
- "--strict",
334
- action="store_true",
335
- help="Treat warnings as errors"
336
- )
337
- validate_parser.add_argument(
338
- "--format",
339
- choices=["text", "json"],
340
- default="text",
341
- help="Output format (default: text)"
342
- )
343
-
344
- # check command (for CI/CD)
345
- check_parser = subparsers.add_parser(
346
- "check",
347
- help="Check annotation coverage and quality (for CI/CD)"
348
- )
349
- check_parser.add_argument(
350
- "paths",
351
- nargs="*",
352
- default=["."],
353
- help="Paths to check (default: current directory)"
354
- )
355
- check_parser.add_argument(
356
- "--min-coverage",
357
- type=int,
358
- default=0,
359
- help="Minimum annotation coverage percentage (0-100)"
360
- )
361
- check_parser.add_argument(
362
- "--min-quality",
363
- type=int,
364
- default=0,
365
- help="Minimum quality score (0-100)"
366
- )
367
-
368
- # export command
369
- export_parser = subparsers.add_parser(
370
- "export",
371
- help="Export documentation to different formats"
372
- )
373
- export_parser.add_argument(
374
- "input_file",
375
- help="Input Markdown file to export"
376
- )
377
- export_parser.add_argument(
378
- "--format",
379
- choices=["html", "pdf"],
380
- required=True,
381
- help="Export format"
382
- )
383
- export_parser.add_argument(
384
- "--output",
385
- help="Output file path (optional)"
386
- )
387
-
388
- # telemetry command
389
- telemetry_parser = subparsers.add_parser(
390
- "telemetry",
391
- help="Manage telemetry settings"
392
- )
393
- telemetry_parser.add_argument(
394
- "action",
395
- choices=["enable", "disable", "status"],
396
- help="Telemetry action"
397
- )
398
-
399
- # context command
400
- context_parser = subparsers.add_parser(
401
- "context",
402
- help="Manage project context for AI assistants"
403
- )
404
- context_subparsers = context_parser.add_subparsers(dest="context_action", help="Context actions")
405
-
406
- # context init
407
- context_init_parser = context_subparsers.add_parser(
408
- "init",
409
- help="Initialize a new .voodocs.context file"
410
- )
411
- context_init_parser.add_argument(
412
- "--force",
413
- action="store_true",
414
- help="Overwrite existing context file"
415
- )
416
-
417
- # context view
418
- context_view_parser = context_subparsers.add_parser(
419
- "view",
420
- help="View context as human-readable Markdown"
421
- )
422
- context_view_parser.add_argument(
423
- "--output",
424
- "-o",
425
- help="Output file path (default: print to console)"
426
- )
427
-
428
- # context status
429
- context_status_parser = context_subparsers.add_parser(
430
- "status",
431
- help="Show context file status"
432
- )
433
-
434
- # context update
435
- context_update_parser = context_subparsers.add_parser(
436
- "update",
437
- help="Update context and increment version"
438
- )
439
- context_update_parser.add_argument(
440
- "--description",
441
- "-d",
442
- help="Description of the changes"
443
- )
444
-
445
- # context sync
446
- context_sync_parser = context_subparsers.add_parser(
447
- "sync",
448
- help="Sync context with code version"
449
- )
450
- context_sync_parser.add_argument(
451
- "--code-version",
452
- "-v",
453
- help="New code version (e.g., 2.0.0)"
454
- )
455
- context_sync_parser.add_argument(
456
- "--force",
457
- action="store_true",
458
- help="Skip confirmation prompts"
459
- )
460
-
461
- # context history
462
- context_history_parser = context_subparsers.add_parser(
463
- "history",
464
- help="Show version history"
465
- )
466
-
467
- # context diff
468
- context_diff_parser = context_subparsers.add_parser(
469
- "diff",
470
- help="Compare context versions"
471
- )
472
- context_diff_parser.add_argument(
473
- "version1",
474
- nargs="?",
475
- help="First version (older)"
476
- )
477
- context_diff_parser.add_argument(
478
- "version2",
479
- nargs="?",
480
- help="Second version (newer, default: current)"
481
- )
482
-
483
- # context validate
484
- context_validate_parser = context_subparsers.add_parser(
485
- "validate",
486
- help="Validate context file"
487
- )
488
- context_validate_parser.add_argument(
489
- "--check-version",
490
- action="store_true",
491
- help="Check if code version matches detected version"
492
- )
493
- context_validate_parser.add_argument(
494
- "--check-invariants",
495
- action="store_true",
496
- help="Check if code respects documented invariants"
497
- )
498
-
499
- # context generate
500
- context_generate_parser = context_subparsers.add_parser(
501
- "generate",
502
- help="Generate context from code annotations"
503
- )
504
- context_generate_parser.add_argument(
505
- "source_dir",
506
- nargs="?",
507
- help="Directory to scan (default: current directory)"
508
- )
509
- context_generate_parser.add_argument(
510
- "--update",
511
- action="store_true",
512
- help="Update existing context file"
513
- )
514
-
515
- # context query
516
- context_query_parser = context_subparsers.add_parser(
517
- "query",
518
- help="Query context like a database"
519
- )
520
- context_query_parser.add_argument(
521
- "query",
522
- help="Search query (keyword or regex)"
523
- )
524
- context_query_parser.add_argument(
525
- "--section",
526
- "-s",
527
- help="Specific section to search"
528
- )
529
- context_query_parser.add_argument(
530
- "--format",
531
- "-f",
532
- choices=["text", "json", "yaml"],
533
- default="text",
534
- help="Output format (default: text)"
535
- )
536
-
537
- # context check
538
- context_check_parser = context_subparsers.add_parser(
539
- "check",
540
- help="Check that code respects documented invariants"
541
- )
542
- context_check_parser.add_argument(
543
- "--module",
544
- "-m",
545
- help="Filter by module name"
546
- )
547
- context_check_parser.add_argument(
548
- "--invariant",
549
- "-i",
550
- help="Filter by invariant text"
551
- )
552
- context_check_parser.add_argument(
553
- "--format",
554
- "-f",
555
- choices=["text", "json"],
556
- default="text",
557
- help="Output format (default: text)"
558
- )
559
-
560
- # context diagram
561
- context_diagram_parser = context_subparsers.add_parser(
562
- "diagram",
563
- help="Generate architecture diagrams from context"
564
- )
565
- context_diagram_parser.add_argument(
566
- "--type",
567
- "-t",
568
- choices=["modules", "dependencies", "flow", "all"],
569
- default="modules",
570
- help="Diagram type (default: modules)"
571
- )
572
- context_diagram_parser.add_argument(
573
- "--format",
574
- "-f",
575
- choices=["mermaid", "d2"],
576
- default="mermaid",
577
- help="Output format (default: mermaid)"
578
- )
579
- context_diagram_parser.add_argument(
580
- "--output",
581
- "-o",
582
- help="Output file path (default: print to console)"
583
- )
584
- context_diagram_parser.add_argument(
585
- "--png",
586
- action="store_true",
587
- help="Render to PNG (requires output file)"
588
- )
589
-
590
- # Add DarkArts subcommands
591
- add_darkarts_subcommands(subparsers)
592
-
593
- args = parser.parse_args()
594
-
595
- # Execute command
596
- if args.command == "init":
597
- cmd_init(args)
598
- elif args.command == "instruct":
599
- cmd_instruct(args)
600
- elif args.command == "generate":
601
- cmd_generate(args)
602
- elif args.command == "status":
603
- cmd_status(args)
604
- elif args.command == "watch":
605
- cmd_watch(args)
606
- elif args.command == "validate":
607
- cmd_validate(args)
608
- elif args.command == "check":
609
- cmd_check(args)
610
- elif args.command == "export":
611
- cmd_export(args)
612
- elif args.command == "telemetry":
613
- cmd_telemetry(args)
614
- elif args.command == "context":
615
- cmd_context(args)
616
- elif args.command == "darkarts":
617
- sys.exit(dispatch_darkarts_command(args))
618
- else:
619
- parser.print_help()
620
- sys.exit(1)
621
-
622
-
623
- @track_command_execution("init")
624
- def cmd_init(args):
625
- """Initialize VooDocs in a project."""
626
- print("🎯 VooDocs Initialization")
627
- print("=" * 60)
628
- print()
629
-
630
- try:
631
- # Interactive prompts if not provided
632
- if not args.project_name:
633
- default_name = Path.cwd().name
634
- response = input(f"Project name [{default_name}]: ").strip()
635
- project_name = response if response else default_name
636
- else:
637
- project_name = args.project_name
638
-
639
- if not args.language:
640
- print("\nSelect primary language:")
641
- print(" 1. Python")
642
- print(" 2. TypeScript")
643
- print(" 3. JavaScript")
644
- response = input("Choice [1]: ").strip()
645
- language_map = {"1": "python", "2": "typescript", "3": "javascript", "": "python"}
646
- language = language_map.get(response, "python")
647
- else:
648
- language = args.language
649
-
650
- print(f"\n📦 Setting up VooDocs for '{project_name}'...")
651
-
652
- # Create .voodocs directory
653
- voodocs_dir = Path(".voodocs")
654
- voodocs_dir.mkdir(exist_ok=True)
655
-
656
- # Create config file with all options
657
- config = {
658
- "project_name": project_name,
659
- "language": language,
660
- "version": "0.2.0",
661
- "output_dir": "./voodocs-output",
662
- "test_framework": "pytest",
663
- "api_format": "openapi",
664
- "exclude_patterns": [
665
- "**/test_*.py",
666
- "**/node_modules/**",
667
- "**/__pycache__/**",
668
- "**/venv/**",
669
- "**/.venv/**"
670
- ],
671
- "include_patterns": [],
672
- "annotation_coverage_threshold": 50
673
- }
674
-
675
- import json
676
- config_file = voodocs_dir / "config.json"
677
- config_file.write_text(json.dumps(config, indent=2))
678
-
679
- print(f"✓ Created .voodocs/config.json")
680
- print(f"✓ Project: {project_name}")
681
- print(f"✓ Language: {language}")
682
- print()
683
- print("Next steps:")
684
- print(" 1. Run 'voodocs instruct' to generate AI assistant instructions")
685
- print(" 2. Start coding with @voodocs annotations")
686
- print(" 3. Run 'voodocs generate <path>' to generate docs and tests")
687
-
688
- except PermissionError:
689
- print("❌ Permission denied: Cannot create .voodocs directory")
690
- print(" Check directory permissions and try again.")
691
- sys.exit(1)
692
-
693
- except Exception as e:
694
- print(f"❌ Failed to initialize VooDocs: {e}")
695
- sys.exit(1)
696
-
697
-
698
- @track_command_execution("instruct")
699
- def cmd_instruct(args):
700
- """Generate AI assistant instructions."""
701
- print("📝 Generating AI assistant instructions...")
702
-
703
- try:
704
- # Load config
705
- config = load_config()
706
-
707
- # Detect or use specified assistant
708
- assistant = args.assistant
709
- if not assistant:
710
- generator = InstructionGenerator()
711
- assistant = generator.detect_assistant()
712
- if assistant:
713
- print(f"✓ Detected AI assistant: {assistant}")
714
- else:
715
- assistant = "generic"
716
- print(f"⚠ Could not detect AI assistant, using generic")
717
-
718
- # Generate instructions
719
- generator = InstructionGenerator(
720
- assistant=assistant,
721
- project_name=config.get("project_name", "Your Project")
722
- )
723
-
724
- output_file = args.output
725
- language = args.language or config.get("language", "python")
726
-
727
- instructions = generator.generate(
728
- output_file=output_file,
729
- language=language
730
- )
731
-
732
- output_path = output_file or generator.assistant_info["config_file"]
733
-
734
- print(f"✓ Generated instructions: {output_path}")
735
- print(f"✓ File size: {len(instructions):,} characters")
736
- print()
737
- print(f"Instructions have been generated for {generator.assistant_info['name']}.")
738
- print(f"Your AI assistant will now automatically add @voodocs annotations!")
739
-
740
- except PermissionError as e:
741
- print(f"❌ Permission denied: Cannot write to {output_path if 'output_path' in locals() else 'output file'}")
742
- print(" Check file permissions and try again.")
743
- sys.exit(1)
744
-
745
- except Exception as e:
746
- print(f"❌ Failed to generate instructions: {e}")
747
- sys.exit(1)
748
-
749
-
750
- @track_command_execution("generate")
751
- def cmd_generate(args):
752
- """Generate documentation, tests, and API specs from annotations."""
753
- print("🚀 VooDocs Generate")
754
- print("=" * 60)
755
-
756
- # Load configuration
757
- config = load_config()
758
-
759
- # Use config values as defaults, CLI args override
760
- output_dir_path = args.output_dir if hasattr(args, 'output_dir') and args.output_dir != './voodocs-output' else config.get('output_dir', './voodocs-output')
761
- test_framework = args.test_framework if hasattr(args, 'test_framework') else config.get('test_framework', 'pytest')
762
- api_format = args.api_format if hasattr(args, 'api_format') else config.get('api_format', 'openapi')
763
-
764
- # Determine what to generate
765
- generate_docs = not (args.tests_only or args.api_only)
766
- generate_tests = not (args.docs_only or args.api_only)
767
- generate_api = not (args.docs_only or args.tests_only)
768
-
769
- if args.docs_only:
770
- generate_docs = True
771
- generate_tests = False
772
- generate_api = False
773
- elif args.tests_only:
774
- generate_docs = False
775
- generate_tests = True
776
- generate_api = False
777
- elif args.api_only:
778
- generate_docs = False
779
- generate_tests = False
780
- generate_api = True
781
-
782
- print(f"📁 Output directory: {output_dir_path}")
783
- print(f"📄 Generate docs: {'✓' if generate_docs else '✗'}")
784
- print(f"🧪 Generate tests: {'✓' if generate_tests else '✗'}")
785
- print(f"🔌 Generate API specs: {'✓' if generate_api else '✗'}")
786
- print()
787
-
788
- # Create output directory
789
- output_dir = Path(output_dir_path)
790
- output_dir.mkdir(parents=True, exist_ok=True)
791
-
792
- # Collect files to process
793
- files_to_process = []
794
- for path_str in args.paths:
795
- path = Path(path_str)
796
- if path.is_file():
797
- files_to_process.append(path)
798
- elif path.is_dir():
799
- if args.recursive:
800
- # Find all Python/TypeScript files
801
- files_to_process.extend(path.rglob("*.py"))
802
- files_to_process.extend(path.rglob("*.ts"))
803
- files_to_process.extend(path.rglob("*.js"))
804
- else:
805
- files_to_process.extend(path.glob("*.py"))
806
- files_to_process.extend(path.glob("*.ts"))
807
- files_to_process.extend(path.glob("*.js"))
808
-
809
- if not files_to_process:
810
- print("❌ No files found to process")
811
- sys.exit(1)
812
-
813
- print(f"📋 Processing {len(files_to_process)} file(s)...")
814
- print()
815
-
816
- # Initialize generators
817
- parser = AnnotationParser()
818
- doc_generator = DocumentationGenerator() if generate_docs else None
819
- test_generator = TestGenerator(framework=args.test_framework) if generate_tests else None
820
- api_generator = APISpecGenerator(format=args.api_format) if generate_api else None
821
-
822
- # Statistics
823
- stats = {
824
- "files_processed": 0,
825
- "files_with_annotations": 0,
826
- "total_functions": 0,
827
- "total_classes": 0,
828
- "docs_generated": 0,
829
- "tests_generated": 0,
830
- "api_specs_generated": 0
831
- }
832
-
833
- # Process each file
834
- for file_path in files_to_process:
835
- if args.verbose:
836
- print(f"Processing: {file_path}")
837
-
838
- try:
839
- # Parse file
840
- parsed = parser.parse_file(str(file_path))
841
- stats["files_processed"] += 1
842
-
843
- if not parsed.has_annotations():
844
- if args.verbose:
845
- print(f" ⚠ No annotations found")
846
- continue
847
-
848
- stats["files_with_annotations"] += 1
849
- stats["total_functions"] += len(parsed.get_all_functions())
850
- stats["total_classes"] += len(parsed.module.classes)
851
-
852
- # Generate relative output path
853
- rel_path = file_path.stem
854
-
855
- # Generate documentation
856
- if generate_docs:
857
- doc_output = output_dir / "docs" / f"{rel_path}.md"
858
- doc_output.parent.mkdir(parents=True, exist_ok=True)
859
- doc_generator.generate(parsed, str(doc_output))
860
- stats["docs_generated"] += 1
861
- if args.verbose:
862
- print(f" ✓ Documentation: {doc_output}")
863
-
864
- # Generate tests
865
- if generate_tests:
866
- test_output = output_dir / "tests" / f"test_{rel_path}.py"
867
- test_output.parent.mkdir(parents=True, exist_ok=True)
868
- test_generator.generate(parsed, str(test_output))
869
- stats["tests_generated"] += 1
870
- if args.verbose:
871
- print(f" ✓ Tests: {test_output}")
872
-
873
- # Generate API spec
874
- if generate_api:
875
- api_ext = ".yaml" if args.api_format in ["openapi", "swagger"] else ".graphql"
876
- api_output = output_dir / "api" / f"{rel_path}{api_ext}"
877
- api_output.parent.mkdir(parents=True, exist_ok=True)
878
- api_generator.generate(parsed, str(api_output))
879
- stats["api_specs_generated"] += 1
880
- if args.verbose:
881
- print(f" ✓ API Spec: {api_output}")
882
-
883
- except ParserNotBuiltError as e:
884
- print(f"\n❌ {e}")
885
- print(f"\nThe TypeScript parser requires building before use.")
886
- print(f"Please run the following commands:")
887
- print(f" cd {Path(__file__).parent / 'lib/darkarts/parsers/typescript'}")
888
- print(f" npm install")
889
- print(f" npm run build")
890
- sys.exit(1)
891
-
892
- except FileNotFoundError as e:
893
- print(f" ❌ File not found: {file_path}")
894
- if args.debug:
895
- traceback.print_exc()
896
- continue
897
-
898
- except SyntaxError as e:
899
- print(f" ❌ Syntax error in {file_path.name}: {e}")
900
- if args.debug:
901
- traceback.print_exc()
902
- continue
903
-
904
- except AnnotationError as e:
905
- print(f" ❌ Annotation error in {file_path.name}: {e}")
906
- if args.debug:
907
- traceback.print_exc()
908
- continue
909
-
910
- except GeneratorError as e:
911
- print(f" ❌ Generator error for {file_path.name}: {e}")
912
- if args.debug:
913
- traceback.print_exc()
914
- continue
915
-
916
- except PermissionError:
917
- print(f" ❌ Permission denied: {file_path}")
918
- print(f" Check file permissions and try again.")
919
- continue
920
-
921
- except Exception as e:
922
- print(f" ❌ Unexpected error in {file_path.name}: {e}")
923
- print(f" Run with --debug for full stack trace.")
924
- if args.debug:
925
- traceback.print_exc()
926
- continue
927
-
928
- # Print summary
929
- print()
930
- print("=" * 60)
931
- print("📊 Generation Summary")
932
- print("=" * 60)
933
- print(f"Files processed: {stats['files_processed']}")
934
- print(f"Files with annotations: {stats['files_with_annotations']}")
935
- print(f"Total functions: {stats['total_functions']}")
936
- print(f"Total classes: {stats['total_classes']}")
937
- print()
938
- if generate_docs:
939
- print(f"✓ Documentation files: {stats['docs_generated']}")
940
- if generate_tests:
941
- print(f"✓ Test files: {stats['tests_generated']}")
942
- if generate_api:
943
- print(f"✓ API specs: {stats['api_specs_generated']}")
944
- print()
945
- print(f"✨ All outputs saved to: {output_dir}")
946
-
947
-
948
- @track_command_execution("status")
949
- def cmd_status(args):
950
- """Show project status and statistics."""
951
- print("📊 VooDocs Project Status")
952
- print("=" * 60)
953
-
954
- # Collect files
955
- files = []
956
- for path_str in args.paths:
957
- path = Path(path_str)
958
- if path.is_file():
959
- files.append(path)
960
- elif path.is_dir():
961
- files.extend(path.rglob("*.py"))
962
- files.extend(path.rglob("*.ts"))
963
- files.extend(path.rglob("*.js"))
964
-
965
- if not files:
966
- print("❌ No files found")
967
- sys.exit(1)
968
-
969
- # Parse and analyze
970
- parser = AnnotationParser()
971
- stats = {
972
- "total_files": len(files),
973
- "files_with_annotations": 0,
974
- "total_functions": 0,
975
- "annotated_functions": 0,
976
- "total_classes": 0,
977
- "annotated_classes": 0,
978
- "annotation_fields": {
979
- "preconditions": 0,
980
- "postconditions": 0,
981
- "complexity": 0,
982
- "error_cases": 0,
983
- "security_implications": 0
984
- }
985
- }
986
-
987
- for file_path in files:
988
- try:
989
- parsed = parser.parse_file(str(file_path))
990
-
991
- if parsed.has_annotations():
992
- stats["files_with_annotations"] += 1
993
-
994
- all_functions = parsed.get_all_functions()
995
- stats["total_functions"] += len(all_functions)
996
-
997
- for func in all_functions:
998
- if func.preconditions or func.postconditions:
999
- stats["annotated_functions"] += 1
1000
- if func.preconditions:
1001
- stats["annotation_fields"]["preconditions"] += 1
1002
- if func.postconditions:
1003
- stats["annotation_fields"]["postconditions"] += 1
1004
- if func.complexity:
1005
- stats["annotation_fields"]["complexity"] += 1
1006
- if func.error_cases:
1007
- stats["annotation_fields"]["error_cases"] += 1
1008
- if func.security_implications:
1009
- stats["annotation_fields"]["security_implications"] += 1
1010
-
1011
- stats["total_classes"] += len(parsed.module.classes)
1012
- for cls in parsed.module.classes:
1013
- if cls.class_invariants or cls.state_transitions:
1014
- stats["annotated_classes"] += 1
1015
-
1016
- except Exception:
1017
- pass
1018
-
1019
- # Calculate percentages
1020
- func_coverage = (stats["annotated_functions"] / stats["total_functions"] * 100) if stats["total_functions"] > 0 else 0
1021
- class_coverage = (stats["annotated_classes"] / stats["total_classes"] * 100) if stats["total_classes"] > 0 else 0
1022
- file_coverage = (stats["files_with_annotations"] / stats["total_files"] * 100) if stats["total_files"] > 0 else 0
1023
-
1024
- # Calculate quality score (0-100)
1025
- quality_score = _calculate_quality_score(stats)
1026
- quality_grade = _get_quality_grade(quality_score)
1027
-
1028
- # Print stats with visual indicators
1029
- print(f"Total files: {stats['total_files']}")
1030
- print(f"Files with annotations: {stats['files_with_annotations']} {_coverage_bar(file_coverage)}")
1031
- print()
1032
- print(f"Total functions: {stats['total_functions']}")
1033
- print(f"Annotated functions: {stats['annotated_functions']} {_coverage_bar(func_coverage)}")
1034
- print()
1035
- print(f"Total classes: {stats['total_classes']}")
1036
- print(f"Annotated classes: {stats['annotated_classes']} {_coverage_bar(class_coverage)}")
1037
- print()
1038
- print("Annotation Field Usage:")
1039
- for field, count in stats["annotation_fields"].items():
1040
- percentage = (count / stats['total_functions'] * 100) if stats['total_functions'] > 0 else 0
1041
- print(f" {field:25} {count:3} ({percentage:5.1f}%)")
1042
- print()
1043
-
1044
- # Quality Score
1045
- print("📈 Documentation Quality")
1046
- print("=" * 60)
1047
- print(f"Overall Score: {quality_score:.1f}/100 {quality_grade}")
1048
- print(f"Coverage: {func_coverage:.1f}% {_coverage_bar(func_coverage)}")
1049
- print()
1050
-
1051
- # Detailed breakdown
1052
- print("Quality Breakdown:")
1053
- print(f" Coverage Score: {_coverage_score(func_coverage):.1f}/40")
1054
- print(f" Completeness Score: {_completeness_score(stats):.1f}/30")
1055
- print(f" Richness Score: {_richness_score(stats):.1f}/30")
1056
- print()
1057
-
1058
- # Recommendations
1059
- if quality_score < 50:
1060
- print("💡 Recommendations:")
1061
- print(" • Run 'voodocs instruct' to help your AI assistant add annotations")
1062
- print(" • Focus on adding preconditions and postconditions first")
1063
- print(" • Add complexity annotations to performance-critical functions")
1064
- elif quality_score < 70:
1065
- print("💡 Good progress! To improve further:")
1066
- print(" • Add error_cases to functions that can fail")
1067
- print(" • Document security_implications for sensitive functions")
1068
- print(" • Aim for 80%+ annotation coverage")
1069
- elif quality_score < 90:
1070
- print("🎉 Great work! Your documentation is solid.")
1071
- print(" • Consider adding more detailed error cases")
1072
- print(" • Document edge cases and security considerations")
1073
- else:
1074
- print("✨ Excellent! Your codebase is exceptionally well-documented with VooDocs.")
1075
-
1076
-
1077
- def _calculate_quality_score(stats):
1078
- """Calculate overall documentation quality score (0-100)."""
1079
- total_funcs = stats['total_functions']
1080
- if total_funcs == 0:
1081
- return 0
1082
-
1083
- # Coverage score (40 points)
1084
- coverage = (stats['annotated_functions'] / total_funcs) * 40
1085
-
1086
- # Completeness score (30 points) - how many have both pre and post
1087
- completeness = _completeness_score(stats)
1088
-
1089
- # Richness score (30 points) - usage of advanced fields
1090
- richness = _richness_score(stats)
1091
-
1092
- return min(100, coverage + completeness + richness)
1093
-
1094
-
1095
- def _coverage_score(coverage_pct):
1096
- """Calculate coverage score (0-40)."""
1097
- return min(40, coverage_pct * 0.4)
1098
-
1099
-
1100
- def _completeness_score(stats):
1101
- """Calculate completeness score (0-30)."""
1102
- total_funcs = stats['total_functions']
1103
- if total_funcs == 0:
1104
- return 0
1105
-
1106
- # Functions with both preconditions and postconditions
1107
- pre = stats['annotation_fields']['preconditions']
1108
- post = stats['annotation_fields']['postconditions']
1109
- complete = min(pre, post)
1110
-
1111
- return (complete / total_funcs) * 30
1112
-
1113
-
1114
- def _richness_score(stats):
1115
- """Calculate richness score (0-30)."""
1116
- total_funcs = stats['total_functions']
1117
- if total_funcs == 0:
1118
- return 0
1119
-
1120
- # Advanced fields: complexity, error_cases, security_implications
1121
- complexity = stats['annotation_fields']['complexity']
1122
- errors = stats['annotation_fields']['error_cases']
1123
- security = stats['annotation_fields']['security_implications']
1124
-
1125
- richness = (complexity + errors + security) / (total_funcs * 3) * 30
1126
- return min(30, richness)
1127
-
1128
-
1129
- def _get_quality_grade(score):
1130
- """Get letter grade for quality score."""
1131
- if score >= 90:
1132
- return "🏆 A+"
1133
- elif score >= 80:
1134
- return "🥇 A"
1135
- elif score >= 70:
1136
- return "🥈 B"
1137
- elif score >= 60:
1138
- return "🥉 C"
1139
- elif score >= 50:
1140
- return "📝 D"
1141
- else:
1142
- return "❌ F"
1143
-
1144
-
1145
- def _coverage_bar(percentage):
1146
- """Generate a visual coverage bar."""
1147
- filled = int(percentage / 5) # 20 blocks for 100%
1148
- empty = 20 - filled
1149
-
1150
- bar = "█" * filled + "░" * empty
1151
-
1152
- if percentage >= 80:
1153
- color = "🟢"
1154
- elif percentage >= 50:
1155
- color = "🟡"
1156
- else:
1157
- color = "🔴"
1158
-
1159
- return f"{color} [{bar}] {percentage:.1f}%"
1160
-
1161
-
1162
- def load_config():
1163
- """Load VooDocs configuration."""
1164
- config_file = Path(".voodocs/config.json")
1165
-
1166
- # Default configuration
1167
- default_config = {
1168
- "project_name": Path.cwd().name,
1169
- "language": "python",
1170
- "version": "0.1.0",
1171
- "output_dir": "./voodocs-output",
1172
- "test_framework": "pytest",
1173
- "api_format": "openapi",
1174
- "exclude_patterns": [
1175
- "**/test_*.py",
1176
- "**/node_modules/**",
1177
- "**/__pycache__/**",
1178
- "**/venv/**",
1179
- "**/.venv/**"
1180
- ],
1181
- "include_patterns": [],
1182
- "annotation_coverage_threshold": 50
1183
- }
1184
-
1185
- if config_file.exists():
1186
- import json
1187
- try:
1188
- user_config = json.loads(config_file.read_text())
1189
- # Merge user config with defaults
1190
- default_config.update(user_config)
1191
- except json.JSONDecodeError:
1192
- print("⚠ Warning: Invalid config file, using defaults")
1193
-
1194
- return default_config
1195
-
1196
-
1197
- @track_command_execution("watch")
1198
- def cmd_watch(args):
1199
- """Watch files and automatically regenerate documentation."""
1200
- import time
1201
- import hashlib
1202
-
1203
- print("👀 VooDocs Watch Mode")
1204
- print("=" * 60)
1205
- print(f"📁 Watching: {', '.join(args.paths)}")
1206
- print(f"📁 Output directory: {args.output_dir}")
1207
- print(f"⏱️ Check interval: {args.interval}s")
1208
- print("\nPress Ctrl+C to stop watching...\n")
1209
-
1210
- # Track file modification times and hashes
1211
- file_states = {}
1212
-
1213
- def get_file_hash(filepath):
1214
- """Get MD5 hash of file content."""
1215
- try:
1216
- with open(filepath, 'rb') as f:
1217
- return hashlib.md5(f.read()).hexdigest()
1218
- except Exception:
1219
- return None
1220
-
1221
- def collect_files(paths):
1222
- """Collect all relevant files from paths."""
1223
- files = []
1224
- for path_str in paths:
1225
- path = Path(path_str)
1226
- if path.is_file():
1227
- if path.suffix in ['.py', '.ts', '.js', '.tsx', '.jsx']:
1228
- files.append(path)
1229
- elif path.is_dir():
1230
- for ext in ['.py', '.ts', '.js', '.tsx', '.jsx']:
1231
- files.extend(path.rglob(f'*{ext}'))
1232
- return files
1233
-
1234
- def regenerate(changed_files):
1235
- """Regenerate documentation for changed files."""
1236
- print(f"\n🔄 Changes detected in {len(changed_files)} file(s):")
1237
- for f in changed_files:
1238
- print(f" - {f}")
1239
-
1240
- print("\n🚀 Regenerating...")
1241
-
1242
- # Create a mock args object for cmd_generate
1243
- class GenerateArgs:
1244
- pass
1245
-
1246
- gen_args = GenerateArgs()
1247
- gen_args.paths = [str(f) for f in changed_files]
1248
- gen_args.output_dir = args.output_dir
1249
- gen_args.docs_only = args.docs_only
1250
- gen_args.tests_only = args.tests_only
1251
- gen_args.api_only = args.api_only
1252
- gen_args.verbose = False
1253
- gen_args.debug = False
1254
-
1255
- try:
1256
- cmd_generate(gen_args)
1257
- print(f"✅ Regeneration complete at {time.strftime('%H:%M:%S')}\n")
1258
- except Exception as e:
1259
- print(f"❌ Error during regeneration: {e}\n")
1260
-
1261
- try:
1262
- # Initial scan
1263
- all_files = collect_files(args.paths)
1264
- print(f"📋 Watching {len(all_files)} file(s)...\n")
1265
-
1266
- for filepath in all_files:
1267
- file_states[str(filepath)] = get_file_hash(filepath)
1268
-
1269
- # Watch loop
1270
- while True:
1271
- time.sleep(args.interval)
1272
-
1273
- # Check for changes
1274
- changed_files = []
1275
- current_files = collect_files(args.paths)
1276
-
1277
- for filepath in current_files:
1278
- filepath_str = str(filepath)
1279
- current_hash = get_file_hash(filepath)
1280
-
1281
- # New file or modified file
1282
- if filepath_str not in file_states or file_states[filepath_str] != current_hash:
1283
- changed_files.append(filepath)
1284
- file_states[filepath_str] = current_hash
1285
-
1286
- # Check for deleted files
1287
- current_file_strs = {str(f) for f in current_files}
1288
- deleted_files = [f for f in file_states.keys() if f not in current_file_strs]
1289
-
1290
- if deleted_files:
1291
- print(f"\n🗑️ Deleted file(s):")
1292
- for f in deleted_files:
1293
- print(f" - {f}")
1294
- del file_states[f]
1295
- print()
1296
-
1297
- # Regenerate if changes detected
1298
- if changed_files:
1299
- regenerate(changed_files)
1300
-
1301
- except KeyboardInterrupt:
1302
- print("\n\n👋 Watch mode stopped.")
1303
- sys.exit(0)
1304
- except Exception as e:
1305
- print(f"\n❌ Watch mode error: {e}")
1306
- if args.debug:
1307
- traceback.print_exc()
1308
- sys.exit(1)
1309
-
1310
-
1311
- @track_command_execution("validate")
1312
- def cmd_validate(args):
1313
- """Validate annotation quality and correctness."""
1314
- import json
1315
-
1316
- print("🔍 VooDocs Validate")
1317
- print("=" * 60)
1318
-
1319
- # Collect files
1320
- files = []
1321
- for path_str in args.paths:
1322
- path = Path(path_str)
1323
- if path.is_file():
1324
- files.append(path)
1325
- elif path.is_dir():
1326
- files.extend(path.rglob("*.py"))
1327
- files.extend(path.rglob("*.ts"))
1328
- files.extend(path.rglob("*.js"))
1329
-
1330
- if not files:
1331
- print("❌ No files found")
1332
- sys.exit(1)
1333
-
1334
- print(f"📋 Validating {len(files)} file(s)...\n")
1335
-
1336
- # Validate each file
1337
- parser = AnnotationParser()
1338
- validator = AnnotationValidator()
1339
- all_issues = []
1340
- files_with_issues = 0
1341
-
1342
- for file_path in files:
1343
- try:
1344
- parsed = parser.parse_file(str(file_path))
1345
- issues = validator.validate(parsed)
1346
-
1347
- if issues:
1348
- files_with_issues += 1
1349
- all_issues.extend(issues)
1350
-
1351
- if args.format == "text":
1352
- print(f"\n📄 {file_path}")
1353
- print("-" * 60)
1354
- for issue in issues:
1355
- print(f" {issue}")
1356
-
1357
- except Exception as e:
1358
- if args.format == "text":
1359
- print(f"\n📄 {file_path}")
1360
- print(f" ❌ [ERROR] Failed to parse: {e}")
1361
- all_issues.append({
1362
- "severity": "error",
1363
- "message": f"Failed to parse: {e}",
1364
- "location": str(file_path)
1365
- })
1366
-
1367
- # Print summary
1368
- if args.format == "text":
1369
- print("\n" + "=" * 60)
1370
- print("📊 Validation Summary")
1371
- print("=" * 60)
1372
-
1373
- summary = validator.get_summary()
1374
- print(f"Files validated: {len(files)}")
1375
- print(f"Files with issues: {files_with_issues}")
1376
- print(f"Total issues: {summary['total']}")
1377
- print(f" Errors: {summary['errors']}")
1378
- print(f" Warnings: {summary['warnings']}")
1379
- print(f" Info: {summary['info']}")
1380
- print()
1381
-
1382
- # Exit code
1383
- if validator.has_errors():
1384
- print("❌ Validation failed with errors")
1385
- sys.exit(1)
1386
- elif args.strict and summary['warnings'] > 0:
1387
- print("❌ Validation failed (strict mode: warnings treated as errors)")
1388
- sys.exit(1)
1389
- elif summary['total'] > 0:
1390
- print("⚠️ Validation passed with warnings")
1391
- sys.exit(0)
1392
- else:
1393
- print("✅ Validation passed!")
1394
- sys.exit(0)
1395
-
1396
- else: # JSON format
1397
- output = {
1398
- "files_validated": len(files),
1399
- "files_with_issues": files_with_issues,
1400
- "summary": validator.get_summary(),
1401
- "issues": [
1402
- {
1403
- "severity": issue.severity,
1404
- "message": issue.message,
1405
- "location": issue.location,
1406
- "suggestion": issue.suggestion
1407
- }
1408
- for issue in all_issues
1409
- ]
1410
- }
1411
- print(json.dumps(output, indent=2))
1412
-
1413
- # Exit code
1414
- if validator.has_errors():
1415
- sys.exit(1)
1416
- elif args.strict and validator.get_summary()['warnings'] > 0:
1417
- sys.exit(1)
1418
- else:
1419
- sys.exit(0)
1420
-
1421
-
1422
-
1423
-
1424
- @track_command_execution("check")
1425
- def cmd_check(args):
1426
- """Check annotation coverage and quality (for CI/CD)."""
1427
- print("🔍 VooDocs Check (CI/CD Mode)")
1428
- print("=" * 60)
1429
-
1430
- # Collect files
1431
- files = []
1432
- for path_str in args.paths:
1433
- path = Path(path_str)
1434
- if path.is_file():
1435
- files.append(path)
1436
- elif path.is_dir():
1437
- files.extend(path.rglob("*.py"))
1438
- files.extend(path.rglob("*.ts"))
1439
- files.extend(path.rglob("*.js"))
1440
-
1441
- if not files:
1442
- print("❌ No files found")
1443
- sys.exit(1)
1444
-
1445
- print(f"📋 Checking {len(files)} file(s)...\n")
1446
-
1447
- # Parse all files and calculate metrics
1448
- parser = AnnotationParser()
1449
- validator = AnnotationValidator()
1450
-
1451
- total_functions = 0
1452
- annotated_functions = 0
1453
- total_files = len(files)
1454
- files_with_annotations = 0
1455
- all_issues = []
1456
-
1457
- for file_path in files:
1458
- try:
1459
- parsed = parser.parse_file(str(file_path))
1460
- functions = parsed.get_all_functions()
1461
- total_functions += len(functions)
1462
-
1463
- # Count annotated functions
1464
- for func in functions:
1465
- if func.preconditions or func.postconditions:
1466
- annotated_functions += 1
1467
-
1468
- # Check if file has any annotations
1469
- if parsed.module.module_purpose or functions:
1470
- files_with_annotations += 1
1471
-
1472
- # Validate
1473
- issues = validator.validate(parsed)
1474
- all_issues.extend(issues)
1475
-
1476
- except Exception as e:
1477
- print(f"⚠️ Failed to parse {file_path}: {e}")
1478
-
1479
- # Calculate coverage
1480
- function_coverage = (annotated_functions / total_functions * 100) if total_functions > 0 else 0
1481
- file_coverage = (files_with_annotations / total_files * 100) if total_files > 0 else 0
1482
-
1483
- # Calculate quality score (same as status command)
1484
- summary = validator.get_summary()
1485
- coverage_score = function_coverage * 0.4 # 40 points
1486
- completeness_score = max(0, 30 - summary['warnings'] * 2) # 30 points, -2 per warning
1487
- richness_score = max(0, 30 - summary['info']) # 30 points, -1 per info
1488
- quality_score = coverage_score + completeness_score + richness_score
1489
-
1490
- # Print results
1491
- print("📊 Results")
1492
- print("=" * 60)
1493
- print(f"Files checked: {total_files}")
1494
- print(f"Files with annotations: {files_with_annotations} ({file_coverage:.1f}%)")
1495
- print(f"Functions checked: {total_functions}")
1496
- print(f"Annotated functions: {annotated_functions} ({function_coverage:.1f}%)")
1497
- print(f"Quality score: {quality_score:.1f}/100")
1498
- print()
1499
- print(f"Validation issues:")
1500
- print(f" Errors: {summary['errors']}")
1501
- print(f" Warnings: {summary['warnings']}")
1502
- print(f" Info: {summary['info']}")
1503
- print()
1504
-
1505
- # Check thresholds
1506
- failed = False
1507
-
1508
- if function_coverage < args.min_coverage:
1509
- print(f"❌ Coverage check failed: {function_coverage:.1f}% < {args.min_coverage}%")
1510
- failed = True
1511
- else:
1512
- print(f"✅ Coverage check passed: {function_coverage:.1f}% >= {args.min_coverage}%")
1513
-
1514
- if quality_score < args.min_quality:
1515
- print(f"❌ Quality check failed: {quality_score:.1f} < {args.min_quality}")
1516
- failed = True
1517
- else:
1518
- print(f"✅ Quality check passed: {quality_score:.1f} >= {args.min_quality}")
1519
-
1520
- if validator.has_errors():
1521
- print(f"❌ Validation failed: {summary['errors']} error(s) found")
1522
- failed = True
1523
- else:
1524
- print(f"✅ Validation passed: No errors")
1525
-
1526
- print()
1527
-
1528
- if failed:
1529
- print("❌ Check failed")
1530
- sys.exit(1)
1531
- else:
1532
- print("✅ All checks passed!")
1533
- sys.exit(0)
1534
-
1535
-
1536
-
1537
- @track_command_execution("export")
1538
- def cmd_export(args):
1539
- """Export documentation to different formats."""
1540
- print(f"📤 Exporting to {args.format.upper()}...")
1541
-
1542
- try:
1543
- if args.format == "html":
1544
- exporter = HTMLExporter()
1545
- output_file = exporter.export(args.input_file, args.output)
1546
- print(f"✅ HTML exported to: {output_file}")
1547
-
1548
- elif args.format == "pdf":
1549
- exporter = PDFExporter()
1550
- output_file = exporter.export(args.input_file, args.output)
1551
- print(f"✅ PDF exported to: {output_file}")
1552
-
1553
- except FileNotFoundError as e:
1554
- print(f"❌ Error: {e}")
1555
- sys.exit(1)
1556
-
1557
- except ImportError as e:
1558
- print(f"❌ Error: {e}")
1559
- sys.exit(1)
1560
-
1561
- except Exception as e:
1562
- print(f"❌ Export failed: {e}")
1563
- sys.exit(1)
1564
-
1565
-
1566
- def cmd_telemetry(args):
1567
- """Manage telemetry settings."""
1568
- client = get_telemetry_client()
1569
-
1570
- if args.action == "enable":
1571
- client.enable()
1572
- elif args.action == "disable":
1573
- client.disable()
1574
- elif args.action == "status":
1575
- client.status()
1576
-
1577
-
1578
- def cmd_context(args):
1579
- """Manage project context."""
1580
- from darkarts.context import (
1581
- cmd_context_init,
1582
- cmd_context_view,
1583
- cmd_context_status,
1584
- cmd_context_update,
1585
- cmd_context_sync,
1586
- cmd_context_history,
1587
- cmd_context_diff,
1588
- cmd_context_validate,
1589
- cmd_context_generate,
1590
- cmd_context_query,
1591
- cmd_context_check,
1592
- cmd_context_diagram
1593
- )
1594
-
1595
- if args.context_action == "init":
1596
- force = getattr(args, 'force', False)
1597
- sys.exit(cmd_context_init(force=force))
1598
- elif args.context_action == "view":
1599
- output_file = getattr(args, 'output', None)
1600
- sys.exit(cmd_context_view(output_file=output_file))
1601
- elif args.context_action == "status":
1602
- sys.exit(cmd_context_status())
1603
- elif args.context_action == "update":
1604
- description = getattr(args, 'description', None)
1605
- sys.exit(cmd_context_update(description=description))
1606
- elif args.context_action == "sync":
1607
- code_version = getattr(args, 'code_version', None)
1608
- force = getattr(args, 'force', False)
1609
- sys.exit(cmd_context_sync(code_version=code_version, force=force))
1610
- elif args.context_action == "history":
1611
- sys.exit(cmd_context_history())
1612
- elif args.context_action == "diff":
1613
- version1 = getattr(args, 'version1', None)
1614
- version2 = getattr(args, 'version2', None)
1615
- sys.exit(cmd_context_diff(version1=version1, version2=version2))
1616
- elif args.context_action == "validate":
1617
- check_version = getattr(args, 'check_version', False)
1618
- check_invariants = getattr(args, 'check_invariants', False)
1619
- sys.exit(cmd_context_validate(check_version=check_version, check_invariants=check_invariants))
1620
- elif args.context_action == "generate":
1621
- source_dir = getattr(args, 'source_dir', None)
1622
- update_existing = getattr(args, 'update', False)
1623
- sys.exit(cmd_context_generate(source_dir=source_dir, update_existing=update_existing))
1624
- elif args.context_action == "query":
1625
- query = getattr(args, 'query', '')
1626
- section = getattr(args, 'section', None)
1627
- format_type = getattr(args, 'format', 'text')
1628
- sys.exit(cmd_context_query(query=query, section=section, format=format_type))
1629
- elif args.context_action == "check":
1630
- module_filter = getattr(args, 'module', None)
1631
- invariant_filter = getattr(args, 'invariant', None)
1632
- format_type = getattr(args, 'format', 'text')
1633
- sys.exit(cmd_context_check(module_filter=module_filter, invariant_filter=invariant_filter, output_format=format_type))
1634
- elif args.context_action == "diagram":
1635
- diagram_type = getattr(args, 'type', 'modules')
1636
- output_format = getattr(args, 'format', 'mermaid')
1637
- output_file = getattr(args, 'output', None)
1638
- render_png = getattr(args, 'png', False)
1639
- sys.exit(cmd_context_diagram(diagram_type=diagram_type, output_format=output_format, output_file=output_file, render_png=render_png))
1640
- else:
1641
- print("Usage: voodocs context {init|view|status|update|sync|history|diff|validate|generate|query|check|diagram}")
1642
- sys.exit(1)
1643
-
1644
-
1645
- if __name__ == "__main__":
1646
- main()