@voodocs/cli 2.5.1 → 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 +30 -0
- package/lib/cli/__init__.py +7 -1
- package/lib/cli/analyze.py +156 -98
- package/lib/cli/convert.py +144 -0
- package/lib/darkarts/voodocs_lite_parser.py +120 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
20
|
+
## [2.5.2] - 2024-12-24
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **CLI Registration**: Fixed analyze, convert, and companion commands not being registered
|
|
24
|
+
- Converted `analyze.py` from argparse to Click command
|
|
25
|
+
- Created `convert.py` as Click command for VooDocs Lite conversion
|
|
26
|
+
- Updated `lib/cli/__init__.py` to import and register all three commands
|
|
27
|
+
- Updated `__version__` from "2.1.3" to "2.5.2" in Python code
|
|
28
|
+
- v2.5.0 and v2.5.1 had files in package but commands weren't accessible via CLI
|
|
29
|
+
- All Priority Analyzer and VooDocs Lite features now fully functional
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
3
33
|
## [2.5.1] - 2024-12-24
|
|
4
34
|
|
|
5
35
|
### Fixed
|
package/lib/cli/__init__.py
CHANGED
|
@@ -16,7 +16,7 @@ This module provides the command-line interface for VooDocs.
|
|
|
16
16
|
import click
|
|
17
17
|
from typing import Optional
|
|
18
18
|
|
|
19
|
-
__version__ = "2.
|
|
19
|
+
__version__ = "2.5.3"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@click.group()
|
|
@@ -41,6 +41,9 @@ from .generate import generate
|
|
|
41
41
|
from .benchmark import benchmark
|
|
42
42
|
from .fix import fix
|
|
43
43
|
from .context import context
|
|
44
|
+
from .analyze import analyze
|
|
45
|
+
from .convert import convert
|
|
46
|
+
from .companion import companion
|
|
44
47
|
|
|
45
48
|
# Register commands
|
|
46
49
|
cli.add_command(init)
|
|
@@ -50,6 +53,9 @@ cli.add_command(generate)
|
|
|
50
53
|
cli.add_command(benchmark)
|
|
51
54
|
cli.add_command(fix)
|
|
52
55
|
cli.add_command(context)
|
|
56
|
+
cli.add_command(analyze)
|
|
57
|
+
cli.add_command(convert)
|
|
58
|
+
cli.add_command(companion)
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
def main():
|
package/lib/cli/analyze.py
CHANGED
|
@@ -1,29 +1,96 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢cli:analyze
|
|
3
|
+
∂{click,pathlib,darkarts.priority_analyzer}
|
|
4
|
+
⚠{python≥3.7,click≥8.0}
|
|
5
|
+
⊨{∀file→analyzed,∀score→calculated}
|
|
6
|
+
🔒{read-only:source-files}
|
|
7
|
+
⚡{O(n):files}
|
|
1
8
|
"""
|
|
2
|
-
CLI command for VooDocs Priority Analyzer
|
|
3
9
|
|
|
4
|
-
Usage: voodocs analyze [path] [options]
|
|
5
10
|
"""
|
|
11
|
+
VooDocs Analyze Command
|
|
6
12
|
|
|
13
|
+
Analyze files to identify which need VooDocs annotations most urgently.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import click
|
|
7
17
|
import os
|
|
8
18
|
import sys
|
|
19
|
+
import json
|
|
20
|
+
import csv
|
|
9
21
|
from pathlib import Path
|
|
10
22
|
|
|
11
23
|
# Add parent directory to path for imports
|
|
12
|
-
sys.path.insert(0,
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
25
|
+
from darkarts.priority_analyzer.analyzer import PriorityAnalyzer
|
|
13
26
|
|
|
14
|
-
from lib.darkarts.priority_analyzer.analyzer import PriorityAnalyzer
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.argument(
|
|
30
|
+
'path',
|
|
31
|
+
type=click.Path(exists=True),
|
|
32
|
+
default='.',
|
|
33
|
+
required=False
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
'-r', '--recursive',
|
|
37
|
+
is_flag=True,
|
|
38
|
+
default=True,
|
|
39
|
+
help='Recursively analyze directories (default: true)'
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
'--top',
|
|
43
|
+
type=int,
|
|
44
|
+
default=10,
|
|
45
|
+
help='Show top N files (default: 10, 0 for all)'
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
'--priority',
|
|
49
|
+
type=click.Choice(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'MINIMAL'], case_sensitive=False),
|
|
50
|
+
help='Filter by priority level'
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
'--min-score',
|
|
54
|
+
type=int,
|
|
55
|
+
help='Minimum priority score to show'
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
'--format',
|
|
59
|
+
'output_format',
|
|
60
|
+
type=click.Choice(['text', 'json', 'csv', 'markdown']),
|
|
61
|
+
default='text',
|
|
62
|
+
help='Output format (default: text)'
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
'--include-annotated',
|
|
66
|
+
is_flag=True,
|
|
67
|
+
help='Include fully annotated files'
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
'--exclude',
|
|
71
|
+
help='Comma-separated list of patterns to exclude'
|
|
72
|
+
)
|
|
73
|
+
def analyze(path, recursive, top, priority, min_score, output_format, include_annotated, exclude):
|
|
74
|
+
"""
|
|
75
|
+
Analyze files to identify which need VooDocs annotations most urgently.
|
|
76
|
+
|
|
77
|
+
Uses heuristics based on:
|
|
78
|
+
- File complexity (LOC, cyclomatic complexity, function count)
|
|
79
|
+
- Security keywords (auth, password, token, etc.)
|
|
80
|
+
- Import/dependency count
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
voodocs analyze src/
|
|
84
|
+
voodocs analyze src/ --top 20
|
|
85
|
+
voodocs analyze src/ --priority critical
|
|
86
|
+
voodocs analyze src/ --format json
|
|
87
|
+
"""
|
|
21
88
|
path = os.path.abspath(path)
|
|
22
89
|
|
|
23
90
|
# Check if path exists
|
|
24
91
|
if not os.path.exists(path):
|
|
25
|
-
|
|
26
|
-
|
|
92
|
+
click.echo(f"❌ Error: Path not found: {path}", err=True)
|
|
93
|
+
sys.exit(1)
|
|
27
94
|
|
|
28
95
|
# Determine if path is file or directory
|
|
29
96
|
is_file = os.path.isfile(path)
|
|
@@ -36,38 +103,34 @@ def cmd_analyze(args):
|
|
|
36
103
|
if is_file:
|
|
37
104
|
scores = [analyzer.analyze_file(path)]
|
|
38
105
|
else:
|
|
39
|
-
recursive = getattr(args, 'recursive', True)
|
|
40
|
-
exclude = getattr(args, 'exclude', None)
|
|
41
106
|
exclude_patterns = exclude.split(',') if exclude else None
|
|
42
107
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
108
|
+
if output_format == 'text':
|
|
109
|
+
click.echo(f"🔍 Analyzing files in: {path}")
|
|
110
|
+
if recursive:
|
|
111
|
+
click.echo(" (recursive mode)")
|
|
112
|
+
click.echo()
|
|
47
113
|
|
|
48
114
|
scores = analyzer.analyze_directory(path, recursive, exclude_patterns)
|
|
49
115
|
|
|
50
116
|
# Filter by priority level if specified
|
|
51
|
-
if
|
|
52
|
-
priority_filter =
|
|
117
|
+
if priority:
|
|
118
|
+
priority_filter = priority.upper()
|
|
53
119
|
scores = [s for s in scores if s.priority_level == priority_filter]
|
|
54
120
|
|
|
55
121
|
# Filter by minimum score
|
|
56
|
-
if
|
|
57
|
-
scores = [s for s in scores if s.priority_score >=
|
|
122
|
+
if min_score:
|
|
123
|
+
scores = [s for s in scores if s.priority_score >= min_score]
|
|
58
124
|
|
|
59
125
|
# Filter out fully annotated files (unless --include-annotated)
|
|
60
|
-
if not
|
|
126
|
+
if not include_annotated:
|
|
61
127
|
scores = [s for s in scores if s.annotation_coverage < 1.0]
|
|
62
128
|
|
|
63
129
|
# Limit to top N
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
scores = scores[:top_n]
|
|
67
|
-
|
|
68
|
-
# Output format
|
|
69
|
-
output_format = getattr(args, 'format', 'text')
|
|
130
|
+
if top and top > 0:
|
|
131
|
+
scores = scores[:top]
|
|
70
132
|
|
|
133
|
+
# Output
|
|
71
134
|
if output_format == 'json':
|
|
72
135
|
_output_json(scores)
|
|
73
136
|
elif output_format == 'csv':
|
|
@@ -77,14 +140,14 @@ def cmd_analyze(args):
|
|
|
77
140
|
else:
|
|
78
141
|
_output_text(scores, path)
|
|
79
142
|
|
|
80
|
-
|
|
143
|
+
sys.exit(0)
|
|
81
144
|
|
|
82
145
|
|
|
83
146
|
def _output_text(scores, path):
|
|
84
147
|
"""Output results in human-readable text format."""
|
|
85
148
|
if not scores:
|
|
86
|
-
|
|
87
|
-
|
|
149
|
+
click.echo("✅ No files need annotations!")
|
|
150
|
+
click.echo(" All files are either fully annotated or below priority threshold.")
|
|
88
151
|
return
|
|
89
152
|
|
|
90
153
|
# Calculate statistics
|
|
@@ -101,34 +164,34 @@ def _output_text(scores, path):
|
|
|
101
164
|
coverage_pct = (annotated_count / total_files * 100) if total_files > 0 else 0
|
|
102
165
|
|
|
103
166
|
# Header
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
167
|
+
click.echo("━" * 70)
|
|
168
|
+
click.echo("VooDocs Priority Analysis")
|
|
169
|
+
click.echo("━" * 70)
|
|
170
|
+
click.echo()
|
|
171
|
+
click.echo(f"Project: {path}")
|
|
172
|
+
click.echo(f"Files analyzed: {total_files}")
|
|
173
|
+
click.echo(f"Annotations found: {annotated_count} ({coverage_pct:.0f}% coverage)")
|
|
174
|
+
click.echo()
|
|
112
175
|
|
|
113
176
|
# Priority distribution
|
|
114
|
-
|
|
177
|
+
click.echo("Priority Distribution:")
|
|
115
178
|
if priority_dist['CRITICAL'] > 0:
|
|
116
|
-
|
|
179
|
+
click.echo(f"🔴 CRITICAL (80-100): {priority_dist['CRITICAL']} files")
|
|
117
180
|
if priority_dist['HIGH'] > 0:
|
|
118
|
-
|
|
181
|
+
click.echo(f"🟠 HIGH (60-79): {priority_dist['HIGH']} files")
|
|
119
182
|
if priority_dist['MEDIUM'] > 0:
|
|
120
|
-
|
|
183
|
+
click.echo(f"🟡 MEDIUM (40-59): {priority_dist['MEDIUM']} files")
|
|
121
184
|
if priority_dist['LOW'] > 0:
|
|
122
|
-
|
|
185
|
+
click.echo(f"🟢 LOW (20-39): {priority_dist['LOW']} files")
|
|
123
186
|
if priority_dist['MINIMAL'] > 0:
|
|
124
|
-
|
|
125
|
-
|
|
187
|
+
click.echo(f"⚪ MINIMAL (0-19): {priority_dist['MINIMAL']} files")
|
|
188
|
+
click.echo()
|
|
126
189
|
|
|
127
190
|
# Top files
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
click.echo("━" * 70)
|
|
192
|
+
click.echo(f"Top {len(scores)} Files Needing Annotations")
|
|
193
|
+
click.echo("━" * 70)
|
|
194
|
+
click.echo()
|
|
132
195
|
|
|
133
196
|
for i, score in enumerate(scores, 1):
|
|
134
197
|
# Priority emoji
|
|
@@ -143,41 +206,39 @@ def _output_text(scores, path):
|
|
|
143
206
|
# Relative path
|
|
144
207
|
rel_path = os.path.relpath(score.filepath, path) if not os.path.isfile(path) else score.filepath
|
|
145
208
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
209
|
+
click.echo(f"{i}. {emoji} {rel_path} (Score: {score.priority_score:.0f})")
|
|
210
|
+
click.echo(f" Complexity: {score.complexity_score} Security: {score.security_score} Dependencies: {score.dependency_score} Annotations: {score.annotation_penalty}")
|
|
211
|
+
click.echo()
|
|
149
212
|
|
|
150
213
|
# Reasons
|
|
151
214
|
if score.reasons:
|
|
152
|
-
|
|
215
|
+
click.echo(" Reasons:")
|
|
153
216
|
for reason in score.reasons:
|
|
154
|
-
|
|
155
|
-
|
|
217
|
+
click.echo(f" • {reason}")
|
|
218
|
+
click.echo()
|
|
156
219
|
|
|
157
220
|
# Suggestions
|
|
158
221
|
if score.suggestions:
|
|
159
|
-
|
|
222
|
+
click.echo(" Suggestions:")
|
|
160
223
|
for suggestion in score.suggestions:
|
|
161
|
-
|
|
162
|
-
|
|
224
|
+
click.echo(f" ✓ {suggestion}")
|
|
225
|
+
click.echo()
|
|
163
226
|
|
|
164
227
|
# Footer
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
228
|
+
click.echo("━" * 70)
|
|
229
|
+
click.echo()
|
|
230
|
+
click.echo("💡 Tip: Start with CRITICAL files first. Use:")
|
|
168
231
|
if scores:
|
|
169
232
|
first_file = scores[0].filepath
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
233
|
+
click.echo(f" voodocs companion {first_file}")
|
|
234
|
+
click.echo()
|
|
235
|
+
click.echo(" Or convert to VooDocs Lite for faster annotation:")
|
|
236
|
+
click.echo(f" voodocs convert {path} --to lite")
|
|
237
|
+
click.echo()
|
|
175
238
|
|
|
176
239
|
|
|
177
240
|
def _output_json(scores):
|
|
178
241
|
"""Output results in JSON format."""
|
|
179
|
-
import json
|
|
180
|
-
|
|
181
242
|
data = []
|
|
182
243
|
for score in scores:
|
|
183
244
|
data.append({
|
|
@@ -197,14 +258,11 @@ def _output_json(scores):
|
|
|
197
258
|
'suggestions': score.suggestions
|
|
198
259
|
})
|
|
199
260
|
|
|
200
|
-
|
|
261
|
+
click.echo(json.dumps(data, indent=2))
|
|
201
262
|
|
|
202
263
|
|
|
203
264
|
def _output_csv(scores):
|
|
204
265
|
"""Output results in CSV format."""
|
|
205
|
-
import csv
|
|
206
|
-
import sys
|
|
207
|
-
|
|
208
266
|
writer = csv.writer(sys.stdout)
|
|
209
267
|
|
|
210
268
|
# Header
|
|
@@ -234,14 +292,14 @@ def _output_csv(scores):
|
|
|
234
292
|
|
|
235
293
|
def _output_markdown(scores):
|
|
236
294
|
"""Output results in Markdown format."""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
295
|
+
click.echo("# VooDocs Priority Analysis")
|
|
296
|
+
click.echo()
|
|
297
|
+
click.echo("## Summary")
|
|
298
|
+
click.echo()
|
|
299
|
+
click.echo(f"- **Files analyzed:** {len(scores)}")
|
|
300
|
+
click.echo()
|
|
301
|
+
click.echo("## Top Files")
|
|
302
|
+
click.echo()
|
|
245
303
|
|
|
246
304
|
for i, score in enumerate(scores, 1):
|
|
247
305
|
emoji = {
|
|
@@ -252,26 +310,26 @@ def _output_markdown(scores):
|
|
|
252
310
|
'MINIMAL': '⚪'
|
|
253
311
|
}.get(score.priority_level, '⚪')
|
|
254
312
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
313
|
+
click.echo(f"### {i}. {emoji} {score.filepath}")
|
|
314
|
+
click.echo()
|
|
315
|
+
click.echo(f"**Score:** {score.priority_score:.0f} ({score.priority_level})")
|
|
316
|
+
click.echo()
|
|
317
|
+
click.echo(f"- Complexity: {score.complexity_score}")
|
|
318
|
+
click.echo(f"- Security: {score.security_score}")
|
|
319
|
+
click.echo(f"- Dependencies: {score.dependency_score}")
|
|
320
|
+
click.echo(f"- Annotation Coverage: {score.annotation_coverage * 100:.0f}%")
|
|
321
|
+
click.echo()
|
|
264
322
|
|
|
265
323
|
if score.reasons:
|
|
266
|
-
|
|
267
|
-
|
|
324
|
+
click.echo("**Reasons:**")
|
|
325
|
+
click.echo()
|
|
268
326
|
for reason in score.reasons:
|
|
269
|
-
|
|
270
|
-
|
|
327
|
+
click.echo(f"- {reason}")
|
|
328
|
+
click.echo()
|
|
271
329
|
|
|
272
330
|
if score.suggestions:
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
click.echo("**Suggestions:**")
|
|
332
|
+
click.echo()
|
|
275
333
|
for suggestion in score.suggestions:
|
|
276
|
-
|
|
277
|
-
|
|
334
|
+
click.echo(f"- {suggestion}")
|
|
335
|
+
click.echo()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢cli:convert
|
|
3
|
+
∂{click,pathlib,darkarts.voodocs_lite_parser}
|
|
4
|
+
⚠{python≥3.7,click≥8.0}
|
|
5
|
+
⊨{∀file→converted|skipped,∀error→reported}
|
|
6
|
+
🔒{read-only:source-files}
|
|
7
|
+
⚡{O(n):files}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
VooDocs Convert Command
|
|
12
|
+
|
|
13
|
+
Convert VooDocs annotations between Standard and Lite formats.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# Import VooDocs Lite parser
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
|
+
from darkarts.voodocs_lite_parser import VooDocsLiteParser
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.command()
|
|
26
|
+
@click.argument(
|
|
27
|
+
'paths',
|
|
28
|
+
nargs=-1,
|
|
29
|
+
type=click.Path(exists=True),
|
|
30
|
+
required=True
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
'--to',
|
|
34
|
+
'target_format',
|
|
35
|
+
type=click.Choice(['lite', 'standard']),
|
|
36
|
+
required=True,
|
|
37
|
+
help='Target format to convert to'
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
'-r', '--recursive',
|
|
41
|
+
is_flag=True,
|
|
42
|
+
help='Recursively process directories'
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
'--dry-run',
|
|
46
|
+
is_flag=True,
|
|
47
|
+
help='Show what would be converted without modifying files'
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
'-v', '--verbose',
|
|
51
|
+
is_flag=True,
|
|
52
|
+
help='Show detailed conversion information'
|
|
53
|
+
)
|
|
54
|
+
def convert(paths, target_format, recursive, dry_run, verbose):
|
|
55
|
+
"""
|
|
56
|
+
Convert VooDocs annotations between Standard and Lite formats.
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
voodocs convert src/ --to lite
|
|
60
|
+
voodocs convert src/ --to standard --recursive
|
|
61
|
+
voodocs convert file.ts --to lite --dry-run
|
|
62
|
+
"""
|
|
63
|
+
parser = VooDocsLiteParser()
|
|
64
|
+
|
|
65
|
+
converted_count = 0
|
|
66
|
+
skipped_count = 0
|
|
67
|
+
error_count = 0
|
|
68
|
+
|
|
69
|
+
for path_str in paths:
|
|
70
|
+
path = Path(path_str)
|
|
71
|
+
|
|
72
|
+
if path.is_file():
|
|
73
|
+
files = [path]
|
|
74
|
+
elif path.is_dir():
|
|
75
|
+
if recursive:
|
|
76
|
+
files = list(path.rglob('*.ts')) + list(path.rglob('*.js')) + \
|
|
77
|
+
list(path.rglob('*.py')) + list(path.rglob('*.sol'))
|
|
78
|
+
else:
|
|
79
|
+
files = list(path.glob('*.ts')) + list(path.glob('*.js')) + \
|
|
80
|
+
list(path.glob('*.py')) + list(path.glob('*.sol'))
|
|
81
|
+
else:
|
|
82
|
+
click.echo(f"❌ Invalid path: {path}", err=True)
|
|
83
|
+
error_count += 1
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
for file_path in files:
|
|
87
|
+
try:
|
|
88
|
+
# Read file
|
|
89
|
+
content = file_path.read_text()
|
|
90
|
+
|
|
91
|
+
# Convert
|
|
92
|
+
if target_format == 'lite':
|
|
93
|
+
converted = parser.convert_file_standard_to_lite(content)
|
|
94
|
+
else:
|
|
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
|
|
109
|
+
|
|
110
|
+
# Check if anything changed
|
|
111
|
+
if converted == content:
|
|
112
|
+
if verbose:
|
|
113
|
+
click.echo(f"⏭️ Skipped (no changes): {file_path}")
|
|
114
|
+
skipped_count += 1
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Write or show
|
|
118
|
+
if dry_run:
|
|
119
|
+
click.echo(f"Would convert: {file_path}")
|
|
120
|
+
if verbose:
|
|
121
|
+
click.echo(f" {len(content)} → {len(converted)} chars")
|
|
122
|
+
else:
|
|
123
|
+
file_path.write_text(converted)
|
|
124
|
+
click.echo(f"✅ Converted: {file_path}")
|
|
125
|
+
|
|
126
|
+
converted_count += 1
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
click.echo(f"❌ Error converting {file_path}: {e}", err=True)
|
|
130
|
+
error_count += 1
|
|
131
|
+
|
|
132
|
+
# Summary
|
|
133
|
+
click.echo()
|
|
134
|
+
click.echo(f"Summary:")
|
|
135
|
+
click.echo(f" ✅ Converted: {converted_count}")
|
|
136
|
+
click.echo(f" ⏭️ Skipped: {skipped_count}")
|
|
137
|
+
if error_count > 0:
|
|
138
|
+
click.echo(f" ❌ Errors: {error_count}")
|
|
139
|
+
|
|
140
|
+
if dry_run:
|
|
141
|
+
click.echo()
|
|
142
|
+
click.echo("(Dry run - no files were modified)")
|
|
143
|
+
|
|
144
|
+
sys.exit(0 if error_count == 0 else 1)
|
|
@@ -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