@voodocs/cli 0.1.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 (52) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +153 -0
  3. package/USAGE.md +314 -0
  4. package/cli.py +1340 -0
  5. package/examples/.cursorrules +437 -0
  6. package/examples/instructions/.claude/instructions.md +372 -0
  7. package/examples/instructions/.cursorrules +437 -0
  8. package/examples/instructions/.windsurfrules +437 -0
  9. package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
  10. package/examples/math_example.py +41 -0
  11. package/examples/phase2_test.py +24 -0
  12. package/examples/test_compound_conditions.py +40 -0
  13. package/examples/test_math_example.py +186 -0
  14. package/lib/darkarts/README.md +115 -0
  15. package/lib/darkarts/__init__.py +16 -0
  16. package/lib/darkarts/annotations/__init__.py +34 -0
  17. package/lib/darkarts/annotations/parser.py +618 -0
  18. package/lib/darkarts/annotations/types.py +181 -0
  19. package/lib/darkarts/cli.py +128 -0
  20. package/lib/darkarts/core/__init__.py +32 -0
  21. package/lib/darkarts/core/interface.py +256 -0
  22. package/lib/darkarts/core/loader.py +231 -0
  23. package/lib/darkarts/core/plugin.py +215 -0
  24. package/lib/darkarts/core/registry.py +146 -0
  25. package/lib/darkarts/exceptions.py +51 -0
  26. package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
  27. package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
  28. package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
  29. package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
  30. package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
  31. package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
  32. package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
  33. package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
  34. package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
  35. package/lib/darkarts/parsers/typescript/package.json +19 -0
  36. package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
  37. package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
  38. package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
  39. package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
  40. package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
  41. package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
  42. package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
  43. package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
  44. package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
  45. package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
  46. package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
  47. package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
  48. package/package.json +70 -0
  49. package/requirements.txt +13 -0
  50. package/templates/ci/github-actions.yml +73 -0
  51. package/templates/ci/gitlab-ci.yml +35 -0
  52. package/templates/ci/pre-commit-hook.sh +26 -0
@@ -0,0 +1,486 @@
1
+ """
2
+ DarkArts API Specification Generator
3
+
4
+ Generates OpenAPI/Swagger specifications from DarkArts annotations.
5
+ """
6
+
7
+ from typing import List, Optional, Dict, Any
8
+ from pathlib import Path
9
+ import json
10
+ import yaml
11
+
12
+ from darkarts.annotations.types import (
13
+ ParsedAnnotations,
14
+ FunctionAnnotation,
15
+ ClassAnnotation,
16
+ )
17
+
18
+
19
+ class APISpecGenerator:
20
+ """Generates API specifications from DarkArts annotations."""
21
+
22
+ def __init__(self, format: str = "openapi"):
23
+ """
24
+ Initialize API spec generator.
25
+
26
+ Args:
27
+ format: Specification format ("openapi", "swagger", "graphql")
28
+ """
29
+ self.format = format
30
+
31
+ def generate(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
32
+ """Generate API specification from parsed annotations."""
33
+ if self.format == "openapi":
34
+ return self._generate_openapi(parsed, output_file)
35
+ elif self.format == "swagger":
36
+ return self._generate_openapi(parsed, output_file) # Same as OpenAPI
37
+ elif self.format == "graphql":
38
+ return self._generate_graphql(parsed, output_file)
39
+ else:
40
+ raise ValueError(f"Unsupported format: {self.format}")
41
+
42
+ def _generate_openapi(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
43
+ """Generate OpenAPI 3.0 specification."""
44
+ spec = {
45
+ "openapi": "3.0.0",
46
+ "info": {
47
+ "title": parsed.module.name.replace("_", " ").title() + " API",
48
+ "version": "1.0.0",
49
+ "description": parsed.module.module_purpose or "API generated from DarkArts annotations"
50
+ },
51
+ "paths": {},
52
+ "components": {
53
+ "schemas": {}
54
+ }
55
+ }
56
+
57
+ # Add dependencies as external docs
58
+ if parsed.module.dependencies:
59
+ spec["externalDocs"] = {
60
+ "description": "Dependencies",
61
+ "url": "#"
62
+ }
63
+
64
+ # Generate paths from functions
65
+ all_functions = parsed.get_all_functions()
66
+ for func in all_functions:
67
+ path = f"/{func.name}"
68
+ spec["paths"][path] = self._generate_openapi_path(func)
69
+
70
+ # Generate schemas from classes
71
+ for cls in parsed.module.classes:
72
+ spec["components"]["schemas"][cls.name] = self._generate_openapi_schema(cls)
73
+
74
+ # Convert to YAML (more readable than JSON for API specs)
75
+ spec_yaml = yaml.dump(spec, default_flow_style=False, sort_keys=False)
76
+
77
+ if output_file:
78
+ output_path = Path(output_file)
79
+ output_path.parent.mkdir(parents=True, exist_ok=True)
80
+
81
+ if output_file.endswith('.json'):
82
+ output_path.write_text(json.dumps(spec, indent=2), encoding='utf-8')
83
+ else:
84
+ output_path.write_text(spec_yaml, encoding='utf-8')
85
+
86
+ return spec_yaml
87
+
88
+ def _generate_openapi_path(self, func: FunctionAnnotation) -> Dict[str, Any]:
89
+ """Generate OpenAPI path item for a function."""
90
+ operation = {
91
+ "post": {
92
+ "summary": func.solve or f"Execute {func.name}",
93
+ "description": self._build_function_description(func),
94
+ "requestBody": {
95
+ "required": True,
96
+ "content": {
97
+ "application/json": {
98
+ "schema": {
99
+ "type": "object",
100
+ "properties": self._extract_parameters_from_preconditions(func)
101
+ },
102
+ "examples": {
103
+ "example1": {
104
+ "summary": "Example request",
105
+ "value": self._generate_request_example(func)
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "responses": {
112
+ "200": {
113
+ "description": "Successful operation",
114
+ "content": {
115
+ "application/json": {
116
+ "schema": {
117
+ "type": "object",
118
+ "properties": self._extract_response_from_postconditions(func)
119
+ },
120
+ "examples": {
121
+ "example1": {
122
+ "summary": "Example response",
123
+ "value": self._generate_response_example(func)
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ # Add error responses
134
+ if func.error_cases:
135
+ for error in func.error_cases:
136
+ error_str = str(error)
137
+ if '→' in error_str:
138
+ condition, error_type = error_str.split('→')
139
+ error_type = error_type.strip()
140
+
141
+ # Map error types to HTTP status codes
142
+ status_code = self._map_error_to_status_code(error_type)
143
+ operation["post"]["responses"][str(status_code)] = {
144
+ "description": error_type,
145
+ "content": {
146
+ "application/json": {
147
+ "schema": {
148
+ "type": "object",
149
+ "properties": {
150
+ "error": {"type": "string"},
151
+ "message": {"type": "string"}
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ # Add performance info
159
+ if func.complexity:
160
+ operation["post"]["x-complexity"] = {
161
+ "time": func.complexity.time,
162
+ "space": func.complexity.space
163
+ }
164
+
165
+ if func.optimize:
166
+ operation["post"]["x-optimization"] = func.optimize
167
+
168
+ return operation
169
+
170
+ def _generate_openapi_schema(self, cls: ClassAnnotation) -> Dict[str, Any]:
171
+ """Generate OpenAPI schema for a class."""
172
+ schema = {
173
+ "type": "object",
174
+ "description": f"{cls.name} class",
175
+ "properties": {},
176
+ "x-invariants": cls.class_invariants or []
177
+ }
178
+
179
+ if cls.state_transitions:
180
+ schema["x-state-transitions"] = [str(st) for st in cls.state_transitions]
181
+
182
+ # Add methods as operations
183
+ if cls.methods:
184
+ schema["x-methods"] = [
185
+ {
186
+ "name": method.name,
187
+ "description": method.solve or "",
188
+ "preconditions": method.preconditions or [],
189
+ "postconditions": method.postconditions or []
190
+ }
191
+ for method in cls.methods
192
+ ]
193
+
194
+ return schema
195
+
196
+ def _build_function_description(self, func: FunctionAnnotation) -> str:
197
+ """Build detailed description for a function."""
198
+ parts = []
199
+
200
+ if func.solve:
201
+ parts.append(func.solve)
202
+
203
+ if func.preconditions:
204
+ parts.append("\n\n**Preconditions:**")
205
+ for pre in func.preconditions:
206
+ parts.append(f"- {pre}")
207
+
208
+ if func.postconditions:
209
+ parts.append("\n\n**Postconditions:**")
210
+ for post in func.postconditions:
211
+ parts.append(f"- {post}")
212
+
213
+ if func.invariants:
214
+ parts.append("\n\n**Invariants:**")
215
+ for inv in func.invariants:
216
+ parts.append(f"- {inv}")
217
+
218
+ return "\n".join(parts)
219
+
220
+ def _extract_parameters_from_preconditions(self, func: FunctionAnnotation) -> Dict[str, Any]:
221
+ """Extract parameter definitions from preconditions with enhanced type inference."""
222
+ import re
223
+ params = {}
224
+
225
+ if func.preconditions:
226
+ for pre in func.preconditions:
227
+ # Extract parameter name and infer type
228
+ param_info = self._infer_param_from_condition(pre)
229
+ if param_info:
230
+ param_name, param_schema = param_info
231
+ if param_name not in params:
232
+ params[param_name] = param_schema
233
+ else:
234
+ # Merge constraints
235
+ if 'minimum' in param_schema and 'minimum' not in params[param_name]:
236
+ params[param_name]['minimum'] = param_schema['minimum']
237
+ if 'maximum' in param_schema and 'maximum' not in params[param_name]:
238
+ params[param_name]['maximum'] = param_schema['maximum']
239
+ if 'enum' in param_schema:
240
+ params[param_name]['enum'] = param_schema['enum']
241
+
242
+ # If no parameters extracted, add a generic one
243
+ if not params:
244
+ params["input"] = {
245
+ "type": "object",
246
+ "description": "Input parameters"
247
+ }
248
+
249
+ return params
250
+
251
+ def _infer_param_from_condition(self, condition: str) -> Optional[tuple]:
252
+ """Infer parameter name and schema from a single condition."""
253
+ import re
254
+
255
+ # Pattern: "x > 0" or "x >= 0"
256
+ if match := re.match(r'(\w+)\s*>=?\s*(\d+)', condition):
257
+ param_name = match.group(1)
258
+ min_val = int(match.group(2))
259
+ return (param_name, {
260
+ "type": "integer",
261
+ "minimum": min_val if '>=' in condition else min_val + 1,
262
+ "description": f"Must satisfy: {condition}"
263
+ })
264
+
265
+ # Pattern: "x < 100" or "x <= 100"
266
+ if match := re.match(r'(\w+)\s*<=?\s*(\d+)', condition):
267
+ param_name = match.group(1)
268
+ max_val = int(match.group(2))
269
+ return (param_name, {
270
+ "type": "integer",
271
+ "maximum": max_val if '<=' in condition else max_val - 1,
272
+ "description": f"Must satisfy: {condition}"
273
+ })
274
+
275
+ # Pattern: "x in [1, 2, 3]"
276
+ if match := re.match(r'(\w+)\s+in\s+\[(.+?)\]', condition):
277
+ param_name = match.group(1)
278
+ values = [v.strip().strip('"\"') for v in match.group(2).split(',')]
279
+ # Try to infer if values are integers
280
+ try:
281
+ enum_values = [int(v) for v in values]
282
+ param_type = "integer"
283
+ except ValueError:
284
+ enum_values = values
285
+ param_type = "string"
286
+
287
+ return (param_name, {
288
+ "type": param_type,
289
+ "enum": enum_values,
290
+ "description": f"Must satisfy: {condition}"
291
+ })
292
+
293
+ # Pattern: "x is string" or "x must be string"
294
+ if match := re.search(r'(\w+).*(?:is|must be).*string', condition, re.IGNORECASE):
295
+ param_name = match.group(1)
296
+ return (param_name, {
297
+ "type": "string",
298
+ "description": f"Must satisfy: {condition}"
299
+ })
300
+
301
+ # Pattern: "x is list" or "x is array"
302
+ if match := re.search(r'(\w+).*(?:is|must be).*(list|array)', condition, re.IGNORECASE):
303
+ param_name = match.group(1)
304
+ return (param_name, {
305
+ "type": "array",
306
+ "items": {"type": "string"},
307
+ "description": f"Must satisfy: {condition}"
308
+ })
309
+
310
+ # Pattern: "x is boolean"
311
+ if match := re.search(r'(\w+).*(?:is|must be).*bool', condition, re.IGNORECASE):
312
+ param_name = match.group(1)
313
+ return (param_name, {
314
+ "type": "boolean",
315
+ "description": f"Must satisfy: {condition}"
316
+ })
317
+
318
+ # Pattern: "len(x) > 0" or "length(x) > 0"
319
+ if match := re.search(r'len(?:gth)?\((\w+)\)\s*>\s*(\d+)', condition):
320
+ param_name = match.group(1)
321
+ min_length = int(match.group(2)) + 1
322
+ return (param_name, {
323
+ "type": "string",
324
+ "minLength": min_length,
325
+ "description": f"Must satisfy: {condition}"
326
+ })
327
+
328
+ # Mathematical set notation: "x ∈ ℝ" or "x ∈ ℤ"
329
+ if match := re.search(r'(\w+)\s*∈\s*([ℝℤℕ])', condition):
330
+ param_name = match.group(1)
331
+ set_symbol = match.group(2)
332
+ param_type = "number" if set_symbol == 'ℝ' else "integer"
333
+ return (param_name, {
334
+ "type": param_type,
335
+ "description": f"Must satisfy: {condition}"
336
+ })
337
+
338
+ # Default: try to extract parameter name
339
+ if match := re.match(r'(\w+)', condition):
340
+ param_name = match.group(1)
341
+ return (param_name, {
342
+ "type": "string",
343
+ "description": f"Must satisfy: {condition}"
344
+ })
345
+
346
+ return None
347
+
348
+ def _extract_response_from_postconditions(self, func: FunctionAnnotation) -> Dict[str, Any]:
349
+ """Extract response schema from postconditions."""
350
+ response = {
351
+ "result": {
352
+ "type": "object",
353
+ "description": "Operation result"
354
+ }
355
+ }
356
+
357
+ if func.postconditions:
358
+ response["result"]["x-postconditions"] = func.postconditions
359
+
360
+ return response
361
+
362
+ def _map_error_to_status_code(self, error_type: str) -> int:
363
+ """Map error type to HTTP status code."""
364
+ error_mapping = {
365
+ "ValueError": 400,
366
+ "ValidationError": 400,
367
+ "InvalidInputError": 400,
368
+ "InsufficientFundsError": 400,
369
+ "AccountInactiveError": 403,
370
+ "UnauthorizedError": 401,
371
+ "ForbiddenError": 403,
372
+ "NotFoundError": 404,
373
+ "ConflictError": 409,
374
+ "InternalError": 500,
375
+ "TimeoutError": 504,
376
+ }
377
+
378
+ return error_mapping.get(error_type, 400)
379
+
380
+ def _generate_graphql(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
381
+ """Generate GraphQL schema."""
382
+ lines = []
383
+
384
+ # Schema header
385
+ lines.append('"""')
386
+ lines.append(f"{parsed.module.name.replace('_', ' ').title()} API")
387
+ lines.append("Generated from DarkArts annotations")
388
+ lines.append('"""')
389
+ lines.append("")
390
+
391
+ # Generate types from classes
392
+ for cls in parsed.module.classes:
393
+ lines.append(f"type {cls.name} {{")
394
+ lines.append(f' """')
395
+ if cls.class_invariants:
396
+ lines.append(" Invariants:")
397
+ for inv in cls.class_invariants:
398
+ lines.append(f" - {inv}")
399
+ lines.append(' """')
400
+ lines.append(" id: ID!")
401
+ lines.append(" # TODO: Add fields based on class definition")
402
+ lines.append("}")
403
+ lines.append("")
404
+
405
+ # Generate queries and mutations from functions
406
+ lines.append("type Query {")
407
+ all_functions = parsed.get_all_functions()
408
+ for func in all_functions:
409
+ if func.name.startswith("get_") or func.name.startswith("list_"):
410
+ lines.append(f' {func.name}: String # {func.solve or ""}')
411
+ lines.append("}")
412
+ lines.append("")
413
+
414
+ lines.append("type Mutation {")
415
+ for func in all_functions:
416
+ if not (func.name.startswith("get_") or func.name.startswith("list_")):
417
+ lines.append(f' {func.name}: String # {func.solve or ""}')
418
+ lines.append("}")
419
+ lines.append("")
420
+
421
+ schema = "\n".join(lines)
422
+
423
+ if output_file:
424
+ output_path = Path(output_file)
425
+ output_path.parent.mkdir(parents=True, exist_ok=True)
426
+ output_path.write_text(schema, encoding='utf-8')
427
+
428
+ return schema
429
+
430
+ def _generate_request_example(self, func: FunctionAnnotation) -> Dict[str, Any]:
431
+ """Generate example request from preconditions."""
432
+ import re
433
+ example = {}
434
+
435
+ if func.preconditions:
436
+ for pre in func.preconditions:
437
+ # Pattern: "x > 0" → x = 1
438
+ if match := re.match(r'(\w+)\s*>\s*(\d+)', pre):
439
+ param_name = match.group(1)
440
+ min_val = int(match.group(2))
441
+ example[param_name] = min_val + 1
442
+
443
+ # Pattern: "x >= 0" → x = 0
444
+ elif match := re.match(r'(\w+)\s*>=\s*(\d+)', pre):
445
+ param_name = match.group(1)
446
+ min_val = int(match.group(2))
447
+ example[param_name] = min_val
448
+
449
+ # Pattern: "x in [1, 2, 3]" → x = 1
450
+ elif match := re.match(r'(\w+)\s+in\s+\[(.+?)\]', pre):
451
+ param_name = match.group(1)
452
+ values = [v.strip().strip('"\'') for v in match.group(2).split(',')]
453
+ try:
454
+ example[param_name] = int(values[0])
455
+ except ValueError:
456
+ example[param_name] = values[0]
457
+
458
+ # Pattern: "x is string" → x = "example"
459
+ elif match := re.search(r'(\w+).*(?:is|must be).*string', pre, re.IGNORECASE):
460
+ param_name = match.group(1)
461
+ example[param_name] = "example"
462
+
463
+ # Pattern: "x is list" → x = []
464
+ elif match := re.search(r'(\w+).*(?:is|must be).*(list|array)', pre, re.IGNORECASE):
465
+ param_name = match.group(1)
466
+ example[param_name] = ["item1", "item2"]
467
+
468
+ # Pattern: "x is boolean" → x = true
469
+ elif match := re.search(r'(\w+).*(?:is|must be).*bool', pre, re.IGNORECASE):
470
+ param_name = match.group(1)
471
+ example[param_name] = True
472
+
473
+ return example if example else {"input": "example"}
474
+
475
+ def _generate_response_example(self, func: FunctionAnnotation) -> Dict[str, Any]:
476
+ """Generate example response from postconditions."""
477
+ example = {"result": "success"}
478
+
479
+ if func.postconditions:
480
+ # Try to infer result structure from postconditions
481
+ for post in func.postconditions:
482
+ if "result" in post.lower():
483
+ example["result"] = "computed_value"
484
+ break
485
+
486
+ return example