@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,618 @@
1
+ """
2
+ DarkArts Annotation Parser
3
+
4
+ Extracts DarkArts annotations from source code in multiple languages.
5
+ """
6
+
7
+ import re
8
+ import ast
9
+ import json
10
+ import subprocess
11
+ from typing import List, Optional, Dict, Any
12
+ from pathlib import Path
13
+
14
+ from .types import (
15
+ ParsedAnnotations,
16
+ ModuleAnnotation,
17
+ ClassAnnotation,
18
+ FunctionAnnotation,
19
+ ComplexityAnnotation,
20
+ ErrorCase,
21
+ StateTransition,
22
+ Language,
23
+ )
24
+
25
+ try:
26
+ from darkarts.exceptions import ParserNotBuiltError
27
+ from darkarts.parser_checker import check_parser_for_language
28
+ except ImportError:
29
+ # Fallback if exceptions module not available
30
+ class ParserNotBuiltError(Exception):
31
+ pass
32
+ def check_parser_for_language(lang):
33
+ return True
34
+
35
+
36
+ class AnnotationParser:
37
+ """Parser for DarkArts annotations."""
38
+
39
+ # Regex patterns for different languages
40
+ PATTERNS = {
41
+ Language.PYTHON: r'"""@voodocs\s*(.*?)\s*"""',
42
+ Language.TYPESCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
43
+ Language.JAVASCRIPT: r'/\*@voodocs\s*(.*?)\s*\*/',
44
+ Language.JAVA: r'/\*@voodocs\s*(.*?)\s*\*/',
45
+ Language.CPP: r'/\*@voodocs\s*(.*?)\s*\*/',
46
+ Language.CSHARP: r'/\*@voodocs\s*(.*?)\s*\*/',
47
+ Language.GO: r'/\*@voodocs\s*(.*?)\s*\*/',
48
+ Language.RUST: r'/\*@voodocs\s*(.*?)\s*\*/',
49
+ }
50
+
51
+ def __init__(self):
52
+ pass
53
+
54
+ def detect_language(self, source_file: str) -> Language:
55
+ """Detect programming language from file extension."""
56
+ ext = Path(source_file).suffix.lower()
57
+
58
+ mapping = {
59
+ '.py': Language.PYTHON,
60
+ '.ts': Language.TYPESCRIPT,
61
+ '.tsx': Language.TYPESCRIPT,
62
+ '.js': Language.JAVASCRIPT,
63
+ '.jsx': Language.JAVASCRIPT,
64
+ '.java': Language.JAVA,
65
+ '.cpp': Language.CPP,
66
+ '.cc': Language.CPP,
67
+ '.cxx': Language.CPP,
68
+ '.h': Language.CPP,
69
+ '.hpp': Language.CPP,
70
+ '.cs': Language.CSHARP,
71
+ '.go': Language.GO,
72
+ '.rs': Language.RUST,
73
+ }
74
+
75
+ return mapping.get(ext, Language.PYTHON)
76
+
77
+ def parse_file(self, source_file: str) -> ParsedAnnotations:
78
+ """Parse annotations from a source file."""
79
+ with open(source_file, 'r', encoding='utf-8') as f:
80
+ source_code = f.read()
81
+
82
+ language = self.detect_language(source_file)
83
+ return self.parse_source(source_code, source_file, language)
84
+
85
+ def parse_source(
86
+ self,
87
+ source_code: str,
88
+ source_file: str = "<string>",
89
+ language: Optional[Language] = None
90
+ ) -> ParsedAnnotations:
91
+ """Parse annotations from source code."""
92
+ if language is None:
93
+ language = self.detect_language(source_file)
94
+
95
+ # Extract all annotation blocks
96
+ pattern = self.PATTERNS[language]
97
+ matches = re.finditer(pattern, source_code, re.DOTALL)
98
+
99
+ annotations = []
100
+ for match in matches:
101
+ annotation_text = match.group(1)
102
+ line_number = source_code[:match.start()].count('\n') + 1
103
+ annotations.append({
104
+ 'text': annotation_text,
105
+ 'line': line_number,
106
+ 'start': match.start(),
107
+ 'end': match.end(),
108
+ })
109
+
110
+ # Parse based on language
111
+ if language == Language.PYTHON:
112
+ return self._parse_python(source_code, source_file, annotations)
113
+ elif language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
114
+ return self._parse_typescript(source_file)
115
+ else:
116
+ return self._parse_generic(source_code, source_file, language, annotations)
117
+
118
+ def _parse_python(
119
+ self,
120
+ source_code: str,
121
+ source_file: str,
122
+ annotations: List[Dict]
123
+ ) -> ParsedAnnotations:
124
+ """Parse Python source code with AST."""
125
+ try:
126
+ tree = ast.parse(source_code)
127
+ except SyntaxError:
128
+ # Fallback to generic parsing
129
+ return self._parse_generic(source_code, source_file, Language.PYTHON, annotations)
130
+
131
+ module = ModuleAnnotation(
132
+ name=Path(source_file).stem,
133
+ source_file=source_file,
134
+ language=Language.PYTHON,
135
+ )
136
+
137
+ # Map annotations to AST nodes
138
+ annotation_map = self._build_annotation_map(source_code, annotations)
139
+
140
+ # Parse module-level annotations (first few lines)
141
+ for line in range(1, 20):
142
+ if line in annotation_map:
143
+ parsed = self._parse_annotation_text(annotation_map[line])
144
+ if 'module_purpose' in parsed or 'dependencies' in parsed or 'assumptions' in parsed:
145
+ self._parse_module_annotation(annotation_map[line], module)
146
+ break
147
+
148
+ # Parse classes and functions
149
+ for node in ast.walk(tree):
150
+ if isinstance(node, ast.ClassDef):
151
+ cls_annotation = self._parse_class_node(node, annotation_map, source_file)
152
+ if cls_annotation:
153
+ module.classes.append(cls_annotation)
154
+
155
+ elif isinstance(node, ast.FunctionDef):
156
+ # Only top-level functions (not methods)
157
+ if isinstance(node, ast.FunctionDef) and node.col_offset == 0:
158
+ func_annotation = self._parse_function_node(node, annotation_map, source_file)
159
+ if func_annotation:
160
+ module.functions.append(func_annotation)
161
+
162
+ return ParsedAnnotations(
163
+ module=module,
164
+ language=Language.PYTHON,
165
+ source_file=source_file,
166
+ )
167
+
168
+ def _build_annotation_map(self, source_code: str, annotations: List[Dict]) -> Dict[int, str]:
169
+ """Build a map from line numbers to annotation text."""
170
+ annotation_map = {}
171
+ for ann in annotations:
172
+ annotation_map[ann['line']] = ann['text']
173
+ return annotation_map
174
+
175
+ def _parse_class_node(
176
+ self,
177
+ node: ast.ClassDef,
178
+ annotation_map: Dict[int, str],
179
+ source_file: str
180
+ ) -> Optional[ClassAnnotation]:
181
+ """Parse a class node."""
182
+ # Look for annotation in docstring (after class definition)
183
+ class_line = node.lineno
184
+ annotation_text = None
185
+
186
+ # Check a few lines after the class definition (docstring)
187
+ for offset in range(1, 10):
188
+ if class_line + offset in annotation_map:
189
+ annotation_text = annotation_map[class_line + offset]
190
+ break
191
+
192
+ if not annotation_text:
193
+ return None
194
+
195
+ cls_annotation = ClassAnnotation(
196
+ name=node.name,
197
+ line_number=class_line,
198
+ source_file=source_file,
199
+ raw_annotation=annotation_text,
200
+ )
201
+
202
+ # Parse class annotation
203
+ parsed = self._parse_annotation_text(annotation_text)
204
+
205
+ if 'class_invariants' in parsed:
206
+ cls_annotation.class_invariants = parsed['class_invariants']
207
+
208
+ if 'state_transitions' in parsed:
209
+ cls_annotation.state_transitions = [
210
+ self._parse_state_transition(st)
211
+ for st in parsed['state_transitions']
212
+ ]
213
+
214
+ # Parse methods
215
+ for item in node.body:
216
+ if isinstance(item, ast.FunctionDef):
217
+ method_annotation = self._parse_function_node(item, annotation_map, source_file)
218
+ if method_annotation:
219
+ cls_annotation.methods.append(method_annotation)
220
+
221
+ return cls_annotation
222
+
223
+ def _parse_function_node(
224
+ self,
225
+ node: ast.FunctionDef,
226
+ annotation_map: Dict[int, str],
227
+ source_file: str
228
+ ) -> Optional[FunctionAnnotation]:
229
+ """Parse a function node."""
230
+ # Look for annotation in docstring (after function definition)
231
+ func_line = node.lineno
232
+ annotation_text = None
233
+
234
+ # Check a few lines after the function definition (docstring)
235
+ for offset in range(1, 10):
236
+ if func_line + offset in annotation_map:
237
+ annotation_text = annotation_map[func_line + offset]
238
+ break
239
+
240
+ if not annotation_text:
241
+ return None
242
+
243
+ func_annotation = FunctionAnnotation(
244
+ name=node.name,
245
+ line_number=func_line,
246
+ source_file=source_file,
247
+ raw_annotation=annotation_text,
248
+ )
249
+
250
+ # Parse function annotation
251
+ parsed = self._parse_annotation_text(annotation_text)
252
+
253
+ if 'solve' in parsed:
254
+ func_annotation.solve = parsed['solve']
255
+
256
+ if 'preconditions' in parsed:
257
+ func_annotation.preconditions = parsed['preconditions']
258
+
259
+ if 'postconditions' in parsed:
260
+ func_annotation.postconditions = parsed['postconditions']
261
+
262
+ if 'invariants' in parsed:
263
+ func_annotation.invariants = parsed['invariants']
264
+
265
+ if 'optimize' in parsed:
266
+ func_annotation.optimize = parsed['optimize']
267
+
268
+ if 'complexity' in parsed:
269
+ func_annotation.complexity = self._parse_complexity(parsed['complexity'])
270
+
271
+ if 'error_cases' in parsed:
272
+ func_annotation.error_cases = [
273
+ self._parse_error_case(ec)
274
+ for ec in parsed['error_cases']
275
+ ]
276
+
277
+ return func_annotation
278
+
279
+ def _parse_module_annotation(self, annotation_text: str, module: ModuleAnnotation):
280
+ """Parse module-level annotation."""
281
+ parsed = self._parse_annotation_text(annotation_text)
282
+
283
+ if 'module_purpose' in parsed:
284
+ module.module_purpose = parsed['module_purpose']
285
+
286
+ if 'dependencies' in parsed:
287
+ module.dependencies = parsed['dependencies']
288
+
289
+ if 'assumptions' in parsed:
290
+ module.assumptions = parsed['assumptions']
291
+
292
+ def _parse_annotation_text(self, text: str) -> Dict[str, Any]:
293
+ """Parse annotation text into structured data."""
294
+ result = {}
295
+
296
+ # Simple key-value parsing
297
+ lines = text.strip().split('\n')
298
+ current_key = None
299
+ current_value = []
300
+
301
+ for line in lines:
302
+ line = line.strip()
303
+ if not line:
304
+ continue
305
+
306
+ # Check if this is a key line
307
+ if ':' in line and not line.startswith('"') and not line.startswith('['):
308
+ # Save previous key-value
309
+ if current_key:
310
+ result[current_key] = self._parse_value(current_key, current_value)
311
+
312
+ # Start new key
313
+ key, value = line.split(':', 1)
314
+ current_key = key.strip()
315
+ current_value = [value.strip()]
316
+ else:
317
+ # Continuation of current value
318
+ if current_key:
319
+ current_value.append(line)
320
+
321
+ # Save last key-value
322
+ if current_key:
323
+ result[current_key] = self._parse_value(current_key, current_value)
324
+
325
+ return result
326
+
327
+ def _parse_value(self, key: str, value_lines: List[str]) -> Any:
328
+ """Parse a value based on its key."""
329
+ text = ' '.join(value_lines).strip()
330
+
331
+ # Remove quotes
332
+ if text.startswith('"') and text.endswith('"'):
333
+ return text[1:-1]
334
+
335
+ # Parse lists
336
+ if text.startswith('['):
337
+ return self._parse_list(text)
338
+
339
+ # Parse objects
340
+ if text.startswith('{'):
341
+ return self._parse_object(text)
342
+
343
+ return text
344
+
345
+ def _parse_list(self, text: str) -> List[str]:
346
+ """Parse a list value."""
347
+ # Remove brackets
348
+ text = text.strip('[]').strip()
349
+
350
+ # Split by commas (simple approach)
351
+ items = []
352
+ current = []
353
+ depth = 0
354
+ in_string = False
355
+
356
+ for char in text:
357
+ if char == '"' and (not current or current[-1] != '\\'):
358
+ in_string = not in_string
359
+ elif char in '{[' and not in_string:
360
+ depth += 1
361
+ elif char in '}]' and not in_string:
362
+ depth -= 1
363
+ elif char == ',' and depth == 0 and not in_string:
364
+ items.append(''.join(current).strip().strip('"'))
365
+ current = []
366
+ continue
367
+
368
+ current.append(char)
369
+
370
+ if current:
371
+ items.append(''.join(current).strip().strip('"'))
372
+
373
+ return items
374
+
375
+ def _parse_object(self, text: str) -> Dict[str, Any]:
376
+ """Parse an object value."""
377
+ # Simple object parsing
378
+ result = {}
379
+ text = text.strip('{}').strip()
380
+
381
+ # Split by commas
382
+ pairs = text.split(',')
383
+ for pair in pairs:
384
+ if ':' in pair:
385
+ key, value = pair.split(':', 1)
386
+ result[key.strip().strip('"')] = value.strip().strip('"')
387
+
388
+ return result
389
+
390
+ def _parse_complexity(self, data: Any) -> ComplexityAnnotation:
391
+ """Parse complexity annotation."""
392
+ if isinstance(data, dict):
393
+ return ComplexityAnnotation(
394
+ time=data.get('time', 'O(1)'),
395
+ space=data.get('space', 'O(1)'),
396
+ best_case=data.get('best_case'),
397
+ worst_case=data.get('worst_case'),
398
+ average_case=data.get('average_case'),
399
+ )
400
+ return ComplexityAnnotation()
401
+
402
+ def _parse_error_case(self, text: str) -> ErrorCase:
403
+ """Parse error case annotation."""
404
+ # Format: "condition → ErrorType"
405
+ if '→' in text:
406
+ condition, error_type = text.split('→', 1)
407
+ return ErrorCase(
408
+ condition=condition.strip(),
409
+ error_type=error_type.strip(),
410
+ )
411
+ return ErrorCase(condition=text, error_type="Error")
412
+
413
+ def _parse_state_transition(self, text: str) -> StateTransition:
414
+ """Parse state transition annotation."""
415
+ # Format: "FROM → TO: condition" or "FROM → TO"
416
+ if ':' in text:
417
+ transition, condition = text.split(':', 1)
418
+ condition = condition.strip()
419
+ else:
420
+ transition = text
421
+ condition = None
422
+
423
+ if '→' in transition:
424
+ from_state, to_state = transition.split('→', 1)
425
+ return StateTransition(
426
+ from_state=from_state.strip(),
427
+ to_state=to_state.strip(),
428
+ condition=condition,
429
+ )
430
+
431
+ return StateTransition(from_state="UNKNOWN", to_state="UNKNOWN")
432
+
433
+ def _parse_generic(
434
+ self,
435
+ source_code: str,
436
+ source_file: str,
437
+ language: Language,
438
+ annotations: List[Dict]
439
+ ) -> ParsedAnnotations:
440
+ """Generic parsing for non-Python languages."""
441
+ module = ModuleAnnotation(
442
+ name=Path(source_file).stem,
443
+ source_file=source_file,
444
+ language=language,
445
+ )
446
+
447
+ # Parse each annotation
448
+ for ann in annotations:
449
+ parsed = self._parse_annotation_text(ann['text'])
450
+
451
+ # Determine annotation type based on content
452
+ if 'module_purpose' in parsed or 'dependencies' in parsed:
453
+ self._parse_module_annotation(ann['text'], module)
454
+ elif 'class_invariants' in parsed:
455
+ # Class annotation
456
+ cls = ClassAnnotation(
457
+ name="UnknownClass",
458
+ line_number=ann['line'],
459
+ source_file=source_file,
460
+ raw_annotation=ann['text'],
461
+ )
462
+ if 'class_invariants' in parsed:
463
+ cls.class_invariants = parsed['class_invariants']
464
+ if 'state_transitions' in parsed:
465
+ cls.state_transitions = [
466
+ self._parse_state_transition(st)
467
+ for st in parsed['state_transitions']
468
+ ]
469
+ module.classes.append(cls)
470
+ else:
471
+ # Function annotation
472
+ func = FunctionAnnotation(
473
+ name="UnknownFunction",
474
+ line_number=ann['line'],
475
+ source_file=source_file,
476
+ raw_annotation=ann['text'],
477
+ )
478
+
479
+ if 'solve' in parsed:
480
+ func.solve = parsed['solve']
481
+ if 'preconditions' in parsed:
482
+ func.preconditions = parsed['preconditions']
483
+ if 'postconditions' in parsed:
484
+ func.postconditions = parsed['postconditions']
485
+ if 'invariants' in parsed:
486
+ func.invariants = parsed['invariants']
487
+ if 'optimize' in parsed:
488
+ func.optimize = parsed['optimize']
489
+ if 'complexity' in parsed:
490
+ func.complexity = self._parse_complexity(parsed['complexity'])
491
+ if 'error_cases' in parsed:
492
+ func.error_cases = [
493
+ self._parse_error_case(ec)
494
+ for ec in parsed['error_cases']
495
+ ]
496
+
497
+ module.functions.append(func)
498
+
499
+ return ParsedAnnotations(
500
+ module=module,
501
+ language=language,
502
+ source_file=source_file,
503
+ )
504
+
505
+ def _parse_typescript(self, source_file: str) -> ParsedAnnotations:
506
+ """Parse TypeScript/JavaScript source code using the TypeScript parser."""
507
+ # Check if TypeScript parser is built
508
+ try:
509
+ check_parser_for_language('typescript')
510
+ except ParserNotBuiltError:
511
+ raise
512
+
513
+ # Get the path to the TypeScript parser CLI
514
+ parser_dir = Path(__file__).parent.parent / 'parsers' / 'typescript'
515
+ parser_cli = parser_dir / 'dist' / 'cli.js'
516
+
517
+ if not parser_cli.exists():
518
+ raise ParserNotBuiltError("TypeScript")
519
+
520
+ # Run the TypeScript parser
521
+ try:
522
+ result = subprocess.run(
523
+ ['node', str(parser_cli), source_file],
524
+ capture_output=True,
525
+ text=True,
526
+ check=True
527
+ )
528
+ data = json.loads(result.stdout)
529
+ except subprocess.CalledProcessError as e:
530
+ raise RuntimeError(f"TypeScript parser failed: {e.stderr}")
531
+ except json.JSONDecodeError as e:
532
+ raise RuntimeError(f"Failed to parse TypeScript parser output: {e}")
533
+
534
+ # Convert the TypeScript parser output to our internal format
535
+ language = Language.TYPESCRIPT if data['language'] == 'typescript' else Language.JAVASCRIPT
536
+
537
+ module = ModuleAnnotation(
538
+ name=Path(source_file).stem,
539
+ source_file=source_file,
540
+ language=language,
541
+ )
542
+
543
+ # Parse module annotations
544
+ if data.get('module_annotations'):
545
+ self._apply_module_annotations(module, data['module_annotations'])
546
+
547
+ # Parse classes
548
+ for cls_data in data.get('classes', []):
549
+ cls = ClassAnnotation(
550
+ name=cls_data['name'],
551
+ line_number=cls_data['line'],
552
+ )
553
+
554
+ if cls_data.get('annotations'):
555
+ self._apply_class_annotations(cls, cls_data['annotations'])
556
+
557
+ # Parse methods
558
+ for method_data in cls_data.get('methods', []):
559
+ method = FunctionAnnotation(
560
+ name=method_data['name'],
561
+ line_number=method_data['line'],
562
+ )
563
+
564
+ if method_data.get('annotations'):
565
+ self._apply_function_annotations(method, method_data['annotations'])
566
+
567
+ cls.methods.append(method)
568
+
569
+ module.classes.append(cls)
570
+
571
+ # Parse functions
572
+ for func_data in data.get('functions', []):
573
+ func = FunctionAnnotation(
574
+ name=func_data['name'],
575
+ line_number=func_data['line'],
576
+ )
577
+
578
+ if func_data.get('annotations'):
579
+ self._apply_function_annotations(func, func_data['annotations'])
580
+
581
+ module.functions.append(func)
582
+
583
+ return ParsedAnnotations(
584
+ module=module,
585
+ language=language,
586
+ source_file=source_file,
587
+ )
588
+
589
+ def _apply_module_annotations(self, module: ModuleAnnotation, annotations: Dict[str, Any]):
590
+ """Apply annotations to a module."""
591
+ if 'module_purpose' in annotations:
592
+ module.purpose = annotations['module_purpose']
593
+ if 'dependencies' in annotations:
594
+ module.dependencies = annotations['dependencies'] if isinstance(annotations['dependencies'], list) else [annotations['dependencies']]
595
+ if 'assumptions' in annotations:
596
+ module.assumptions = annotations['assumptions'] if isinstance(annotations['assumptions'], list) else [annotations['assumptions']]
597
+
598
+ def _apply_class_annotations(self, cls: ClassAnnotation, annotations: Dict[str, Any]):
599
+ """Apply annotations to a class."""
600
+ if 'class_invariants' in annotations:
601
+ cls.invariants = annotations['class_invariants'] if isinstance(annotations['class_invariants'], list) else [annotations['class_invariants']]
602
+ if 'state_transitions' in annotations:
603
+ transitions = annotations['state_transitions']
604
+ if isinstance(transitions, list):
605
+ cls.state_transitions = [self._parse_state_transition(t) for t in transitions]
606
+
607
+ def _apply_function_annotations(self, func: FunctionAnnotation, annotations: Dict[str, Any]):
608
+ """Apply annotations to a function."""
609
+ if 'preconditions' in annotations:
610
+ func.preconditions = annotations['preconditions'] if isinstance(annotations['preconditions'], list) else [annotations['preconditions']]
611
+ if 'postconditions' in annotations:
612
+ func.postconditions = annotations['postconditions'] if isinstance(annotations['postconditions'], list) else [annotations['postconditions']]
613
+ if 'invariants' in annotations:
614
+ func.invariants = annotations['invariants'] if isinstance(annotations['invariants'], list) else [annotations['invariants']]
615
+ if 'complexity' in annotations:
616
+ func.complexity = self._parse_complexity(annotations['complexity'])
617
+ if 'side_effects' in annotations:
618
+ func.side_effects = annotations['side_effects'] if isinstance(annotations['side_effects'], list) else [annotations['side_effects']]