@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.
@@ -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.2.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",