@voodocs/cli 2.5.2 → 3.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.
@@ -0,0 +1,486 @@
1
+ """@darkarts
2
+ ⊢{darkarts:parser-v3}
3
+ ∂{re,typing,dataclasses}
4
+ ⚠{@darkarts∈docstrings,unicode-support}
5
+ ⊨{∀parse→structured-output,¬modify-src,handle-errors}
6
+ ⊕{validates annotations}
7
+ ⚡{O(n)|n=annotation-length}
8
+ 🔒{read-only}
9
+ """
10
+
11
+ """
12
+ DarkArts v3.0.0 - Enhanced Parser
13
+
14
+ Parses @darkarts symbolic annotations with support for:
15
+ - Pattern shortcuts (:uuid, :email, ≥N, etc.)
16
+ - Enhanced abbreviations (200+ terms)
17
+ - New symbols (⇄, ⊕, ⊗, ≈, ∴, ∀, ∃)
18
+ - Better validation and error messages
19
+ """
20
+
21
+ import re
22
+ from typing import Dict, List, Optional, Any, Tuple
23
+ from dataclasses import dataclass, field
24
+
25
+ from .darkarts_symbols import (
26
+ ALL_SYMBOLS,
27
+ get_symbol_name,
28
+ get_symbol_description,
29
+ is_valid_symbol,
30
+ get_symbol_rules,
31
+ )
32
+ from .darkarts_patterns import (
33
+ compress_pattern,
34
+ expand_pattern,
35
+ validate_pattern,
36
+ )
37
+ from .darkarts_abbreviations import (
38
+ compress,
39
+ expand,
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class DarkArtsAnnotation:
45
+ """Parsed DarkArts annotation with v3.0.0 features."""
46
+
47
+ # Core fields (v2.x)
48
+ purpose: Optional[str] = None # ⊢
49
+ dependencies: List[str] = field(default_factory=list) # ∂
50
+ assumptions: List[str] = field(default_factory=list) # ⚠
51
+ preconditions: List[str] = field(default_factory=list) # ⊳
52
+ postconditions: List[str] = field(default_factory=list) # ⊲
53
+ invariants: List[str] = field(default_factory=list) # ⊨
54
+ complexity: Optional[str] = None # ⚡
55
+ security: List[str] = field(default_factory=list) # 🔒
56
+
57
+ # New fields (v3.0.0)
58
+ bidirectional: List[str] = field(default_factory=list) # ⇄
59
+ side_effects: List[str] = field(default_factory=list) # ⊕
60
+ forbidden: List[str] = field(default_factory=list) # ⊗
61
+ approximation: List[str] = field(default_factory=list) # ≈
62
+ consequence: List[str] = field(default_factory=list) # ∴
63
+ universal: List[str] = field(default_factory=list) # ∀
64
+ existential: List[str] = field(default_factory=list) # ∃
65
+
66
+ # Metadata
67
+ raw_text: str = ""
68
+ line_number: int = 0
69
+ validation_errors: List[str] = field(default_factory=list)
70
+ validation_warnings: List[str] = field(default_factory=list)
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ """Convert to dictionary."""
74
+ return {
75
+ 'purpose': self.purpose,
76
+ 'dependencies': self.dependencies,
77
+ 'assumptions': self.assumptions,
78
+ 'preconditions': self.preconditions,
79
+ 'postconditions': self.postconditions,
80
+ 'invariants': self.invariants,
81
+ 'complexity': self.complexity,
82
+ 'security': self.security,
83
+ 'bidirectional': self.bidirectional,
84
+ 'side_effects': self.side_effects,
85
+ 'forbidden': self.forbidden,
86
+ 'approximation': self.approximation,
87
+ 'consequence': self.consequence,
88
+ 'universal': self.universal,
89
+ 'existential': self.existential,
90
+ }
91
+
92
+ def has_errors(self) -> bool:
93
+ """Check if annotation has validation errors."""
94
+ return len(self.validation_errors) > 0
95
+
96
+ def has_warnings(self) -> bool:
97
+ """Check if annotation has validation warnings."""
98
+ return len(self.validation_warnings) > 0
99
+
100
+
101
+ class DarkArtsParserV3:
102
+ """
103
+ Enhanced parser for DarkArts v3.0.0 annotations.
104
+
105
+ Features:
106
+ - Pattern shortcuts (:uuid, :email, ≥N, etc.)
107
+ - Enhanced abbreviations (200+ terms)
108
+ - New symbols (⇄, ⊕, ⊗, ≈, ∴, ∀, ∃)
109
+ - Better validation and error messages
110
+ """
111
+
112
+ # Pattern to match @darkarts annotations
113
+ PATTERN = r'/\*\*@darkarts\s*(.*?)\s*\*/'
114
+ PATTERN_MULTILINE = r'"""@darkarts\s*(.*?)\s*"""'
115
+
116
+ # Symbol to field mapping
117
+ SYMBOL_FIELDS = {
118
+ '⊢': 'purpose',
119
+ '∂': 'dependencies',
120
+ '⚠': 'assumptions',
121
+ '⊳': 'preconditions',
122
+ '⊲': 'postconditions',
123
+ '⊨': 'invariants',
124
+ '⚡': 'complexity',
125
+ '🔒': 'security',
126
+ '⇄': 'bidirectional',
127
+ '⊕': 'side_effects',
128
+ '⊗': 'forbidden',
129
+ '≈': 'approximation',
130
+ '∴': 'consequence',
131
+ '∀': 'universal',
132
+ '∃': 'existential',
133
+ }
134
+
135
+ def __init__(self, enable_compression: bool = False, enable_validation: bool = True):
136
+ """
137
+ Initialize parser.
138
+
139
+ Args:
140
+ enable_compression: Enable abbreviation compression
141
+ enable_validation: Enable validation checks
142
+ """
143
+ self.enable_compression = enable_compression
144
+ self.enable_validation = enable_validation
145
+
146
+ def parse(self, source_code: str) -> List[DarkArtsAnnotation]:
147
+ """
148
+ Parse all @darkarts annotations from source code.
149
+
150
+ Args:
151
+ source_code: Source code containing @darkarts annotations
152
+
153
+ Returns:
154
+ List of parsed annotations
155
+ """
156
+ annotations = []
157
+
158
+ # Try both patterns (JavaScript-style and Python-style)
159
+ patterns = [
160
+ (self.PATTERN, re.DOTALL),
161
+ (self.PATTERN_MULTILINE, re.DOTALL),
162
+ ]
163
+
164
+ for pattern, flags in patterns:
165
+ matches = re.finditer(pattern, source_code, flags)
166
+
167
+ for match in matches:
168
+ annotation_text = match.group(1)
169
+ line_number = source_code[:match.start()].count('\n') + 1
170
+ annotation = self.parse_annotation(annotation_text, line_number)
171
+ annotations.append(annotation)
172
+
173
+ return annotations
174
+
175
+ def parse_annotation(self, text: str, line_number: int = 0) -> DarkArtsAnnotation:
176
+ """
177
+ Parse a single @darkarts annotation.
178
+
179
+ Args:
180
+ text: Annotation text (without @darkarts marker)
181
+ line_number: Line number in source file
182
+
183
+ Returns:
184
+ Parsed annotation
185
+ """
186
+ annotation = DarkArtsAnnotation(raw_text=text, line_number=line_number)
187
+
188
+ # Split into lines
189
+ lines = text.strip().split('\n')
190
+
191
+ # Track which symbols we've seen
192
+ seen_symbols = set()
193
+
194
+ for line_idx, line in enumerate(lines):
195
+ line = line.strip()
196
+ if not line:
197
+ continue
198
+
199
+ # Find symbol at start of line
200
+ symbol = self._extract_symbol(line)
201
+
202
+ if symbol:
203
+ # Validate symbol
204
+ if not is_valid_symbol(symbol):
205
+ annotation.validation_errors.append(
206
+ f"Line {line_number + line_idx}: Unknown symbol '{symbol}'"
207
+ )
208
+ continue
209
+
210
+ # Check for duplicate symbols (if not allowed)
211
+ rules = get_symbol_rules(symbol)
212
+ if not rules.get('multiple', False) and symbol in seen_symbols:
213
+ annotation.validation_errors.append(
214
+ f"Line {line_number + line_idx}: Duplicate symbol '{symbol}'. "
215
+ f"Only one {get_symbol_name(symbol)} allowed."
216
+ )
217
+
218
+ seen_symbols.add(symbol)
219
+
220
+ # Extract content after symbol
221
+ content = line[len(symbol):].strip()
222
+
223
+ # Remove surrounding braces if present
224
+ if content.startswith('{') and content.endswith('}'):
225
+ content = content[1:-1]
226
+
227
+ # Parse field
228
+ self._parse_field(annotation, symbol, content, line_number + line_idx)
229
+
230
+ # Validate annotation
231
+ if self.enable_validation:
232
+ self._validate_annotation(annotation)
233
+
234
+ return annotation
235
+
236
+ def _extract_symbol(self, line: str) -> Optional[str]:
237
+ """Extract symbol from start of line."""
238
+ for symbol in ALL_SYMBOLS.keys():
239
+ if line.startswith(symbol):
240
+ return symbol
241
+ return None
242
+
243
+ def _parse_field(self, annotation: DarkArtsAnnotation, symbol: str,
244
+ content: str, line_number: int):
245
+ """
246
+ Parse a field and update annotation.
247
+
248
+ Args:
249
+ annotation: Annotation to update
250
+ symbol: Symbol (⊢, ∂, etc.)
251
+ content: Content to parse
252
+ line_number: Line number for error messages
253
+ """
254
+ field_name = self.SYMBOL_FIELDS.get(symbol)
255
+ if not field_name:
256
+ return
257
+
258
+ # Apply compression if enabled
259
+ if self.enable_compression:
260
+ content = compress(content)
261
+
262
+ # Parse based on field type
263
+ if field_name == 'purpose':
264
+ annotation.purpose = content
265
+
266
+ elif field_name == 'complexity':
267
+ annotation.complexity = content
268
+
269
+ elif field_name in ['dependencies', 'assumptions', 'preconditions',
270
+ 'postconditions', 'invariants', 'security',
271
+ 'bidirectional', 'side_effects', 'forbidden',
272
+ 'approximation', 'consequence', 'universal', 'existential']:
273
+ # Parse as list
274
+ items = self._parse_list(content)
275
+
276
+ # Validate patterns in preconditions
277
+ if field_name == 'preconditions' and self.enable_validation:
278
+ for item in items:
279
+ # Check if item contains a pattern
280
+ if ':' in item or any(op in item for op in ['≥', '≤', '>', '<', '∈', '[']):
281
+ is_valid, error = validate_pattern(item)
282
+ if not is_valid:
283
+ annotation.validation_errors.append(
284
+ f"Line {line_number}: {error}"
285
+ )
286
+
287
+ setattr(annotation, field_name, items)
288
+
289
+ def _parse_list(self, content: str) -> List[str]:
290
+ """
291
+ Parse comma-separated list.
292
+
293
+ Args:
294
+ content: Content like "a,b,c" or "{a,b,c}"
295
+
296
+ Returns:
297
+ List of items
298
+ """
299
+ # Split by comma, respecting nested braces
300
+ items = []
301
+ current = []
302
+ depth = 0
303
+
304
+ for char in content:
305
+ if char == '{':
306
+ depth += 1
307
+ elif char == '}':
308
+ depth -= 1
309
+ elif char == ',' and depth == 0:
310
+ item = ''.join(current).strip()
311
+ if item:
312
+ items.append(item)
313
+ current = []
314
+ continue
315
+
316
+ current.append(char)
317
+
318
+ # Add last item
319
+ item = ''.join(current).strip()
320
+ if item:
321
+ items.append(item)
322
+
323
+ return items
324
+
325
+ def _validate_annotation(self, annotation: DarkArtsAnnotation):
326
+ """
327
+ Validate annotation and add warnings/errors.
328
+
329
+ Args:
330
+ annotation: Annotation to validate
331
+ """
332
+ # Check required fields
333
+ if not annotation.purpose:
334
+ annotation.validation_errors.append(
335
+ f"Line {annotation.line_number}: Missing required purpose (⊢)"
336
+ )
337
+
338
+ # Check for contradictions
339
+ self._check_contradictions(annotation)
340
+
341
+ # Check for missing common fields
342
+ self._check_missing_fields(annotation)
343
+
344
+ def _check_contradictions(self, annotation: DarkArtsAnnotation):
345
+ """Check for logical contradictions."""
346
+ # Check invariants vs side effects
347
+ if annotation.invariants and annotation.side_effects:
348
+ for invariant in annotation.invariants:
349
+ if 'no db mod' in invariant or 'no modification' in invariant:
350
+ for effect in annotation.side_effects:
351
+ if 'db' in effect or 'database' in effect:
352
+ annotation.validation_errors.append(
353
+ f"Line {annotation.line_number}: Contradiction - "
354
+ f"invariant says no db modification, but side effect modifies db"
355
+ )
356
+
357
+ # Check forbidden vs side effects
358
+ if annotation.forbidden and annotation.side_effects:
359
+ for forbidden in annotation.forbidden:
360
+ for effect in annotation.side_effects:
361
+ # Simple overlap check
362
+ forbidden_words = set(forbidden.lower().split())
363
+ effect_words = set(effect.lower().split())
364
+ if forbidden_words & effect_words:
365
+ annotation.validation_warnings.append(
366
+ f"Line {annotation.line_number}: Possible contradiction - "
367
+ f"forbidden operation '{forbidden}' but side effect '{effect}'"
368
+ )
369
+
370
+ def _check_missing_fields(self, annotation: DarkArtsAnnotation):
371
+ """Check for commonly missing fields."""
372
+ # If has side effects, should probably have security considerations
373
+ if annotation.side_effects and not annotation.security:
374
+ annotation.validation_warnings.append(
375
+ f"Line {annotation.line_number}: Side effects present but no security considerations"
376
+ )
377
+
378
+ # If has dependencies, should probably have assumptions
379
+ if annotation.dependencies and not annotation.assumptions:
380
+ annotation.validation_warnings.append(
381
+ f"Line {annotation.line_number}: Dependencies present but no assumptions about their availability"
382
+ )
383
+
384
+ def expand_annotation(self, annotation: DarkArtsAnnotation) -> DarkArtsAnnotation:
385
+ """
386
+ Expand abbreviated annotation to full form.
387
+
388
+ Args:
389
+ annotation: Annotation with abbreviations
390
+
391
+ Returns:
392
+ Annotation with expanded text
393
+ """
394
+ expanded = DarkArtsAnnotation(
395
+ raw_text=annotation.raw_text,
396
+ line_number=annotation.line_number,
397
+ )
398
+
399
+ # Expand purpose
400
+ if annotation.purpose:
401
+ expanded.purpose = expand(annotation.purpose)
402
+
403
+ # Expand lists
404
+ for field_name in ['dependencies', 'assumptions', 'preconditions',
405
+ 'postconditions', 'invariants', 'security',
406
+ 'bidirectional', 'side_effects', 'forbidden',
407
+ 'approximation', 'consequence', 'universal', 'existential']:
408
+ items = getattr(annotation, field_name)
409
+ if items:
410
+ expanded_items = [expand(item) for item in items]
411
+ # Also expand patterns in preconditions
412
+ if field_name == 'preconditions':
413
+ expanded_items = [expand_pattern(item) for item in expanded_items]
414
+ setattr(expanded, field_name, expanded_items)
415
+
416
+ # Copy complexity as-is
417
+ expanded.complexity = annotation.complexity
418
+
419
+ return expanded
420
+
421
+ def format_annotation(self, annotation: DarkArtsAnnotation,
422
+ expanded: bool = False) -> str:
423
+ """
424
+ Format annotation back to DarkArts notation.
425
+
426
+ Args:
427
+ annotation: Annotation to format
428
+ expanded: Use expanded form (no abbreviations)
429
+
430
+ Returns:
431
+ Formatted annotation string
432
+ """
433
+ if expanded:
434
+ annotation = self.expand_annotation(annotation)
435
+
436
+ lines = []
437
+
438
+ # Add purpose (required)
439
+ if annotation.purpose:
440
+ lines.append(f'⊢{{{annotation.purpose}}}')
441
+
442
+ # Add other fields if present
443
+ if annotation.dependencies:
444
+ lines.append(f'∂{{{",".join(annotation.dependencies)}}}')
445
+
446
+ if annotation.assumptions:
447
+ lines.append(f'⚠{{{",".join(annotation.assumptions)}}}')
448
+
449
+ if annotation.preconditions:
450
+ lines.append(f'⊳{{{",".join(annotation.preconditions)}}}')
451
+
452
+ if annotation.postconditions:
453
+ lines.append(f'⊲{{{",".join(annotation.postconditions)}}}')
454
+
455
+ if annotation.invariants:
456
+ lines.append(f'⊨{{{",".join(annotation.invariants)}}}')
457
+
458
+ if annotation.complexity:
459
+ lines.append(f'⚡{{{annotation.complexity}}}')
460
+
461
+ if annotation.security:
462
+ lines.append(f'🔒{{{",".join(annotation.security)}}}')
463
+
464
+ # v3.0.0 fields
465
+ if annotation.bidirectional:
466
+ lines.append(f'⇄{{{",".join(annotation.bidirectional)}}}')
467
+
468
+ if annotation.side_effects:
469
+ lines.append(f'⊕{{{",".join(annotation.side_effects)}}}')
470
+
471
+ if annotation.forbidden:
472
+ lines.append(f'⊗{{{",".join(annotation.forbidden)}}}')
473
+
474
+ if annotation.approximation:
475
+ lines.append(f'≈{{{",".join(annotation.approximation)}}}')
476
+
477
+ if annotation.consequence:
478
+ lines.append(f'∴{{{",".join(annotation.consequence)}}}')
479
+
480
+ if annotation.universal:
481
+ lines.append(f'∀{{{",".join(annotation.universal)}}}')
482
+
483
+ if annotation.existential:
484
+ lines.append(f'∃{{{",".join(annotation.existential)}}}')
485
+
486
+ return '\n'.join(lines)