@voodocs/cli 2.5.2 → 2.5.3
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 +17 -0
- package/lib/cli/__init__.py +1 -1
- package/lib/cli/convert.py +15 -2
- package/lib/darkarts/voodocs_lite_parser.py +120 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [2.5.3] - 2024-12-24
|
|
4
|
+
|
|
5
|
+
### Fixed - CRITICAL BUG
|
|
6
|
+
- **Convert Command File Corruption**: Fixed critical bug where `voodocs convert` emptied all files
|
|
7
|
+
- Root cause: `standard_to_lite()` and `lite_to_standard()` only converted annotation blocks, not entire files
|
|
8
|
+
- Added `convert_file_standard_to_lite()` and `convert_file_lite_to_standard()` methods
|
|
9
|
+
- These new methods find and replace annotations while preserving all other file content
|
|
10
|
+
- Updated `convert.py` to use file-level conversion methods
|
|
11
|
+
- **Safety Checks**: Added validation to prevent file corruption
|
|
12
|
+
- Reject conversions that result in nearly empty files (<10 chars)
|
|
13
|
+
- Reject conversions that shrink file size by >90%
|
|
14
|
+
- Display warnings when safety checks trigger
|
|
15
|
+
|
|
16
|
+
**IMPORTANT**: If you used `voodocs convert` from v2.5.2, restore your files from git immediately!
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
3
20
|
## [2.5.2] - 2024-12-24
|
|
4
21
|
|
|
5
22
|
### Fixed
|
package/lib/cli/__init__.py
CHANGED
package/lib/cli/convert.py
CHANGED
|
@@ -90,9 +90,22 @@ def convert(paths, target_format, recursive, dry_run, verbose):
|
|
|
90
90
|
|
|
91
91
|
# Convert
|
|
92
92
|
if target_format == 'lite':
|
|
93
|
-
converted = parser.
|
|
93
|
+
converted = parser.convert_file_standard_to_lite(content)
|
|
94
94
|
else:
|
|
95
|
-
converted = parser.
|
|
95
|
+
converted = parser.convert_file_lite_to_standard(content)
|
|
96
|
+
|
|
97
|
+
# Safety check: Ensure conversion didn't empty the file
|
|
98
|
+
if not converted or len(converted.strip()) < 10:
|
|
99
|
+
click.echo(f"⚠️ Warning: Conversion resulted in nearly empty file, skipping: {file_path}", err=True)
|
|
100
|
+
error_count += 1
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Safety check: Ensure file size didn't shrink by more than 90%
|
|
104
|
+
if len(converted) < len(content) * 0.1:
|
|
105
|
+
click.echo(f"⚠️ Warning: Conversion would reduce file size by >90%, skipping: {file_path}", err=True)
|
|
106
|
+
click.echo(f" Original: {len(content)} chars, Converted: {len(converted)} chars", err=True)
|
|
107
|
+
error_count += 1
|
|
108
|
+
continue
|
|
96
109
|
|
|
97
110
|
# Check if anything changed
|
|
98
111
|
if converted == content:
|
|
@@ -44,6 +44,64 @@ class VooDocsLiteParser:
|
|
|
44
44
|
'security': r'🔒\{([^}]+)\}',
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
@classmethod
|
|
48
|
+
def convert_file_standard_to_lite(cls, file_content: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Convert entire file from Standard to Lite format.
|
|
51
|
+
|
|
52
|
+
Finds @darkarts annotations and converts them to @darkarts-lite.
|
|
53
|
+
Preserves all other content unchanged.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_content: Full file content
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
File content with annotations converted to Lite format
|
|
60
|
+
"""
|
|
61
|
+
# Pattern to match @darkarts comment blocks
|
|
62
|
+
pattern = r'/\*\*@darkarts\n(.*?)\*/'
|
|
63
|
+
|
|
64
|
+
def replace_annotation(match):
|
|
65
|
+
annotation_content = match.group(1)
|
|
66
|
+
# Convert the annotation
|
|
67
|
+
lite_content = cls.standard_to_lite(annotation_content, compress_abbreviations=True)
|
|
68
|
+
# Return as @darkarts-lite
|
|
69
|
+
return f'/**@darkarts-lite\n{lite_content}\n*/'
|
|
70
|
+
|
|
71
|
+
# Replace all @darkarts annotations
|
|
72
|
+
result = re.sub(pattern, replace_annotation, file_content, flags=re.DOTALL)
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def convert_file_lite_to_standard(cls, file_content: str) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Convert entire file from Lite to Standard format.
|
|
80
|
+
|
|
81
|
+
Finds @darkarts-lite annotations and converts them to @darkarts.
|
|
82
|
+
Preserves all other content unchanged.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_content: Full file content
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
File content with annotations converted to Standard format
|
|
89
|
+
"""
|
|
90
|
+
# Pattern to match @darkarts-lite comment blocks
|
|
91
|
+
pattern = r'/\*\*@darkarts-lite\n(.*?)\*/'
|
|
92
|
+
|
|
93
|
+
def replace_annotation(match):
|
|
94
|
+
annotation_content = match.group(1)
|
|
95
|
+
# Convert the annotation
|
|
96
|
+
standard_content = cls.lite_to_standard(annotation_content)
|
|
97
|
+
# Return as @darkarts
|
|
98
|
+
return f'/**@darkarts\n{standard_content}\n*/'
|
|
99
|
+
|
|
100
|
+
# Replace all @darkarts-lite annotations
|
|
101
|
+
result = re.sub(pattern, replace_annotation, file_content, flags=re.DOTALL)
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
|
|
47
105
|
@classmethod
|
|
48
106
|
def parse_lite(cls, content: str) -> Dict[str, any]:
|
|
49
107
|
"""
|
|
@@ -122,7 +180,7 @@ class VooDocsLiteParser:
|
|
|
122
180
|
@classmethod
|
|
123
181
|
def parse_standard(cls, content: str) -> Dict[str, any]:
|
|
124
182
|
"""
|
|
125
|
-
Parse
|
|
183
|
+
Parse VooDocs Standard format annotation.
|
|
126
184
|
|
|
127
185
|
Args:
|
|
128
186
|
content: Standard format annotation text
|
|
@@ -141,33 +199,50 @@ class VooDocsLiteParser:
|
|
|
141
199
|
'security': [],
|
|
142
200
|
}
|
|
143
201
|
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
202
|
+
# Purpose
|
|
203
|
+
match = re.search(cls.STANDARD_PATTERNS['purpose'], content)
|
|
204
|
+
if match:
|
|
205
|
+
result['purpose'] = match.group(1).strip()
|
|
206
|
+
|
|
207
|
+
# Dependencies
|
|
208
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['dependencies'], content):
|
|
209
|
+
deps = match.group(1).strip()
|
|
210
|
+
result['dependencies'].extend([d.strip() for d in deps.split(',')])
|
|
211
|
+
|
|
212
|
+
# Assumptions
|
|
213
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['assumptions'], content):
|
|
214
|
+
result['assumptions'].append(match.group(1).strip())
|
|
215
|
+
|
|
216
|
+
# Preconditions
|
|
217
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['preconditions'], content):
|
|
218
|
+
result['preconditions'].append(match.group(1).strip())
|
|
219
|
+
|
|
220
|
+
# Postconditions
|
|
221
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['postconditions'], content):
|
|
222
|
+
result['postconditions'].append(match.group(1).strip())
|
|
223
|
+
|
|
224
|
+
# Invariants
|
|
225
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['invariants'], content):
|
|
226
|
+
result['invariants'].append(match.group(1).strip())
|
|
227
|
+
|
|
228
|
+
# Complexity
|
|
229
|
+
match = re.search(cls.STANDARD_PATTERNS['complexity'], content)
|
|
230
|
+
if match:
|
|
231
|
+
result['complexity'] = match.group(1).strip()
|
|
232
|
+
|
|
233
|
+
# Security
|
|
234
|
+
for match in re.finditer(cls.STANDARD_PATTERNS['security'], content):
|
|
235
|
+
result['security'].append(match.group(1).strip())
|
|
160
236
|
|
|
161
237
|
return result
|
|
162
238
|
|
|
163
239
|
@classmethod
|
|
164
|
-
def lite_to_standard(cls, lite_content: str
|
|
240
|
+
def lite_to_standard(cls, lite_content: str) -> str:
|
|
165
241
|
"""
|
|
166
242
|
Convert Lite format to Standard format.
|
|
167
243
|
|
|
168
244
|
Args:
|
|
169
245
|
lite_content: Lite format annotation
|
|
170
|
-
expand_abbreviations: Whether to expand abbreviations
|
|
171
246
|
|
|
172
247
|
Returns:
|
|
173
248
|
Standard format annotation
|
|
@@ -178,62 +253,46 @@ class VooDocsLiteParser:
|
|
|
178
253
|
|
|
179
254
|
# Purpose
|
|
180
255
|
if parsed['purpose']:
|
|
181
|
-
text = parsed['purpose']
|
|
182
|
-
if expand_abbreviations:
|
|
183
|
-
text = expand_text(text)
|
|
256
|
+
text = ultra_expand(parsed['purpose'])
|
|
184
257
|
lines.append(f"⊢{{{text}}}")
|
|
185
258
|
|
|
186
259
|
# Dependencies
|
|
187
260
|
if parsed['dependencies']:
|
|
188
|
-
deps = ',
|
|
189
|
-
if expand_abbreviations:
|
|
190
|
-
deps = expand_text(deps)
|
|
261
|
+
deps = ','.join([ultra_expand(d) for d in parsed['dependencies']])
|
|
191
262
|
lines.append(f"∂{{{deps}}}")
|
|
192
263
|
|
|
193
264
|
# Assumptions
|
|
194
265
|
if parsed['assumptions']:
|
|
195
266
|
for assumption in parsed['assumptions']:
|
|
196
|
-
text = assumption
|
|
197
|
-
if expand_abbreviations:
|
|
198
|
-
text = expand_text(text)
|
|
267
|
+
text = ultra_expand(assumption)
|
|
199
268
|
lines.append(f"⚠{{{text}}}")
|
|
200
269
|
|
|
201
270
|
# Preconditions
|
|
202
271
|
if parsed['preconditions']:
|
|
203
272
|
for precond in parsed['preconditions']:
|
|
204
|
-
text = precond
|
|
205
|
-
if expand_abbreviations:
|
|
206
|
-
text = expand_text(text)
|
|
273
|
+
text = ultra_expand(precond)
|
|
207
274
|
lines.append(f"⊳{{{text}}}")
|
|
208
275
|
|
|
209
276
|
# Postconditions
|
|
210
277
|
if parsed['postconditions']:
|
|
211
278
|
for postcond in parsed['postconditions']:
|
|
212
|
-
text = postcond
|
|
213
|
-
if expand_abbreviations:
|
|
214
|
-
text = expand_text(text)
|
|
279
|
+
text = ultra_expand(postcond)
|
|
215
280
|
lines.append(f"⊲{{{text}}}")
|
|
216
281
|
|
|
217
282
|
# Invariants
|
|
218
283
|
if parsed['invariants']:
|
|
219
284
|
for invariant in parsed['invariants']:
|
|
220
|
-
text = invariant
|
|
221
|
-
if expand_abbreviations:
|
|
222
|
-
text = expand_text(text)
|
|
285
|
+
text = ultra_expand(invariant)
|
|
223
286
|
lines.append(f"⊨{{{text}}}")
|
|
224
287
|
|
|
225
288
|
# Complexity
|
|
226
289
|
if parsed['complexity']:
|
|
227
|
-
|
|
228
|
-
# Don't expand complexity (O(n) should stay as-is)
|
|
229
|
-
lines.append(f"⚡{{{text}}}")
|
|
290
|
+
lines.append(f"⚡{{{parsed['complexity']}}}")
|
|
230
291
|
|
|
231
292
|
# Security
|
|
232
293
|
if parsed['security']:
|
|
233
294
|
for security in parsed['security']:
|
|
234
|
-
text = security
|
|
235
|
-
if expand_abbreviations:
|
|
236
|
-
text = expand_text(text)
|
|
295
|
+
text = ultra_expand(security)
|
|
237
296
|
lines.append(f"🔒{{{text}}}")
|
|
238
297
|
|
|
239
298
|
return '\n'.join(lines)
|
|
@@ -319,25 +378,27 @@ class VooDocsLiteParser:
|
|
|
319
378
|
@classmethod
|
|
320
379
|
def detect_format(cls, content: str) -> str:
|
|
321
380
|
"""
|
|
322
|
-
Detect
|
|
381
|
+
Detect if content is Standard or Lite format.
|
|
323
382
|
|
|
383
|
+
Args:
|
|
384
|
+
content: Annotation content
|
|
385
|
+
|
|
324
386
|
Returns:
|
|
325
|
-
'
|
|
387
|
+
'standard', 'lite', or 'unknown'
|
|
326
388
|
"""
|
|
327
|
-
# Check for
|
|
328
|
-
|
|
329
|
-
|
|
389
|
+
# Check for standard symbols
|
|
390
|
+
if any(symbol in content for symbol in ['⊢', '∂', '⚠', '⊳', '⊲', '⊨', '⚡', '🔒']):
|
|
391
|
+
return 'standard'
|
|
330
392
|
|
|
331
|
-
# Check for
|
|
332
|
-
|
|
333
|
-
|
|
393
|
+
# Check for lite symbols at start of lines
|
|
394
|
+
lines = content.strip().split('\n')
|
|
395
|
+
lite_symbols = 0
|
|
396
|
+
for line in lines:
|
|
397
|
+
line = line.strip()
|
|
398
|
+
if line and line[0] in ['>', '@', '!', '<', '=', '~', '#']:
|
|
399
|
+
lite_symbols += 1
|
|
334
400
|
|
|
335
|
-
if
|
|
401
|
+
if lite_symbols > 0:
|
|
336
402
|
return 'lite'
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
elif has_lite and has_standard:
|
|
340
|
-
# Mixed format, prefer standard
|
|
341
|
-
return 'standard'
|
|
342
|
-
else:
|
|
343
|
-
return 'unknown'
|
|
403
|
+
|
|
404
|
+
return 'unknown'
|
package/package.json
CHANGED