@voodocs/cli 2.2.2 → 2.3.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 +76 -1693
- package/README.md +92 -0
- package/USAGE.md +138 -2
- package/lib/cli/companion.py +137 -0
- package/lib/cli/generate.py +53 -3
- package/lib/cli/init.py +38 -9
- package/lib/darkarts/companion_files.py +299 -0
- package/package.json +2 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
⊢companion_files:scanner
|
|
3
|
+
∂{pathlib,typing,re}
|
|
4
|
+
⚠{python≥3.7}
|
|
5
|
+
⊨{∀companion_file→matches_source_file}
|
|
6
|
+
🔒{read:files}
|
|
7
|
+
⚡{O(n)|n=files}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
VooDocs - Companion File Scanner
|
|
12
|
+
|
|
13
|
+
Scans for .voodocs.md companion documentation files alongside source files.
|
|
14
|
+
Particularly useful for compiled languages like Solidity where inline annotations
|
|
15
|
+
may interfere with compilation.
|
|
16
|
+
|
|
17
|
+
File naming convention:
|
|
18
|
+
contracts/SubdomainRegistry.sol → contracts/SubdomainRegistry.voodocs.md
|
|
19
|
+
src/auth.service.ts → src/auth.service.voodocs.md
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CompanionFileScanner:
|
|
28
|
+
"""
|
|
29
|
+
Scans for companion documentation files (.voodocs.md) alongside source files.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
COMPANION_EXTENSION = '.voodocs.md'
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def find_companion_file(source_file: Path) -> Optional[Path]:
|
|
36
|
+
"""
|
|
37
|
+
Find the companion documentation file for a given source file.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
source_file: Path to source code file
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Path to companion file if it exists, None otherwise
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> scanner = CompanionFileScanner()
|
|
47
|
+
>>> source = Path('contracts/Registry.sol')
|
|
48
|
+
>>> companion = scanner.find_companion_file(source)
|
|
49
|
+
>>> print(companion) # contracts/Registry.voodocs.md
|
|
50
|
+
"""
|
|
51
|
+
companion_path = source_file.parent / f"{source_file.stem}{CompanionFileScanner.COMPANION_EXTENSION}"
|
|
52
|
+
return companion_path if companion_path.exists() else None
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def scan_directory(directory: Path, recursive: bool = True) -> Dict[Path, Optional[Path]]:
|
|
56
|
+
"""
|
|
57
|
+
Scan a directory for source files and their companion files.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
directory: Directory to scan
|
|
61
|
+
recursive: Whether to scan subdirectories
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dictionary mapping source files to their companion files (or None)
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> scanner = CompanionFileScanner()
|
|
68
|
+
>>> mapping = scanner.scan_directory(Path('contracts/'))
|
|
69
|
+
>>> for source, companion in mapping.items():
|
|
70
|
+
... print(f"{source} → {companion}")
|
|
71
|
+
"""
|
|
72
|
+
extensions = ['*.py', '*.ts', '*.js', '*.jsx', '*.tsx', '*.sol']
|
|
73
|
+
source_files = []
|
|
74
|
+
|
|
75
|
+
if recursive:
|
|
76
|
+
for ext in extensions:
|
|
77
|
+
source_files.extend([f for f in directory.glob(f"**/{ext}") if f.is_file()])
|
|
78
|
+
else:
|
|
79
|
+
for ext in extensions:
|
|
80
|
+
source_files.extend([f for f in directory.glob(ext) if f.is_file()])
|
|
81
|
+
|
|
82
|
+
mapping = {}
|
|
83
|
+
for source_file in source_files:
|
|
84
|
+
companion = CompanionFileScanner.find_companion_file(source_file)
|
|
85
|
+
mapping[source_file] = companion
|
|
86
|
+
|
|
87
|
+
return mapping
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def parse_companion_file(companion_file: Path) -> Dict[str, str]:
|
|
91
|
+
"""
|
|
92
|
+
Parse a companion documentation file and extract structured sections.
|
|
93
|
+
|
|
94
|
+
Expected format:
|
|
95
|
+
# FileName.voodocs.md
|
|
96
|
+
|
|
97
|
+
## Purpose
|
|
98
|
+
⊢{Module purpose description}
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
- **Depends On**: Dep1, Dep2
|
|
102
|
+
- **Depended By**: Parent1, Parent2
|
|
103
|
+
|
|
104
|
+
## Invariants
|
|
105
|
+
⊨{Invariant 1}
|
|
106
|
+
⊨{Invariant 2}
|
|
107
|
+
|
|
108
|
+
## Assumptions
|
|
109
|
+
⊲{Assumption 1}
|
|
110
|
+
|
|
111
|
+
## Critical Sections
|
|
112
|
+
- functionName() - Description
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
companion_file: Path to companion .voodocs.md file
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dictionary with sections: purpose, architecture, invariants, assumptions, critical_sections
|
|
119
|
+
"""
|
|
120
|
+
content = companion_file.read_text(encoding='utf-8')
|
|
121
|
+
|
|
122
|
+
sections = {
|
|
123
|
+
'purpose': '',
|
|
124
|
+
'architecture': '',
|
|
125
|
+
'invariants': [],
|
|
126
|
+
'assumptions': [],
|
|
127
|
+
'critical_sections': [],
|
|
128
|
+
'security': [],
|
|
129
|
+
'dependencies': [],
|
|
130
|
+
'depended_by': []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Extract Purpose section
|
|
134
|
+
purpose_match = re.search(r'##\s+Purpose\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
135
|
+
if purpose_match:
|
|
136
|
+
purpose_text = purpose_match.group(1).strip()
|
|
137
|
+
# Extract ⊢{} notation
|
|
138
|
+
purpose_symbol = re.search(r'⊢\{([^}]+)\}', purpose_text)
|
|
139
|
+
if purpose_symbol:
|
|
140
|
+
sections['purpose'] = purpose_symbol.group(1).strip()
|
|
141
|
+
else:
|
|
142
|
+
sections['purpose'] = purpose_text
|
|
143
|
+
|
|
144
|
+
# Extract Architecture section
|
|
145
|
+
arch_match = re.search(r'##\s+Architecture\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
146
|
+
if arch_match:
|
|
147
|
+
arch_text = arch_match.group(1).strip()
|
|
148
|
+
sections['architecture'] = arch_text
|
|
149
|
+
|
|
150
|
+
# Extract dependencies
|
|
151
|
+
deps_match = re.search(r'\*\*Depends On\*\*:\s*(.+)', arch_text)
|
|
152
|
+
if deps_match:
|
|
153
|
+
deps = [d.strip() for d in deps_match.group(1).split(',')]
|
|
154
|
+
sections['dependencies'] = deps
|
|
155
|
+
|
|
156
|
+
# Extract dependents
|
|
157
|
+
depended_match = re.search(r'\*\*Depended By\*\*:\s*(.+)', arch_text)
|
|
158
|
+
if depended_match:
|
|
159
|
+
dependents = [d.strip() for d in depended_match.group(1).split(',')]
|
|
160
|
+
sections['depended_by'] = dependents
|
|
161
|
+
|
|
162
|
+
# Extract Invariants section
|
|
163
|
+
inv_match = re.search(r'##\s+Invariants\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
164
|
+
if inv_match:
|
|
165
|
+
inv_text = inv_match.group(1).strip()
|
|
166
|
+
# Find all ⊨{} notations
|
|
167
|
+
invariants = re.findall(r'⊨\{([^}]+)\}', inv_text)
|
|
168
|
+
sections['invariants'] = [inv.strip() for inv in invariants]
|
|
169
|
+
|
|
170
|
+
# Also capture plain list items
|
|
171
|
+
if not invariants:
|
|
172
|
+
list_items = re.findall(r'^[-*]\s+(.+)$', inv_text, re.MULTILINE)
|
|
173
|
+
sections['invariants'] = [item.strip() for item in list_items]
|
|
174
|
+
|
|
175
|
+
# Extract Assumptions section
|
|
176
|
+
assume_match = re.search(r'##\s+Assumptions\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
177
|
+
if assume_match:
|
|
178
|
+
assume_text = assume_match.group(1).strip()
|
|
179
|
+
# Find all ⊲{} notations
|
|
180
|
+
assumptions = re.findall(r'⊲\{([^}]+)\}', assume_text)
|
|
181
|
+
sections['assumptions'] = [a.strip() for a in assumptions]
|
|
182
|
+
|
|
183
|
+
# Also capture plain list items
|
|
184
|
+
if not assumptions:
|
|
185
|
+
list_items = re.findall(r'^[-*]\s+(.+)$', assume_text, re.MULTILINE)
|
|
186
|
+
sections['assumptions'] = [item.strip() for item in list_items]
|
|
187
|
+
|
|
188
|
+
# Extract Critical Sections
|
|
189
|
+
critical_match = re.search(r'##\s+Critical Sections\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
190
|
+
if critical_match:
|
|
191
|
+
critical_text = critical_match.group(1).strip()
|
|
192
|
+
list_items = re.findall(r'^[-*]\s+(.+)$', critical_text, re.MULTILINE)
|
|
193
|
+
sections['critical_sections'] = [item.strip() for item in list_items]
|
|
194
|
+
|
|
195
|
+
# Extract Security Considerations
|
|
196
|
+
security_match = re.search(r'##\s+Security(?:\s+Considerations)?\s*\n(.*?)(?=\n##|\Z)', content, re.DOTALL)
|
|
197
|
+
if security_match:
|
|
198
|
+
security_text = security_match.group(1).strip()
|
|
199
|
+
# Find all ⊨{} notations
|
|
200
|
+
security_items = re.findall(r'⊨\{([^}]+)\}', security_text)
|
|
201
|
+
sections['security'] = [s.strip() for s in security_items]
|
|
202
|
+
|
|
203
|
+
# Also capture plain list items
|
|
204
|
+
if not security_items:
|
|
205
|
+
list_items = re.findall(r'^[-*]\s+(.+)$', security_text, re.MULTILINE)
|
|
206
|
+
sections['security'] = [item.strip() for item in list_items]
|
|
207
|
+
|
|
208
|
+
return sections
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def merge_with_source(source_annotations: Dict, companion_data: Dict) -> Dict:
|
|
212
|
+
"""
|
|
213
|
+
Merge companion file data with source file annotations.
|
|
214
|
+
|
|
215
|
+
Companion file data takes precedence for documentation-specific fields
|
|
216
|
+
(purpose, invariants, assumptions), while source file provides code structure.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
source_annotations: Annotations extracted from source code
|
|
220
|
+
companion_data: Data parsed from companion file
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Merged annotation dictionary
|
|
224
|
+
"""
|
|
225
|
+
merged = source_annotations.copy()
|
|
226
|
+
|
|
227
|
+
# Override/enhance with companion file data
|
|
228
|
+
if companion_data.get('purpose'):
|
|
229
|
+
merged['module_id'] = companion_data['purpose']
|
|
230
|
+
|
|
231
|
+
if companion_data.get('dependencies'):
|
|
232
|
+
merged['dependencies'] = ', '.join(companion_data['dependencies'])
|
|
233
|
+
|
|
234
|
+
if companion_data.get('invariants'):
|
|
235
|
+
# Merge invariants from both sources
|
|
236
|
+
existing_invs = merged.get('invariants', '').split('\n') if merged.get('invariants') else []
|
|
237
|
+
all_invs = existing_invs + companion_data['invariants']
|
|
238
|
+
merged['invariants'] = '\n'.join(filter(None, all_invs))
|
|
239
|
+
|
|
240
|
+
if companion_data.get('assumptions'):
|
|
241
|
+
existing_assumes = merged.get('assumptions', '').split('\n') if merged.get('assumptions') else []
|
|
242
|
+
all_assumes = existing_assumes + companion_data['assumptions']
|
|
243
|
+
merged['assumptions'] = '\n'.join(filter(None, all_assumes))
|
|
244
|
+
|
|
245
|
+
if companion_data.get('security'):
|
|
246
|
+
existing_sec = merged.get('security', '').split('\n') if merged.get('security') else []
|
|
247
|
+
all_sec = existing_sec + companion_data['security']
|
|
248
|
+
merged['security'] = '\n'.join(filter(None, all_sec))
|
|
249
|
+
|
|
250
|
+
# Add companion-specific fields
|
|
251
|
+
if companion_data.get('critical_sections'):
|
|
252
|
+
merged['critical_sections'] = '\n'.join(companion_data['critical_sections'])
|
|
253
|
+
|
|
254
|
+
if companion_data.get('architecture'):
|
|
255
|
+
merged['architecture'] = companion_data['architecture']
|
|
256
|
+
|
|
257
|
+
return merged
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def create_template(source_file: Path) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Create a template companion file for a given source file.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
source_file: Path to source file
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Template content as string
|
|
269
|
+
"""
|
|
270
|
+
filename = source_file.name
|
|
271
|
+
stem = source_file.stem
|
|
272
|
+
|
|
273
|
+
template = f"""# {stem}.voodocs.md
|
|
274
|
+
|
|
275
|
+
## Purpose
|
|
276
|
+
⊢{{[Describe the module's primary purpose and responsibilities]}}
|
|
277
|
+
|
|
278
|
+
## Architecture
|
|
279
|
+
- **Depends On**: [List dependencies]
|
|
280
|
+
- **Depended By**: [List modules that depend on this]
|
|
281
|
+
- **Storage**: [Data storage information if applicable]
|
|
282
|
+
|
|
283
|
+
## Invariants
|
|
284
|
+
⊨{{[Invariant 1: Describe a condition that must always hold]}}
|
|
285
|
+
⊨{{[Invariant 2: Another invariant]}}
|
|
286
|
+
|
|
287
|
+
## Assumptions
|
|
288
|
+
⊲{{[Assumption 1: Describe assumptions about inputs, environment, etc.]}}
|
|
289
|
+
|
|
290
|
+
## Critical Sections
|
|
291
|
+
- `functionName()` - [Description of why this is critical]
|
|
292
|
+
|
|
293
|
+
## Security Considerations
|
|
294
|
+
⊨{{[Security-specific invariant or requirement]}}
|
|
295
|
+
|
|
296
|
+
## Notes
|
|
297
|
+
[Any additional notes, diagrams, or references]
|
|
298
|
+
"""
|
|
299
|
+
return template
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voodocs/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI-Native Symbolic Documentation System - The world's first documentation tool using mathematical notation with semantic validation",
|
|
5
5
|
"main": "voodocs_cli.py",
|
|
6
6
|
"bin": {
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"lib/darkarts/documentation/",
|
|
64
64
|
"lib/darkarts/exceptions.py",
|
|
65
65
|
"lib/darkarts/telemetry.py",
|
|
66
|
+
"lib/darkarts/companion_files.py",
|
|
66
67
|
"lib/darkarts/parsers/typescript/dist/",
|
|
67
68
|
"lib/darkarts/parsers/typescript/src/",
|
|
68
69
|
"lib/darkarts/parsers/typescript/package.json",
|