@voodocs/cli 0.4.1 → 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.
- package/CHANGELOG.md +322 -0
- package/lib/cli/__init__.py +53 -0
- package/lib/cli/benchmark.py +311 -0
- package/lib/cli/fix.py +244 -0
- package/lib/cli/generate.py +310 -0
- package/lib/cli/test_cli.py +215 -0
- package/lib/cli/validate.py +364 -0
- package/lib/darkarts/__init__.py +11 -5
- package/lib/darkarts/annotations/__init__.py +11 -3
- package/lib/darkarts/annotations/darkarts_parser.py +1 -1
- package/lib/darkarts/annotations/types.py +16 -3
- package/lib/darkarts/cli_darkarts.py +385 -0
- package/lib/darkarts/context/__init__.py +11 -3
- package/lib/darkarts/context/ai_integrations.py +7 -21
- package/lib/darkarts/context/commands.py +1 -1
- package/lib/darkarts/context/diagram.py +8 -22
- package/lib/darkarts/context/models.py +7 -22
- package/lib/darkarts/context/module_utils.py +1 -1
- package/lib/darkarts/context/ui.py +1 -1
- package/lib/darkarts/context/validation.py +1 -1
- package/lib/darkarts/context/yaml_utils.py +8 -23
- package/lib/darkarts/core/__init__.py +12 -2
- package/lib/darkarts/core/interface.py +16 -2
- package/lib/darkarts/core/loader.py +17 -2
- package/lib/darkarts/core/plugin.py +16 -3
- package/lib/darkarts/core/registry.py +17 -2
- package/lib/darkarts/exceptions.py +17 -3
- package/lib/darkarts/plugins/voodocs/__init__.py +12 -2
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +16 -5
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +16 -3
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +16 -3
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +16 -3
- package/lib/darkarts/plugins/voodocs/html_exporter.py +16 -3
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +1 -1
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +16 -3
- package/lib/darkarts/plugins/voodocs/test_generator.py +16 -3
- package/lib/darkarts/telemetry.py +16 -3
- package/lib/darkarts/validation/README.md +147 -0
- package/lib/darkarts/validation/__init__.py +91 -0
- package/lib/darkarts/validation/autofix.py +297 -0
- package/lib/darkarts/validation/benchmark.py +426 -0
- package/lib/darkarts/validation/benchmark_wrapper.py +22 -0
- package/lib/darkarts/validation/config.py +257 -0
- package/lib/darkarts/validation/performance.py +412 -0
- package/lib/darkarts/validation/performance_wrapper.py +37 -0
- package/lib/darkarts/validation/semantic.py +461 -0
- package/lib/darkarts/validation/semantic_wrapper.py +77 -0
- package/lib/darkarts/validation/test_validation.py +160 -0
- package/lib/darkarts/validation/types.py +97 -0
- package/lib/darkarts/validation/watch.py +239 -0
- package/package.json +20 -6
- package/voodocs_cli.py +28 -0
- package/cli.py +0 -1646
- package/lib/darkarts/cli.py +0 -128
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢autofix:validation.automatic-repair
|
|
3
|
+
∂{re,pathlib,typing,semantic_validator}
|
|
4
|
+
⚠{python≥3.7,file:writable,backup:recommended}
|
|
5
|
+
⊨{∀fix→annotation-updated,∀backup→created,¬data-loss,idempotent:safe-to-rerun}
|
|
6
|
+
🔒{read-write:files,write:backups,¬network,¬exec}
|
|
7
|
+
⚡{O(n)|n=file-size,file-io:linear}
|
|
8
|
+
|
|
9
|
+
Auto-fix Mode for @darkarts Annotations
|
|
10
|
+
|
|
11
|
+
Automatically updates ∂{} sections to match actual imports with:
|
|
12
|
+
- Safe updates (creates backups before modifying)
|
|
13
|
+
- Dry-run mode (preview changes without applying)
|
|
14
|
+
- Selective fixing (fix only specific issues)
|
|
15
|
+
- Rollback support (restore from backups)
|
|
16
|
+
- Batch operations (fix entire directories)
|
|
17
|
+
- Validation after fix (ensure correctness)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional, List, Set, Tuple
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
|
|
26
|
+
from . import semantic
|
|
27
|
+
from .types import ValidationResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FixResult:
|
|
32
|
+
"""Result of an auto-fix operation."""
|
|
33
|
+
file_path: str
|
|
34
|
+
success: bool
|
|
35
|
+
original_deps: Set[str]
|
|
36
|
+
fixed_deps: Set[str]
|
|
37
|
+
backup_path: Optional[str]
|
|
38
|
+
error: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
if not self.success:
|
|
42
|
+
return f"❌ {self.file_path}: {self.error}"
|
|
43
|
+
|
|
44
|
+
added = self.fixed_deps - self.original_deps
|
|
45
|
+
removed = self.original_deps - self.fixed_deps
|
|
46
|
+
|
|
47
|
+
lines = [f"✅ {self.file_path}: Fixed"]
|
|
48
|
+
if added:
|
|
49
|
+
lines.append(f" Added: {', '.join(sorted(added))}")
|
|
50
|
+
if removed:
|
|
51
|
+
lines.append(f" Removed: {', '.join(sorted(removed))}")
|
|
52
|
+
if self.backup_path:
|
|
53
|
+
lines.append(f" Backup: {self.backup_path}")
|
|
54
|
+
|
|
55
|
+
return "\n".join(lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AutoFixer:
|
|
59
|
+
"""Automatically fixes @darkarts annotation dependencies."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, backup_dir: Optional[Path] = None, dry_run: bool = False):
|
|
62
|
+
"""
|
|
63
|
+
Initialize the auto-fixer.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
backup_dir: Directory to store backups (default: .backups/)
|
|
67
|
+
dry_run: If True, preview changes without applying them
|
|
68
|
+
"""
|
|
69
|
+
self.backup_dir = backup_dir or Path(".backups")
|
|
70
|
+
self.dry_run = dry_run
|
|
71
|
+
|
|
72
|
+
if not dry_run:
|
|
73
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
def create_backup(self, file_path: Path) -> Path:
|
|
76
|
+
"""
|
|
77
|
+
Create a backup of the file before modifying.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
file_path: Path to the file to backup
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Path to the backup file
|
|
84
|
+
"""
|
|
85
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
86
|
+
backup_name = f"{file_path.name}.{timestamp}.bak"
|
|
87
|
+
backup_path = self.backup_dir / backup_name
|
|
88
|
+
|
|
89
|
+
content = file_path.read_text(encoding='utf-8')
|
|
90
|
+
backup_path.write_text(content, encoding='utf-8')
|
|
91
|
+
|
|
92
|
+
return backup_path
|
|
93
|
+
|
|
94
|
+
def extract_annotation_section(self, content: str) -> Tuple[str, int, int]:
|
|
95
|
+
"""
|
|
96
|
+
Extract the @darkarts annotation section from file content.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
content: File content
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (annotation_text, start_index, end_index)
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If no annotation found
|
|
106
|
+
"""
|
|
107
|
+
if not content.startswith('"""@darkarts'):
|
|
108
|
+
raise ValueError("No @darkarts annotation found")
|
|
109
|
+
|
|
110
|
+
# Find the end of the docstring
|
|
111
|
+
end_idx = content.find('"""', 3)
|
|
112
|
+
if end_idx == -1:
|
|
113
|
+
raise ValueError("Malformed annotation: no closing quotes")
|
|
114
|
+
|
|
115
|
+
annotation = content[3:end_idx] # Skip opening """
|
|
116
|
+
return annotation, 3, end_idx
|
|
117
|
+
|
|
118
|
+
def update_dependencies_section(self, annotation: str, new_deps: Set[str]) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Update the ∂{} section in an annotation.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
annotation: The annotation text
|
|
124
|
+
new_deps: New set of dependencies
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Updated annotation text
|
|
128
|
+
"""
|
|
129
|
+
# Format dependencies (sorted for consistency)
|
|
130
|
+
deps_str = ','.join(sorted(new_deps)) if new_deps else ''
|
|
131
|
+
new_deps_section = f"∂{{{deps_str}}}"
|
|
132
|
+
|
|
133
|
+
# Replace existing ∂{} section
|
|
134
|
+
pattern = r'∂\{[^}]*\}'
|
|
135
|
+
|
|
136
|
+
if re.search(pattern, annotation):
|
|
137
|
+
# Replace existing section
|
|
138
|
+
updated = re.sub(pattern, new_deps_section, annotation)
|
|
139
|
+
else:
|
|
140
|
+
# Add new section after ⊢ line
|
|
141
|
+
lines = annotation.split('\n')
|
|
142
|
+
for i, line in enumerate(lines):
|
|
143
|
+
if line.startswith('⊢'):
|
|
144
|
+
lines.insert(i + 1, new_deps_section)
|
|
145
|
+
break
|
|
146
|
+
updated = '\n'.join(lines)
|
|
147
|
+
|
|
148
|
+
return updated
|
|
149
|
+
|
|
150
|
+
def fix_file(self, file_path: Path) -> FixResult:
|
|
151
|
+
"""
|
|
152
|
+
Fix the dependencies in a single file.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
file_path: Path to the file to fix
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
FixResult with details of the fix
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
# Validate first to see what needs fixing
|
|
162
|
+
validation = semantic.validate_file(file_path)
|
|
163
|
+
|
|
164
|
+
if validation.is_valid:
|
|
165
|
+
return FixResult(
|
|
166
|
+
file_path=str(file_path),
|
|
167
|
+
success=True,
|
|
168
|
+
original_deps=set(),
|
|
169
|
+
fixed_deps=set(),
|
|
170
|
+
backup_path=None,
|
|
171
|
+
error="Already valid (no fix needed)"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Read file content
|
|
175
|
+
content = file_path.read_text(encoding='utf-8')
|
|
176
|
+
|
|
177
|
+
# Extract annotation
|
|
178
|
+
annotation, start_idx, end_idx = self.extract_annotation_section(content)
|
|
179
|
+
|
|
180
|
+
# Get current and correct dependencies
|
|
181
|
+
original_deps = semantic.parse_dependencies(annotation)
|
|
182
|
+
correct_deps = semantic.extract_imports(file_path)
|
|
183
|
+
|
|
184
|
+
# Update annotation
|
|
185
|
+
updated_annotation = self.update_dependencies_section(annotation, correct_deps)
|
|
186
|
+
|
|
187
|
+
# Reconstruct file content
|
|
188
|
+
updated_content = (
|
|
189
|
+
content[:start_idx] +
|
|
190
|
+
updated_annotation +
|
|
191
|
+
content[end_idx:]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Create backup and write (unless dry-run)
|
|
195
|
+
backup_path = None
|
|
196
|
+
if not self.dry_run:
|
|
197
|
+
backup_path = self.create_backup(file_path)
|
|
198
|
+
file_path.write_text(updated_content, encoding='utf-8')
|
|
199
|
+
|
|
200
|
+
return FixResult(
|
|
201
|
+
file_path=str(file_path),
|
|
202
|
+
success=True,
|
|
203
|
+
original_deps=original_deps,
|
|
204
|
+
fixed_deps=correct_deps,
|
|
205
|
+
backup_path=str(backup_path) if backup_path else None
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
return FixResult(
|
|
210
|
+
file_path=str(file_path),
|
|
211
|
+
success=False,
|
|
212
|
+
original_deps=set(),
|
|
213
|
+
fixed_deps=set(),
|
|
214
|
+
backup_path=None,
|
|
215
|
+
error=str(e)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def fix_directory(self, directory: Path, recursive: bool = True) -> List[FixResult]:
|
|
219
|
+
"""
|
|
220
|
+
Fix all Python files in a directory.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
directory: Path to the directory
|
|
224
|
+
recursive: Whether to search recursively
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of FixResult objects
|
|
228
|
+
"""
|
|
229
|
+
results = []
|
|
230
|
+
|
|
231
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
232
|
+
|
|
233
|
+
for py_file in directory.glob(pattern):
|
|
234
|
+
if py_file.name.startswith('.'):
|
|
235
|
+
continue # Skip hidden files
|
|
236
|
+
|
|
237
|
+
result = self.fix_file(py_file)
|
|
238
|
+
results.append(result)
|
|
239
|
+
|
|
240
|
+
return results
|
|
241
|
+
|
|
242
|
+
def rollback(self, backup_path: Path, original_path: Path) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Restore a file from backup.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
backup_path: Path to the backup file
|
|
248
|
+
original_path: Path to restore to
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
True if successful, False otherwise
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
content = backup_path.read_text(encoding='utf-8')
|
|
255
|
+
original_path.write_text(content, encoding='utf-8')
|
|
256
|
+
return True
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f"Rollback failed: {e}")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def preview_fix(file_path: Path) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Preview what would be changed without actually modifying the file.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: Path to the file
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Human-readable preview of changes
|
|
271
|
+
"""
|
|
272
|
+
fixer = AutoFixer(dry_run=True)
|
|
273
|
+
result = fixer.fix_file(file_path)
|
|
274
|
+
|
|
275
|
+
if not result.success:
|
|
276
|
+
return f"❌ Cannot fix: {result.error}"
|
|
277
|
+
|
|
278
|
+
if not result.original_deps and not result.fixed_deps:
|
|
279
|
+
return "✅ No changes needed (already valid)"
|
|
280
|
+
|
|
281
|
+
lines = [f"Preview for {file_path}:"]
|
|
282
|
+
lines.append(f" Current: ∂{{{','.join(sorted(result.original_deps))}}}")
|
|
283
|
+
lines.append(f" Fixed: ∂{{{','.join(sorted(result.fixed_deps))}}}")
|
|
284
|
+
|
|
285
|
+
added = result.fixed_deps - result.original_deps
|
|
286
|
+
removed = result.original_deps - result.fixed_deps
|
|
287
|
+
|
|
288
|
+
if added:
|
|
289
|
+
lines.append(f" + Add: {', '.join(sorted(added))}")
|
|
290
|
+
if removed:
|
|
291
|
+
lines.append(f" - Remove: {', '.join(sorted(removed))}")
|
|
292
|
+
|
|
293
|
+
return "\n".join(lines)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# CLI interface removed - use darkarts.validation API or voodocs CLI instead
|